Add color support to matter lights (#87366)
* Added the correct attributes to ColorTemperatureLight and ExtendedColorLight and added CurrentY to codespell ignore word list * Added enums for matter color modes * Added support for reading color and color temperature settings from matter api * Added away of getting the ColorControl featureMap while the get_cluster(ColorControl) function is fixed * Initial working implementation of color and color temperature * Full supports for lights with both hs and xy * Added checks to make sure color features are supported before making matter call * Changed how color mode is figured out * Moved the logic that gets the brightness to its own function * Adds matter light tests for hass triggered events * Adds full test coverage for matter all types of matter lights * Adds full test coverage for matter all types of matter lights * Moved light convertion logic to util.py * Reorderd codespell ignore list and removed unused code * Adds brightness state test
This commit is contained in:
parent
096f6eb554
commit
e84a11963e
5 changed files with 2606 additions and 57 deletions
|
@ -31,7 +31,7 @@ repos:
|
||||||
hooks:
|
hooks:
|
||||||
- id: codespell
|
- id: codespell
|
||||||
args:
|
args:
|
||||||
- --ignore-words-list=additionals,alle,alot,ba,bre,bund,datas,dof,dur,ether,farenheit,falsy,fo,haa,hass,hist,iam,iff,iif,incomfort,ines,ist,lightsensor,mut,nam,nd,pres,pullrequests,referer,resset,rime,ser,serie,sur,te,technik,ue,uint,unsecure,visability,wan,wanna,withing,zar
|
- --ignore-words-list=additionals,alle,alot,ba,bre,bund,currenty,datas,dof,dur,ether,farenheit,falsy,fo,haa,hass,hist,iam,iff,iif,incomfort,ines,ist,lightsensor,mut,nam,nd,pres,pullrequests,referer,resset,rime,ser,serie,sur,te,technik,ue,uint,unsecure,visability,wan,wanna,withing,zar
|
||||||
- --skip="./.*,*.csv,*.json"
|
- --skip="./.*,*.csv,*.json"
|
||||||
- --quiet-level=2
|
- --quiet-level=2
|
||||||
exclude_types: [csv, json]
|
exclude_types: [csv, json]
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from enum import Enum
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
@ -10,6 +11,9 @@ from matter_server.common.models import device_types
|
||||||
|
|
||||||
from homeassistant.components.light import (
|
from homeassistant.components.light import (
|
||||||
ATTR_BRIGHTNESS,
|
ATTR_BRIGHTNESS,
|
||||||
|
ATTR_COLOR_TEMP,
|
||||||
|
ATTR_HS_COLOR,
|
||||||
|
ATTR_XY_COLOR,
|
||||||
ColorMode,
|
ColorMode,
|
||||||
LightEntity,
|
LightEntity,
|
||||||
LightEntityDescription,
|
LightEntityDescription,
|
||||||
|
@ -19,9 +23,41 @@ from homeassistant.const import Platform
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
from .const import LOGGER
|
||||||
from .entity import MatterEntity, MatterEntityDescriptionBaseClass
|
from .entity import MatterEntity, MatterEntityDescriptionBaseClass
|
||||||
from .helpers import get_matter
|
from .helpers import get_matter
|
||||||
from .util import renormalize
|
from .util import (
|
||||||
|
convert_to_hass_hs,
|
||||||
|
convert_to_hass_xy,
|
||||||
|
convert_to_matter_hs,
|
||||||
|
convert_to_matter_xy,
|
||||||
|
renormalize,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MatterColorMode(Enum):
|
||||||
|
"""Matter color mode."""
|
||||||
|
|
||||||
|
HS = 0
|
||||||
|
XY = 1
|
||||||
|
COLOR_TEMP = 2
|
||||||
|
|
||||||
|
|
||||||
|
COLOR_MODE_MAP = {
|
||||||
|
MatterColorMode.HS: ColorMode.HS,
|
||||||
|
MatterColorMode.XY: ColorMode.XY,
|
||||||
|
MatterColorMode.COLOR_TEMP: ColorMode.COLOR_TEMP,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class MatterColorControlFeatures(Enum):
|
||||||
|
"""Matter color control features."""
|
||||||
|
|
||||||
|
HS = 0 # Hue and saturation (Optional if device is color capable)
|
||||||
|
EHUE = 1 # Enhanced hue and saturation (Optional if device is color capable)
|
||||||
|
COLOR_LOOP = 2 # Color loop (Optional if device is color capable)
|
||||||
|
XY = 3 # XY (Mandatory if device is color capable)
|
||||||
|
COLOR_TEMP = 4 # Color temperature (Mandatory if device is color capable)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
|
@ -39,79 +75,305 @@ class MatterLight(MatterEntity, LightEntity):
|
||||||
|
|
||||||
entity_description: MatterLightEntityDescription
|
entity_description: MatterLightEntityDescription
|
||||||
|
|
||||||
|
def _supports_feature(
|
||||||
|
self, feature_map: int, feature: MatterColorControlFeatures
|
||||||
|
) -> bool:
|
||||||
|
"""Return if device supports given feature."""
|
||||||
|
|
||||||
|
return (feature_map & (1 << feature.value)) != 0
|
||||||
|
|
||||||
|
def _supports_color_mode(self, color_feature: MatterColorControlFeatures) -> bool:
|
||||||
|
"""Return if device supports given color mode."""
|
||||||
|
|
||||||
|
feature_map = self._device_type_instance.node.get_attribute(
|
||||||
|
self._device_type_instance.endpoint,
|
||||||
|
clusters.ColorControl,
|
||||||
|
clusters.ColorControl.Attributes.FeatureMap,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert isinstance(feature_map.value, int)
|
||||||
|
|
||||||
|
return self._supports_feature(feature_map.value, color_feature)
|
||||||
|
|
||||||
|
def _supports_hs_color(self) -> bool:
|
||||||
|
"""Return if device supports hs color."""
|
||||||
|
|
||||||
|
return self._supports_color_mode(MatterColorControlFeatures.HS)
|
||||||
|
|
||||||
|
def _supports_xy_color(self) -> bool:
|
||||||
|
"""Return if device supports xy color."""
|
||||||
|
|
||||||
|
return self._supports_color_mode(MatterColorControlFeatures.XY)
|
||||||
|
|
||||||
|
def _supports_color_temperature(self) -> bool:
|
||||||
|
"""Return if device supports color temperature."""
|
||||||
|
|
||||||
|
return self._supports_color_mode(MatterColorControlFeatures.COLOR_TEMP)
|
||||||
|
|
||||||
def _supports_brightness(self) -> bool:
|
def _supports_brightness(self) -> bool:
|
||||||
"""Return if device supports brightness."""
|
"""Return if device supports brightness."""
|
||||||
|
|
||||||
return (
|
return (
|
||||||
clusters.LevelControl.Attributes.CurrentLevel
|
clusters.LevelControl.Attributes.CurrentLevel
|
||||||
in self.entity_description.subscribe_attributes
|
in self.entity_description.subscribe_attributes
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
def _supports_color(self) -> bool:
|
||||||
"""Turn light on."""
|
"""Return if device supports color."""
|
||||||
if ATTR_BRIGHTNESS not in kwargs or not self._supports_brightness():
|
|
||||||
await self.matter_client.send_device_command(
|
|
||||||
node_id=self._device_type_instance.node.node_id,
|
|
||||||
endpoint=self._device_type_instance.endpoint,
|
|
||||||
command=clusters.OnOff.Commands.On(),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
clusters.ColorControl.Attributes.ColorMode
|
||||||
|
in self.entity_description.subscribe_attributes
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _set_xy_color(self, xy_color: tuple[float, float]) -> None:
|
||||||
|
"""Set xy color."""
|
||||||
|
|
||||||
|
matter_xy = convert_to_matter_xy(xy_color)
|
||||||
|
|
||||||
|
LOGGER.debug("Setting xy color to %s", matter_xy)
|
||||||
|
await self.send_device_command(
|
||||||
|
clusters.ColorControl.Commands.MoveToColor(
|
||||||
|
colorX=int(matter_xy[0]),
|
||||||
|
colorY=int(matter_xy[1]),
|
||||||
|
# It's required in TLV. We don't implement transition time yet.
|
||||||
|
transitionTime=0,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _set_hs_color(self, hs_color: tuple[float, float]) -> None:
|
||||||
|
"""Set hs color."""
|
||||||
|
|
||||||
|
matter_hs = convert_to_matter_hs(hs_color)
|
||||||
|
|
||||||
|
LOGGER.debug("Setting hs color to %s", matter_hs)
|
||||||
|
await self.send_device_command(
|
||||||
|
clusters.ColorControl.Commands.MoveToHueAndSaturation(
|
||||||
|
hue=int(matter_hs[0]),
|
||||||
|
saturation=int(matter_hs[1]),
|
||||||
|
# It's required in TLV. We don't implement transition time yet.
|
||||||
|
transitionTime=0,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _set_color_temp(self, color_temp: int) -> None:
|
||||||
|
"""Set color temperature."""
|
||||||
|
|
||||||
|
LOGGER.debug("Setting color temperature to %s", color_temp)
|
||||||
|
await self.send_device_command(
|
||||||
|
clusters.ColorControl.Commands.MoveToColorTemperature(
|
||||||
|
colorTemperature=color_temp,
|
||||||
|
# It's required in TLV. We don't implement transition time yet.
|
||||||
|
transitionTime=0,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _set_brightness(self, brightness: int) -> None:
|
||||||
|
"""Set brightness."""
|
||||||
|
|
||||||
|
LOGGER.debug("Setting brightness to %s", brightness)
|
||||||
level_control = self._device_type_instance.get_cluster(clusters.LevelControl)
|
level_control = self._device_type_instance.get_cluster(clusters.LevelControl)
|
||||||
# We check above that the device supports brightness, ie level control.
|
|
||||||
assert level_control is not None
|
assert level_control is not None
|
||||||
|
|
||||||
level = round(
|
level = round(
|
||||||
renormalize(
|
renormalize(
|
||||||
kwargs[ATTR_BRIGHTNESS],
|
brightness,
|
||||||
(0, 255),
|
(0, 255),
|
||||||
(level_control.minLevel, level_control.maxLevel),
|
(level_control.minLevel, level_control.maxLevel),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
await self.matter_client.send_device_command(
|
await self.send_device_command(
|
||||||
node_id=self._device_type_instance.node.node_id,
|
clusters.LevelControl.Commands.MoveToLevelWithOnOff(
|
||||||
endpoint=self._device_type_instance.endpoint,
|
|
||||||
command=clusters.LevelControl.Commands.MoveToLevelWithOnOff(
|
|
||||||
level=level,
|
level=level,
|
||||||
# It's required in TLV. We don't implement transition time yet.
|
# It's required in TLV. We don't implement transition time yet.
|
||||||
transitionTime=0,
|
transitionTime=0,
|
||||||
),
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_xy_color(self) -> tuple[float, float]:
|
||||||
|
"""Get xy color from matter."""
|
||||||
|
|
||||||
|
x_color = self.get_matter_attribute(clusters.ColorControl.Attributes.CurrentX)
|
||||||
|
y_color = self.get_matter_attribute(clusters.ColorControl.Attributes.CurrentY)
|
||||||
|
|
||||||
|
assert x_color is not None
|
||||||
|
assert y_color is not None
|
||||||
|
|
||||||
|
xy_color = convert_to_hass_xy((x_color.value, y_color.value))
|
||||||
|
LOGGER.debug(
|
||||||
|
"Got xy color %s for %s",
|
||||||
|
xy_color,
|
||||||
|
self._device_type_instance,
|
||||||
|
)
|
||||||
|
|
||||||
|
return xy_color
|
||||||
|
|
||||||
|
def _get_hs_color(self) -> tuple[float, float]:
|
||||||
|
"""Get hs color from matter."""
|
||||||
|
|
||||||
|
hue = self.get_matter_attribute(clusters.ColorControl.Attributes.CurrentHue)
|
||||||
|
|
||||||
|
saturation = self.get_matter_attribute(
|
||||||
|
clusters.ColorControl.Attributes.CurrentSaturation
|
||||||
|
)
|
||||||
|
|
||||||
|
assert hue is not None
|
||||||
|
assert saturation is not None
|
||||||
|
|
||||||
|
hs_color = convert_to_hass_hs((hue.value, saturation.value))
|
||||||
|
|
||||||
|
LOGGER.debug(
|
||||||
|
"Got hs color %s for %s",
|
||||||
|
hs_color,
|
||||||
|
self._device_type_instance,
|
||||||
|
)
|
||||||
|
|
||||||
|
return hs_color
|
||||||
|
|
||||||
|
def _get_color_temperature(self) -> int:
|
||||||
|
"""Get color temperature from matter."""
|
||||||
|
|
||||||
|
color_temp = self.get_matter_attribute(
|
||||||
|
clusters.ColorControl.Attributes.ColorTemperatureMireds
|
||||||
|
)
|
||||||
|
|
||||||
|
assert color_temp is not None
|
||||||
|
|
||||||
|
LOGGER.debug(
|
||||||
|
"Got color temperature %s for %s",
|
||||||
|
color_temp.value,
|
||||||
|
self._device_type_instance,
|
||||||
|
)
|
||||||
|
|
||||||
|
return int(color_temp.value)
|
||||||
|
|
||||||
|
def _get_brightness(self) -> int:
|
||||||
|
"""Get brightness from matter."""
|
||||||
|
|
||||||
|
level_control = self._device_type_instance.get_cluster(clusters.LevelControl)
|
||||||
|
|
||||||
|
# We should not get here if brightness is not supported.
|
||||||
|
assert level_control is not None
|
||||||
|
|
||||||
|
LOGGER.debug(
|
||||||
|
"Got brightness %s for %s",
|
||||||
|
level_control.currentLevel,
|
||||||
|
self._device_type_instance,
|
||||||
|
)
|
||||||
|
|
||||||
|
return round(
|
||||||
|
renormalize(
|
||||||
|
level_control.currentLevel,
|
||||||
|
(level_control.minLevel, level_control.maxLevel),
|
||||||
|
(0, 255),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_color_mode(self) -> ColorMode:
|
||||||
|
"""Get color mode from matter."""
|
||||||
|
|
||||||
|
color_mode = self.get_matter_attribute(
|
||||||
|
clusters.ColorControl.Attributes.ColorMode
|
||||||
|
)
|
||||||
|
|
||||||
|
assert color_mode is not None
|
||||||
|
|
||||||
|
ha_color_mode = COLOR_MODE_MAP[MatterColorMode(color_mode.value)]
|
||||||
|
|
||||||
|
LOGGER.debug(
|
||||||
|
"Got color mode (%s) for %s", ha_color_mode, self._device_type_instance
|
||||||
|
)
|
||||||
|
|
||||||
|
return ha_color_mode
|
||||||
|
|
||||||
|
async def send_device_command(self, command: Any) -> None:
|
||||||
|
"""Send device command."""
|
||||||
|
await self.matter_client.send_device_command(
|
||||||
|
node_id=self._device_type_instance.node.node_id,
|
||||||
|
endpoint=self._device_type_instance.endpoint,
|
||||||
|
command=command,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn light on."""
|
||||||
|
|
||||||
|
hs_color = kwargs.get(ATTR_HS_COLOR)
|
||||||
|
xy_color = kwargs.get(ATTR_XY_COLOR)
|
||||||
|
color_temp = kwargs.get(ATTR_COLOR_TEMP)
|
||||||
|
brightness = kwargs.get(ATTR_BRIGHTNESS)
|
||||||
|
|
||||||
|
if self._supports_color():
|
||||||
|
if hs_color is not None and self._supports_hs_color():
|
||||||
|
await self._set_hs_color(hs_color)
|
||||||
|
elif xy_color is not None and self._supports_xy_color():
|
||||||
|
await self._set_xy_color(xy_color)
|
||||||
|
elif color_temp is not None and self._supports_color_temperature():
|
||||||
|
await self._set_color_temp(color_temp)
|
||||||
|
|
||||||
|
if brightness is not None and self._supports_brightness():
|
||||||
|
await self._set_brightness(brightness)
|
||||||
|
return
|
||||||
|
|
||||||
|
await self.send_device_command(
|
||||||
|
clusters.OnOff.Commands.On(),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
"""Turn light off."""
|
"""Turn light off."""
|
||||||
await self.matter_client.send_device_command(
|
await self.send_device_command(
|
||||||
node_id=self._device_type_instance.node.node_id,
|
clusters.OnOff.Commands.Off(),
|
||||||
endpoint=self._device_type_instance.endpoint,
|
|
||||||
command=clusters.OnOff.Commands.Off(),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _update_from_device(self) -> None:
|
def _update_from_device(self) -> None:
|
||||||
"""Update from device."""
|
"""Update from device."""
|
||||||
supports_brigthness = self._supports_brightness()
|
|
||||||
|
|
||||||
if self._attr_supported_color_modes is None and supports_brigthness:
|
supports_color = self._supports_color()
|
||||||
self._attr_supported_color_modes = {ColorMode.BRIGHTNESS}
|
supports_color_temperature = (
|
||||||
|
self._supports_color_temperature() if supports_color else False
|
||||||
|
)
|
||||||
|
supports_brightness = self._supports_brightness()
|
||||||
|
|
||||||
|
if self._attr_supported_color_modes is None:
|
||||||
|
supported_color_modes = set()
|
||||||
|
if supports_color:
|
||||||
|
supported_color_modes.add(ColorMode.XY)
|
||||||
|
if self._supports_hs_color():
|
||||||
|
supported_color_modes.add(ColorMode.HS)
|
||||||
|
|
||||||
|
if supports_color_temperature:
|
||||||
|
supported_color_modes.add(ColorMode.COLOR_TEMP)
|
||||||
|
|
||||||
|
if supports_brightness:
|
||||||
|
supported_color_modes.add(ColorMode.BRIGHTNESS)
|
||||||
|
|
||||||
|
self._attr_supported_color_modes = (
|
||||||
|
supported_color_modes if supported_color_modes else None
|
||||||
|
)
|
||||||
|
|
||||||
|
LOGGER.debug(
|
||||||
|
"Supported color modes: %s for %s",
|
||||||
|
self._attr_supported_color_modes,
|
||||||
|
self._device_type_instance,
|
||||||
|
)
|
||||||
|
|
||||||
|
if supports_color:
|
||||||
|
self._attr_color_mode = self._get_color_mode()
|
||||||
|
if self._attr_color_mode == ColorMode.HS:
|
||||||
|
self._attr_hs_color = self._get_hs_color()
|
||||||
|
else:
|
||||||
|
self._attr_xy_color = self._get_xy_color()
|
||||||
|
|
||||||
|
if supports_color_temperature:
|
||||||
|
self._attr_color_temp = self._get_color_temperature()
|
||||||
|
|
||||||
if attr := self.get_matter_attribute(clusters.OnOff.Attributes.OnOff):
|
if attr := self.get_matter_attribute(clusters.OnOff.Attributes.OnOff):
|
||||||
self._attr_is_on = attr.value
|
self._attr_is_on = attr.value
|
||||||
|
|
||||||
if supports_brigthness:
|
if supports_brightness:
|
||||||
level_control = self._device_type_instance.get_cluster(
|
self._attr_brightness = self._get_brightness()
|
||||||
clusters.LevelControl
|
|
||||||
)
|
|
||||||
# We check above that the device supports brightness, ie level control.
|
|
||||||
assert level_control is not None
|
|
||||||
|
|
||||||
# Convert brightness to Home Assistant = 0..255
|
|
||||||
self._attr_brightness = round(
|
|
||||||
renormalize(
|
|
||||||
level_control.currentLevel,
|
|
||||||
(level_control.minLevel, level_control.maxLevel),
|
|
||||||
(0, 255),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@ -159,7 +421,7 @@ DEVICE_ENTITY: dict[
|
||||||
subscribe_attributes=(
|
subscribe_attributes=(
|
||||||
clusters.OnOff.Attributes.OnOff,
|
clusters.OnOff.Attributes.OnOff,
|
||||||
clusters.LevelControl.Attributes.CurrentLevel,
|
clusters.LevelControl.Attributes.CurrentLevel,
|
||||||
clusters.ColorControl,
|
clusters.ColorControl.Attributes.ColorTemperatureMireds,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
device_types.ExtendedColorLight: MatterLightEntityDescriptionFactory(
|
device_types.ExtendedColorLight: MatterLightEntityDescriptionFactory(
|
||||||
|
@ -167,7 +429,12 @@ DEVICE_ENTITY: dict[
|
||||||
subscribe_attributes=(
|
subscribe_attributes=(
|
||||||
clusters.OnOff.Attributes.OnOff,
|
clusters.OnOff.Attributes.OnOff,
|
||||||
clusters.LevelControl.Attributes.CurrentLevel,
|
clusters.LevelControl.Attributes.CurrentLevel,
|
||||||
clusters.ColorControl,
|
clusters.ColorControl.Attributes.ColorMode,
|
||||||
|
clusters.ColorControl.Attributes.CurrentHue,
|
||||||
|
clusters.ColorControl.Attributes.CurrentSaturation,
|
||||||
|
clusters.ColorControl.Attributes.CurrentX,
|
||||||
|
clusters.ColorControl.Attributes.CurrentY,
|
||||||
|
clusters.ColorControl.Attributes.ColorTemperatureMireds,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
"""Provide integration utilities."""
|
"""Provide integration utilities."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
XY_COLOR_FACTOR = 65536
|
||||||
|
|
||||||
|
|
||||||
def renormalize(
|
def renormalize(
|
||||||
number: float, from_range: tuple[float, float], to_range: tuple[float, float]
|
number: float, from_range: tuple[float, float], to_range: tuple[float, float]
|
||||||
|
@ -9,3 +11,33 @@ def renormalize(
|
||||||
delta1 = from_range[1] - from_range[0]
|
delta1 = from_range[1] - from_range[0]
|
||||||
delta2 = to_range[1] - to_range[0]
|
delta2 = to_range[1] - to_range[0]
|
||||||
return (delta2 * (number - from_range[0]) / delta1) + to_range[0]
|
return (delta2 * (number - from_range[0]) / delta1) + to_range[0]
|
||||||
|
|
||||||
|
|
||||||
|
def convert_to_matter_hs(hass_hs: tuple[float, float]) -> tuple[float, float]:
|
||||||
|
"""Convert Home Assistant HS to Matter HS."""
|
||||||
|
|
||||||
|
return (
|
||||||
|
hass_hs[0] / 360 * 254,
|
||||||
|
renormalize(hass_hs[1], (0, 100), (0, 254)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def convert_to_hass_hs(matter_hs: tuple[float, float]) -> tuple[float, float]:
|
||||||
|
"""Convert Matter HS to Home Assistant HS."""
|
||||||
|
|
||||||
|
return (
|
||||||
|
matter_hs[0] * 360 / 254,
|
||||||
|
renormalize(matter_hs[1], (0, 254), (0, 100)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def convert_to_matter_xy(hass_xy: tuple[float, float]) -> tuple[float, float]:
|
||||||
|
"""Convert Home Assistant XY to Matter XY."""
|
||||||
|
|
||||||
|
return (hass_xy[0] * XY_COLOR_FACTOR, hass_xy[1] * XY_COLOR_FACTOR)
|
||||||
|
|
||||||
|
|
||||||
|
def convert_to_hass_xy(matter_xy: tuple[float, float]) -> tuple[float, float]:
|
||||||
|
"""Convert Matter XY to Home Assistant XY."""
|
||||||
|
|
||||||
|
return (matter_xy[0] / XY_COLOR_FACTOR, matter_xy[1] / XY_COLOR_FACTOR)
|
||||||
|
|
2110
tests/components/matter/fixtures/nodes/extended-color-light.json
Normal file
2110
tests/components/matter/fixtures/nodes/extended-color-light.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -20,7 +20,7 @@ async def light_node_fixture(
|
||||||
) -> MatterNode:
|
) -> MatterNode:
|
||||||
"""Fixture for a light node."""
|
"""Fixture for a light node."""
|
||||||
return await setup_integration_with_node_fixture(
|
return await setup_integration_with_node_fixture(
|
||||||
hass, "dimmable-light", matter_client
|
hass, "extended-color-light", matter_client
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -30,22 +30,13 @@ async def test_turn_on(
|
||||||
light_node: MatterNode,
|
light_node: MatterNode,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test turning on a light."""
|
"""Test turning on a light."""
|
||||||
state = hass.states.get("light.mock_dimmable_light")
|
|
||||||
assert state
|
|
||||||
assert state.state == "on"
|
|
||||||
|
|
||||||
set_node_attribute(light_node, 1, 6, 0, False)
|
|
||||||
await trigger_subscription_callback(hass, matter_client)
|
|
||||||
|
|
||||||
state = hass.states.get("light.mock_dimmable_light")
|
|
||||||
assert state
|
|
||||||
assert state.state == "off"
|
|
||||||
|
|
||||||
|
# OnOff test
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
"light",
|
"light",
|
||||||
"turn_on",
|
"turn_on",
|
||||||
{
|
{
|
||||||
"entity_id": "light.mock_dimmable_light",
|
"entity_id": "light.mock_extended_color_light",
|
||||||
},
|
},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
|
@ -58,11 +49,12 @@ async def test_turn_on(
|
||||||
)
|
)
|
||||||
matter_client.send_device_command.reset_mock()
|
matter_client.send_device_command.reset_mock()
|
||||||
|
|
||||||
|
# Brightness test
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
"light",
|
"light",
|
||||||
"turn_on",
|
"turn_on",
|
||||||
{
|
{
|
||||||
"entity_id": "light.mock_dimmable_light",
|
"entity_id": "light.mock_extended_color_light",
|
||||||
"brightness": 128,
|
"brightness": 128,
|
||||||
},
|
},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
|
@ -77,6 +69,154 @@ async def test_turn_on(
|
||||||
transitionTime=0,
|
transitionTime=0,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
matter_client.send_device_command.reset_mock()
|
||||||
|
|
||||||
|
# HS Color test
|
||||||
|
await hass.services.async_call(
|
||||||
|
"light",
|
||||||
|
"turn_on",
|
||||||
|
{
|
||||||
|
"entity_id": "light.mock_extended_color_light",
|
||||||
|
"hs_color": [0, 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=1,
|
||||||
|
command=clusters.ColorControl.Commands.MoveToHueAndSaturation(
|
||||||
|
hue=0,
|
||||||
|
saturation=0,
|
||||||
|
transitionTime=0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
call(
|
||||||
|
node_id=light_node.node_id,
|
||||||
|
endpoint=1,
|
||||||
|
command=clusters.OnOff.Commands.On(),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
matter_client.send_device_command.reset_mock()
|
||||||
|
|
||||||
|
# XY Color test
|
||||||
|
await hass.services.async_call(
|
||||||
|
"light",
|
||||||
|
"turn_on",
|
||||||
|
{
|
||||||
|
"entity_id": "light.mock_extended_color_light",
|
||||||
|
"xy_color": [0.5, 0.5],
|
||||||
|
},
|
||||||
|
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=1,
|
||||||
|
command=clusters.ColorControl.Commands.MoveToColor(
|
||||||
|
colorX=(0.5 * 65536),
|
||||||
|
colorY=(0.5 * 65536),
|
||||||
|
transitionTime=0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
call(
|
||||||
|
node_id=light_node.node_id,
|
||||||
|
endpoint=1,
|
||||||
|
command=clusters.OnOff.Commands.On(),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
matter_client.send_device_command.reset_mock()
|
||||||
|
|
||||||
|
# Color Temperature test
|
||||||
|
await hass.services.async_call(
|
||||||
|
"light",
|
||||||
|
"turn_on",
|
||||||
|
{
|
||||||
|
"entity_id": "light.mock_extended_color_light",
|
||||||
|
"color_temp": 300,
|
||||||
|
},
|
||||||
|
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=1,
|
||||||
|
command=clusters.ColorControl.Commands.MoveToColorTemperature(
|
||||||
|
colorTemperature=300,
|
||||||
|
transitionTime=0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
call(
|
||||||
|
node_id=light_node.node_id,
|
||||||
|
endpoint=1,
|
||||||
|
command=clusters.OnOff.Commands.On(),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
matter_client.send_device_command.reset_mock()
|
||||||
|
|
||||||
|
state = hass.states.get("light.mock_extended_color_light")
|
||||||
|
assert state
|
||||||
|
assert state.state == "on"
|
||||||
|
|
||||||
|
# HS Color Test
|
||||||
|
set_node_attribute(light_node, 1, 768, 8, 0)
|
||||||
|
set_node_attribute(light_node, 1, 768, 1, 50)
|
||||||
|
set_node_attribute(light_node, 1, 768, 0, 100)
|
||||||
|
await trigger_subscription_callback(hass, matter_client)
|
||||||
|
|
||||||
|
state = hass.states.get("light.mock_extended_color_light")
|
||||||
|
assert state
|
||||||
|
assert state.attributes["color_mode"] == "hs"
|
||||||
|
assert state.attributes["hs_color"] == (141.732, 19.685)
|
||||||
|
|
||||||
|
# XY Color Test
|
||||||
|
set_node_attribute(light_node, 1, 768, 8, 1)
|
||||||
|
set_node_attribute(light_node, 1, 768, 3, 50)
|
||||||
|
set_node_attribute(light_node, 1, 768, 4, 100)
|
||||||
|
await trigger_subscription_callback(hass, matter_client)
|
||||||
|
|
||||||
|
state = hass.states.get("light.mock_extended_color_light")
|
||||||
|
assert state
|
||||||
|
assert state.attributes["color_mode"] == "xy"
|
||||||
|
assert state.attributes["xy_color"] == (0.0007630, 0.001526)
|
||||||
|
|
||||||
|
# Color Temperature Test
|
||||||
|
set_node_attribute(light_node, 1, 768, 8, 2)
|
||||||
|
set_node_attribute(light_node, 1, 768, 7, 100)
|
||||||
|
await trigger_subscription_callback(hass, matter_client)
|
||||||
|
|
||||||
|
state = hass.states.get("light.mock_extended_color_light")
|
||||||
|
assert state
|
||||||
|
assert state.attributes["color_mode"] == "color_temp"
|
||||||
|
assert state.attributes["color_temp"] == 100
|
||||||
|
|
||||||
|
# Brightness state test
|
||||||
|
set_node_attribute(light_node, 1, 8, 0, 50)
|
||||||
|
await trigger_subscription_callback(hass, matter_client)
|
||||||
|
|
||||||
|
state = hass.states.get("light.mock_extended_color_light")
|
||||||
|
assert state
|
||||||
|
assert state.attributes["brightness"] == 49
|
||||||
|
|
||||||
|
# Off state test
|
||||||
|
set_node_attribute(light_node, 1, 6, 0, False)
|
||||||
|
await trigger_subscription_callback(hass, matter_client)
|
||||||
|
|
||||||
|
state = hass.states.get("light.mock_extended_color_light")
|
||||||
|
assert state
|
||||||
|
assert state.state == "off"
|
||||||
|
|
||||||
|
|
||||||
async def test_turn_off(
|
async def test_turn_off(
|
||||||
|
@ -85,7 +225,7 @@ async def test_turn_off(
|
||||||
light_node: MatterNode,
|
light_node: MatterNode,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test turning off a light."""
|
"""Test turning off a light."""
|
||||||
state = hass.states.get("light.mock_dimmable_light")
|
state = hass.states.get("light.mock_extended_color_light")
|
||||||
assert state
|
assert state
|
||||||
assert state.state == "on"
|
assert state.state == "on"
|
||||||
|
|
||||||
|
@ -93,7 +233,7 @@ async def test_turn_off(
|
||||||
"light",
|
"light",
|
||||||
"turn_off",
|
"turn_off",
|
||||||
{
|
{
|
||||||
"entity_id": "light.mock_dimmable_light",
|
"entity_id": "light.mock_extended_color_light",
|
||||||
},
|
},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue