Home Assistant Unofficial Reference 2024.12.1
light.py
Go to the documentation of this file.
1 """Matter light."""
2 
3 from __future__ import annotations
4 
5 from typing import Any
6 
7 from chip.clusters import Objects as clusters
8 from matter_server.client.models import device_types
9 
11  ATTR_BRIGHTNESS,
12  ATTR_COLOR_TEMP,
13  ATTR_HS_COLOR,
14  ATTR_TRANSITION,
15  ATTR_XY_COLOR,
16  ColorMode,
17  LightEntity,
18  LightEntityDescription,
19  LightEntityFeature,
20  filter_supported_color_modes,
21 )
22 from homeassistant.config_entries import ConfigEntry
23 from homeassistant.const import Platform
24 from homeassistant.core import HomeAssistant, callback
25 from homeassistant.helpers.entity_platform import AddEntitiesCallback
26 
27 from .const import LOGGER
28 from .entity import MatterEntity
29 from .helpers import get_matter
30 from .models import MatterDiscoverySchema
31 from .util import (
32  convert_to_hass_hs,
33  convert_to_hass_xy,
34  convert_to_matter_hs,
35  convert_to_matter_xy,
36  renormalize,
37 )
38 
39 COLOR_MODE_MAP = {
40  clusters.ColorControl.Enums.ColorMode.kCurrentHueAndCurrentSaturation: ColorMode.HS,
41  clusters.ColorControl.Enums.ColorMode.kCurrentXAndCurrentY: ColorMode.XY,
42  clusters.ColorControl.Enums.ColorMode.kColorTemperature: ColorMode.COLOR_TEMP,
43 }
44 
45 # there's a bug in (at least) Espressif's implementation of light transitions
46 # on devices based on Matter 1.0. Mark potential devices with this issue.
47 # https://github.com/home-assistant/core/issues/113775
48 # vendorid (attributeKey 0/40/2)
49 # productid (attributeKey 0/40/4)
50 # hw version (attributeKey 0/40/8)
51 # sw version (attributeKey 0/40/10)
52 TRANSITION_BLOCKLIST = (
53  (4107, 8475, "v1.0", "v1.0"),
54  (4107, 8550, "v1.0", "v1.0"),
55  (4107, 8551, "v1.0", "v1.0"),
56  (4107, 8571, "v1.0", "v1.0"),
57  (4107, 8656, "v1.0", "v1.0"),
58  (4448, 36866, "V1", "V1.0.0.5"),
59  (4456, 1011, "1.0.0", "2.00.00"),
60  (4488, 260, "1.0", "1.0.0"),
61  (4488, 514, "1.0", "1.0.0"),
62  (4921, 42, "1.0", "1.01.060"),
63  (4921, 43, "1.0", "1.01.060"),
64  (4999, 24875, "1.0", "27.0"),
65  (4999, 25057, "1.0", "27.0"),
66  (5009, 514, "1.0", "1.0.0"),
67  (5010, 769, "3.0", "1.0.0"),
68  (5130, 544, "v0.4", "6.7.196e9d4e08-14"),
69  (5127, 4232, "ver_0.1", "v1.00.51"),
70  (5245, 1412, "1.0", "1.0.21"),
71 )
72 
73 
75  hass: HomeAssistant,
76  config_entry: ConfigEntry,
77  async_add_entities: AddEntitiesCallback,
78 ) -> None:
79  """Set up Matter Light from Config Entry."""
80  matter = get_matter(hass)
81  matter.register_platform_handler(Platform.LIGHT, async_add_entities)
82 
83 
85  """Representation of a Matter light."""
86 
87  entity_description: LightEntityDescription
88  _supports_brightness = False
89  _supports_color = False
90  _supports_color_temperature = False
91  _transitions_disabled = False
92  _platform_translation_key = "light"
93 
94  async def _set_xy_color(
95  self, xy_color: tuple[float, float], transition: float = 0.0
96  ) -> None:
97  """Set xy color."""
98 
99  matter_xy = convert_to_matter_xy(xy_color)
100 
101  await self.send_device_commandsend_device_command(
102  clusters.ColorControl.Commands.MoveToColor(
103  colorX=int(matter_xy[0]),
104  colorY=int(matter_xy[1]),
105  # transition in matter is measured in tenths of a second
106  transitionTime=int(transition * 10),
107  # allow setting the color while the light is off,
108  # by setting the optionsMask to 1 (=ExecuteIfOff)
109  optionsMask=1,
110  optionsOverride=1,
111  )
112  )
113 
114  async def _set_hs_color(
115  self, hs_color: tuple[float, float], transition: float = 0.0
116  ) -> None:
117  """Set hs color."""
118 
119  matter_hs = convert_to_matter_hs(hs_color)
120 
121  await self.send_device_commandsend_device_command(
122  clusters.ColorControl.Commands.MoveToHueAndSaturation(
123  hue=int(matter_hs[0]),
124  saturation=int(matter_hs[1]),
125  # transition in matter is measured in tenths of a second
126  transitionTime=int(transition * 10),
127  # allow setting the color while the light is off,
128  # by setting the optionsMask to 1 (=ExecuteIfOff)
129  optionsMask=1,
130  optionsOverride=1,
131  )
132  )
133 
134  async def _set_color_temp(self, color_temp: int, transition: float = 0.0) -> None:
135  """Set color temperature."""
136 
137  await self.send_device_commandsend_device_command(
138  clusters.ColorControl.Commands.MoveToColorTemperature(
139  colorTemperatureMireds=color_temp,
140  # transition in matter is measured in tenths of a second
141  transitionTime=int(transition * 10),
142  # allow setting the color while the light is off,
143  # by setting the optionsMask to 1 (=ExecuteIfOff)
144  optionsMask=1,
145  optionsOverride=1,
146  )
147  )
148 
149  async def _set_brightness(self, brightness: int, transition: float = 0.0) -> None:
150  """Set brightness."""
151 
152  level_control = self._endpoint_endpoint.get_cluster(clusters.LevelControl)
153 
154  assert level_control is not None
155 
156  level = round( # type: ignore[unreachable]
157  renormalize(
158  brightness,
159  (0, 255),
160  (level_control.minLevel or 1, level_control.maxLevel or 254),
161  )
162  )
163 
164  await self.send_device_commandsend_device_command(
165  clusters.LevelControl.Commands.MoveToLevelWithOnOff(
166  level=level,
167  # transition in matter is measured in tenths of a second
168  transitionTime=int(transition * 10),
169  )
170  )
171 
172  def _get_xy_color(self) -> tuple[float, float]:
173  """Get xy color from matter."""
174 
175  x_color = self.get_matter_attribute_valueget_matter_attribute_value(
176  clusters.ColorControl.Attributes.CurrentX
177  )
178  y_color = self.get_matter_attribute_valueget_matter_attribute_value(
179  clusters.ColorControl.Attributes.CurrentY
180  )
181 
182  assert x_color is not None
183  assert y_color is not None
184 
185  xy_color = convert_to_hass_xy((x_color, y_color))
186  LOGGER.debug(
187  "Got xy color %s for %s",
188  xy_color,
189  self.entity_identity_id,
190  )
191 
192  return xy_color
193 
194  def _get_hs_color(self) -> tuple[float, float]:
195  """Get hs color from matter."""
196 
197  hue = self.get_matter_attribute_valueget_matter_attribute_value(
198  clusters.ColorControl.Attributes.CurrentHue
199  )
200 
201  saturation = self.get_matter_attribute_valueget_matter_attribute_value(
202  clusters.ColorControl.Attributes.CurrentSaturation
203  )
204 
205  assert hue is not None
206  assert saturation is not None
207 
208  hs_color = convert_to_hass_hs((hue, saturation))
209 
210  LOGGER.debug(
211  "Got hs color %s for %s",
212  hs_color,
213  self.entity_identity_id,
214  )
215 
216  return hs_color
217 
218  def _get_color_temperature(self) -> int:
219  """Get color temperature from matter."""
220 
221  color_temp = self.get_matter_attribute_valueget_matter_attribute_value(
222  clusters.ColorControl.Attributes.ColorTemperatureMireds
223  )
224 
225  assert color_temp is not None
226 
227  LOGGER.debug(
228  "Got color temperature %s for %s",
229  color_temp,
230  self.entity_identity_id,
231  )
232 
233  return int(color_temp)
234 
235  def _get_brightness(self) -> int:
236  """Get brightness from matter."""
237 
238  level_control = self._endpoint_endpoint.get_cluster(clusters.LevelControl)
239 
240  # We should not get here if brightness is not supported.
241  assert level_control is not None
242 
243  LOGGER.debug( # type: ignore[unreachable]
244  "Got brightness %s for %s",
245  level_control.currentLevel,
246  self.entity_identity_id,
247  )
248 
249  return round(
250  renormalize(
251  level_control.currentLevel,
252  (level_control.minLevel or 1, level_control.maxLevel or 254),
253  (0, 255),
254  )
255  )
256 
257  def _get_color_mode(self) -> ColorMode:
258  """Get color mode from matter."""
259 
260  color_mode = self.get_matter_attribute_valueget_matter_attribute_value(
261  clusters.ColorControl.Attributes.ColorMode
262  )
263 
264  assert color_mode is not None
265 
266  ha_color_mode = COLOR_MODE_MAP[color_mode]
267 
268  LOGGER.debug(
269  "Got color mode (%s) for %s",
270  ha_color_mode,
271  self.entity_identity_id,
272  )
273 
274  return ha_color_mode
275 
276  async def send_device_command(self, command: Any) -> None:
277  """Send device command."""
278  await self.matter_clientmatter_client.send_device_command(
279  node_id=self._endpoint_endpoint.node.node_id,
280  endpoint_id=self._endpoint_endpoint.endpoint_id,
281  command=command,
282  )
283 
284  async def async_turn_on(self, **kwargs: Any) -> None:
285  """Turn light on."""
286 
287  hs_color = kwargs.get(ATTR_HS_COLOR)
288  xy_color = kwargs.get(ATTR_XY_COLOR)
289  color_temp = kwargs.get(ATTR_COLOR_TEMP)
290  brightness = kwargs.get(ATTR_BRIGHTNESS)
291  transition = kwargs.get(ATTR_TRANSITION, 0)
292  if self._transitions_disabled_transitions_disabled_transitions_disabled:
293  transition = 0
294 
295  if self.supported_color_modessupported_color_modes is not None:
296  if hs_color is not None and ColorMode.HS in self.supported_color_modessupported_color_modes:
297  await self._set_hs_color_set_hs_color(hs_color, transition)
298  elif xy_color is not None and ColorMode.XY in self.supported_color_modessupported_color_modes:
299  await self._set_xy_color_set_xy_color(xy_color, transition)
300  elif (
301  color_temp is not None
302  and ColorMode.COLOR_TEMP in self.supported_color_modessupported_color_modes
303  ):
304  await self._set_color_temp_set_color_temp(color_temp, transition)
305 
306  if brightness is not None and self._supports_brightness_supports_brightness_supports_brightness:
307  await self._set_brightness_set_brightness(brightness, transition)
308  return
309 
310  await self.send_device_commandsend_device_command(
311  clusters.OnOff.Commands.On(),
312  )
313 
314  async def async_turn_off(self, **kwargs: Any) -> None:
315  """Turn light off."""
316  await self.send_device_commandsend_device_command(
317  clusters.OnOff.Commands.Off(),
318  )
319 
320  @callback
321  def _update_from_device(self) -> None:
322  """Update from device."""
323  if self._attr_supported_color_modes_attr_supported_color_modes is None:
324  # work out what (color)features are supported
325  supported_color_modes = {ColorMode.ONOFF}
326  # brightness support
327  if self._entity_info_entity_info.endpoint.has_attribute(
328  None, clusters.LevelControl.Attributes.CurrentLevel
329  ) and self._entity_info_entity_info.endpoint.device_types != {device_types.OnOffLight}:
330  # We need to filter out the OnOffLight device type here because
331  # that can have an optional LevelControl cluster present
332  # which we should ignore.
333  supported_color_modes.add(ColorMode.BRIGHTNESS)
334  self._supports_brightness_supports_brightness_supports_brightness = True
335  # colormode(s)
336  if self._entity_info_entity_info.endpoint.has_attribute(
337  None, clusters.ColorControl.Attributes.ColorMode
338  ) and self._entity_info_entity_info.endpoint.has_attribute(
339  None, clusters.ColorControl.Attributes.ColorCapabilities
340  ):
341  capabilities = self.get_matter_attribute_valueget_matter_attribute_value(
342  clusters.ColorControl.Attributes.ColorCapabilities
343  )
344 
345  assert capabilities is not None
346 
347  if (
348  capabilities
349  & clusters.ColorControl.Bitmaps.ColorCapabilities.kHueSaturationSupported
350  ):
351  supported_color_modes.add(ColorMode.HS)
352  self._supports_color_supports_color_supports_color = True
353 
354  if (
355  capabilities
356  & clusters.ColorControl.Bitmaps.ColorCapabilities.kXYAttributesSupported
357  ):
358  supported_color_modes.add(ColorMode.XY)
359  self._supports_color_supports_color_supports_color = True
360 
361  if (
362  capabilities
363  & clusters.ColorControl.Bitmaps.ColorCapabilities.kColorTemperatureSupported
364  ):
365  supported_color_modes.add(ColorMode.COLOR_TEMP)
366  self._supports_color_temperature_supports_color_temperature_supports_color_temperature = True
367  min_mireds = self.get_matter_attribute_valueget_matter_attribute_value(
368  clusters.ColorControl.Attributes.ColorTempPhysicalMinMireds
369  )
370  if min_mireds > 0:
371  self._attr_min_mireds_attr_min_mireds = min_mireds
372  max_mireds = self.get_matter_attribute_valueget_matter_attribute_value(
373  clusters.ColorControl.Attributes.ColorTempPhysicalMaxMireds
374  )
375  if min_mireds > 0:
376  self._attr_max_mireds_attr_max_mireds = max_mireds
377 
378  supported_color_modes = filter_supported_color_modes(supported_color_modes)
379  self._attr_supported_color_modes_attr_supported_color_modes = supported_color_modes
380  self._check_transition_blocklist_check_transition_blocklist()
381  # flag support for transition as soon as we support setting brightness and/or color
382  if (
383  supported_color_modes != {ColorMode.ONOFF}
384  and not self._transitions_disabled_transitions_disabled_transitions_disabled
385  ):
386  self._attr_supported_features |= LightEntityFeature.TRANSITION
387 
388  LOGGER.debug(
389  "Supported color modes: %s for %s",
390  self._attr_supported_color_modes_attr_supported_color_modes,
391  self.entity_identity_id,
392  )
393 
394  # set current values
395  self._attr_is_on_attr_is_on = self.get_matter_attribute_valueget_matter_attribute_value(
396  clusters.OnOff.Attributes.OnOff
397  )
398 
399  if self._supports_brightness_supports_brightness_supports_brightness:
400  self._attr_brightness_attr_brightness = self._get_brightness_get_brightness()
401 
402  if self._supports_color_temperature_supports_color_temperature_supports_color_temperature:
403  self._attr_color_temp_attr_color_temp = self._get_color_temperature_get_color_temperature()
404 
405  if self._supports_color_supports_color_supports_color:
406  self._attr_color_mode_attr_color_mode = color_mode = self._get_color_mode_get_color_mode()
407  if (
408  ColorMode.HS in self._attr_supported_color_modes_attr_supported_color_modes
409  and color_mode == ColorMode.HS
410  ):
411  self._attr_hs_color_attr_hs_color = self._get_hs_color_get_hs_color()
412  elif (
413  ColorMode.XY in self._attr_supported_color_modes_attr_supported_color_modes
414  and color_mode == ColorMode.XY
415  ):
416  self._attr_xy_color_attr_xy_color = self._get_xy_color_get_xy_color()
417  elif self._attr_color_temp_attr_color_temp is not None:
418  self._attr_color_mode_attr_color_mode = ColorMode.COLOR_TEMP
419  elif self._attr_brightness_attr_brightness is not None:
420  self._attr_color_mode_attr_color_mode = ColorMode.BRIGHTNESS
421  else:
422  self._attr_color_mode_attr_color_mode = ColorMode.ONOFF
423 
424  def _check_transition_blocklist(self) -> None:
425  """Check if this device is reported to have non working transitions."""
426  device_info = self._endpoint_endpoint.device_info
427  if isinstance(device_info, clusters.BridgedDeviceBasicInformation):
428  return
429  if (
430  device_info.vendorID,
431  device_info.productID,
432  device_info.hardwareVersionString,
433  device_info.softwareVersionString,
434  ) in TRANSITION_BLOCKLIST:
435  self._transitions_disabled_transitions_disabled_transitions_disabled = True
436  LOGGER.warning(
437  "Detected a device that has been reported to have firmware issues "
438  "with light transitions. Transitions will be disabled for this light"
439  )
440 
441 
442 # Discovery schema(s) to map Matter Attributes to HA entities
443 DISCOVERY_SCHEMAS = [
445  platform=Platform.LIGHT,
446  entity_description=LightEntityDescription(
447  key="MatterLight",
448  name=None,
449  ),
450  entity_class=MatterLight,
451  required_attributes=(clusters.OnOff.Attributes.OnOff,),
452  optional_attributes=(
453  clusters.LevelControl.Attributes.CurrentLevel,
454  clusters.ColorControl.Attributes.ColorMode,
455  clusters.ColorControl.Attributes.CurrentHue,
456  clusters.ColorControl.Attributes.CurrentSaturation,
457  clusters.ColorControl.Attributes.CurrentX,
458  clusters.ColorControl.Attributes.CurrentY,
459  clusters.ColorControl.Attributes.ColorTemperatureMireds,
460  ),
461  device_type=(
462  device_types.ColorTemperatureLight,
463  device_types.DimmableLight,
464  device_types.DimmablePlugInUnit,
465  device_types.ExtendedColorLight,
466  device_types.OnOffLight,
467  device_types.DimmerSwitch,
468  device_types.ColorDimmerSwitch,
469  ),
470  ),
471  # Additional schema to match (HS Color) lights with incorrect/missing device type
473  platform=Platform.LIGHT,
474  entity_description=LightEntityDescription(
475  key="MatterHSColorLightFallback",
476  name=None,
477  ),
478  entity_class=MatterLight,
479  required_attributes=(
480  clusters.OnOff.Attributes.OnOff,
481  clusters.ColorControl.Attributes.CurrentHue,
482  clusters.ColorControl.Attributes.CurrentSaturation,
483  ),
484  optional_attributes=(
485  clusters.LevelControl.Attributes.CurrentLevel,
486  clusters.ColorControl.Attributes.ColorTemperatureMireds,
487  clusters.ColorControl.Attributes.ColorMode,
488  clusters.ColorControl.Attributes.CurrentX,
489  clusters.ColorControl.Attributes.CurrentY,
490  ),
491  ),
492  # Additional schema to match (XY Color) lights with incorrect/missing device type
494  platform=Platform.LIGHT,
495  entity_description=LightEntityDescription(
496  key="MatterXYColorLightFallback",
497  name=None,
498  ),
499  entity_class=MatterLight,
500  required_attributes=(
501  clusters.OnOff.Attributes.OnOff,
502  clusters.ColorControl.Attributes.CurrentX,
503  clusters.ColorControl.Attributes.CurrentY,
504  ),
505  optional_attributes=(
506  clusters.LevelControl.Attributes.CurrentLevel,
507  clusters.ColorControl.Attributes.ColorTemperatureMireds,
508  clusters.ColorControl.Attributes.ColorMode,
509  clusters.ColorControl.Attributes.CurrentHue,
510  clusters.ColorControl.Attributes.CurrentSaturation,
511  ),
512  ),
513  # Additional schema to match (color temperature) lights with incorrect/missing device type
515  platform=Platform.LIGHT,
516  entity_description=LightEntityDescription(
517  key="MatterColorTemperatureLightFallback",
518  name=None,
519  ),
520  entity_class=MatterLight,
521  required_attributes=(
522  clusters.OnOff.Attributes.OnOff,
523  clusters.LevelControl.Attributes.CurrentLevel,
524  clusters.ColorControl.Attributes.ColorTemperatureMireds,
525  ),
526  optional_attributes=(clusters.ColorControl.Attributes.ColorMode,),
527  ),
528 ]
set[ColorMode]|set[str]|None supported_color_modes(self)
Definition: __init__.py:1302
Any get_matter_attribute_value(self, type[ClusterAttributeDescriptor] attribute, bool null_as_none=True)
Definition: entity.py:206
None send_device_command(self, Any command)
Definition: light.py:276
tuple[float, float] _get_xy_color(self)
Definition: light.py:172
None async_turn_off(self, **Any kwargs)
Definition: light.py:314
tuple[float, float] _get_hs_color(self)
Definition: light.py:194
None async_turn_on(self, **Any kwargs)
Definition: light.py:284
None _set_brightness(self, int brightness, float transition=0.0)
Definition: light.py:149
None _set_color_temp(self, int color_temp, float transition=0.0)
Definition: light.py:134
None _set_hs_color(self, tuple[float, float] hs_color, float transition=0.0)
Definition: light.py:116
None _set_xy_color(self, tuple[float, float] xy_color, float transition=0.0)
Definition: light.py:96
set[ColorMode] filter_supported_color_modes(Iterable[ColorMode] color_modes)
Definition: __init__.py:122
MatterAdapter get_matter(HomeAssistant hass)
Definition: helpers.py:35
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: light.py:78
tuple[float, float] convert_to_hass_xy(tuple[float, float] matter_xy)
Definition: util.py:41
tuple[float, float] convert_to_matter_hs(tuple[float, float] hass_hs)
Definition: util.py:17
float renormalize(float number, tuple[float, float] from_range, tuple[float, float] to_range)
Definition: util.py:10
tuple[float, float] convert_to_hass_hs(tuple[float, float] matter_hs)
Definition: util.py:26
tuple[float, float] convert_to_matter_xy(tuple[float, float] hass_xy)
Definition: util.py:35