diff --git a/homeassistant/components/dynalite/cover.py b/homeassistant/components/dynalite/cover.py index e5c38996a89..bbca16d3db6 100644 --- a/homeassistant/components/dynalite/cover.py +++ b/homeassistant/components/dynalite/cover.py @@ -2,7 +2,12 @@ from typing import Any -from homeassistant.components.cover import DEVICE_CLASSES, CoverDeviceClass, CoverEntity +from homeassistant.components.cover import ( + ATTR_CURRENT_POSITION, + DEVICE_CLASSES, + CoverDeviceClass, + CoverEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -78,6 +83,12 @@ class DynaliteCover(DynaliteBase, CoverEntity): """Stop the cover.""" await self._device.async_stop_cover(**kwargs) + def initialize_state(self, state): + """Initialize the state from cache.""" + target_level = state.attributes.get(ATTR_CURRENT_POSITION) + if target_level is not None: + self._device.init_level(target_level) + class DynaliteCoverWithTilt(DynaliteCover): """Representation of a Dynalite Channel as a Home Assistant Cover that uses up and down for tilt.""" diff --git a/homeassistant/components/dynalite/dynalitebase.py b/homeassistant/components/dynalite/dynalitebase.py index b4b8285cbb0..3ebf04ab219 100644 --- a/homeassistant/components/dynalite/dynalitebase.py +++ b/homeassistant/components/dynalite/dynalitebase.py @@ -1,14 +1,16 @@ """Support for the Dynalite devices as entities.""" from __future__ import annotations +from abc import ABC, abstractmethod from collections.abc import Callable from typing import Any from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.restore_state import RestoreEntity from .bridge import DynaliteBridge from .const import DOMAIN, LOGGER @@ -36,7 +38,7 @@ def async_setup_entry_base( bridge.register_add_devices(platform, async_add_entities_platform) -class DynaliteBase(Entity): +class DynaliteBase(RestoreEntity, ABC): """Base class for the Dynalite entities.""" def __init__(self, device: Any, bridge: DynaliteBridge) -> None: @@ -70,8 +72,16 @@ class DynaliteBase(Entity): ) async def async_added_to_hass(self) -> None: - """Added to hass so need to register to dispatch.""" + """Added to hass so need to restore state and register to dispatch.""" # register for device specific update + await super().async_added_to_hass() + + cur_state = await self.async_get_last_state() + if cur_state: + self.initialize_state(cur_state) + else: + LOGGER.info("Restore state not available for %s", self.entity_id) + self._unsub_dispatchers.append( async_dispatcher_connect( self.hass, @@ -88,6 +98,10 @@ class DynaliteBase(Entity): ) ) + @abstractmethod + def initialize_state(self, state): + """Initialize the state from cache.""" + async def async_will_remove_from_hass(self) -> None: """Unregister signal dispatch listeners when being removed.""" for unsub in self._unsub_dispatchers: diff --git a/homeassistant/components/dynalite/light.py b/homeassistant/components/dynalite/light.py index 27cd6f8cae8..ffb97da49c1 100644 --- a/homeassistant/components/dynalite/light.py +++ b/homeassistant/components/dynalite/light.py @@ -2,7 +2,7 @@ from typing import Any -from homeassistant.components.light import ColorMode, LightEntity +from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -44,3 +44,9 @@ class DynaliteLight(DynaliteBase, LightEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" await self._device.async_turn_off(**kwargs) + + def initialize_state(self, state): + """Initialize the state from cache.""" + target_level = state.attributes.get(ATTR_BRIGHTNESS) + if target_level is not None: + self._device.init_level(target_level) diff --git a/homeassistant/components/dynalite/manifest.json b/homeassistant/components/dynalite/manifest.json index d403291a081..57010666019 100644 --- a/homeassistant/components/dynalite/manifest.json +++ b/homeassistant/components/dynalite/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dynalite", "codeowners": ["@ziv1234"], - "requirements": ["dynalite_devices==0.1.46"], + "requirements": ["dynalite_devices==0.1.47"], "iot_class": "local_push", "loggers": ["dynalite_devices_lib"] } diff --git a/homeassistant/components/dynalite/switch.py b/homeassistant/components/dynalite/switch.py index 3e459e45847..54e9b919b89 100644 --- a/homeassistant/components/dynalite/switch.py +++ b/homeassistant/components/dynalite/switch.py @@ -4,6 +4,7 @@ from typing import Any from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -36,3 +37,8 @@ class DynaliteSwitch(DynaliteBase, SwitchEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" await self._device.async_turn_off() + + def initialize_state(self, state): + """Initialize the state from cache.""" + target_level = 1 if state.state == STATE_ON else 0 + self._device.init_level(target_level) diff --git a/requirements_all.txt b/requirements_all.txt index 6290759fe21..5f1a1150442 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -609,7 +609,7 @@ dwdwfsapi==1.0.5 dweepy==0.3.0 # homeassistant.components.dynalite -dynalite_devices==0.1.46 +dynalite_devices==0.1.47 # homeassistant.components.rainforest_eagle eagle100==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d07aeff4b7b..d43ecede70c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -471,7 +471,7 @@ doorbirdpy==2.1.0 dsmr_parser==0.33 # homeassistant.components.dynalite -dynalite_devices==0.1.46 +dynalite_devices==0.1.47 # homeassistant.components.rainforest_eagle eagle100==0.1.1 diff --git a/tests/components/dynalite/test_bridge.py b/tests/components/dynalite/test_bridge.py index 363a9671f59..f5cfaec7a97 100644 --- a/tests/components/dynalite/test_bridge.py +++ b/tests/components/dynalite/test_bridge.py @@ -70,10 +70,12 @@ async def test_add_devices_then_register(hass): device1.category = "light" device1.name = "NAME" device1.unique_id = "unique1" + device1.brightness = 1 device2 = Mock() device2.category = "switch" device2.name = "NAME2" device2.unique_id = "unique2" + device2.brightness = 1 new_device_func([device1, device2]) device3 = Mock() device3.category = "switch" @@ -103,10 +105,12 @@ async def test_register_then_add_devices(hass): device1.category = "light" device1.name = "NAME" device1.unique_id = "unique1" + device1.brightness = 1 device2 = Mock() device2.category = "switch" device2.name = "NAME2" device2.unique_id = "unique2" + device2.brightness = 1 new_device_func([device1, device2]) await hass.async_block_till_done() assert hass.states.get("light.name") diff --git a/tests/components/dynalite/test_cover.py b/tests/components/dynalite/test_cover.py index fd671365ba1..5fbb22b91a7 100644 --- a/tests/components/dynalite/test_cover.py +++ b/tests/components/dynalite/test_cover.py @@ -1,8 +1,25 @@ """Test Dynalite cover.""" +from unittest.mock import Mock + from dynalite_devices_lib.cover import DynaliteTimeCoverWithTiltDevice import pytest -from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME +from homeassistant.components.cover import ( + ATTR_CURRENT_POSITION, + ATTR_CURRENT_TILT_POSITION, + ATTR_POSITION, + ATTR_TILT_POSITION, + CoverDeviceClass, +) +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_FRIENDLY_NAME, + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, +) +from homeassistant.core import State from homeassistant.exceptions import HomeAssistantError from .common import ( @@ -14,12 +31,25 @@ from .common import ( run_service_tests, ) +from tests.common import mock_restore_cache + @pytest.fixture def mock_device(): """Mock a Dynalite device.""" mock_dev = create_mock_device("cover", DynaliteTimeCoverWithTiltDevice) - mock_dev.device_class = "blind" + mock_dev.device_class = CoverDeviceClass.BLIND.value + mock_dev.current_cover_position = 0 + mock_dev.current_cover_tilt_position = 0 + mock_dev.is_opening = False + mock_dev.is_closing = False + mock_dev.is_closed = True + + def mock_init_level(target): + mock_dev.is_closed = target == 0 + + type(mock_dev).init_level = Mock(side_effect=mock_init_level) + return mock_dev @@ -29,11 +59,11 @@ async def test_cover_setup(hass, mock_device): entity_state = hass.states.get("cover.name") assert entity_state.attributes[ATTR_FRIENDLY_NAME] == mock_device.name assert ( - entity_state.attributes["current_position"] + entity_state.attributes[ATTR_CURRENT_POSITION] == mock_device.current_cover_position ) assert ( - entity_state.attributes["current_tilt_position"] + entity_state.attributes[ATTR_CURRENT_TILT_POSITION] == mock_device.current_cover_tilt_position ) assert entity_state.attributes[ATTR_DEVICE_CLASS] == mock_device.device_class @@ -48,7 +78,7 @@ async def test_cover_setup(hass, mock_device): { ATTR_SERVICE: "set_cover_position", ATTR_METHOD: "async_set_cover_position", - ATTR_ARGS: {"position": 50}, + ATTR_ARGS: {ATTR_POSITION: 50}, }, {ATTR_SERVICE: "open_cover_tilt", ATTR_METHOD: "async_open_cover_tilt"}, {ATTR_SERVICE: "close_cover_tilt", ATTR_METHOD: "async_close_cover_tilt"}, @@ -56,7 +86,7 @@ async def test_cover_setup(hass, mock_device): { ATTR_SERVICE: "set_cover_tilt_position", ATTR_METHOD: "async_set_cover_tilt_position", - ATTR_ARGS: {"tilt_position": 50}, + ATTR_ARGS: {ATTR_TILT_POSITION: 50}, }, ], ) @@ -91,14 +121,38 @@ async def test_cover_positions(hass, mock_device): """Test that the state updates in the various positions.""" update_func = await create_entity_from_device(hass, mock_device) await check_cover_position( - hass, update_func, mock_device, True, False, False, "closing" + hass, update_func, mock_device, True, False, False, STATE_CLOSING ) await check_cover_position( - hass, update_func, mock_device, False, True, False, "opening" + hass, update_func, mock_device, False, True, False, STATE_OPENING ) await check_cover_position( - hass, update_func, mock_device, False, False, True, "closed" + hass, update_func, mock_device, False, False, True, STATE_CLOSED ) await check_cover_position( - hass, update_func, mock_device, False, False, False, "open" + hass, update_func, mock_device, False, False, False, STATE_OPEN ) + + +async def test_cover_restore_state(hass, mock_device): + """Test restore from cache.""" + mock_restore_cache( + hass, + [State("cover.name", STATE_OPEN, attributes={ATTR_CURRENT_POSITION: 77})], + ) + await create_entity_from_device(hass, mock_device) + mock_device.init_level.assert_called_once_with(77) + entity_state = hass.states.get("cover.name") + assert entity_state.state == STATE_OPEN + + +async def test_cover_restore_state_bad_cache(hass, mock_device): + """Test restore from a cache without the attribute.""" + mock_restore_cache( + hass, + [State("cover.name", STATE_OPEN, attributes={"bla bla": 77})], + ) + await create_entity_from_device(hass, mock_device) + mock_device.init_level.assert_not_called() + entity_state = hass.states.get("cover.name") + assert entity_state.state == STATE_CLOSED diff --git a/tests/components/dynalite/test_light.py b/tests/components/dynalite/test_light.py index b100cf8d3f6..337f0a415e6 100644 --- a/tests/components/dynalite/test_light.py +++ b/tests/components/dynalite/test_light.py @@ -1,8 +1,11 @@ """Test Dynalite light.""" +from unittest.mock import Mock, PropertyMock + from dynalite_devices_lib.light import DynaliteChannelLightDevice import pytest from homeassistant.components.light import ( + ATTR_BRIGHTNESS, ATTR_COLOR_MODE, ATTR_SUPPORTED_COLOR_MODES, ColorMode, @@ -10,8 +13,11 @@ from homeassistant.components.light import ( from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_SUPPORTED_FEATURES, + STATE_OFF, + STATE_ON, STATE_UNAVAILABLE, ) +from homeassistant.core import State from .common import ( ATTR_METHOD, @@ -22,11 +28,25 @@ from .common import ( run_service_tests, ) +from tests.common import mock_restore_cache + @pytest.fixture def mock_device(): """Mock a Dynalite device.""" - return create_mock_device("light", DynaliteChannelLightDevice) + mock_dev = create_mock_device("light", DynaliteChannelLightDevice) + mock_dev.brightness = 0 + + def mock_is_on(): + return mock_dev.brightness != 0 + + type(mock_dev).is_on = PropertyMock(side_effect=mock_is_on) + + def mock_init_level(target): + mock_dev.brightness = target + + type(mock_dev).init_level = Mock(side_effect=mock_init_level) + return mock_dev async def test_light_setup(hass, mock_device): @@ -34,10 +54,9 @@ async def test_light_setup(hass, mock_device): await create_entity_from_device(hass, mock_device) entity_state = hass.states.get("light.name") assert entity_state.attributes[ATTR_FRIENDLY_NAME] == mock_device.name - assert entity_state.attributes["brightness"] == mock_device.brightness - assert entity_state.attributes[ATTR_COLOR_MODE] == ColorMode.BRIGHTNESS assert entity_state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.BRIGHTNESS] assert entity_state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + assert entity_state.state == STATE_OFF await run_service_tests( hass, mock_device, @@ -67,3 +86,29 @@ async def test_remove_config_entry(hass, mock_device): assert await hass.config_entries.async_remove(entry_id) await hass.async_block_till_done() assert not hass.states.get("light.name") + + +async def test_light_restore_state(hass, mock_device): + """Test restore from cache.""" + mock_restore_cache( + hass, + [State("light.name", STATE_ON, attributes={ATTR_BRIGHTNESS: 77})], + ) + await create_entity_from_device(hass, mock_device) + mock_device.init_level.assert_called_once_with(77) + entity_state = hass.states.get("light.name") + assert entity_state.state == STATE_ON + assert entity_state.attributes[ATTR_BRIGHTNESS] == 77 + assert entity_state.attributes[ATTR_COLOR_MODE] == ColorMode.BRIGHTNESS + + +async def test_light_restore_state_bad_cache(hass, mock_device): + """Test restore from a cache without the attribute.""" + mock_restore_cache( + hass, + [State("light.name", "abc", attributes={"blabla": 77})], + ) + await create_entity_from_device(hass, mock_device) + mock_device.init_level.assert_not_called() + entity_state = hass.states.get("light.name") + assert entity_state.state == STATE_OFF diff --git a/tests/components/dynalite/test_switch.py b/tests/components/dynalite/test_switch.py index de375e3b348..95ab64ef197 100644 --- a/tests/components/dynalite/test_switch.py +++ b/tests/components/dynalite/test_switch.py @@ -1,9 +1,12 @@ """Test Dynalite switch.""" +from unittest.mock import Mock + from dynalite_devices_lib.switch import DynalitePresetSwitchDevice import pytest -from homeassistant.const import ATTR_FRIENDLY_NAME +from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_OFF, STATE_ON +from homeassistant.core import State from .common import ( ATTR_METHOD, @@ -13,11 +16,20 @@ from .common import ( run_service_tests, ) +from tests.common import mock_restore_cache + @pytest.fixture def mock_device(): """Mock a Dynalite device.""" - return create_mock_device("switch", DynalitePresetSwitchDevice) + mock_dev = create_mock_device("switch", DynalitePresetSwitchDevice) + mock_dev.is_on = False + + def mock_init_level(level): + mock_dev.is_on = level + + type(mock_dev).init_level = Mock(side_effect=mock_init_level) + return mock_dev async def test_switch_setup(hass, mock_device): @@ -25,6 +37,7 @@ async def test_switch_setup(hass, mock_device): await create_entity_from_device(hass, mock_device) entity_state = hass.states.get("switch.name") assert entity_state.attributes[ATTR_FRIENDLY_NAME] == mock_device.name + assert entity_state.state == STATE_OFF await run_service_tests( hass, mock_device, @@ -34,3 +47,21 @@ async def test_switch_setup(hass, mock_device): {ATTR_SERVICE: "turn_off", ATTR_METHOD: "async_turn_off"}, ], ) + + +@pytest.mark.parametrize("saved_state, level", [(STATE_ON, 1), (STATE_OFF, 0)]) +async def test_switch_restore_state(hass, mock_device, saved_state, level): + """Test restore from cache.""" + mock_restore_cache( + hass, + [ + State( + "switch.name", + saved_state, + ) + ], + ) + await create_entity_from_device(hass, mock_device) + mock_device.init_level.assert_called_once_with(level) + entity_state = hass.states.get("switch.name") + assert entity_state.state == saved_state