Move Switcher handle_coordinator_update to base entity (#143738)

This commit is contained in:
Shay Levy 2025-04-27 00:01:44 +03:00 committed by GitHub
parent 40752dcfb6
commit 868b8ad318
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 180 additions and 84 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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}"

View File

@ -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 = (

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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,

View File

@ -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",