Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
60 changes: 58 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,57 @@ 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)

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
67 changes: 51 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 @@ def max_mireds(self) -> int | None:
"""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 @@ async def async_handle_color_commands(
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 @@ async def async_handle_color_commands(
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 @@ async def async_handle_color_commands(
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 @@ def __init__(
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 @@ def __init__(
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 @@ def __init__(
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[
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
Comment thread
puddly marked this conversation as resolved.
Outdated
assert self._color_cluster_handler
if (
self._color_cluster_handler.color_mode
== Color.ColorMode.Color_temperature
):
self._color_mode = ColorMode.COLOR_TEMP
else:
self._color_mode = ColorMode.XY

if self._identify_cluster_handler:
self._supported_features |= LightEntityFeature.FLASH
Expand Down Expand Up @@ -1050,6 +1078,7 @@ def _assume_group_state(self, update_params) -> None:
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])
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 Down Expand Up @@ -1190,6 +1219,12 @@ def available(self) -> bool:
"""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
Comment thread
puddly marked this conversation as resolved.
Outdated
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
4 changes: 1 addition & 3 deletions zha/zigbee/cluster_handlers/lighting.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

from __future__ import annotations

from functools import cached_property

from zigpy.zcl.clusters.lighting import Ballast, Color

from zha.zigbee.cluster_handlers import (
Expand Down Expand Up @@ -65,7 +63,7 @@ class ColorClusterHandler(ClusterHandler):
Color.AttributeDefs.options.name: True,
}

@cached_property
Comment thread
puddly marked this conversation as resolved.
@property
def color_capabilities(self) -> Color.ColorCapabilities:
"""Return ZCL color capabilities of the light."""
color_capabilities = self.cluster.get(
Expand Down