diff --git a/homeassistant/components/switcher_kis/button.py b/homeassistant/components/switcher_kis/button.py index 30597ed0738..efd07698eee 100644 --- a/homeassistant/components/switcher_kis/button.py +++ b/homeassistant/components/switcher_kis/button.py @@ -6,14 +6,10 @@ from collections.abc import Callable, Coroutine from dataclasses import dataclass from typing import Any, cast -from aioswitcher.api import ( - DeviceState, - SwitcherApi, - SwitcherBaseResponse, - ThermostatSwing, -) +from aioswitcher.api import SwitcherApi +from aioswitcher.api.messages import SwitcherBaseResponse from aioswitcher.api.remotes import SwitcherBreezeRemote -from aioswitcher.device import DeviceCategory +from aioswitcher.device import DeviceCategory, DeviceState, ThermostatSwing from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.const import EntityCategory diff --git a/homeassistant/components/switcher_kis/climate.py b/homeassistant/components/switcher_kis/climate.py index c8bf33eca09..1b5ac2bfc18 100644 --- a/homeassistant/components/switcher_kis/climate.py +++ b/homeassistant/components/switcher_kis/climate.py @@ -26,7 +26,7 @@ from homeassistant.components.climate import ( HVACMode, ) from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback @@ -117,20 +117,15 @@ class SwitcherClimateEntity(SwitcherEntity, ClimateEntity): self._attr_supported_features |= ( ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON ) - self._update_data(True) - - @callback - def _handle_coordinator_update(self) -> None: - """Handle updated data from the coordinator.""" self._update_data() - self.async_write_ha_state() - def _update_data(self, force_update: bool = False) -> None: + def _update_data(self) -> None: """Update data from device.""" data = cast(SwitcherThermostat, self.coordinator.data) features = self._remote.modes_features[data.mode] - if data.target_temperature == 0 and not force_update: + # Ignore empty update from device that was power cycled + if data.target_temperature == 0 and self.target_temperature is not None: return self._attr_current_temperature = data.temperature diff --git a/homeassistant/components/switcher_kis/config_flow.py b/homeassistant/components/switcher_kis/config_flow.py index 2e4b3478e8c..ee015cb1a25 100644 --- a/homeassistant/components/switcher_kis/config_flow.py +++ b/homeassistant/components/switcher_kis/config_flow.py @@ -6,7 +6,7 @@ from collections.abc import Mapping import logging from typing import Any, Final -from aioswitcher.bridge import SwitcherBase +from aioswitcher.device import SwitcherBase from aioswitcher.device.tools import validate_token import voluptuous as vol diff --git a/homeassistant/components/switcher_kis/cover.py b/homeassistant/components/switcher_kis/cover.py index 5d8e4a4b0ac..c0ab90e1268 100644 --- a/homeassistant/components/switcher_kis/cover.py +++ b/homeassistant/components/switcher_kis/cover.py @@ -69,12 +69,6 @@ class SwitcherBaseCoverEntity(SwitcherEntity, CoverEntity): ) _cover_id: int - @callback - def _handle_coordinator_update(self) -> None: - """Handle updated data from the coordinator.""" - self._update_data() - self.async_write_ha_state() - def _update_data(self) -> None: """Update data from device.""" data = cast(SwitcherShutter, self.coordinator.data) diff --git a/homeassistant/components/switcher_kis/entity.py b/homeassistant/components/switcher_kis/entity.py index 82b892d548d..0cd56d2c462 100644 --- a/homeassistant/components/switcher_kis/entity.py +++ b/homeassistant/components/switcher_kis/entity.py @@ -6,6 +6,7 @@ from typing import Any from aioswitcher.api import SwitcherApi from aioswitcher.api.messages import SwitcherBaseResponse +from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import DeviceInfo @@ -28,6 +29,15 @@ class SwitcherEntity(CoordinatorEntity[SwitcherDataUpdateCoordinator]): connections={(dr.CONNECTION_NETWORK_MAC, coordinator.mac_address)} ) + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._update_data() + super()._handle_coordinator_update() + + def _update_data(self) -> None: + """Update data from device.""" + async def _async_call_api(self, api: str, *args: Any, **kwargs: Any) -> None: """Call Switcher API.""" _LOGGER.debug("Calling api for %s, api: '%s', args: %s", self.name, api, args) diff --git a/homeassistant/components/switcher_kis/light.py b/homeassistant/components/switcher_kis/light.py index 472b89cdec7..77e2a8cdd97 100644 --- a/homeassistant/components/switcher_kis/light.py +++ b/homeassistant/components/switcher_kis/light.py @@ -68,32 +68,28 @@ class SwitcherBaseLightEntity(SwitcherEntity, LightEntity): super().__init__(coordinator) self._light_id = light_id self.control_result: bool | None = None + self._update_data() - @callback - def _handle_coordinator_update(self) -> None: - """When device updates, clear control result that overrides state.""" - self.control_result = None - self.async_write_ha_state() - - @property - def is_on(self) -> bool: - """Return True if entity is on.""" + def _update_data(self) -> None: + """Update data from device.""" if self.control_result is not None: - return self.control_result + self._attr_is_on = self.control_result + self.control_result = None + return data = cast(SwitcherLight, self.coordinator.data) - return bool(data.light[self._light_id] == DeviceState.ON) + self._attr_is_on = bool(data.light[self._light_id] == DeviceState.ON) async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" await self._async_call_api(API_SET_LIGHT, DeviceState.ON, self._light_id) - self.control_result = True + self._attr_is_on = self.control_result = True self.async_write_ha_state() async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" await self._async_call_api(API_SET_LIGHT, DeviceState.OFF, self._light_id) - self.control_result = False + self._attr_is_on = self.control_result = False self.async_write_ha_state() @@ -109,8 +105,6 @@ class SwitcherSingleLightEntity(SwitcherBaseLightEntity): ) -> None: """Initialize the entity.""" super().__init__(coordinator, light_id) - - # Entity class attributes self._attr_unique_id = f"{coordinator.device_id}-{coordinator.mac_address}" @@ -126,8 +120,6 @@ class SwitcherMultiLightEntity(SwitcherBaseLightEntity): ) -> None: """Initialize the entity.""" super().__init__(coordinator, light_id) - - # Entity class attributes self._attr_translation_placeholders = {"light_id": str(light_id + 1)} self._attr_unique_id = ( f"{coordinator.device_id}-{coordinator.mac_address}-{light_id}" diff --git a/homeassistant/components/switcher_kis/switch.py b/homeassistant/components/switcher_kis/switch.py index 6111ab71909..1e602061c2c 100644 --- a/homeassistant/components/switcher_kis/switch.py +++ b/homeassistant/components/switcher_kis/switch.py @@ -111,34 +111,28 @@ class SwitcherBaseSwitchEntity(SwitcherEntity, SwitchEntity): """Initialize the entity.""" super().__init__(coordinator) self.control_result: bool | None = None - - # Entity class attributes self._attr_unique_id = f"{coordinator.device_id}-{coordinator.mac_address}" + self._update_data() - @callback - def _handle_coordinator_update(self) -> None: - """When device updates, clear control result that overrides state.""" - self.control_result = None - self.async_write_ha_state() - - @property - def is_on(self) -> bool: - """Return True if entity is on.""" + def _update_data(self) -> None: + """Update data from device.""" if self.control_result is not None: - return self.control_result + self._attr_is_on = self.control_result + self.control_result = None + return - return bool(self.coordinator.data.device_state == DeviceState.ON) + self._attr_is_on = bool(self.coordinator.data.device_state == DeviceState.ON) async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" await self._async_call_api(API_CONTROL_DEVICE, Command.ON) - self.control_result = True + self._attr_is_on = self.control_result = True self.async_write_ha_state() async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" await self._async_call_api(API_CONTROL_DEVICE, Command.OFF) - self.control_result = False + self._attr_is_on = self.control_result = False self.async_write_ha_state() async def async_set_auto_off_service(self, auto_off: timedelta) -> None: @@ -177,44 +171,45 @@ class SwitcherWaterHeaterSwitchEntity(SwitcherBaseSwitchEntity): async def async_turn_on_with_timer_service(self, timer_minutes: int) -> None: """Use for turning device on with a timer service calls.""" await self._async_call_api(API_CONTROL_DEVICE, Command.ON, timer_minutes) - self.control_result = True + self._attr_is_on = self.control_result = True self.async_write_ha_state() class SwitcherShutterChildLockBaseSwitchEntity(SwitcherEntity, SwitchEntity): - """Representation of a Switcher shutter base switch entity.""" + """Representation of a Switcher child lock base switch entity.""" _attr_device_class = SwitchDeviceClass.SWITCH _attr_entity_category = EntityCategory.CONFIG _attr_icon = "mdi:lock-open" _cover_id: int - def __init__(self, coordinator: SwitcherDataUpdateCoordinator) -> None: + def __init__( + self, + coordinator: SwitcherDataUpdateCoordinator, + cover_id: int, + ) -> None: """Initialize the entity.""" super().__init__(coordinator) + self._cover_id = cover_id self.control_result: bool | None = None + self._update_data() - @callback - def _handle_coordinator_update(self) -> None: - """When device updates, clear control result that overrides state.""" - self.control_result = None - super()._handle_coordinator_update() - - @property - def is_on(self) -> bool: - """Return True if entity is on.""" + def _update_data(self) -> None: + """Update data from device.""" if self.control_result is not None: - return self.control_result + self._attr_is_on = self.control_result + self.control_result = None + return data = cast(SwitcherShutter, self.coordinator.data) - return bool(data.child_lock[self._cover_id] == ShutterChildLock.ON) + self._attr_is_on = bool(data.child_lock[self._cover_id] == ShutterChildLock.ON) async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" await self._async_call_api( API_SET_CHILD_LOCK, ShutterChildLock.ON, self._cover_id ) - self.control_result = True + self._attr_is_on = self.control_result = True self.async_write_ha_state() async def async_turn_off(self, **kwargs: Any) -> None: @@ -222,7 +217,7 @@ class SwitcherShutterChildLockBaseSwitchEntity(SwitcherEntity, SwitchEntity): await self._async_call_api( API_SET_CHILD_LOCK, ShutterChildLock.OFF, self._cover_id ) - self.control_result = False + self._attr_is_on = self.control_result = False self.async_write_ha_state() @@ -239,9 +234,7 @@ class SwitcherShutterChildLockSingleSwitchEntity( cover_id: int, ) -> None: """Initialize the entity.""" - super().__init__(coordinator) - self._cover_id = cover_id - + super().__init__(coordinator, cover_id) self._attr_unique_id = ( f"{coordinator.device_id}-{coordinator.mac_address}-child_lock" ) @@ -260,8 +253,7 @@ class SwitcherShutterChildLockMultiSwitchEntity( cover_id: int, ) -> None: """Initialize the entity.""" - super().__init__(coordinator) - self._cover_id = cover_id + super().__init__(coordinator, cover_id) self._attr_translation_placeholders = {"cover_id": str(cover_id + 1)} self._attr_unique_id = ( diff --git a/homeassistant/components/switcher_kis/utils.py b/homeassistant/components/switcher_kis/utils.py index 50bfb883e6c..44f906aef44 100644 --- a/homeassistant/components/switcher_kis/utils.py +++ b/homeassistant/components/switcher_kis/utils.py @@ -6,7 +6,8 @@ import asyncio import logging from aioswitcher.api.remotes import SwitcherBreezeRemoteManager -from aioswitcher.bridge import SwitcherBase, SwitcherBridge +from aioswitcher.bridge import SwitcherBridge +from aioswitcher.device import SwitcherBase from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import singleton diff --git a/tests/components/switcher_kis/test_button.py b/tests/components/switcher_kis/test_button.py index 6ebd82363e4..bf48647176b 100644 --- a/tests/components/switcher_kis/test_button.py +++ b/tests/components/switcher_kis/test_button.py @@ -2,7 +2,8 @@ from unittest.mock import ANY, patch -from aioswitcher.api import DeviceState, SwitcherBaseResponse, ThermostatSwing +from aioswitcher.api.messages import SwitcherBaseResponse +from aioswitcher.device import DeviceState, ThermostatSwing import pytest from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS diff --git a/tests/components/switcher_kis/test_climate.py b/tests/components/switcher_kis/test_climate.py index 72a25d20d04..426c52640c1 100644 --- a/tests/components/switcher_kis/test_climate.py +++ b/tests/components/switcher_kis/test_climate.py @@ -2,7 +2,7 @@ from unittest.mock import ANY, patch -from aioswitcher.api import SwitcherBaseResponse +from aioswitcher.api.messages import SwitcherBaseResponse from aioswitcher.device import ( DeviceState, ThermostatFanLevel, diff --git a/tests/components/switcher_kis/test_cover.py b/tests/components/switcher_kis/test_cover.py index 5829d6345ef..767389a3352 100644 --- a/tests/components/switcher_kis/test_cover.py +++ b/tests/components/switcher_kis/test_cover.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from aioswitcher.api import SwitcherBaseResponse +from aioswitcher.api.messages import SwitcherBaseResponse from aioswitcher.device import ShutterDirection import pytest diff --git a/tests/components/switcher_kis/test_light.py b/tests/components/switcher_kis/test_light.py index 51d0eb6332f..715110fb02b 100644 --- a/tests/components/switcher_kis/test_light.py +++ b/tests/components/switcher_kis/test_light.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from aioswitcher.api import SwitcherBaseResponse +from aioswitcher.api.messages import SwitcherBaseResponse from aioswitcher.device import DeviceState import pytest @@ -111,6 +111,44 @@ async def test_light( assert state.state == STATE_OFF +@pytest.mark.parametrize("mock_bridge", [[DEVICE]], indirect=True) +async def test_light_ignore_previous_async_state( + hass: HomeAssistant, mock_bridge, mock_api +) -> None: + """Test light ignores previous async state.""" + await init_integration(hass, USERNAME, TOKEN) + assert mock_bridge + + entity_id = f"{LIGHT_DOMAIN}.{slugify(DEVICE.name)}_light_1" + + # Test initial state - light on + state = hass.states.get(entity_id) + assert state.state == STATE_ON + + # Test turning off light + with patch( + "homeassistant.components.switcher_kis.entity.SwitcherApi.set_light" + ) as mock_set_light: + await hass.services.async_call( + LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + + # Push old state and makge sure it is ignored + mock_bridge.mock_callbacks([DEVICE]) + await hass.async_block_till_done() + + assert mock_api.call_count == 2 + mock_set_light.assert_called_once_with(DeviceState.OFF, 0) + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + + # Verify new state is not ignored + mock_bridge.mock_callbacks([DEVICE]) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_ON + + @pytest.mark.parametrize( ("device", "entity_id", "light_id", "device_state"), [ @@ -133,7 +171,6 @@ async def test_light_control_fail( mock_bridge, mock_api, monkeypatch: pytest.MonkeyPatch, - caplog: pytest.LogCaptureFixture, device, entity_id: str, light_id: int, diff --git a/tests/components/switcher_kis/test_switch.py b/tests/components/switcher_kis/test_switch.py index c20149de074..52391f4dd08 100644 --- a/tests/components/switcher_kis/test_switch.py +++ b/tests/components/switcher_kis/test_switch.py @@ -2,8 +2,9 @@ from unittest.mock import patch -from aioswitcher.api import Command, ShutterChildLock, SwitcherBaseResponse -from aioswitcher.device import DeviceState +from aioswitcher.api import Command +from aioswitcher.api.messages import SwitcherBaseResponse +from aioswitcher.device import DeviceState, ShutterChildLock import pytest from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN @@ -86,6 +87,45 @@ async def test_switch( assert state.state == STATE_OFF +@pytest.mark.parametrize("mock_bridge", [[DUMMY_WATER_HEATER_DEVICE]], indirect=True) +async def test_switch_ignore_previous_async_state( + hass: HomeAssistant, mock_bridge, mock_api +) -> None: + """Test switch ignores previous async state.""" + await init_integration(hass) + assert mock_bridge + + device = DUMMY_WATER_HEATER_DEVICE + entity_id = f"{SWITCH_DOMAIN}.{slugify(device.name)}" + + # Test initial state - on + state = hass.states.get(entity_id) + assert state.state == STATE_ON + + # Test turning off + with patch( + "homeassistant.components.switcher_kis.entity.SwitcherApi.control_device" + ) as mock_control_device: + await hass.services.async_call( + SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + + # Push old state and makge sure it is ignored + mock_bridge.mock_callbacks([DUMMY_WATER_HEATER_DEVICE]) + await hass.async_block_till_done() + + assert mock_api.call_count == 2 + mock_control_device.assert_called_once_with(Command.OFF) + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + + # Verify new state is not ignored + mock_bridge.mock_callbacks([DUMMY_WATER_HEATER_DEVICE]) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_ON + + @pytest.mark.parametrize("mock_bridge", [[DUMMY_PLUG_DEVICE]], indirect=True) async def test_switch_control_fail( hass: HomeAssistant, @@ -240,6 +280,44 @@ async def test_child_lock_switch( assert state.state == STATE_OFF +@pytest.mark.parametrize("mock_bridge", [[DEVICE]], indirect=True) +async def test_child_lock_switch_ignore_previous_async_state( + hass: HomeAssistant, mock_bridge, mock_api +) -> None: + """Test child lock switch ignores previous async state.""" + await init_integration(hass) + assert mock_bridge + + entity_id = f"{SWITCH_DOMAIN}.{slugify(DEVICE.name)}_child_lock" + + # Test initial state - on + state = hass.states.get(entity_id) + assert state.state == STATE_ON + + # Test turning off + with patch( + "homeassistant.components.switcher_kis.entity.SwitcherApi.set_shutter_child_lock" + ) as mock_control_device: + await hass.services.async_call( + SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + + # Push old state and makge sure it is ignored + mock_bridge.mock_callbacks([DEVICE]) + await hass.async_block_till_done() + + assert mock_api.call_count == 2 + mock_control_device.assert_called_once_with(ShutterChildLock.OFF, 0) + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + + # Verify new state is not ignored + mock_bridge.mock_callbacks([DEVICE]) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_ON + + @pytest.mark.parametrize( ( "device",