Add transition support to Matter light platform (#109803)
* Add support for transitions to Matter light platform * fix the feature check * add testspull/109843/head
parent
252baa93aa
commit
fd5efd1f79
|
@ -10,10 +10,12 @@ from homeassistant.components.light import (
|
||||||
ATTR_BRIGHTNESS,
|
ATTR_BRIGHTNESS,
|
||||||
ATTR_COLOR_TEMP,
|
ATTR_COLOR_TEMP,
|
||||||
ATTR_HS_COLOR,
|
ATTR_HS_COLOR,
|
||||||
|
ATTR_TRANSITION,
|
||||||
ATTR_XY_COLOR,
|
ATTR_XY_COLOR,
|
||||||
ColorMode,
|
ColorMode,
|
||||||
LightEntity,
|
LightEntity,
|
||||||
LightEntityDescription,
|
LightEntityDescription,
|
||||||
|
LightEntityFeature,
|
||||||
filter_supported_color_modes,
|
filter_supported_color_modes,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
@ -38,6 +40,7 @@ COLOR_MODE_MAP = {
|
||||||
clusters.ColorControl.Enums.ColorMode.kCurrentXAndCurrentY: ColorMode.XY,
|
clusters.ColorControl.Enums.ColorMode.kCurrentXAndCurrentY: ColorMode.XY,
|
||||||
clusters.ColorControl.Enums.ColorMode.kColorTemperature: ColorMode.COLOR_TEMP,
|
clusters.ColorControl.Enums.ColorMode.kColorTemperature: ColorMode.COLOR_TEMP,
|
||||||
}
|
}
|
||||||
|
DEFAULT_TRANSITION = 0.2
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
|
@ -58,7 +61,9 @@ class MatterLight(MatterEntity, LightEntity):
|
||||||
_supports_color = False
|
_supports_color = False
|
||||||
_supports_color_temperature = False
|
_supports_color_temperature = False
|
||||||
|
|
||||||
async def _set_xy_color(self, xy_color: tuple[float, float]) -> None:
|
async def _set_xy_color(
|
||||||
|
self, xy_color: tuple[float, float], transition: float = 0.0
|
||||||
|
) -> None:
|
||||||
"""Set xy color."""
|
"""Set xy color."""
|
||||||
|
|
||||||
matter_xy = convert_to_matter_xy(xy_color)
|
matter_xy = convert_to_matter_xy(xy_color)
|
||||||
|
@ -67,8 +72,8 @@ class MatterLight(MatterEntity, LightEntity):
|
||||||
clusters.ColorControl.Commands.MoveToColor(
|
clusters.ColorControl.Commands.MoveToColor(
|
||||||
colorX=int(matter_xy[0]),
|
colorX=int(matter_xy[0]),
|
||||||
colorY=int(matter_xy[1]),
|
colorY=int(matter_xy[1]),
|
||||||
# It's required in TLV. We don't implement transition time yet.
|
# transition in matter is measured in tenths of a second
|
||||||
transitionTime=0,
|
transitionTime=int(transition * 10),
|
||||||
# allow setting the color while the light is off,
|
# allow setting the color while the light is off,
|
||||||
# by setting the optionsMask to 1 (=ExecuteIfOff)
|
# by setting the optionsMask to 1 (=ExecuteIfOff)
|
||||||
optionsMask=1,
|
optionsMask=1,
|
||||||
|
@ -76,7 +81,9 @@ class MatterLight(MatterEntity, LightEntity):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _set_hs_color(self, hs_color: tuple[float, float]) -> None:
|
async def _set_hs_color(
|
||||||
|
self, hs_color: tuple[float, float], transition: float = 0.0
|
||||||
|
) -> None:
|
||||||
"""Set hs color."""
|
"""Set hs color."""
|
||||||
|
|
||||||
matter_hs = convert_to_matter_hs(hs_color)
|
matter_hs = convert_to_matter_hs(hs_color)
|
||||||
|
@ -85,8 +92,8 @@ class MatterLight(MatterEntity, LightEntity):
|
||||||
clusters.ColorControl.Commands.MoveToHueAndSaturation(
|
clusters.ColorControl.Commands.MoveToHueAndSaturation(
|
||||||
hue=int(matter_hs[0]),
|
hue=int(matter_hs[0]),
|
||||||
saturation=int(matter_hs[1]),
|
saturation=int(matter_hs[1]),
|
||||||
# It's required in TLV. We don't implement transition time yet.
|
# transition in matter is measured in tenths of a second
|
||||||
transitionTime=0,
|
transitionTime=int(transition * 10),
|
||||||
# allow setting the color while the light is off,
|
# allow setting the color while the light is off,
|
||||||
# by setting the optionsMask to 1 (=ExecuteIfOff)
|
# by setting the optionsMask to 1 (=ExecuteIfOff)
|
||||||
optionsMask=1,
|
optionsMask=1,
|
||||||
|
@ -94,14 +101,14 @@ class MatterLight(MatterEntity, LightEntity):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _set_color_temp(self, color_temp: int) -> None:
|
async def _set_color_temp(self, color_temp: int, transition: float = 0.0) -> None:
|
||||||
"""Set color temperature."""
|
"""Set color temperature."""
|
||||||
|
|
||||||
await self.send_device_command(
|
await self.send_device_command(
|
||||||
clusters.ColorControl.Commands.MoveToColorTemperature(
|
clusters.ColorControl.Commands.MoveToColorTemperature(
|
||||||
colorTemperatureMireds=color_temp,
|
colorTemperatureMireds=color_temp,
|
||||||
# It's required in TLV. We don't implement transition time yet.
|
# transition in matter is measured in tenths of a second
|
||||||
transitionTime=0,
|
transitionTime=int(transition * 10),
|
||||||
# allow setting the color while the light is off,
|
# allow setting the color while the light is off,
|
||||||
# by setting the optionsMask to 1 (=ExecuteIfOff)
|
# by setting the optionsMask to 1 (=ExecuteIfOff)
|
||||||
optionsMask=1,
|
optionsMask=1,
|
||||||
|
@ -109,7 +116,7 @@ class MatterLight(MatterEntity, LightEntity):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _set_brightness(self, brightness: int) -> None:
|
async def _set_brightness(self, brightness: int, transition: float = 0.0) -> None:
|
||||||
"""Set brightness."""
|
"""Set brightness."""
|
||||||
|
|
||||||
level_control = self._endpoint.get_cluster(clusters.LevelControl)
|
level_control = self._endpoint.get_cluster(clusters.LevelControl)
|
||||||
|
@ -127,8 +134,8 @@ class MatterLight(MatterEntity, LightEntity):
|
||||||
await self.send_device_command(
|
await self.send_device_command(
|
||||||
clusters.LevelControl.Commands.MoveToLevelWithOnOff(
|
clusters.LevelControl.Commands.MoveToLevelWithOnOff(
|
||||||
level=level,
|
level=level,
|
||||||
# It's required in TLV. We don't implement transition time yet.
|
# transition in matter is measured in tenths of a second
|
||||||
transitionTime=0,
|
transitionTime=int(transition * 10),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -251,20 +258,21 @@ class MatterLight(MatterEntity, LightEntity):
|
||||||
xy_color = kwargs.get(ATTR_XY_COLOR)
|
xy_color = kwargs.get(ATTR_XY_COLOR)
|
||||||
color_temp = kwargs.get(ATTR_COLOR_TEMP)
|
color_temp = kwargs.get(ATTR_COLOR_TEMP)
|
||||||
brightness = kwargs.get(ATTR_BRIGHTNESS)
|
brightness = kwargs.get(ATTR_BRIGHTNESS)
|
||||||
|
transition = kwargs.get(ATTR_TRANSITION, DEFAULT_TRANSITION)
|
||||||
|
|
||||||
if self.supported_color_modes is not None:
|
if self.supported_color_modes is not None:
|
||||||
if hs_color is not None and ColorMode.HS in self.supported_color_modes:
|
if hs_color is not None and ColorMode.HS in self.supported_color_modes:
|
||||||
await self._set_hs_color(hs_color)
|
await self._set_hs_color(hs_color, transition)
|
||||||
elif xy_color is not None and ColorMode.XY in self.supported_color_modes:
|
elif xy_color is not None and ColorMode.XY in self.supported_color_modes:
|
||||||
await self._set_xy_color(xy_color)
|
await self._set_xy_color(xy_color, transition)
|
||||||
elif (
|
elif (
|
||||||
color_temp is not None
|
color_temp is not None
|
||||||
and ColorMode.COLOR_TEMP in self.supported_color_modes
|
and ColorMode.COLOR_TEMP in self.supported_color_modes
|
||||||
):
|
):
|
||||||
await self._set_color_temp(color_temp)
|
await self._set_color_temp(color_temp, transition)
|
||||||
|
|
||||||
if brightness is not None and self._supports_brightness:
|
if brightness is not None and self._supports_brightness:
|
||||||
await self._set_brightness(brightness)
|
await self._set_brightness(brightness, transition)
|
||||||
return
|
return
|
||||||
|
|
||||||
await self.send_device_command(
|
await self.send_device_command(
|
||||||
|
@ -324,6 +332,9 @@ class MatterLight(MatterEntity, LightEntity):
|
||||||
|
|
||||||
supported_color_modes = filter_supported_color_modes(supported_color_modes)
|
supported_color_modes = filter_supported_color_modes(supported_color_modes)
|
||||||
self._attr_supported_color_modes = supported_color_modes
|
self._attr_supported_color_modes = supported_color_modes
|
||||||
|
# flag support for transition as soon as we support setting brightness and/or color
|
||||||
|
if supported_color_modes != {ColorMode.ONOFF}:
|
||||||
|
self._attr_supported_features |= LightEntityFeature.TRANSITION
|
||||||
|
|
||||||
LOGGER.debug(
|
LOGGER.debug(
|
||||||
"Supported color modes: %s for %s",
|
"Supported color modes: %s for %s",
|
||||||
|
|
|
@ -142,7 +142,26 @@ async def test_dimmable_light(
|
||||||
endpoint_id=1,
|
endpoint_id=1,
|
||||||
command=clusters.LevelControl.Commands.MoveToLevelWithOnOff(
|
command=clusters.LevelControl.Commands.MoveToLevelWithOnOff(
|
||||||
level=128,
|
level=128,
|
||||||
transitionTime=0,
|
transitionTime=2,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
matter_client.send_device_command.reset_mock()
|
||||||
|
|
||||||
|
# Change brightness with custom transition
|
||||||
|
await hass.services.async_call(
|
||||||
|
"light",
|
||||||
|
"turn_on",
|
||||||
|
{"entity_id": entity_id, "brightness": 128, "transition": 3},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert matter_client.send_device_command.call_count == 1
|
||||||
|
assert matter_client.send_device_command.call_args == call(
|
||||||
|
node_id=light_node.node_id,
|
||||||
|
endpoint_id=1,
|
||||||
|
command=clusters.LevelControl.Commands.MoveToLevelWithOnOff(
|
||||||
|
level=128,
|
||||||
|
transitionTime=30,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
matter_client.send_device_command.reset_mock()
|
matter_client.send_device_command.reset_mock()
|
||||||
|
@ -201,7 +220,37 @@ async def test_color_temperature_light(
|
||||||
endpoint_id=1,
|
endpoint_id=1,
|
||||||
command=clusters.ColorControl.Commands.MoveToColorTemperature(
|
command=clusters.ColorControl.Commands.MoveToColorTemperature(
|
||||||
colorTemperatureMireds=300,
|
colorTemperatureMireds=300,
|
||||||
transitionTime=0,
|
transitionTime=2,
|
||||||
|
optionsMask=1,
|
||||||
|
optionsOverride=1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
call(
|
||||||
|
node_id=light_node.node_id,
|
||||||
|
endpoint_id=1,
|
||||||
|
command=clusters.OnOff.Commands.On(),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
matter_client.send_device_command.reset_mock()
|
||||||
|
|
||||||
|
# Change color temperature with custom transition
|
||||||
|
await hass.services.async_call(
|
||||||
|
"light",
|
||||||
|
"turn_on",
|
||||||
|
{"entity_id": entity_id, "color_temp": 300, "transition": 4.0},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert matter_client.send_device_command.call_count == 2
|
||||||
|
matter_client.send_device_command.assert_has_calls(
|
||||||
|
[
|
||||||
|
call(
|
||||||
|
node_id=light_node.node_id,
|
||||||
|
endpoint_id=1,
|
||||||
|
command=clusters.ColorControl.Commands.MoveToColorTemperature(
|
||||||
|
colorTemperatureMireds=300,
|
||||||
|
transitionTime=40,
|
||||||
optionsMask=1,
|
optionsMask=1,
|
||||||
optionsOverride=1,
|
optionsOverride=1,
|
||||||
),
|
),
|
||||||
|
@ -282,7 +331,38 @@ async def test_extended_color_light(
|
||||||
command=clusters.ColorControl.Commands.MoveToColor(
|
command=clusters.ColorControl.Commands.MoveToColor(
|
||||||
colorX=0.5 * 65536,
|
colorX=0.5 * 65536,
|
||||||
colorY=0.5 * 65536,
|
colorY=0.5 * 65536,
|
||||||
transitionTime=0,
|
transitionTime=2,
|
||||||
|
optionsMask=1,
|
||||||
|
optionsOverride=1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
call(
|
||||||
|
node_id=light_node.node_id,
|
||||||
|
endpoint_id=1,
|
||||||
|
command=clusters.OnOff.Commands.On(),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
matter_client.send_device_command.reset_mock()
|
||||||
|
|
||||||
|
# Turn the light on with XY color and custom transition
|
||||||
|
await hass.services.async_call(
|
||||||
|
"light",
|
||||||
|
"turn_on",
|
||||||
|
{"entity_id": entity_id, "xy_color": (0.5, 0.5), "transition": 4.0},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert matter_client.send_device_command.call_count == 2
|
||||||
|
matter_client.send_device_command.assert_has_calls(
|
||||||
|
[
|
||||||
|
call(
|
||||||
|
node_id=light_node.node_id,
|
||||||
|
endpoint_id=1,
|
||||||
|
command=clusters.ColorControl.Commands.MoveToColor(
|
||||||
|
colorX=0.5 * 65536,
|
||||||
|
colorY=0.5 * 65536,
|
||||||
|
transitionTime=40,
|
||||||
optionsMask=1,
|
optionsMask=1,
|
||||||
optionsOverride=1,
|
optionsOverride=1,
|
||||||
),
|
),
|
||||||
|
@ -316,7 +396,42 @@ async def test_extended_color_light(
|
||||||
command=clusters.ColorControl.Commands.MoveToHueAndSaturation(
|
command=clusters.ColorControl.Commands.MoveToHueAndSaturation(
|
||||||
hue=167,
|
hue=167,
|
||||||
saturation=254,
|
saturation=254,
|
||||||
transitionTime=0,
|
transitionTime=2,
|
||||||
|
optionsMask=1,
|
||||||
|
optionsOverride=1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
call(
|
||||||
|
node_id=light_node.node_id,
|
||||||
|
endpoint_id=1,
|
||||||
|
command=clusters.OnOff.Commands.On(),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
matter_client.send_device_command.reset_mock()
|
||||||
|
|
||||||
|
# Turn the light on with HS color and custom transition
|
||||||
|
await hass.services.async_call(
|
||||||
|
"light",
|
||||||
|
"turn_on",
|
||||||
|
{
|
||||||
|
"entity_id": entity_id,
|
||||||
|
"hs_color": (236.69291338582678, 100.0),
|
||||||
|
"transition": 4.0,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert matter_client.send_device_command.call_count == 2
|
||||||
|
matter_client.send_device_command.assert_has_calls(
|
||||||
|
[
|
||||||
|
call(
|
||||||
|
node_id=1,
|
||||||
|
endpoint_id=1,
|
||||||
|
command=clusters.ColorControl.Commands.MoveToHueAndSaturation(
|
||||||
|
hue=167,
|
||||||
|
saturation=254,
|
||||||
|
transitionTime=40,
|
||||||
optionsMask=1,
|
optionsMask=1,
|
||||||
optionsOverride=1,
|
optionsOverride=1,
|
||||||
),
|
),
|
||||||
|
|
Loading…
Reference in New Issue