From 5e43708a406f988cb78c9be89048518a1422fb81 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 9 Jan 2026 07:01:43 +0100 Subject: [PATCH] Skip Tuya update if it is not relevent (#160407) --- .../components/tuya/binary_sensor.py | 10 +- homeassistant/components/tuya/number.py | 10 ++ homeassistant/components/tuya/select.py | 10 ++ homeassistant/components/tuya/sensor.py | 10 ++ homeassistant/components/tuya/siren.py | 10 ++ homeassistant/components/tuya/switch.py | 10 ++ homeassistant/components/tuya/valve.py | 10 ++ tests/components/tuya/__init__.py | 38 +++++++ tests/components/tuya/test_binary_sensor.py | 106 ++++++++---------- tests/components/tuya/test_number.py | 52 ++++++++- tests/components/tuya/test_select.py | 52 ++++++++- tests/components/tuya/test_sensor.py | 52 ++++++++- tests/components/tuya/test_siren.py | 51 ++++++++- tests/components/tuya/test_switch.py | 51 ++++++++- tests/components/tuya/test_valve.py | 51 ++++++++- 15 files changed, 455 insertions(+), 68 deletions(-) diff --git a/homeassistant/components/tuya/binary_sensor.py b/homeassistant/components/tuya/binary_sensor.py index 5b3cc50c72ea..f416f33e468d 100644 --- a/homeassistant/components/tuya/binary_sensor.py +++ b/homeassistant/components/tuya/binary_sensor.py @@ -463,6 +463,11 @@ class TuyaBinarySensorEntity(TuyaEntity, BinarySensorEntity): self._attr_unique_id = f"{super().unique_id}{description.key}" self._dpcode_wrapper = dpcode_wrapper + @property + def is_on(self) -> bool | None: + """Return true if sensor is on.""" + return self._read_wrapper(self._dpcode_wrapper) + async def _handle_state_update( self, updated_status_properties: list[str] | None, @@ -472,8 +477,3 @@ class TuyaBinarySensorEntity(TuyaEntity, BinarySensorEntity): if self._dpcode_wrapper.skip_update(self.device, updated_status_properties): return self.async_write_ha_state() - - @property - def is_on(self) -> bool | None: - """Return true if sensor is on.""" - return self._read_wrapper(self._dpcode_wrapper) diff --git a/homeassistant/components/tuya/number.py b/homeassistant/components/tuya/number.py index accd1d47695b..13ab3472aa8a 100644 --- a/homeassistant/components/tuya/number.py +++ b/homeassistant/components/tuya/number.py @@ -551,6 +551,16 @@ class TuyaNumberEntity(TuyaEntity, NumberEntity): """Return the entity value to represent the entity state.""" return self._read_wrapper(self._dpcode_wrapper) + async def _handle_state_update( + self, + updated_status_properties: list[str] | None, + dp_timestamps: dict | None = None, + ) -> None: + """Handle state update, only if this entity's dpcode was actually updated.""" + if self._dpcode_wrapper.skip_update(self.device, updated_status_properties): + return + self.async_write_ha_state() + async def async_set_native_value(self, value: float) -> None: """Set new value.""" await self._async_send_wrapper_updates(self._dpcode_wrapper, value) diff --git a/homeassistant/components/tuya/select.py b/homeassistant/components/tuya/select.py index b23abccbab5d..ac61b46e919d 100644 --- a/homeassistant/components/tuya/select.py +++ b/homeassistant/components/tuya/select.py @@ -407,6 +407,16 @@ class TuyaSelectEntity(TuyaEntity, SelectEntity): """Return the selected entity option to represent the entity state.""" return self._read_wrapper(self._dpcode_wrapper) + async def _handle_state_update( + self, + updated_status_properties: list[str] | None, + dp_timestamps: dict | None = None, + ) -> None: + """Handle state update, only if this entity's dpcode was actually updated.""" + if self._dpcode_wrapper.skip_update(self.device, updated_status_properties): + return + self.async_write_ha_state() + async def async_select_option(self, option: str) -> None: """Change the selected option.""" await self._async_send_wrapper_updates(self._dpcode_wrapper, option) diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index 121524be488f..f5e51a280904 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -1849,3 +1849,13 @@ class TuyaSensorEntity(TuyaEntity, SensorEntity): def native_value(self) -> StateType: """Return the value reported by the sensor.""" return self._read_wrapper(self._dpcode_wrapper) + + async def _handle_state_update( + self, + updated_status_properties: list[str] | None, + dp_timestamps: dict | None = None, + ) -> None: + """Handle state update, only if this entity's dpcode was actually updated.""" + if self._dpcode_wrapper.skip_update(self.device, updated_status_properties): + return + self.async_write_ha_state() diff --git a/homeassistant/components/tuya/siren.py b/homeassistant/components/tuya/siren.py index 711a6fdb0647..4adc42adead6 100644 --- a/homeassistant/components/tuya/siren.py +++ b/homeassistant/components/tuya/siren.py @@ -107,6 +107,16 @@ class TuyaSirenEntity(TuyaEntity, SirenEntity): """Return true if siren is on.""" return self._read_wrapper(self._dpcode_wrapper) + async def _handle_state_update( + self, + updated_status_properties: list[str] | None, + dp_timestamps: dict | None = None, + ) -> None: + """Handle state update, only if this entity's dpcode was actually updated.""" + if self._dpcode_wrapper.skip_update(self.device, updated_status_properties): + return + self.async_write_ha_state() + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the siren on.""" await self._async_send_wrapper_updates(self._dpcode_wrapper, True) diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index 2175e252ecc2..50b4712bc2b6 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -1040,6 +1040,16 @@ class TuyaSwitchEntity(TuyaEntity, SwitchEntity): """Return true if switch is on.""" return self._read_wrapper(self._dpcode_wrapper) + async def _handle_state_update( + self, + updated_status_properties: list[str] | None, + dp_timestamps: dict | None = None, + ) -> None: + """Handle state update, only if this entity's dpcode was actually updated.""" + if self._dpcode_wrapper.skip_update(self.device, updated_status_properties): + return + self.async_write_ha_state() + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" await self._async_send_wrapper_updates(self._dpcode_wrapper, True) diff --git a/homeassistant/components/tuya/valve.py b/homeassistant/components/tuya/valve.py index 1101ab191a0e..be2bd905bb1f 100644 --- a/homeassistant/components/tuya/valve.py +++ b/homeassistant/components/tuya/valve.py @@ -137,6 +137,16 @@ class TuyaValveEntity(TuyaEntity, ValveEntity): return None return not is_open + async def _handle_state_update( + self, + updated_status_properties: list[str] | None, + dp_timestamps: dict | None = None, + ) -> None: + """Handle state update, only if this entity's dpcode was actually updated.""" + if self._dpcode_wrapper.skip_update(self.device, updated_status_properties): + return + self.async_write_ha_state() + async def async_open_valve(self) -> None: """Open the valve.""" await self._async_send_wrapper_updates(self._dpcode_wrapper, True) diff --git a/tests/components/tuya/__init__.py b/tests/components/tuya/__init__.py index c38460a004e1..a7752b52859c 100644 --- a/tests/components/tuya/__init__.py +++ b/tests/components/tuya/__init__.py @@ -6,6 +6,7 @@ import pathlib from typing import Any from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory from tuya_sharing import CustomerDevice, Manager from homeassistant.components.tuya import DeviceListener @@ -61,3 +62,40 @@ async def initialize_entry( with patch("homeassistant.components.tuya.Manager", return_value=mock_manager): await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() + + +async def check_selective_state_update( + hass: HomeAssistant, + mock_device: CustomerDevice, + mock_listener: MockDeviceListener, + freezer: FrozenDateTimeFactory, + *, + entity_id: str, + dpcode: str, + initial_state: str, + updates: dict[str, Any], + expected_state: str, + last_reported: str, +) -> None: + """Test selective state update. + + This test verifies that when an update event comes with properties that do NOT + include the dpcode (e.g., a battery event for a door sensor), + the entity state is not changed and last_reported is not updated. + """ + initial_reported = "2024-01-01T00:00:00+00:00" + assert hass.states.get(entity_id).state == initial_state + assert hass.states.get(entity_id).last_reported.isoformat() == initial_reported + + # Force update the dpcode and trigger device update + freezer.tick(30) + mock_device.status[dpcode] = None + await mock_listener.async_send_device_update(hass, mock_device, {}) + assert hass.states.get(entity_id).state == initial_state + assert hass.states.get(entity_id).last_reported.isoformat() == initial_reported + + # Trigger device update with provided updates + freezer.tick(30) + await mock_listener.async_send_device_update(hass, mock_device, updates) + assert hass.states.get(entity_id).state == expected_state + assert hass.states.get(entity_id).last_reported.isoformat() == last_reported diff --git a/tests/components/tuya/test_binary_sensor.py b/tests/components/tuya/test_binary_sensor.py index c86e02a44fed..0366732155b2 100644 --- a/tests/components/tuya/test_binary_sensor.py +++ b/tests/components/tuya/test_binary_sensor.py @@ -14,7 +14,7 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from . import MockDeviceListener, initialize_entry +from . import MockDeviceListener, check_selective_state_update, initialize_entry from tests.common import MockConfigEntry, snapshot_platform @@ -35,6 +35,54 @@ async def test_platform_setup_and_discovery( await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) +@pytest.mark.parametrize( + "mock_device_code", + ["mcs_oxslv1c9"], +) +@pytest.mark.parametrize( + ("updates", "expected_state", "last_reported"), + [ + # Update without dpcode - state should not change, last_reported stays at initial + ({"battery_percentage": 80}, "off", "2024-01-01T00:00:00+00:00"), + # Update with dpcode - state should change, last_reported advances + ({"doorcontact_state": True}, "on", "2024-01-01T00:01:00+00:00"), + # Update with multiple properties including dpcode - state should change + ( + {"battery_percentage": 50, "doorcontact_state": True}, + "on", + "2024-01-01T00:01:00+00:00", + ), + ], +) +@patch("homeassistant.components.tuya.PLATFORMS", [Platform.BINARY_SENSOR]) +@pytest.mark.freeze_time("2024-01-01") +async def test_selective_state_update( + hass: HomeAssistant, + mock_manager: Manager, + mock_config_entry: MockConfigEntry, + mock_device: CustomerDevice, + mock_listener: MockDeviceListener, + freezer: FrozenDateTimeFactory, + updates: dict[str, Any], + expected_state: str, + last_reported: str, +) -> None: + """Test skip_update/last_reported.""" + await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) + await check_selective_state_update( + hass, + mock_device, + mock_listener, + freezer, + entity_id="binary_sensor.window_downstairs_door", + dpcode="doorcontact_state", + initial_state="off", + updates=updates, + expected_state=expected_state, + last_reported=last_reported, + ) + + @pytest.mark.parametrize( "mock_device_code", ["cs_zibqa9dutqyaxym2"], @@ -75,59 +123,3 @@ async def test_bitmap( assert hass.states.get("binary_sensor.dehumidifier_tank_full").state == tankfull assert hass.states.get("binary_sensor.dehumidifier_defrost").state == defrost assert hass.states.get("binary_sensor.dehumidifier_wet").state == wet - - -@pytest.mark.parametrize( - "mock_device_code", - ["mcs_oxslv1c9"], -) -@pytest.mark.parametrize( - ("updates", "expected_state", "last_reported"), - [ - # Update without dpcode - state should not change, last_reported stays at initial - ({"battery_percentage": 80}, "off", "2024-01-01T00:00:00+00:00"), - # Update with dpcode - state should change, last_reported advances - ({"doorcontact_state": True}, "on", "2024-01-01T00:01:00+00:00"), - # Update with multiple properties including dpcode - state should change - ( - {"battery_percentage": 50, "doorcontact_state": True}, - "on", - "2024-01-01T00:01:00+00:00", - ), - ], -) -@patch("homeassistant.components.tuya.PLATFORMS", [Platform.BINARY_SENSOR]) -@pytest.mark.freeze_time("2024-01-01") -async def test_selective_state_update( - hass: HomeAssistant, - mock_manager: Manager, - mock_config_entry: MockConfigEntry, - mock_device: CustomerDevice, - mock_listener: MockDeviceListener, - freezer: FrozenDateTimeFactory, - updates: dict[str, Any], - expected_state: str, - last_reported: str, -) -> None: - """Test binary sensor only updates when its dpcode is in updated properties. - - This test verifies that when an update event comes with properties that do NOT - include the binary sensor's dpcode (e.g., a battery event for a door sensor), - the binary sensor state is not changed and last_reported is not updated. - """ - entity_id = "binary_sensor.window_downstairs_door" - await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) - - assert hass.states.get(entity_id).state == "off" - assert ( - hass.states.get(entity_id).last_reported.isoformat() - == "2024-01-01T00:00:00+00:00" - ) - - # Force update the dpcode - should be ignored unless event contains the property - mock_device.status["doorcontact_state"] = True - freezer.tick(60) - await mock_listener.async_send_device_update(hass, mock_device, updates) - - assert hass.states.get(entity_id).state == expected_state - assert hass.states.get(entity_id).last_reported.isoformat() == last_reported diff --git a/tests/components/tuya/test_number.py b/tests/components/tuya/test_number.py index 61ba98ec956e..af59b13f5caa 100644 --- a/tests/components/tuya/test_number.py +++ b/tests/components/tuya/test_number.py @@ -2,8 +2,10 @@ from __future__ import annotations +from typing import Any from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion from tuya_sharing import CustomerDevice, Manager @@ -17,7 +19,7 @@ from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from . import initialize_entry +from . import MockDeviceListener, check_selective_state_update, initialize_entry from tests.common import MockConfigEntry, snapshot_platform @@ -37,6 +39,54 @@ async def test_platform_setup_and_discovery( await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) +@pytest.mark.parametrize( + "mock_device_code", + ["mal_gyitctrjj1kefxp2"], +) +@pytest.mark.parametrize( + ("updates", "expected_state", "last_reported"), + [ + # Update without dpcode - state should not change, last_reported stays at initial + ({"switch_alarm_sound": True}, "15.0", "2024-01-01T00:00:00+00:00"), + # Update with dpcode - state should change, last_reported advances + ({"delay_set": 17}, "17.0", "2024-01-01T00:01:00+00:00"), + # Update with multiple properties including dpcode - state should change + ( + {"switch_alarm_sound": True, "delay_set": 17}, + "17.0", + "2024-01-01T00:01:00+00:00", + ), + ], +) +@patch("homeassistant.components.tuya.PLATFORMS", [Platform.NUMBER]) +@pytest.mark.freeze_time("2024-01-01") +async def test_selective_state_update( + hass: HomeAssistant, + mock_manager: Manager, + mock_config_entry: MockConfigEntry, + mock_device: CustomerDevice, + mock_listener: MockDeviceListener, + freezer: FrozenDateTimeFactory, + updates: dict[str, Any], + expected_state: str, + last_reported: str, +) -> None: + """Test skip_update/last_reported.""" + await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) + await check_selective_state_update( + hass, + mock_device, + mock_listener, + freezer, + entity_id="number.multifunction_alarm_arm_delay", + dpcode="delay_set", + initial_state="15.0", + updates=updates, + expected_state=expected_state, + last_reported=last_reported, + ) + + @pytest.mark.parametrize( "mock_device_code", ["mal_gyitctrjj1kefxp2"], diff --git a/tests/components/tuya/test_select.py b/tests/components/tuya/test_select.py index ecc9570584d3..d28d8f9d2bac 100644 --- a/tests/components/tuya/test_select.py +++ b/tests/components/tuya/test_select.py @@ -2,8 +2,10 @@ from __future__ import annotations +from typing import Any from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion from tuya_sharing import CustomerDevice, Manager @@ -18,7 +20,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import entity_registry as er -from . import initialize_entry +from . import MockDeviceListener, check_selective_state_update, initialize_entry from tests.common import MockConfigEntry, snapshot_platform @@ -38,6 +40,54 @@ async def test_platform_setup_and_discovery( await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) +@pytest.mark.parametrize( + "mock_device_code", + ["cl_zah67ekd"], +) +@pytest.mark.parametrize( + ("updates", "expected_state", "last_reported"), + [ + # Update without dpcode - state should not change, last_reported stays at initial + ({"control": "stop"}, "forward", "2024-01-01T00:00:00+00:00"), + # Update with dpcode - state should change, last_reported advances + ({"control_back_mode": "back"}, "back", "2024-01-01T00:01:00+00:00"), + # Update with multiple properties including dpcode - state should change + ( + {"control": "stop", "control_back_mode": "back"}, + "back", + "2024-01-01T00:01:00+00:00", + ), + ], +) +@patch("homeassistant.components.tuya.PLATFORMS", [Platform.SELECT]) +@pytest.mark.freeze_time("2024-01-01") +async def test_selective_state_update( + hass: HomeAssistant, + mock_manager: Manager, + mock_config_entry: MockConfigEntry, + mock_device: CustomerDevice, + mock_listener: MockDeviceListener, + freezer: FrozenDateTimeFactory, + updates: dict[str, Any], + expected_state: str, + last_reported: str, +) -> None: + """Test skip_update/last_reported.""" + await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) + await check_selective_state_update( + hass, + mock_device, + mock_listener, + freezer, + entity_id="select.kitchen_blinds_motor_mode", + dpcode="control_back_mode", + initial_state="forward", + updates=updates, + expected_state=expected_state, + last_reported=last_reported, + ) + + @pytest.mark.parametrize( "mock_device_code", ["cl_zah67ekd"], diff --git a/tests/components/tuya/test_sensor.py b/tests/components/tuya/test_sensor.py index 034f19ea7ae4..ae79efd4a86e 100644 --- a/tests/components/tuya/test_sensor.py +++ b/tests/components/tuya/test_sensor.py @@ -2,8 +2,10 @@ from __future__ import annotations +from typing import Any from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion from tuya_sharing import CustomerDevice, Manager @@ -12,7 +14,7 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from . import initialize_entry +from . import MockDeviceListener, check_selective_state_update, initialize_entry from tests.common import MockConfigEntry, snapshot_platform @@ -31,3 +33,51 @@ async def test_platform_setup_and_discovery( await initialize_entry(hass, mock_manager, mock_config_entry, mock_devices) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.parametrize( + "mock_device_code", + ["mcs_8yhypbo7"], +) +@pytest.mark.parametrize( + ("updates", "expected_state", "last_reported"), + [ + # Update without dpcode - state should not change, last_reported stays at initial + ({"doorcontact_state": True}, "62.0", "2024-01-01T00:00:00+00:00"), + # Update with dpcode - state should change, last_reported advances + ({"battery_percentage": 50}, "50.0", "2024-01-01T00:01:00+00:00"), + # Update with multiple properties including dpcode - state should change + ( + {"doorcontact_state": True, "battery_percentage": 50}, + "50.0", + "2024-01-01T00:01:00+00:00", + ), + ], +) +@patch("homeassistant.components.tuya.PLATFORMS", [Platform.SENSOR]) +@pytest.mark.freeze_time("2024-01-01") +async def test_selective_state_update( + hass: HomeAssistant, + mock_manager: Manager, + mock_config_entry: MockConfigEntry, + mock_device: CustomerDevice, + mock_listener: MockDeviceListener, + freezer: FrozenDateTimeFactory, + updates: dict[str, Any], + expected_state: str, + last_reported: str, +) -> None: + """Test skip_update/last_reported.""" + await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) + await check_selective_state_update( + hass, + mock_device, + mock_listener, + freezer, + entity_id="sensor.boite_aux_lettres_arriere_battery", + dpcode="battery_percentage", + initial_state="62.0", + updates=updates, + expected_state=expected_state, + last_reported=last_reported, + ) diff --git a/tests/components/tuya/test_siren.py b/tests/components/tuya/test_siren.py index 1cd91656e017..e0d09418bc47 100644 --- a/tests/components/tuya/test_siren.py +++ b/tests/components/tuya/test_siren.py @@ -5,6 +5,7 @@ from __future__ import annotations from typing import Any from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion from tuya_sharing import CustomerDevice, Manager @@ -18,7 +19,7 @@ from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from . import initialize_entry +from . import MockDeviceListener, check_selective_state_update, initialize_entry from tests.common import MockConfigEntry, snapshot_platform @@ -38,6 +39,54 @@ async def test_platform_setup_and_discovery( await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) +@pytest.mark.parametrize( + "mock_device_code", + ["sp_sdd5f5f2dl5wydjf"], +) +@pytest.mark.parametrize( + ("updates", "expected_state", "last_reported"), + [ + # Update without dpcode - state should not change, last_reported stays at initial + ({"basic_wdr": False}, "off", "2024-01-01T00:00:00+00:00"), + # Update with dpcode - state should change, last_reported advances + ({"siren_switch": True}, "on", "2024-01-01T00:01:00+00:00"), + # Update with multiple properties including dpcode - state should change + ( + {"basic_wdr": False, "siren_switch": True}, + "on", + "2024-01-01T00:01:00+00:00", + ), + ], +) +@patch("homeassistant.components.tuya.PLATFORMS", [Platform.SIREN]) +@pytest.mark.freeze_time("2024-01-01") +async def test_selective_state_update( + hass: HomeAssistant, + mock_manager: Manager, + mock_config_entry: MockConfigEntry, + mock_device: CustomerDevice, + mock_listener: MockDeviceListener, + freezer: FrozenDateTimeFactory, + updates: dict[str, Any], + expected_state: str, + last_reported: str, +) -> None: + """Test skip_update/last_reported.""" + await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) + await check_selective_state_update( + hass, + mock_device, + mock_listener, + freezer, + entity_id="siren.c9", + dpcode="siren_switch", + initial_state="off", + updates=updates, + expected_state=expected_state, + last_reported=last_reported, + ) + + @patch("homeassistant.components.tuya.PLATFORMS", [Platform.SIREN]) @pytest.mark.parametrize( "mock_device_code", diff --git a/tests/components/tuya/test_switch.py b/tests/components/tuya/test_switch.py index 072efdb6ee18..b6cdb560de24 100644 --- a/tests/components/tuya/test_switch.py +++ b/tests/components/tuya/test_switch.py @@ -5,6 +5,7 @@ from __future__ import annotations from typing import Any from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion from tuya_sharing import CustomerDevice, Manager @@ -19,7 +20,7 @@ from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er, issue_registry as ir -from . import initialize_entry +from . import MockDeviceListener, check_selective_state_update, initialize_entry from tests.common import MockConfigEntry, snapshot_platform @@ -39,6 +40,54 @@ async def test_platform_setup_and_discovery( await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) +@pytest.mark.parametrize( + "mock_device_code", + ["cz_PGEkBctAbtzKOZng"], +) +@pytest.mark.parametrize( + ("updates", "expected_state", "last_reported"), + [ + # Update without dpcode - state should not change, last_reported stays at initial + ({"countdown_1": 50}, "off", "2024-01-01T00:00:00+00:00"), + # Update with dpcode - state should change, last_reported advances + ({"switch": True}, "on", "2024-01-01T00:01:00+00:00"), + # Update with multiple properties including dpcode - state should change + ( + {"countdown_1": 50, "switch": True}, + "on", + "2024-01-01T00:01:00+00:00", + ), + ], +) +@patch("homeassistant.components.tuya.PLATFORMS", [Platform.SWITCH]) +@pytest.mark.freeze_time("2024-01-01") +async def test_selective_state_update( + hass: HomeAssistant, + mock_manager: Manager, + mock_config_entry: MockConfigEntry, + mock_device: CustomerDevice, + mock_listener: MockDeviceListener, + freezer: FrozenDateTimeFactory, + updates: dict[str, Any], + expected_state: str, + last_reported: str, +) -> None: + """Test skip_update/last_reported.""" + await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) + await check_selective_state_update( + hass, + mock_device, + mock_listener, + freezer, + entity_id="switch.din_socket", + dpcode="switch", + initial_state="off", + updates=updates, + expected_state=expected_state, + last_reported=last_reported, + ) + + @pytest.mark.parametrize( ("preexisting_entity", "disabled_by", "expected_entity", "expected_issue"), [ diff --git a/tests/components/tuya/test_valve.py b/tests/components/tuya/test_valve.py index a315227a6f80..903f1b59efce 100644 --- a/tests/components/tuya/test_valve.py +++ b/tests/components/tuya/test_valve.py @@ -5,6 +5,7 @@ from __future__ import annotations from typing import Any from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion from tuya_sharing import CustomerDevice, Manager @@ -18,7 +19,7 @@ from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from . import initialize_entry +from . import MockDeviceListener, check_selective_state_update, initialize_entry from tests.common import MockConfigEntry, snapshot_platform @@ -38,6 +39,54 @@ async def test_platform_setup_and_discovery( await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) +@pytest.mark.parametrize( + "mock_device_code", + ["sfkzq_ed7frwissyqrejic"], +) +@pytest.mark.parametrize( + ("updates", "expected_state", "last_reported"), + [ + # Update without dpcode - state should not change, last_reported stays at initial + ({"battery_percentage": 50}, "open", "2024-01-01T00:00:00+00:00"), + # Update with dpcode - state should change, last_reported advances + ({"switch_1": False}, "closed", "2024-01-01T00:01:00+00:00"), + # Update with multiple properties including dpcode - state should change + ( + {"battery_percentage": 50, "switch_1": False}, + "closed", + "2024-01-01T00:01:00+00:00", + ), + ], +) +@patch("homeassistant.components.tuya.PLATFORMS", [Platform.VALVE]) +@pytest.mark.freeze_time("2024-01-01") +async def test_selective_state_update( + hass: HomeAssistant, + mock_manager: Manager, + mock_config_entry: MockConfigEntry, + mock_device: CustomerDevice, + mock_listener: MockDeviceListener, + freezer: FrozenDateTimeFactory, + updates: dict[str, Any], + expected_state: str, + last_reported: str, +) -> None: + """Test skip_update/last_reported.""" + await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) + await check_selective_state_update( + hass, + mock_device, + mock_listener, + freezer, + entity_id="valve.jie_hashui_fa_valve_1", + dpcode="switch_1", + initial_state="open", + updates=updates, + expected_state=expected_state, + last_reported=last_reported, + ) + + @patch("homeassistant.components.tuya.PLATFORMS", [Platform.VALVE]) @pytest.mark.parametrize( "mock_device_code",