Home Assistant Unofficial Reference 2024.12.1
cover.py
Go to the documentation of this file.
1 """Platform allowing several cover to be grouped into one cover."""
2 
3 from __future__ import annotations
4 
5 from typing import Any
6 
7 import voluptuous as vol
8 
10  ATTR_CURRENT_POSITION,
11  ATTR_CURRENT_TILT_POSITION,
12  ATTR_POSITION,
13  ATTR_TILT_POSITION,
14  DOMAIN as COVER_DOMAIN,
15  PLATFORM_SCHEMA as COVER_PLATFORM_SCHEMA,
16  CoverEntity,
17  CoverEntityFeature,
18  CoverState,
19 )
20 from homeassistant.config_entries import ConfigEntry
21 from homeassistant.const import (
22  ATTR_ENTITY_ID,
23  ATTR_SUPPORTED_FEATURES,
24  CONF_ENTITIES,
25  CONF_NAME,
26  CONF_UNIQUE_ID,
27  SERVICE_CLOSE_COVER,
28  SERVICE_CLOSE_COVER_TILT,
29  SERVICE_OPEN_COVER,
30  SERVICE_OPEN_COVER_TILT,
31  SERVICE_SET_COVER_POSITION,
32  SERVICE_SET_COVER_TILT_POSITION,
33  SERVICE_STOP_COVER,
34  SERVICE_STOP_COVER_TILT,
35  STATE_UNAVAILABLE,
36  STATE_UNKNOWN,
37 )
38 from homeassistant.core import HomeAssistant, State, callback
39 from homeassistant.helpers import config_validation as cv, entity_registry as er
40 from homeassistant.helpers.entity_platform import AddEntitiesCallback
41 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
42 
43 from .entity import GroupEntity
44 from .util import reduce_attribute
45 
46 KEY_OPEN_CLOSE = "open_close"
47 KEY_STOP = "stop"
48 KEY_POSITION = "position"
49 
50 DEFAULT_NAME = "Cover Group"
51 
52 # No limit on parallel updates to enable a group calling another group
53 PARALLEL_UPDATES = 0
54 
55 PLATFORM_SCHEMA = COVER_PLATFORM_SCHEMA.extend(
56  {
57  vol.Required(CONF_ENTITIES): cv.entities_domain(COVER_DOMAIN),
58  vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
59  vol.Optional(CONF_UNIQUE_ID): cv.string,
60  }
61 )
62 
63 
65  hass: HomeAssistant,
66  config: ConfigType,
67  async_add_entities: AddEntitiesCallback,
68  discovery_info: DiscoveryInfoType | None = None,
69 ) -> None:
70  """Set up the Cover Group platform."""
72  [
73  CoverGroup(
74  config.get(CONF_UNIQUE_ID), config[CONF_NAME], config[CONF_ENTITIES]
75  )
76  ]
77  )
78 
79 
81  hass: HomeAssistant,
82  config_entry: ConfigEntry,
83  async_add_entities: AddEntitiesCallback,
84 ) -> None:
85  """Initialize Cover Group config entry."""
86  registry = er.async_get(hass)
87  entities = er.async_validate_entity_ids(
88  registry, config_entry.options[CONF_ENTITIES]
89  )
90 
92  [CoverGroup(config_entry.entry_id, config_entry.title, entities)]
93  )
94 
95 
96 @callback
98  hass: HomeAssistant, name: str, validated_config: dict[str, Any]
99 ) -> CoverGroup:
100  """Create a preview sensor."""
101  return CoverGroup(
102  None,
103  name,
104  validated_config[CONF_ENTITIES],
105  )
106 
107 
109  """Representation of a CoverGroup."""
110 
111  _attr_available: bool = False
112  _attr_is_closed: bool | None = None
113  _attr_is_opening: bool | None = False
114  _attr_is_closing: bool | None = False
115  _attr_current_cover_position: int | None = 100
116 
117  def __init__(self, unique_id: str | None, name: str, entities: list[str]) -> None:
118  """Initialize a CoverGroup entity."""
119  self._entity_ids_entity_ids = entities
120  self._covers: dict[str, set[str]] = {
121  KEY_OPEN_CLOSE: set(),
122  KEY_STOP: set(),
123  KEY_POSITION: set(),
124  }
125  self._tilts: dict[str, set[str]] = {
126  KEY_OPEN_CLOSE: set(),
127  KEY_STOP: set(),
128  KEY_POSITION: set(),
129  }
130 
131  self._attr_name_attr_name = name
132  self._attr_extra_state_attributes_attr_extra_state_attributes = {ATTR_ENTITY_ID: entities}
133  self._attr_unique_id_attr_unique_id = unique_id
134 
135  @callback
137  self,
138  entity_id: str,
139  new_state: State | None,
140  ) -> None:
141  """Update dictionaries with supported features."""
142  if not new_state:
143  for values in self._covers.values():
144  values.discard(entity_id)
145  for values in self._tilts.values():
146  values.discard(entity_id)
147  return
148 
149  features = new_state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
150 
151  if features & (CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE):
152  self._covers[KEY_OPEN_CLOSE].add(entity_id)
153  else:
154  self._covers[KEY_OPEN_CLOSE].discard(entity_id)
155  if features & (CoverEntityFeature.STOP):
156  self._covers[KEY_STOP].add(entity_id)
157  else:
158  self._covers[KEY_STOP].discard(entity_id)
159  if features & (CoverEntityFeature.SET_POSITION):
160  self._covers[KEY_POSITION].add(entity_id)
161  else:
162  self._covers[KEY_POSITION].discard(entity_id)
163 
164  if features & (CoverEntityFeature.OPEN_TILT | CoverEntityFeature.CLOSE_TILT):
165  self._tilts[KEY_OPEN_CLOSE].add(entity_id)
166  else:
167  self._tilts[KEY_OPEN_CLOSE].discard(entity_id)
168  if features & (CoverEntityFeature.STOP_TILT):
169  self._tilts[KEY_STOP].add(entity_id)
170  else:
171  self._tilts[KEY_STOP].discard(entity_id)
172  if features & (CoverEntityFeature.SET_TILT_POSITION):
173  self._tilts[KEY_POSITION].add(entity_id)
174  else:
175  self._tilts[KEY_POSITION].discard(entity_id)
176 
177  async def async_open_cover(self, **kwargs: Any) -> None:
178  """Move the covers up."""
179  data = {ATTR_ENTITY_ID: self._covers[KEY_OPEN_CLOSE]}
180  await self.hasshass.services.async_call(
181  COVER_DOMAIN, SERVICE_OPEN_COVER, data, blocking=True, context=self._context_context
182  )
183 
184  async def async_close_cover(self, **kwargs: Any) -> None:
185  """Move the covers down."""
186  data = {ATTR_ENTITY_ID: self._covers[KEY_OPEN_CLOSE]}
187  await self.hasshass.services.async_call(
188  COVER_DOMAIN,
189  SERVICE_CLOSE_COVER,
190  data,
191  blocking=True,
192  context=self._context_context,
193  )
194 
195  async def async_stop_cover(self, **kwargs: Any) -> None:
196  """Fire the stop action."""
197  data = {ATTR_ENTITY_ID: self._covers[KEY_STOP]}
198  await self.hasshass.services.async_call(
199  COVER_DOMAIN, SERVICE_STOP_COVER, data, blocking=True, context=self._context_context
200  )
201 
202  async def async_set_cover_position(self, **kwargs: Any) -> None:
203  """Set covers position."""
204  data = {
205  ATTR_ENTITY_ID: self._covers[KEY_POSITION],
206  ATTR_POSITION: kwargs[ATTR_POSITION],
207  }
208  await self.hasshass.services.async_call(
209  COVER_DOMAIN,
210  SERVICE_SET_COVER_POSITION,
211  data,
212  blocking=True,
213  context=self._context_context,
214  )
215 
216  async def async_open_cover_tilt(self, **kwargs: Any) -> None:
217  """Tilt covers open."""
218  data = {ATTR_ENTITY_ID: self._tilts[KEY_OPEN_CLOSE]}
219  await self.hasshass.services.async_call(
220  COVER_DOMAIN,
221  SERVICE_OPEN_COVER_TILT,
222  data,
223  blocking=True,
224  context=self._context_context,
225  )
226 
227  async def async_close_cover_tilt(self, **kwargs: Any) -> None:
228  """Tilt covers closed."""
229  data = {ATTR_ENTITY_ID: self._tilts[KEY_OPEN_CLOSE]}
230  await self.hasshass.services.async_call(
231  COVER_DOMAIN,
232  SERVICE_CLOSE_COVER_TILT,
233  data,
234  blocking=True,
235  context=self._context_context,
236  )
237 
238  async def async_stop_cover_tilt(self, **kwargs: Any) -> None:
239  """Stop cover tilt."""
240  data = {ATTR_ENTITY_ID: self._tilts[KEY_STOP]}
241  await self.hasshass.services.async_call(
242  COVER_DOMAIN,
243  SERVICE_STOP_COVER_TILT,
244  data,
245  blocking=True,
246  context=self._context_context,
247  )
248 
249  async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
250  """Set tilt position."""
251  data = {
252  ATTR_ENTITY_ID: self._tilts[KEY_POSITION],
253  ATTR_TILT_POSITION: kwargs[ATTR_TILT_POSITION],
254  }
255  await self.hasshass.services.async_call(
256  COVER_DOMAIN,
257  SERVICE_SET_COVER_TILT_POSITION,
258  data,
259  blocking=True,
260  context=self._context_context,
261  )
262 
263  @callback
264  def async_update_group_state(self) -> None:
265  """Update state and attributes."""
266  states = [
267  state.state
268  for entity_id in self._entity_ids_entity_ids
269  if (state := self.hasshass.states.get(entity_id)) is not None
270  ]
271 
272  valid_state = any(
273  state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) for state in states
274  )
275 
276  # Set group as unavailable if all members are unavailable or missing
277  self._attr_available_attr_available = any(state != STATE_UNAVAILABLE for state in states)
278 
279  self._attr_is_closed_attr_is_closed = True
280  self._attr_is_closing_attr_is_closing = False
281  self._attr_is_opening_attr_is_opening = False
282  for entity_id in self._entity_ids_entity_ids:
283  if not (state := self.hasshass.states.get(entity_id)):
284  continue
285  if state.state == CoverState.OPEN:
286  self._attr_is_closed_attr_is_closed = False
287  continue
288  if state.state == CoverState.CLOSED:
289  continue
290  if state.state == CoverState.CLOSING:
291  self._attr_is_closing_attr_is_closing = True
292  continue
293  if state.state == CoverState.OPENING:
294  self._attr_is_opening_attr_is_opening = True
295  continue
296  if not valid_state:
297  # Set as unknown if all members are unknown or unavailable
298  self._attr_is_closed_attr_is_closed = None
299 
300  position_covers = self._covers[KEY_POSITION]
301  all_position_states = [self.hasshass.states.get(x) for x in position_covers]
302  position_states: list[State] = list(filter(None, all_position_states))
303  self._attr_current_cover_position_attr_current_cover_position = reduce_attribute(
304  position_states, ATTR_CURRENT_POSITION
305  )
306 
307  tilt_covers = self._tilts[KEY_POSITION]
308  all_tilt_states = [self.hasshass.states.get(x) for x in tilt_covers]
309  tilt_states: list[State] = list(filter(None, all_tilt_states))
310  self._attr_current_cover_tilt_position_attr_current_cover_tilt_position = reduce_attribute(
311  tilt_states, ATTR_CURRENT_TILT_POSITION
312  )
313 
314  supported_features = CoverEntityFeature(0)
315  if self._covers[KEY_OPEN_CLOSE]:
316  supported_features |= CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE
317  supported_features |= CoverEntityFeature.STOP if self._covers[KEY_STOP] else 0
318  if self._covers[KEY_POSITION]:
319  supported_features |= CoverEntityFeature.SET_POSITION
320  if self._tilts[KEY_OPEN_CLOSE]:
321  supported_features |= (
322  CoverEntityFeature.OPEN_TILT | CoverEntityFeature.CLOSE_TILT
323  )
324  if self._tilts[KEY_STOP]:
325  supported_features |= CoverEntityFeature.STOP_TILT
326  if self._tilts[KEY_POSITION]:
327  supported_features |= CoverEntityFeature.SET_TILT_POSITION
328  self._attr_supported_features_attr_supported_features = supported_features
None async_open_cover_tilt(self, **Any kwargs)
Definition: cover.py:216
None async_set_cover_tilt_position(self, **Any kwargs)
Definition: cover.py:249
None async_stop_cover_tilt(self, **Any kwargs)
Definition: cover.py:238
None async_set_cover_position(self, **Any kwargs)
Definition: cover.py:202
None async_open_cover(self, **Any kwargs)
Definition: cover.py:177
None async_stop_cover(self, **Any kwargs)
Definition: cover.py:195
None async_update_supported_features(self, str entity_id, State|None new_state)
Definition: cover.py:140
None __init__(self, str|None unique_id, str name, list[str] entities)
Definition: cover.py:117
None async_close_cover_tilt(self, **Any kwargs)
Definition: cover.py:227
None async_close_cover(self, **Any kwargs)
Definition: cover.py:184
bool add(self, _T matcher)
Definition: match.py:185
None async_setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback async_add_entities, DiscoveryInfoType|None discovery_info=None)
Definition: cover.py:69
CoverGroup async_create_preview_cover(HomeAssistant hass, str name, dict[str, Any] validated_config)
Definition: cover.py:99
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: cover.py:84
Any reduce_attribute(list[State] states, str key, Any|None default=None, Callable[..., Any] reduce=mean_int)
Definition: util.py:72