Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement external state restoration #58

Merged
merged 37 commits into from
Jul 5, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
a87531d
Consolidate init-time state reading for Light entity
puddly Jul 1, 2024
22b85c7
Read the color mode from cache as well
puddly Jul 1, 2024
58f9b63
Remove duplicate `supported_color_modes` variable
puddly Jul 1, 2024
da3c155
Switch `zcl_color_mode_to_entity_color_mode` to a static dictionary
puddly Jul 1, 2024
eafe473
Only set the color mode from the supported color modes if it is uncached
puddly Jul 1, 2024
c54bdda
Update the attribute cache color mode after the color has been succes…
puddly Jul 1, 2024
a8bc97a
Do not persist the color mode for groups
puddly Jul 1, 2024
8585328
Test that the color mode changes
puddly Jul 1, 2024
c21fd00
Account for invalid ZCL color modes
puddly Jul 1, 2024
6d8395d
Add a quick test for HS
puddly Jul 1, 2024
da2cf75
Unit test enhanced hue as well
puddly Jul 1, 2024
bd22c2b
Re-introduce erroneously removed `cached_property`
puddly Jul 2, 2024
ba9ed65
Add `restore_extra_state_attributes`
puddly Jul 2, 2024
4e164c0
Persist the door lock state after locking/unlocking
puddly Jul 2, 2024
73c4be7
Remove unused lock `kwargs`
puddly Jul 2, 2024
b951f8b
Add `restore_external_state_attributes`
puddly Jul 3, 2024
c075f46
Implement external state for `cover`
puddly Jul 3, 2024
9eaf159
Implement external state for `select`
puddly Jul 3, 2024
7ef8ba3
Implement external state for `siren`
puddly Jul 3, 2024
890a826
Remove unnecessary `_persist_lock_state`
puddly Jul 3, 2024
8aebd9a
Revert "Implement external state for `siren`"
puddly Jul 3, 2024
1802d95
Implement a stub `restore_external_state_attributes` for non-ZCL selects
puddly Jul 3, 2024
c6c15b2
Migrate coverage to `pyproject.toml` and exclude NotImplementedError
puddly Jul 3, 2024
8aebec5
Update zha/application/platforms/light/__init__.py
puddly Jul 3, 2024
3eb8222
Migrate lighting to use explicit state restoration instead of ZCL cache
puddly Jul 3, 2024
cd36b2f
Reduce diff size
puddly Jul 3, 2024
4680d66
Only restore the state if the attribute isn't `None`
puddly Jul 3, 2024
d0703bd
Migrate lock to use state restoration
puddly Jul 3, 2024
50f6d16
Add some unit tests
puddly Jul 3, 2024
d8c25ef
Offload validation to Core
puddly Jul 3, 2024
3b069b6
Implement an `undefined` type
puddly Jul 3, 2024
d6165c5
Migrate remaining platforms to use `UNDEFINED` as well, where appropr…
puddly Jul 4, 2024
4df1b9a
Finish unit tests
puddly Jul 4, 2024
462a1ff
Remove `UNDEFINED`
puddly Jul 5, 2024
221fe2d
Only restore (most) light state attributes if they are not `None`
puddly Jul 5, 2024
26874c1
Fix `number` entity name
puddly Jul 5, 2024
b8d5c57
Revert `cached_property` -> `property` change
puddly Jul 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 59 additions & 2 deletions tests/test_light.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,8 +334,11 @@ async def test_light(
"color_temperature": 100,
"color_temp_physical_min": 0,
"color_temp_physical_max": 600,
"color_capabilities": lighting.ColorCapabilities.XY_attributes
| lighting.ColorCapabilities.Color_temperature,
"color_capabilities": (
lighting.ColorCapabilities.XY_attributes
| lighting.ColorCapabilities.Color_temperature
| lighting.ColorCapabilities.Hue_and_saturation
),
}
update_attribute_cache(cluster_color)
zha_device = await device_joined(zigpy_device)
Expand Down Expand Up @@ -398,6 +401,7 @@ async def test_light(
assert entity.state["color_temp"] != 200
await entity.async_turn_on(brightness=50, transition=10, color_temp=200)
await zha_gateway.async_block_till_done()
assert entity.state["color_mode"] == ColorMode.COLOR_TEMP
assert entity.state["brightness"] == 50
assert entity.state["color_temp"] == 200
assert bool(entity.state["on"]) is True
Expand All @@ -419,6 +423,7 @@ async def test_light(
assert entity.state["xy_color"] != [13369, 18087]
await entity.async_turn_on(brightness=50, xy_color=[13369, 18087])
await zha_gateway.async_block_till_done()
assert entity.state["color_mode"] == ColorMode.XY
assert entity.state["brightness"] == 50
assert entity.state["xy_color"] == [13369, 18087]
assert cluster_color.request.call_count == 1
Expand All @@ -437,6 +442,58 @@ async def test_light(

cluster_color.request.reset_mock()

# test color hs from the client
assert entity.state["hs_color"] != [12, 34]
await entity.async_turn_on(brightness=50, hs_color=[12, 34])
await zha_gateway.async_block_till_done()
assert entity.state["color_mode"] == ColorMode.HS
assert entity.state["brightness"] == 50
assert entity.state["hs_color"] == [12, 34]
assert cluster_color.request.call_count == 1
assert cluster_color.request.await_count == 1
assert cluster_color.request.call_args == call(
False,
6,
cluster_color.commands_by_name["move_to_hue_and_saturation"].schema,
hue=8,
saturation=86,
transition_time=0,
expect_reply=True,
manufacturer=None,
tsn=None,
)

cluster_color.request.reset_mock()

# test enhanced hue support
cluster_color.PLUGGED_ATTR_READS["color_capabilities"] |= (
lighting.ColorCapabilities.Enhanced_hue
)
update_attribute_cache(cluster_color)
del entity._color_cluster_handler.color_capabilities

assert entity.state["hs_color"] != [56, 78]
await entity.async_turn_on(brightness=50, hs_color=[56, 78])
await zha_gateway.async_block_till_done()
assert entity.state["color_mode"] == ColorMode.HS
assert entity.state["brightness"] == 50
assert entity.state["hs_color"] == [56, 78]
assert cluster_color.request.call_count == 1
assert cluster_color.request.await_count == 1
assert cluster_color.request.call_args == call(
False,
67,
cluster_color.commands_by_name[
"enhanced_move_to_hue_and_saturation"
].schema,
enhanced_hue=10194,
saturation=198,
transition_time=0,
expect_reply=True,
manufacturer=None,
tsn=None,
)


async def async_test_on_off_from_light(
zha_gateway: Gateway,
Expand Down
7 changes: 7 additions & 0 deletions zha/application/platforms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,13 @@
return self._attr_extra_state_attribute_names
return None

def restore_external_state_attributes(self, **kwargs: Any) -> None:
"""Restore entity specific state attributes from an external source.

Entities implementing this must accept a keyword argument for each attribute.
"""
raise NotImplementedError

Check warning on line 232 in zha/application/platforms/__init__.py

View check run for this annotation

Codecov / codecov/patch

zha/application/platforms/__init__.py#L232

Added line #L232 was not covered by tests

async def on_remove(self) -> None:
"""Cancel tasks and timers this entity owns."""
for handle in self._tracked_handles:
Expand Down
24 changes: 22 additions & 2 deletions zha/application/platforms/cover/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import asyncio
import functools
import logging
from typing import TYPE_CHECKING, Any, cast
from typing import TYPE_CHECKING, Any, Literal, cast

from zigpy.zcl.clusters.general import OnOff
from zigpy.zcl.foundation import Status
Expand Down Expand Up @@ -57,6 +57,10 @@
PLATFORM = Platform.COVER

_attr_translation_key: str = "cover"
_attr_extra_state_attribute_names: set[str] = {
"target_lift_position",
"target_tilt_position",
}

def __init__(
self,
Expand Down Expand Up @@ -84,7 +88,7 @@
)
self._target_lift_position: int | None = None
self._target_tilt_position: int | None = None
self._state: str
self._state: str = STATE_OPEN
self._determine_initial_state()
self._cover_cluster_handler.on_event(
CLUSTER_HANDLER_ATTRIBUTE_UPDATED,
Expand All @@ -102,10 +106,26 @@
"is_opening": self.is_opening,
"is_closing": self.is_closing,
"is_closed": self.is_closed,
"target_lift_position": self._target_lift_position,
"target_tilt_position": self._target_tilt_position,
}
)
return response

def restore_external_state_attributes(
self,
*,
state: Literal[
"open", "opening", "closed", "closing"
], # FIXME: why must these be expanded?
target_lift_position: int | None,
target_tilt_position: int | None,
):
"""Restore external state attributes."""
self._state = state
self._target_lift_position = target_lift_position
self._target_tilt_position = target_tilt_position

Check warning on line 127 in zha/application/platforms/cover/__init__.py

View check run for this annotation

Codecov / codecov/patch

zha/application/platforms/cover/__init__.py#L125-L127

Added lines #L125 - L127 were not covered by tests

@property
def is_closed(self) -> bool | None:
"""Return True if the cover is closed.
Expand Down
8 changes: 4 additions & 4 deletions zha/application/platforms/cover/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@
ATTR_POSITION: Final[str] = "position"
ATTR_TILT_POSITION: Final[str] = "tilt_position"

STATE_OPEN: Final[str] = "open"
STATE_OPENING: Final[str] = "opening"
STATE_CLOSED: Final[str] = "closed"
STATE_CLOSING: Final[str] = "closing"
STATE_OPEN: Final = "open"
STATE_OPENING: Final = "opening"
STATE_CLOSED: Final = "closed"
STATE_CLOSING: Final = "closing"


class CoverDeviceClass(StrEnum):
Expand Down
79 changes: 63 additions & 16 deletions zha/application/platforms/light/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

from zigpy.types import EUI64
from zigpy.zcl.clusters.general import Identify, LevelControl, OnOff
from zigpy.zcl.clusters.lighting import Color
from zigpy.zcl.clusters.lighting import Color, ColorMode as ZclColorMode
from zigpy.zcl.foundation import Status

from zha.application import Platform
Expand Down Expand Up @@ -61,6 +61,8 @@
LightEntityFeature,
)
from zha.application.platforms.light.helpers import (
ENTITY_TO_ZCL_COLOR_MODE,
ZCL_TO_ENTITY_COLOR_MODE,
brightness_supported,
filter_supported_color_modes,
)
Expand Down Expand Up @@ -233,6 +235,13 @@
"""Return the warmest color_temp that this light supports."""
return self._max_mireds

def _persist_color_mode(self, color_mode: ZclColorMode) -> None:
"""Persist the color mode."""
self._color_cluster_handler.cluster.update_attribute(
attrid=Color.AttributeDefs.color_mode.id,
value=color_mode,
)

def handle_cluster_handler_set_level(self, event: LevelChangeEvent) -> None:
"""Set the brightness of this light between 0..254.

Expand Down Expand Up @@ -581,6 +590,8 @@
if result[1] is not Status.SUCCESS:
return False
self._color_mode = ColorMode.COLOR_TEMP
self._persist_color_mode(ZclColorMode.Color_temperature)

self._color_temp = temperature
self._xy_color = None
self._hs_color = None
Expand All @@ -606,6 +617,8 @@
if result[1] is not Status.SUCCESS:
return False
self._color_mode = ColorMode.HS
self._persist_color_mode(ZclColorMode.Hue_and_saturation)

self._hs_color = hs_color
self._xy_color = None
self._color_temp = None
Expand All @@ -621,6 +634,8 @@
if result[1] is not Status.SUCCESS:
return False
self._color_mode = ColorMode.XY
self._persist_color_mode(ZclColorMode.X_and_Y)

self._xy_color = xy_color
self._color_temp = None
self._hs_color = None
Expand Down Expand Up @@ -713,17 +728,15 @@
self._on_off_cluster_handler: ClusterHandler = self.cluster_handlers[
CLUSTER_HANDLER_ON_OFF
]
self._state: bool = bool(self._on_off_cluster_handler.on_off)
self._level_cluster_handler: ClusterHandler = self.cluster_handlers.get(
CLUSTER_HANDLER_LEVEL
)
self._color_cluster_handler: ClusterHandler = self.cluster_handlers.get(
CLUSTER_HANDLER_COLOR
)
self._identify_cluster_handler: ClusterHandler = device.identify_ch
if self._color_cluster_handler:
self._min_mireds: int = self._color_cluster_handler.min_mireds
self._max_mireds: int = self._color_cluster_handler.max_mireds
self._state: bool = bool(self._on_off_cluster_handler.on_off)

self._cancel_refresh_handle: Callable | None = None
effect_list = []

Expand All @@ -739,6 +752,9 @@
self._brightness = self._level_cluster_handler.current_level

if self._color_cluster_handler:
self._min_mireds: int = self._color_cluster_handler.min_mireds
self._max_mireds: int = self._color_cluster_handler.max_mireds

if self._color_cluster_handler.color_temp_supported:
self._supported_color_modes.add(ColorMode.COLOR_TEMP)
self._color_temp = self._color_cluster_handler.color_temperature
Expand Down Expand Up @@ -787,20 +803,32 @@
effect_list.append(EFFECT_COLORLOOP)
if self._color_cluster_handler.color_loop_active == 1:
self._effect = EFFECT_COLORLOOP
self._external_supported_color_modes = supported_color_modes = (
filter_supported_color_modes(self._supported_color_modes)
)
if len(supported_color_modes) == 1:
self._color_mode = next(iter(supported_color_modes))
else: # Light supports color_temp + hs, determine which mode the light is in
assert self._color_cluster_handler

if (
self._color_cluster_handler.color_mode
== Color.ColorMode.Color_temperature
self._color_cluster_handler.color_mode is not None
and self._color_cluster_handler.color_mode in ZCL_TO_ENTITY_COLOR_MODE
):
self._color_mode = ColorMode.COLOR_TEMP
self._color_mode = ZCL_TO_ENTITY_COLOR_MODE[

Check warning on line 811 in zha/application/platforms/light/__init__.py

View check run for this annotation

Codecov / codecov/patch

zha/application/platforms/light/__init__.py#L811

Added line #L811 was not covered by tests
self._color_cluster_handler.color_mode
]

self._external_supported_color_modes = filter_supported_color_modes(
self._supported_color_modes
)

if self._color_mode == ColorMode.UNKNOWN:
if len(self._external_supported_color_modes) == 1:
self._color_mode = next(iter(self._external_supported_color_modes))
else:
self._color_mode = ColorMode.XY
# Light supports color_temp + hs, determine which mode the light is in
puddly marked this conversation as resolved.
Show resolved Hide resolved
assert self._color_cluster_handler
if (
self._color_cluster_handler.color_mode
== Color.ColorMode.Color_temperature
):
self._color_mode = ColorMode.COLOR_TEMP

Check warning on line 829 in zha/application/platforms/light/__init__.py

View check run for this annotation

Codecov / codecov/patch

zha/application/platforms/light/__init__.py#L829

Added line #L829 was not covered by tests
else:
self._color_mode = ColorMode.XY

if self._identify_cluster_handler:
self._supported_features |= LightEntityFeature.FLASH
Expand Down Expand Up @@ -1050,6 +1078,7 @@
self._brightness = brightness
if color_mode is not None and color_mode in supported_modes:
self._color_mode = color_mode
self._persist_color_mode(ENTITY_TO_ZCL_COLOR_MODE[color_mode])

Check warning on line 1081 in zha/application/platforms/light/__init__.py

View check run for this annotation

Codecov / codecov/patch

zha/application/platforms/light/__init__.py#L1081

Added line #L1081 was not covered by tests
if color_temp is not None and ColorMode.COLOR_TEMP in supported_modes:
self._color_temp = color_temp
if xy_color is not None and ColorMode.XY in supported_modes:
Expand All @@ -1064,6 +1093,18 @@

self.maybe_emit_state_changed_event()

def restore_external_state_attributes(
self,
*,
state: bool | None,
off_with_transition: bool | None,
off_brightness: int | None,
) -> None:
"""Restore extra state attributes that are stored outside of the ZCL cache."""
self._state = state
TheJulianJES marked this conversation as resolved.
Show resolved Hide resolved
puddly marked this conversation as resolved.
Show resolved Hide resolved
puddly marked this conversation as resolved.
Show resolved Hide resolved
self._off_with_transition = off_with_transition
self._off_brightness = off_brightness

Check warning on line 1106 in zha/application/platforms/light/__init__.py

View check run for this annotation

Codecov / codecov/patch

zha/application/platforms/light/__init__.py#L1104-L1106

Added lines #L1104 - L1106 were not covered by tests


@STRICT_MATCH(
cluster_handler_names=CLUSTER_HANDLER_ON_OFF,
Expand Down Expand Up @@ -1190,6 +1231,12 @@
"""Return entity availability."""
return self._available

def _persist_color_mode(self, color_mode: ZclColorMode) -> None:
"""Persist the color mode."""

# FIXME: Groups use raw clusters, not cluster handlers
puddly marked this conversation as resolved.
Show resolved Hide resolved
pass

async def on_remove(self) -> None:
"""Cancel tasks this entity owns."""
await super().on_remove()
Expand Down
11 changes: 11 additions & 0 deletions zha/application/platforms/light/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,20 @@

from collections.abc import Iterable

from zigpy.zcl.clusters.lighting import ColorMode as ZclColorMode

from zha.application.platforms.light.const import COLOR_MODES_BRIGHTNESS, ColorMode
from zha.exceptions import ZHAException

ZCL_TO_ENTITY_COLOR_MODE = {
None: ColorMode.UNKNOWN,
ZclColorMode.Hue_and_saturation: ColorMode.HS,
ZclColorMode.X_and_Y: ColorMode.XY,
ZclColorMode.Color_temperature: ColorMode.COLOR_TEMP,
}

ENTITY_TO_ZCL_COLOR_MODE = {v: k for k, v in ZCL_TO_ENTITY_COLOR_MODE.items()}


def filter_supported_color_modes(color_modes: Iterable[ColorMode]) -> set[ColorMode]:
"""Filter the given color modes."""
Expand Down
Loading
Loading