mirror of
https://github.com/home-assistant/core.git
synced 2026-04-28 20:53:45 +01:00
Support for Matter MountedDimmableLoadControl device type: Matter MountedDimmableLoadControl device was wrongly reco gnized as Switch entity. This PR fix thte behavior and makes it recognized as Light entity as expected. There is no Matter MountedDimmableLoadControl device in the market yet so it doesn't really breaks anything.
545 lines
19 KiB
Python
545 lines
19 KiB
Python
"""Matter light."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from chip.clusters import Objects as clusters
|
|
from chip.clusters.Objects import NullValue
|
|
from matter_server.client.models import device_types
|
|
|
|
from homeassistant.components.light import (
|
|
ATTR_BRIGHTNESS,
|
|
ATTR_COLOR_TEMP_KELVIN,
|
|
ATTR_HS_COLOR,
|
|
ATTR_TRANSITION,
|
|
ATTR_XY_COLOR,
|
|
DEFAULT_MAX_KELVIN,
|
|
DEFAULT_MIN_KELVIN,
|
|
ColorMode,
|
|
LightEntity,
|
|
LightEntityDescription,
|
|
LightEntityFeature,
|
|
filter_supported_color_modes,
|
|
)
|
|
from homeassistant.config_entries import ConfigEntry
|
|
from homeassistant.const import Platform
|
|
from homeassistant.core import HomeAssistant, callback
|
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|
from homeassistant.util import color as color_util
|
|
|
|
from .const import LOGGER
|
|
from .entity import MatterEntity
|
|
from .helpers import get_matter
|
|
from .models import MatterDiscoverySchema
|
|
from .util import (
|
|
convert_to_hass_hs,
|
|
convert_to_hass_xy,
|
|
convert_to_matter_hs,
|
|
convert_to_matter_xy,
|
|
renormalize,
|
|
)
|
|
|
|
COLOR_MODE_MAP = {
|
|
clusters.ColorControl.Enums.ColorModeEnum.kCurrentHueAndCurrentSaturation: ColorMode.HS,
|
|
clusters.ColorControl.Enums.ColorModeEnum.kCurrentXAndCurrentY: ColorMode.XY,
|
|
clusters.ColorControl.Enums.ColorModeEnum.kColorTemperatureMireds: ColorMode.COLOR_TEMP,
|
|
}
|
|
|
|
# there's a bug in (at least) Espressif's implementation of light transitions
|
|
# on devices based on Matter 1.0. Mark potential devices with this issue.
|
|
# https://github.com/home-assistant/core/issues/113775
|
|
# vendorid (attributeKey 0/40/2)
|
|
# productid (attributeKey 0/40/4)
|
|
# hw version (attributeKey 0/40/8)
|
|
# sw version (attributeKey 0/40/10)
|
|
TRANSITION_BLOCKLIST = (
|
|
(4107, 8475, "v1.0", "v1.0"),
|
|
(4107, 8550, "v1.0", "v1.0"),
|
|
(4107, 8551, "v1.0", "v1.0"),
|
|
(4107, 8571, "v1.0", "v1.0"),
|
|
(4107, 8656, "v1.0", "v1.0"),
|
|
(4448, 36866, "V1", "V1.0.0.5"),
|
|
(4456, 1011, "1.0.0", "2.00.00"),
|
|
(4488, 260, "1.0", "1.0.0"),
|
|
(4488, 514, "1.0", "1.0.0"),
|
|
(4921, 42, "1.0", "1.01.060"),
|
|
(4921, 43, "1.0", "1.01.060"),
|
|
(4999, 24875, "1.0", "27.0"),
|
|
(4999, 25057, "1.0", "27.0"),
|
|
(5009, 514, "1.0", "1.0.0"),
|
|
(5010, 769, "3.0", "1.0.0"),
|
|
(5130, 544, "v0.4", "6.7.196e9d4e08-14"),
|
|
(5127, 4232, "ver_0.1", "v1.00.51"),
|
|
(5245, 1412, "1.0", "1.0.21"),
|
|
)
|
|
|
|
|
|
async def async_setup_entry(
|
|
hass: HomeAssistant,
|
|
config_entry: ConfigEntry,
|
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
|
) -> 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: LightEntityDescription
|
|
_supports_brightness = False
|
|
_supports_color = False
|
|
_supports_color_temperature = False
|
|
_transitions_disabled = False
|
|
_platform_translation_key = "light"
|
|
_attr_min_color_temp_kelvin = DEFAULT_MIN_KELVIN
|
|
_attr_max_color_temp_kelvin = DEFAULT_MAX_KELVIN
|
|
|
|
async def _set_xy_color(
|
|
self, xy_color: tuple[float, float], transition: float = 0.0
|
|
) -> None:
|
|
"""Set xy color."""
|
|
|
|
matter_xy = convert_to_matter_xy(xy_color)
|
|
|
|
await self.send_device_command(
|
|
clusters.ColorControl.Commands.MoveToColor(
|
|
colorX=int(matter_xy[0]),
|
|
colorY=int(matter_xy[1]),
|
|
# transition in matter is measured in tenths of a second
|
|
transitionTime=int(transition * 10),
|
|
# allow setting the color while the light is off,
|
|
# by setting the optionsMask to 1 (=ExecuteIfOff)
|
|
optionsMask=1,
|
|
optionsOverride=1,
|
|
)
|
|
)
|
|
|
|
async def _set_hs_color(
|
|
self, hs_color: tuple[float, float], transition: float = 0.0
|
|
) -> None:
|
|
"""Set hs color."""
|
|
|
|
matter_hs = convert_to_matter_hs(hs_color)
|
|
|
|
await self.send_device_command(
|
|
clusters.ColorControl.Commands.MoveToHueAndSaturation(
|
|
hue=int(matter_hs[0]),
|
|
saturation=int(matter_hs[1]),
|
|
# transition in matter is measured in tenths of a second
|
|
transitionTime=int(transition * 10),
|
|
# allow setting the color while the light is off,
|
|
# by setting the optionsMask to 1 (=ExecuteIfOff)
|
|
optionsMask=1,
|
|
optionsOverride=1,
|
|
)
|
|
)
|
|
|
|
async def _set_color_temp(
|
|
self, color_temp_kelvin: int, transition: float = 0.0
|
|
) -> None:
|
|
"""Set color temperature."""
|
|
color_temp_mired = color_util.color_temperature_kelvin_to_mired(
|
|
color_temp_kelvin
|
|
)
|
|
await self.send_device_command(
|
|
clusters.ColorControl.Commands.MoveToColorTemperature(
|
|
colorTemperatureMireds=color_temp_mired,
|
|
# transition in matter is measured in tenths of a second
|
|
transitionTime=int(transition * 10),
|
|
# allow setting the color while the light is off,
|
|
# by setting the optionsMask to 1 (=ExecuteIfOff)
|
|
optionsMask=1,
|
|
optionsOverride=1,
|
|
)
|
|
)
|
|
|
|
async def _set_brightness(self, brightness: int, transition: float = 0.0) -> None:
|
|
"""Set brightness."""
|
|
|
|
level_control = self._endpoint.get_cluster(clusters.LevelControl)
|
|
|
|
assert level_control is not None
|
|
|
|
level = round(
|
|
renormalize(
|
|
brightness,
|
|
(0, 255),
|
|
(level_control.minLevel or 1, level_control.maxLevel or 254),
|
|
)
|
|
)
|
|
|
|
await self.send_device_command(
|
|
clusters.LevelControl.Commands.MoveToLevelWithOnOff(
|
|
level=level,
|
|
# transition in matter is measured in tenths of a second
|
|
transitionTime=int(transition * 10),
|
|
)
|
|
)
|
|
|
|
def _get_xy_color(self) -> tuple[float, float]:
|
|
"""Get xy color from matter."""
|
|
|
|
x_color = self.get_matter_attribute_value(
|
|
clusters.ColorControl.Attributes.CurrentX
|
|
)
|
|
y_color = self.get_matter_attribute_value(
|
|
clusters.ColorControl.Attributes.CurrentY
|
|
)
|
|
|
|
assert x_color is not None
|
|
assert y_color is not None
|
|
|
|
xy_color = convert_to_hass_xy((x_color, y_color))
|
|
LOGGER.debug(
|
|
"Got xy color %s for %s",
|
|
xy_color,
|
|
self.entity_id,
|
|
)
|
|
|
|
return xy_color
|
|
|
|
def _get_hs_color(self) -> tuple[float, float]:
|
|
"""Get hs color from matter."""
|
|
|
|
hue = self.get_matter_attribute_value(
|
|
clusters.ColorControl.Attributes.CurrentHue
|
|
)
|
|
|
|
saturation = self.get_matter_attribute_value(
|
|
clusters.ColorControl.Attributes.CurrentSaturation
|
|
)
|
|
|
|
assert hue is not None
|
|
assert saturation is not None
|
|
|
|
hs_color = convert_to_hass_hs((hue, saturation))
|
|
|
|
LOGGER.debug(
|
|
"Got hs color %s for %s",
|
|
hs_color,
|
|
self.entity_id,
|
|
)
|
|
|
|
return hs_color
|
|
|
|
def _get_color_temperature(self) -> int:
|
|
"""Get color temperature from matter."""
|
|
|
|
color_temp = self.get_matter_attribute_value(
|
|
clusters.ColorControl.Attributes.ColorTemperatureMireds
|
|
)
|
|
|
|
assert color_temp is not None
|
|
|
|
LOGGER.debug(
|
|
"Got color temperature %s for %s",
|
|
color_temp,
|
|
self.entity_id,
|
|
)
|
|
|
|
return int(color_temp)
|
|
|
|
def _get_brightness(self) -> int | None:
|
|
"""Get brightness from matter."""
|
|
|
|
level_control = self._endpoint.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.entity_id,
|
|
)
|
|
|
|
if level_control.currentLevel is NullValue:
|
|
# currentLevel is a nullable value.
|
|
return None
|
|
|
|
return round(
|
|
renormalize(
|
|
level_control.currentLevel,
|
|
(level_control.minLevel or 1, level_control.maxLevel or 254),
|
|
(0, 255),
|
|
)
|
|
)
|
|
|
|
def _get_color_mode(self) -> ColorMode:
|
|
"""Get color mode from matter."""
|
|
|
|
color_mode = self.get_matter_attribute_value(
|
|
clusters.ColorControl.Attributes.ColorMode
|
|
)
|
|
|
|
assert color_mode is not None
|
|
|
|
ha_color_mode = COLOR_MODE_MAP[color_mode]
|
|
|
|
LOGGER.debug(
|
|
"Got color mode (%s) for %s",
|
|
ha_color_mode,
|
|
self.entity_id,
|
|
)
|
|
|
|
return ha_color_mode
|
|
|
|
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_kelvin = kwargs.get(ATTR_COLOR_TEMP_KELVIN)
|
|
brightness = kwargs.get(ATTR_BRIGHTNESS)
|
|
transition = kwargs.get(ATTR_TRANSITION, 0)
|
|
if self._transitions_disabled:
|
|
transition = 0
|
|
|
|
if self.supported_color_modes is not None:
|
|
if hs_color is not None and ColorMode.HS in self.supported_color_modes:
|
|
await self._set_hs_color(hs_color, transition)
|
|
elif xy_color is not None and ColorMode.XY in self.supported_color_modes:
|
|
await self._set_xy_color(xy_color, transition)
|
|
elif (
|
|
color_temp_kelvin is not None
|
|
and ColorMode.COLOR_TEMP in self.supported_color_modes
|
|
):
|
|
await self._set_color_temp(color_temp_kelvin, transition)
|
|
|
|
if brightness is not None and self._supports_brightness:
|
|
await self._set_brightness(brightness, transition)
|
|
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."""
|
|
if self._attr_supported_color_modes is None:
|
|
# work out what (color)features are supported
|
|
supported_color_modes = {ColorMode.ONOFF}
|
|
# brightness support
|
|
if self._entity_info.endpoint.has_attribute(
|
|
None, clusters.LevelControl.Attributes.CurrentLevel
|
|
) and self._entity_info.endpoint.device_types != {device_types.OnOffLight}:
|
|
# We need to filter out the OnOffLight device type here because
|
|
# that can have an optional LevelControl cluster present
|
|
# which we should ignore.
|
|
supported_color_modes.add(ColorMode.BRIGHTNESS)
|
|
self._supports_brightness = True
|
|
# colormode(s)
|
|
if self._entity_info.endpoint.has_attribute(
|
|
None, clusters.ColorControl.Attributes.ColorMode
|
|
) and self._entity_info.endpoint.has_attribute(
|
|
None, clusters.ColorControl.Attributes.ColorCapabilities
|
|
):
|
|
capabilities = self.get_matter_attribute_value(
|
|
clusters.ColorControl.Attributes.ColorCapabilities
|
|
)
|
|
|
|
assert capabilities is not None
|
|
|
|
if (
|
|
capabilities
|
|
& clusters.ColorControl.Bitmaps.ColorCapabilitiesBitmap.kHueSaturation
|
|
):
|
|
supported_color_modes.add(ColorMode.HS)
|
|
self._supports_color = True
|
|
|
|
if (
|
|
capabilities
|
|
& clusters.ColorControl.Bitmaps.ColorCapabilitiesBitmap.kXy
|
|
):
|
|
supported_color_modes.add(ColorMode.XY)
|
|
self._supports_color = True
|
|
|
|
if (
|
|
capabilities
|
|
& clusters.ColorControl.Bitmaps.ColorCapabilitiesBitmap.kColorTemperature
|
|
):
|
|
supported_color_modes.add(ColorMode.COLOR_TEMP)
|
|
self._supports_color_temperature = True
|
|
min_mireds = self.get_matter_attribute_value(
|
|
clusters.ColorControl.Attributes.ColorTempPhysicalMinMireds
|
|
)
|
|
if min_mireds > 0:
|
|
self._attr_max_color_temp_kelvin = (
|
|
color_util.color_temperature_mired_to_kelvin(min_mireds)
|
|
)
|
|
max_mireds = self.get_matter_attribute_value(
|
|
clusters.ColorControl.Attributes.ColorTempPhysicalMaxMireds
|
|
)
|
|
if max_mireds > 0:
|
|
self._attr_min_color_temp_kelvin = (
|
|
color_util.color_temperature_mired_to_kelvin(max_mireds)
|
|
)
|
|
|
|
supported_color_modes = filter_supported_color_modes(supported_color_modes)
|
|
self._attr_supported_color_modes = supported_color_modes
|
|
self._check_transition_blocklist()
|
|
# flag support for transition as soon as we support setting brightness and/or color
|
|
if (
|
|
supported_color_modes != {ColorMode.ONOFF}
|
|
and not self._transitions_disabled
|
|
):
|
|
self._attr_supported_features |= LightEntityFeature.TRANSITION
|
|
|
|
LOGGER.debug(
|
|
"Supported color modes: %s for %s",
|
|
self._attr_supported_color_modes,
|
|
self.entity_id,
|
|
)
|
|
|
|
# set current values
|
|
self._attr_is_on = self.get_matter_attribute_value(
|
|
clusters.OnOff.Attributes.OnOff
|
|
)
|
|
|
|
if self._supports_brightness:
|
|
self._attr_brightness = self._get_brightness()
|
|
|
|
if (
|
|
self._supports_color_temperature
|
|
and (color_temperature := self._get_color_temperature()) > 0
|
|
):
|
|
self._attr_color_temp_kelvin = color_util.color_temperature_mired_to_kelvin(
|
|
color_temperature
|
|
)
|
|
|
|
if self._supports_color:
|
|
self._attr_color_mode = color_mode = self._get_color_mode()
|
|
if (
|
|
ColorMode.HS in self._attr_supported_color_modes
|
|
and color_mode == ColorMode.HS
|
|
):
|
|
self._attr_hs_color = self._get_hs_color()
|
|
elif (
|
|
ColorMode.XY in self._attr_supported_color_modes
|
|
and color_mode == ColorMode.XY
|
|
):
|
|
self._attr_xy_color = self._get_xy_color()
|
|
elif self._attr_color_temp_kelvin is not None:
|
|
self._attr_color_mode = ColorMode.COLOR_TEMP
|
|
elif self._attr_brightness is not None:
|
|
self._attr_color_mode = ColorMode.BRIGHTNESS
|
|
else:
|
|
self._attr_color_mode = ColorMode.ONOFF
|
|
|
|
def _check_transition_blocklist(self) -> None:
|
|
"""Check if this device is reported to have non working transitions."""
|
|
device_info = self._endpoint.device_info
|
|
if isinstance(device_info, clusters.BridgedDeviceBasicInformation):
|
|
return
|
|
if (
|
|
device_info.vendorID,
|
|
device_info.productID,
|
|
device_info.hardwareVersionString,
|
|
device_info.softwareVersionString,
|
|
) in TRANSITION_BLOCKLIST:
|
|
self._transitions_disabled = True
|
|
LOGGER.warning(
|
|
"Detected a device that has been reported to have firmware issues "
|
|
"with light transitions. Transitions will be disabled for this light"
|
|
)
|
|
|
|
|
|
# Discovery schema(s) to map Matter Attributes to HA entities
|
|
DISCOVERY_SCHEMAS = [
|
|
MatterDiscoverySchema(
|
|
platform=Platform.LIGHT,
|
|
entity_description=LightEntityDescription(
|
|
key="MatterLight",
|
|
name=None,
|
|
),
|
|
entity_class=MatterLight,
|
|
required_attributes=(clusters.OnOff.Attributes.OnOff,),
|
|
optional_attributes=(
|
|
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,
|
|
),
|
|
device_type=(
|
|
device_types.ColorTemperatureLight,
|
|
device_types.DimmableLight,
|
|
device_types.DimmablePlugInUnit,
|
|
device_types.MountedDimmableLoadControl,
|
|
device_types.ExtendedColorLight,
|
|
device_types.OnOffLight,
|
|
device_types.DimmerSwitch,
|
|
device_types.ColorDimmerSwitch,
|
|
),
|
|
),
|
|
# Additional schema to match (HS Color) lights with incorrect/missing device type
|
|
MatterDiscoverySchema(
|
|
platform=Platform.LIGHT,
|
|
entity_description=LightEntityDescription(
|
|
key="MatterHSColorLightFallback",
|
|
name=None,
|
|
),
|
|
entity_class=MatterLight,
|
|
required_attributes=(
|
|
clusters.OnOff.Attributes.OnOff,
|
|
clusters.ColorControl.Attributes.CurrentHue,
|
|
clusters.ColorControl.Attributes.CurrentSaturation,
|
|
),
|
|
optional_attributes=(
|
|
clusters.LevelControl.Attributes.CurrentLevel,
|
|
clusters.ColorControl.Attributes.ColorTemperatureMireds,
|
|
clusters.ColorControl.Attributes.ColorMode,
|
|
clusters.ColorControl.Attributes.CurrentX,
|
|
clusters.ColorControl.Attributes.CurrentY,
|
|
),
|
|
),
|
|
# Additional schema to match (XY Color) lights with incorrect/missing device type
|
|
MatterDiscoverySchema(
|
|
platform=Platform.LIGHT,
|
|
entity_description=LightEntityDescription(
|
|
key="MatterXYColorLightFallback",
|
|
name=None,
|
|
),
|
|
entity_class=MatterLight,
|
|
required_attributes=(
|
|
clusters.OnOff.Attributes.OnOff,
|
|
clusters.ColorControl.Attributes.CurrentX,
|
|
clusters.ColorControl.Attributes.CurrentY,
|
|
),
|
|
optional_attributes=(
|
|
clusters.LevelControl.Attributes.CurrentLevel,
|
|
clusters.ColorControl.Attributes.ColorTemperatureMireds,
|
|
clusters.ColorControl.Attributes.ColorMode,
|
|
clusters.ColorControl.Attributes.CurrentHue,
|
|
clusters.ColorControl.Attributes.CurrentSaturation,
|
|
),
|
|
),
|
|
# Additional schema to match (color temperature) lights with incorrect/missing device type
|
|
MatterDiscoverySchema(
|
|
platform=Platform.LIGHT,
|
|
entity_description=LightEntityDescription(
|
|
key="MatterColorTemperatureLightFallback",
|
|
name=None,
|
|
),
|
|
entity_class=MatterLight,
|
|
required_attributes=(
|
|
clusters.OnOff.Attributes.OnOff,
|
|
clusters.LevelControl.Attributes.CurrentLevel,
|
|
clusters.ColorControl.Attributes.ColorTemperatureMireds,
|
|
),
|
|
optional_attributes=(clusters.ColorControl.Attributes.ColorMode,),
|
|
),
|
|
]
|