core/homeassistant/components/matter/light.py

441 lines
14 KiB
Python

"""Matter light."""
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
from functools import partial
from typing import Any
from chip.clusters import Objects as clusters
from matter_server.common.models import device_types
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP,
ATTR_HS_COLOR,
ATTR_XY_COLOR,
ColorMode,
LightEntity,
LightEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import LOGGER
from .entity import MatterEntity, MatterEntityDescriptionBaseClass
from .helpers import get_matter
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(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Matter Light from Config Entry."""
matter = get_matter(hass)
matter.register_platform_handler(Platform.LIGHT, async_add_entities)
class MatterLight(MatterEntity, LightEntity):
"""Representation of a Matter light."""
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:
"""Return if device supports brightness."""
return (
clusters.LevelControl.Attributes.CurrentLevel
in self.entity_description.subscribe_attributes
)
def _supports_color(self) -> bool:
"""Return if device supports color."""
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)
assert level_control is not None
level = round(
renormalize(
brightness,
(0, 255),
(level_control.minLevel, level_control.maxLevel),
)
)
await self.send_device_command(
clusters.LevelControl.Commands.MoveToLevelWithOnOff(
level=level,
# It's required in TLV. We don't implement transition time yet.
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:
"""Turn light off."""
await self.send_device_command(
clusters.OnOff.Commands.Off(),
)
@callback
def _update_from_device(self) -> None:
"""Update from device."""
supports_color = self._supports_color()
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):
self._attr_is_on = attr.value
if supports_brightness:
self._attr_brightness = self._get_brightness()
@dataclass
class MatterLightEntityDescription(
LightEntityDescription,
MatterEntityDescriptionBaseClass,
):
"""Matter light entity description."""
# You can't set default values on inherited data classes
MatterLightEntityDescriptionFactory = partial(
MatterLightEntityDescription, entity_cls=MatterLight
)
# Mapping of a Matter Device type to Light Entity Description.
# A Matter device type (instance) can consist of multiple attributes.
# For example a Color Light which has an attribute to control brightness
# but also for color.
DEVICE_ENTITY: dict[
type[device_types.DeviceType],
MatterEntityDescriptionBaseClass | list[MatterEntityDescriptionBaseClass],
] = {
device_types.OnOffLight: MatterLightEntityDescriptionFactory(
key=device_types.OnOffLight,
subscribe_attributes=(clusters.OnOff.Attributes.OnOff,),
),
device_types.DimmableLight: MatterLightEntityDescriptionFactory(
key=device_types.DimmableLight,
subscribe_attributes=(
clusters.OnOff.Attributes.OnOff,
clusters.LevelControl.Attributes.CurrentLevel,
),
),
device_types.DimmablePlugInUnit: MatterLightEntityDescriptionFactory(
key=device_types.DimmablePlugInUnit,
subscribe_attributes=(
clusters.OnOff.Attributes.OnOff,
clusters.LevelControl.Attributes.CurrentLevel,
),
),
device_types.ColorTemperatureLight: MatterLightEntityDescriptionFactory(
key=device_types.ColorTemperatureLight,
subscribe_attributes=(
clusters.OnOff.Attributes.OnOff,
clusters.LevelControl.Attributes.CurrentLevel,
clusters.ColorControl.Attributes.ColorTemperatureMireds,
),
),
device_types.ExtendedColorLight: MatterLightEntityDescriptionFactory(
key=device_types.ExtendedColorLight,
subscribe_attributes=(
clusters.OnOff.Attributes.OnOff,
clusters.LevelControl.Attributes.CurrentLevel,
clusters.ColorControl.Attributes.ColorMode,
clusters.ColorControl.Attributes.CurrentHue,
clusters.ColorControl.Attributes.CurrentSaturation,
clusters.ColorControl.Attributes.CurrentX,
clusters.ColorControl.Attributes.CurrentY,
clusters.ColorControl.Attributes.ColorTemperatureMireds,
),
),
}