-
-
Notifications
You must be signed in to change notification settings - Fork 31.3k
/
entity.py
182 lines (158 loc) · 6.85 KB
/
entity.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
"""Matter entity base class."""
from __future__ import annotations
from abc import abstractmethod
from collections.abc import Callable
from dataclasses import dataclass
from functools import cached_property
import logging
from typing import TYPE_CHECKING, Any, cast
from chip.clusters import Objects as clusters
from chip.clusters.Objects import ClusterAttributeDescriptor, NullValue
from matter_server.common.helpers.util import create_attribute_path
from matter_server.common.models import EventType, ServerInfoMessage
from homeassistant.core import callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.typing import UndefinedType
from .const import DOMAIN, ID_TYPE_DEVICE_ID
from .helpers import get_device_id
if TYPE_CHECKING:
from matter_server.client import MatterClient
from matter_server.client.models.node import MatterEndpoint
from .discovery import MatterEntityInfo
LOGGER = logging.getLogger(__name__)
@dataclass(frozen=True)
class MatterEntityDescription(EntityDescription):
"""Describe the Matter entity."""
# convert the value from the primary attribute to the value used by HA
measurement_to_ha: Callable[[Any], Any] | None = None
ha_to_native_value: Callable[[Any], Any] | None = None
class MatterEntity(Entity):
"""Entity class for Matter devices."""
_attr_has_entity_name = True
_attr_should_poll = False
_name_postfix: str | None = None
def __init__(
self,
matter_client: MatterClient,
endpoint: MatterEndpoint,
entity_info: MatterEntityInfo,
) -> None:
"""Initialize the entity."""
self.matter_client = matter_client
self._endpoint = endpoint
self._entity_info = entity_info
self.entity_description = entity_info.entity_description
self._unsubscribes: list[Callable] = []
# for fast lookups we create a mapping to the attribute paths
self._attributes_map: dict[type, str] = {}
# The server info is set when the client connects to the server.
server_info = cast(ServerInfoMessage, self.matter_client.server_info)
# create unique_id based on "Operational Instance Name" and endpoint/device type
node_device_id = get_device_id(server_info, endpoint)
self._attr_unique_id = (
f"{node_device_id}-"
f"{endpoint.endpoint_id}-"
f"{entity_info.entity_description.key}-"
f"{entity_info.primary_attribute.cluster_id}-"
f"{entity_info.primary_attribute.attribute_id}"
)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{ID_TYPE_DEVICE_ID}_{node_device_id}")}
)
self._attr_available = self._endpoint.node.available
# mark endpoint postfix if the device has the primary attribute on multiple endpoints
if not self._endpoint.node.is_bridge_device and any(
ep
for ep in self._endpoint.node.endpoints.values()
if ep != self._endpoint
and ep.has_attribute(None, entity_info.primary_attribute)
):
self._name_postfix = str(self._endpoint.endpoint_id)
# prefer the label attribute for the entity name
# Matter has a way for users and/or vendors to specify a name for an endpoint
# which is always preferred over a standard HA (generated) name
for attr in (
clusters.FixedLabel.Attributes.LabelList,
clusters.UserLabel.Attributes.LabelList,
):
if not (labels := self.get_matter_attribute_value(attr)):
continue
for label in labels:
if label.label not in ["Label", "Button"]:
continue
# fixed or user label found: use it
label_value: str = label.value
# in the case the label is only the label id, use it as postfix only
if label_value.isnumeric():
self._name_postfix = label_value
else:
self._attr_name = label_value
break
# make sure to update the attributes once
self._update_from_device()
async def async_added_to_hass(self) -> None:
"""Handle being added to Home Assistant."""
await super().async_added_to_hass()
# Subscribe to attribute updates.
sub_paths: list[str] = []
for attr_cls in self._entity_info.attributes_to_watch:
attr_path = self.get_matter_attribute_path(attr_cls)
if attr_path in sub_paths:
# prevent duplicate subscriptions
continue
self._attributes_map[attr_cls] = attr_path
sub_paths.append(attr_path)
self._unsubscribes.append(
self.matter_client.subscribe_events(
callback=self._on_matter_event,
event_filter=EventType.ATTRIBUTE_UPDATED,
node_filter=self._endpoint.node.node_id,
attr_path_filter=attr_path,
)
)
# subscribe to node (availability changes)
self._unsubscribes.append(
self.matter_client.subscribe_events(
callback=self._on_matter_event,
event_filter=EventType.NODE_UPDATED,
node_filter=self._endpoint.node.node_id,
)
)
@cached_property
def name(self) -> str | UndefinedType | None:
"""Return the name of the entity."""
if hasattr(self, "_attr_name"):
# an explicit entity name was defined, we use that
return self._attr_name
name = super().name
if name and self._name_postfix:
name = f"{name} ({self._name_postfix})"
return name
@callback
def _on_matter_event(self, event: EventType, data: Any = None) -> None:
"""Call on update from the device."""
self._attr_available = self._endpoint.node.available
self._update_from_device()
self.async_write_ha_state()
@callback
@abstractmethod
def _update_from_device(self) -> None:
"""Update data from Matter device."""
@callback
def get_matter_attribute_value(
self, attribute: type[ClusterAttributeDescriptor], null_as_none: bool = True
) -> Any:
"""Get current value for given attribute."""
value = self._endpoint.get_attribute_value(None, attribute)
if null_as_none and value == NullValue:
return None
return value
@callback
def get_matter_attribute_path(
self, attribute: type[ClusterAttributeDescriptor]
) -> str:
"""Return AttributePath by providing the endpoint and Attribute class."""
return create_attribute_path(
self._endpoint.endpoint_id, attribute.cluster_id, attribute.attribute_id
)