Home Assistant Unofficial Reference 2024.12.1
entity.py
Go to the documentation of this file.
1 """Matter entity base class."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable
6 from dataclasses import dataclass
7 import logging
8 from typing import TYPE_CHECKING, Any, cast
9 
10 from chip.clusters import Objects as clusters
11 from chip.clusters.Objects import ClusterAttributeDescriptor, NullValue
12 from matter_server.common.helpers.util import create_attribute_path
13 from matter_server.common.models import EventType, ServerInfoMessage
14 from propcache import cached_property
15 
16 from homeassistant.core import callback
17 from homeassistant.helpers.device_registry import DeviceInfo
18 from homeassistant.helpers.entity import Entity, EntityDescription
20 from homeassistant.helpers.typing import UndefinedType
21 
22 from .const import DOMAIN, FEATUREMAP_ATTRIBUTE_ID, ID_TYPE_DEVICE_ID
23 from .helpers import get_device_id
24 
25 if TYPE_CHECKING:
26  from matter_server.client import MatterClient
27  from matter_server.client.models.node import MatterEndpoint
28 
29  from .discovery import MatterEntityInfo
30 
31 LOGGER = logging.getLogger(__name__)
32 
33 
34 @dataclass(frozen=True)
36  """Describe the Matter entity."""
37 
38  # convert the value from the primary attribute to the value used by HA
39  measurement_to_ha: Callable[[Any], Any] | None = None
40  ha_to_native_value: Callable[[Any], Any] | None = None
41 
42 
44  """Entity class for Matter devices."""
45 
46  _attr_has_entity_name = True
47  _attr_should_poll = False
48  _name_postfix: str | None = None
49  _platform_translation_key: str | None = None
50 
51  def __init__(
52  self,
53  matter_client: MatterClient,
54  endpoint: MatterEndpoint,
55  entity_info: MatterEntityInfo,
56  ) -> None:
57  """Initialize the entity."""
58  self.matter_clientmatter_client = matter_client
59  self._endpoint_endpoint = endpoint
60  self._entity_info_entity_info = entity_info
61  self.entity_descriptionentity_description = entity_info.entity_description
62  self._unsubscribes: list[Callable] = []
63  # for fast lookups we create a mapping to the attribute paths
64  self._attributes_map: dict[type, str] = {}
65  # The server info is set when the client connects to the server.
66  server_info = cast(ServerInfoMessage, self.matter_clientmatter_client.server_info)
67  # create unique_id based on "Operational Instance Name" and endpoint/device type
68  node_device_id = get_device_id(server_info, endpoint)
69  self._attr_unique_id_attr_unique_id = (
70  f"{node_device_id}-"
71  f"{endpoint.endpoint_id}-"
72  f"{entity_info.entity_description.key}-"
73  f"{entity_info.primary_attribute.cluster_id}-"
74  f"{entity_info.primary_attribute.attribute_id}"
75  )
76  self._attr_device_info_attr_device_info = DeviceInfo(
77  identifiers={(DOMAIN, f"{ID_TYPE_DEVICE_ID}_{node_device_id}")}
78  )
79  self._attr_available_attr_available = self._endpoint_endpoint.node.available
80  # mark endpoint postfix if the device has the primary attribute on multiple endpoints
81  if not self._endpoint_endpoint.node.is_bridge_device and any(
82  ep
83  for ep in self._endpoint_endpoint.node.endpoints.values()
84  if ep != self._endpoint_endpoint
85  and ep.has_attribute(None, entity_info.primary_attribute)
86  ):
87  self._name_postfix_name_postfix = str(self._endpoint_endpoint.endpoint_id)
88  if self._platform_translation_key and not self.translation_keytranslation_key:
89  self._attr_translation_key_attr_translation_key = self._platform_translation_key
90 
91  # prefer the label attribute for the entity name
92  # Matter has a way for users and/or vendors to specify a name for an endpoint
93  # which is always preferred over a standard HA (generated) name
94  for attr in (
95  clusters.FixedLabel.Attributes.LabelList,
96  clusters.UserLabel.Attributes.LabelList,
97  ):
98  if not (labels := self.get_matter_attribute_valueget_matter_attribute_value(attr)):
99  continue
100  for label in labels:
101  if label.label not in ["Label", "Button"]:
102  continue
103  # fixed or user label found: use it
104  label_value: str = label.value
105  # in the case the label is only the label id, use it as postfix only
106  if label_value.isnumeric():
107  self._name_postfix_name_postfix = label_value
108  else:
109  self._attr_name_attr_name = label_value
110  break
111 
112  # make sure to update the attributes once
113  self._update_from_device_update_from_device()
114 
115  async def async_added_to_hass(self) -> None:
116  """Handle being added to Home Assistant."""
117  await super().async_added_to_hass()
118 
119  # Subscribe to attribute updates.
120  sub_paths: list[str] = []
121  for attr_cls in self._entity_info_entity_info.attributes_to_watch:
122  attr_path = self.get_matter_attribute_pathget_matter_attribute_path(attr_cls)
123  if attr_path in sub_paths:
124  # prevent duplicate subscriptions
125  continue
126  self._attributes_map[attr_cls] = attr_path
127  sub_paths.append(attr_path)
128  self._unsubscribes.append(
129  self.matter_clientmatter_client.subscribe_events(
130  callback=self._on_matter_event_on_matter_event,
131  event_filter=EventType.ATTRIBUTE_UPDATED,
132  node_filter=self._endpoint_endpoint.node.node_id,
133  attr_path_filter=attr_path,
134  )
135  )
136  # subscribe to node (availability changes)
137  self._unsubscribes.append(
138  self.matter_clientmatter_client.subscribe_events(
139  callback=self._on_matter_event_on_matter_event,
140  event_filter=EventType.NODE_UPDATED,
141  node_filter=self._endpoint_endpoint.node.node_id,
142  )
143  )
144  # subscribe to FeatureMap attribute (as that can dynamically change)
145  self._unsubscribes.append(
146  self.matter_clientmatter_client.subscribe_events(
147  callback=self._on_featuremap_update_on_featuremap_update,
148  event_filter=EventType.ATTRIBUTE_UPDATED,
149  node_filter=self._endpoint_endpoint.node.node_id,
150  attr_path_filter=create_attribute_path(
151  endpoint=self._endpoint_endpoint.endpoint_id,
152  cluster_id=self._entity_info_entity_info.primary_attribute.cluster_id,
153  attribute_id=FEATUREMAP_ATTRIBUTE_ID,
154  ),
155  )
156  )
157 
158  @cached_property
159  def name(self) -> str | UndefinedType | None:
160  """Return the name of the entity."""
161  if hasattr(self, "_attr_name"):
162  # an explicit entity name was defined, we use that
163  return self._attr_name_attr_name
164  name = super().name
165  if name and self._name_postfix_name_postfix:
166  name = f"{name} ({self._name_postfix})"
167  return name
168 
169  @callback
170  def _on_matter_event(self, event: EventType, data: Any = None) -> None:
171  """Call on update from the device."""
172  self._attr_available_attr_available = self._endpoint_endpoint.node.available
173  self._update_from_device_update_from_device()
174  self.async_write_ha_stateasync_write_ha_state()
175 
176  @callback
178  self, event: EventType, data: tuple[int, str, int] | None
179  ) -> None:
180  """Handle FeatureMap attribute updates."""
181  if data is None:
182  return
183  new_value = data[2]
184  # handle edge case where a Feature is removed from a cluster
185  if (
186  self._entity_info_entity_info.discovery_schema.featuremap_contains is not None
187  and not bool(
188  new_value & self._entity_info_entity_info.discovery_schema.featuremap_contains
189  )
190  ):
191  # this entity is no longer supported by the device
192  ent_reg = er.async_get(self.hasshass)
193  ent_reg.async_remove(self.entity_identity_id)
194 
195  return
196  # all other cases, just update the entity
197  self._on_matter_event_on_matter_event(event, data)
198 
199  @callback
200  def _update_from_device(self) -> None:
201  """Update data from Matter device."""
202 
203  @callback
205  self, attribute: type[ClusterAttributeDescriptor], null_as_none: bool = True
206  ) -> Any:
207  """Get current value for given attribute."""
208  value = self._endpoint_endpoint.get_attribute_value(None, attribute)
209  if null_as_none and value == NullValue:
210  return None
211  return value
212 
213  @callback
215  self, attribute: type[ClusterAttributeDescriptor]
216  ) -> str:
217  """Return AttributePath by providing the endpoint and Attribute class."""
218  return create_attribute_path(
219  self._endpoint_endpoint.endpoint_id, attribute.cluster_id, attribute.attribute_id
220  )
None __init__(self, MatterClient matter_client, MatterEndpoint endpoint, MatterEntityInfo entity_info)
Definition: entity.py:56
str get_matter_attribute_path(self, type[ClusterAttributeDescriptor] attribute)
Definition: entity.py:216
None _on_matter_event(self, EventType event, Any data=None)
Definition: entity.py:170
None _on_featuremap_update(self, EventType event, tuple[int, str, int]|None data)
Definition: entity.py:179
Any get_matter_attribute_value(self, type[ClusterAttributeDescriptor] attribute, bool null_as_none=True)
Definition: entity.py:206
str get_device_id(ServerInfoMessage server_info, MatterEndpoint endpoint)
Definition: helpers.py:59