Home Assistant Unofficial Reference 2024.12.1
cover.py
Go to the documentation of this file.
1 """Matter cover."""
2 
3 from __future__ import annotations
4 
5 from enum import IntEnum
6 from math import floor
7 from typing import Any
8 
9 from chip.clusters import Objects as clusters
10 
12  ATTR_POSITION,
13  ATTR_TILT_POSITION,
14  CoverDeviceClass,
15  CoverEntity,
16  CoverEntityDescription,
17  CoverEntityFeature,
18 )
19 from homeassistant.config_entries import ConfigEntry
20 from homeassistant.const import Platform
21 from homeassistant.core import HomeAssistant, callback
22 from homeassistant.helpers.entity_platform import AddEntitiesCallback
23 
24 from .const import LOGGER
25 from .entity import MatterEntity
26 from .helpers import get_matter
27 from .models import MatterDiscoverySchema
28 
29 # The MASK used for extracting bits 0 to 1 of the byte.
30 OPERATIONAL_STATUS_MASK = 0b11
31 
32 # map Matter window cover types to HA device class
33 TYPE_MAP = {
34  clusters.WindowCovering.Enums.Type.kAwning: CoverDeviceClass.AWNING,
35  clusters.WindowCovering.Enums.Type.kDrapery: CoverDeviceClass.CURTAIN,
36 }
37 
38 
39 class OperationalStatus(IntEnum):
40  """Currently ongoing operations enumeration for coverings, as defined in the Matter spec."""
41 
42  COVERING_IS_CURRENTLY_NOT_MOVING = 0b00
43  COVERING_IS_CURRENTLY_OPENING = 0b01
44  COVERING_IS_CURRENTLY_CLOSING = 0b10
45  RESERVED = 0b11
46 
47 
49  hass: HomeAssistant,
50  config_entry: ConfigEntry,
51  async_add_entities: AddEntitiesCallback,
52 ) -> None:
53  """Set up Matter Cover from Config Entry."""
54  matter = get_matter(hass)
55  matter.register_platform_handler(Platform.COVER, async_add_entities)
56 
57 
59  """Representation of a Matter Cover."""
60 
61  entity_description: CoverEntityDescription
62 
63  @property
64  def is_closed(self) -> bool | None:
65  """Return true if cover is closed, if there is no position report, return None."""
66  if not self._entity_info_entity_info.endpoint.has_attribute(
67  None, clusters.WindowCovering.Attributes.CurrentPositionLiftPercent100ths
68  ):
69  return None
70 
71  return (
73  if self.current_cover_positioncurrent_cover_positioncurrent_cover_positioncurrent_cover_position is not None
74  else None
75  )
76 
77  async def async_stop_cover(self, **kwargs: Any) -> None:
78  """Stop the cover movement."""
79  await self.send_device_commandsend_device_command(clusters.WindowCovering.Commands.StopMotion())
80 
81  async def async_open_cover(self, **kwargs: Any) -> None:
82  """Open the cover."""
83  await self.send_device_commandsend_device_command(clusters.WindowCovering.Commands.UpOrOpen())
84 
85  async def async_close_cover(self, **kwargs: Any) -> None:
86  """Close the cover."""
87  await self.send_device_commandsend_device_command(clusters.WindowCovering.Commands.DownOrClose())
88 
89  async def async_set_cover_position(self, **kwargs: Any) -> None:
90  """Set the cover to a specific position."""
91  position = kwargs[ATTR_POSITION]
92  await self.send_device_commandsend_device_command(
93  # value needs to be inverted and is sent in 100ths
94  clusters.WindowCovering.Commands.GoToLiftPercentage((100 - position) * 100)
95  )
96 
97  async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
98  """Set the cover tilt to a specific position."""
99  position = kwargs[ATTR_TILT_POSITION]
100  await self.send_device_commandsend_device_command(
101  # value needs to be inverted and is sent in 100ths
102  clusters.WindowCovering.Commands.GoToTiltPercentage((100 - position) * 100)
103  )
104 
105  async def send_device_command(self, command: Any) -> None:
106  """Send device command."""
107  await self.matter_clientmatter_client.send_device_command(
108  node_id=self._endpoint_endpoint.node.node_id,
109  endpoint_id=self._endpoint_endpoint.endpoint_id,
110  command=command,
111  )
112 
113  @callback
114  def _update_from_device(self) -> None:
115  """Update from device."""
116  operational_status = self.get_matter_attribute_valueget_matter_attribute_value(
117  clusters.WindowCovering.Attributes.OperationalStatus
118  )
119 
120  assert operational_status is not None
121 
122  LOGGER.debug(
123  "Operational status %s for %s",
124  f"{operational_status:#010b}",
125  self.entity_identity_id,
126  )
127 
128  state = operational_status & OPERATIONAL_STATUS_MASK
129  match state:
130  case OperationalStatus.COVERING_IS_CURRENTLY_OPENING:
131  self._attr_is_opening_attr_is_opening = True
132  self._attr_is_closing_attr_is_closing = False
133  case OperationalStatus.COVERING_IS_CURRENTLY_CLOSING:
134  self._attr_is_opening_attr_is_opening = False
135  self._attr_is_closing_attr_is_closing = True
136  case _:
137  self._attr_is_opening_attr_is_opening = False
138  self._attr_is_closing_attr_is_closing = False
139 
140  if self._entity_info_entity_info.endpoint.has_attribute(
141  None, clusters.WindowCovering.Attributes.CurrentPositionLiftPercent100ths
142  ):
143  # current position is inverted in matter (100 is closed, 0 is open)
144  current_cover_position = self.get_matter_attribute_valueget_matter_attribute_value(
145  clusters.WindowCovering.Attributes.CurrentPositionLiftPercent100ths
146  )
147  self._attr_current_cover_position_attr_current_cover_position = (
148  100 - floor(current_cover_position / 100)
149  if current_cover_position is not None
150  else None
151  )
152 
153  LOGGER.debug(
154  "Current position for %s - raw: %s - corrected: %s",
155  self.entity_identity_id,
156  current_cover_position,
158  )
159 
160  if self._entity_info_entity_info.endpoint.has_attribute(
161  None, clusters.WindowCovering.Attributes.CurrentPositionTiltPercent100ths
162  ):
163  # current tilt position is inverted in matter (100 is closed, 0 is open)
164  current_cover_tilt_position = self.get_matter_attribute_valueget_matter_attribute_value(
165  clusters.WindowCovering.Attributes.CurrentPositionTiltPercent100ths
166  )
167  self._attr_current_cover_tilt_position_attr_current_cover_tilt_position = (
168  100 - floor(current_cover_tilt_position / 100)
169  if current_cover_tilt_position is not None
170  else None
171  )
172 
173  LOGGER.debug(
174  "Current tilt position for %s - raw: %s - corrected: %s",
175  self.entity_identity_id,
176  current_cover_tilt_position,
177  self.current_cover_tilt_positioncurrent_cover_tilt_positioncurrent_cover_tilt_position,
178  )
179 
180  # map matter type to HA deviceclass
181  device_type: clusters.WindowCovering.Enums.Type = (
182  self.get_matter_attribute_valueget_matter_attribute_value(clusters.WindowCovering.Attributes.Type)
183  )
184  self._attr_device_class_attr_device_class = TYPE_MAP.get(device_type, CoverDeviceClass.AWNING)
185 
186  supported_features = (
187  CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE | CoverEntityFeature.STOP
188  )
189  commands = self.get_matter_attribute_valueget_matter_attribute_value(
190  clusters.WindowCovering.Attributes.AcceptedCommandList
191  )
192  if clusters.WindowCovering.Commands.GoToLiftPercentage.command_id in commands:
193  supported_features |= CoverEntityFeature.SET_POSITION
194  if clusters.WindowCovering.Commands.GoToTiltPercentage.command_id in commands:
195  supported_features |= CoverEntityFeature.SET_TILT_POSITION
196  self._attr_supported_features_attr_supported_features = supported_features
197 
198 
199 # Discovery schema(s) to map Matter Attributes to HA entities
200 DISCOVERY_SCHEMAS = [
202  platform=Platform.COVER,
203  entity_description=CoverEntityDescription(
204  key="MatterCover",
205  name=None,
206  ),
207  entity_class=MatterCover,
208  required_attributes=(
209  clusters.WindowCovering.Attributes.OperationalStatus,
210  clusters.WindowCovering.Attributes.Type,
211  ),
212  absent_attributes=(
213  clusters.WindowCovering.Attributes.CurrentPositionLiftPercent100ths,
214  clusters.WindowCovering.Attributes.CurrentPositionTiltPercent100ths,
215  ),
216  ),
218  platform=Platform.COVER,
219  entity_description=CoverEntityDescription(
220  key="MatterCoverPositionAwareLift", name=None
221  ),
222  entity_class=MatterCover,
223  required_attributes=(
224  clusters.WindowCovering.Attributes.OperationalStatus,
225  clusters.WindowCovering.Attributes.Type,
226  clusters.WindowCovering.Attributes.CurrentPositionLiftPercent100ths,
227  ),
228  absent_attributes=(
229  clusters.WindowCovering.Attributes.CurrentPositionTiltPercent100ths,
230  ),
231  ),
233  platform=Platform.COVER,
234  entity_description=CoverEntityDescription(
235  key="MatterCoverPositionAwareTilt", name=None
236  ),
237  entity_class=MatterCover,
238  required_attributes=(
239  clusters.WindowCovering.Attributes.OperationalStatus,
240  clusters.WindowCovering.Attributes.Type,
241  clusters.WindowCovering.Attributes.CurrentPositionTiltPercent100ths,
242  ),
243  absent_attributes=(
244  clusters.WindowCovering.Attributes.CurrentPositionLiftPercent100ths,
245  ),
246  ),
248  platform=Platform.COVER,
249  entity_description=CoverEntityDescription(
250  key="MatterCoverPositionAwareLiftAndTilt", name=None
251  ),
252  entity_class=MatterCover,
253  required_attributes=(
254  clusters.WindowCovering.Attributes.OperationalStatus,
255  clusters.WindowCovering.Attributes.Type,
256  clusters.WindowCovering.Attributes.CurrentPositionLiftPercent100ths,
257  clusters.WindowCovering.Attributes.CurrentPositionTiltPercent100ths,
258  ),
259  ),
260 ]
None async_stop_cover(self, **Any kwargs)
Definition: cover.py:77
None async_open_cover(self, **Any kwargs)
Definition: cover.py:81
None send_device_command(self, Any command)
Definition: cover.py:105
None async_set_cover_tilt_position(self, **Any kwargs)
Definition: cover.py:97
None async_close_cover(self, **Any kwargs)
Definition: cover.py:85
None async_set_cover_position(self, **Any kwargs)
Definition: cover.py:89
Any get_matter_attribute_value(self, type[ClusterAttributeDescriptor] attribute, bool null_as_none=True)
Definition: entity.py:206
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: cover.py:52
MatterAdapter get_matter(HomeAssistant hass)
Definition: helpers.py:35