Home Assistant Unofficial Reference 2024.12.1
switch.py
Go to the documentation of this file.
1 """Entity representing a Sonos Alarm."""
2 
3 from __future__ import annotations
4 
5 import datetime
6 import logging
7 from typing import Any, cast
8 
9 from soco.alarms import Alarm
10 from soco.exceptions import SoCoSlaveException, SoCoUPnPException
11 
12 from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchEntity
13 from homeassistant.config_entries import ConfigEntry
14 from homeassistant.const import ATTR_TIME, EntityCategory
15 from homeassistant.core import HomeAssistant, callback
16 from homeassistant.helpers import device_registry as dr, entity_registry as er
17 from homeassistant.helpers.dispatcher import async_dispatcher_connect
18 from homeassistant.helpers.entity_platform import AddEntitiesCallback
19 from homeassistant.helpers.event import async_track_time_change
20 
21 from .const import (
22  DATA_SONOS,
23  DOMAIN as SONOS_DOMAIN,
24  SONOS_ALARMS_UPDATED,
25  SONOS_CREATE_ALARM,
26  SONOS_CREATE_SWITCHES,
27 )
28 from .entity import SonosEntity, SonosPollingEntity
29 from .helpers import soco_error
30 from .speaker import SonosSpeaker
31 
32 _LOGGER = logging.getLogger(__name__)
33 
34 ATTR_DURATION = "duration"
35 ATTR_ID = "alarm_id"
36 ATTR_PLAY_MODE = "play_mode"
37 ATTR_RECURRENCE = "recurrence"
38 ATTR_SCHEDULED_TODAY = "scheduled_today"
39 ATTR_VOLUME = "volume"
40 ATTR_INCLUDE_LINKED_ZONES = "include_linked_zones"
41 
42 ATTR_CROSSFADE = "cross_fade"
43 ATTR_LOUDNESS = "loudness"
44 ATTR_MUSIC_PLAYBACK_FULL_VOLUME = "surround_mode"
45 ATTR_NIGHT_SOUND = "night_mode"
46 ATTR_SPEECH_ENHANCEMENT = "dialog_level"
47 ATTR_STATUS_LIGHT = "status_light"
48 ATTR_SUB_ENABLED = "sub_enabled"
49 ATTR_SURROUND_ENABLED = "surround_enabled"
50 ATTR_TOUCH_CONTROLS = "buttons_enabled"
51 
52 ALL_FEATURES = (
53  ATTR_TOUCH_CONTROLS,
54  ATTR_CROSSFADE,
55  ATTR_LOUDNESS,
56  ATTR_MUSIC_PLAYBACK_FULL_VOLUME,
57  ATTR_NIGHT_SOUND,
58  ATTR_SPEECH_ENHANCEMENT,
59  ATTR_SUB_ENABLED,
60  ATTR_SURROUND_ENABLED,
61  ATTR_STATUS_LIGHT,
62 )
63 
64 COORDINATOR_FEATURES = ATTR_CROSSFADE
65 
66 POLL_REQUIRED = (
67  ATTR_TOUCH_CONTROLS,
68  ATTR_STATUS_LIGHT,
69 )
70 
71 WEEKEND_DAYS = (0, 6)
72 
73 
75  hass: HomeAssistant,
76  config_entry: ConfigEntry,
77  async_add_entities: AddEntitiesCallback,
78 ) -> None:
79  """Set up Sonos from a config entry."""
80 
81  async def _async_create_alarms(speaker: SonosSpeaker, alarm_ids: list[str]) -> None:
82  entities = []
83  created_alarms = (
84  hass.data[DATA_SONOS].alarms[speaker.household_id].created_alarm_ids
85  )
86  for alarm_id in alarm_ids:
87  if alarm_id in created_alarms:
88  continue
89  _LOGGER.debug("Creating alarm %s on %s", alarm_id, speaker.zone_name)
90  created_alarms.add(alarm_id)
91  entities.append(SonosAlarmEntity(alarm_id, speaker))
92  async_add_entities(entities)
93 
94  def available_soco_attributes(speaker: SonosSpeaker) -> list[str]:
95  features = []
96  for feature_type in ALL_FEATURES:
97  try:
98  if (state := getattr(speaker.soco, feature_type, None)) is not None:
99  setattr(speaker, feature_type, state)
100  features.append(feature_type)
101  except SoCoSlaveException:
102  features.append(feature_type)
103  return features
104 
105  async def _async_create_switches(speaker: SonosSpeaker) -> None:
106  entities = []
107  available_features = await hass.async_add_executor_job(
108  available_soco_attributes, speaker
109  )
110  for feature_type in available_features:
111  _LOGGER.debug(
112  "Creating %s switch on %s",
113  feature_type,
114  speaker.zone_name,
115  )
116  entities.append(SonosSwitchEntity(feature_type, speaker))
117  async_add_entities(entities)
118 
119  config_entry.async_on_unload(
120  async_dispatcher_connect(hass, SONOS_CREATE_ALARM, _async_create_alarms)
121  )
122  config_entry.async_on_unload(
123  async_dispatcher_connect(hass, SONOS_CREATE_SWITCHES, _async_create_switches)
124  )
125 
126 
128  """Representation of a Sonos feature switch."""
129 
130  def __init__(self, feature_type: str, speaker: SonosSpeaker) -> None:
131  """Initialize the switch."""
132  super().__init__(speaker)
133  self.feature_typefeature_type = feature_type
134  self.needs_coordinatorneeds_coordinator = feature_type in COORDINATOR_FEATURES
135  self._attr_entity_category_attr_entity_category = EntityCategory.CONFIG
136  self._attr_translation_key_attr_translation_key = feature_type
137  self._attr_unique_id_attr_unique_id = f"{speaker.soco.uid}-{feature_type}"
138 
139  if feature_type in POLL_REQUIRED:
140  self._attr_entity_registry_enabled_default_attr_entity_registry_enabled_default = False
141  self._attr_should_poll_attr_should_poll_attr_should_poll = True
142 
143  async def _async_fallback_poll(self) -> None:
144  """Handle polling for subscription-based switches when subscription fails."""
145  if not self.should_pollshould_poll:
146  await self.hasshass.async_add_executor_job(self.poll_statepoll_statepoll_state)
147 
148  @soco_error()
149  def poll_state(self) -> None:
150  """Poll the current state of the switch."""
151  state = getattr(self.socosoco, self.feature_typefeature_type)
152  setattr(self.speakerspeaker, self.feature_typefeature_type, state)
153 
154  @property
155  def is_on(self) -> bool:
156  """Return True if entity is on."""
157  if self.needs_coordinatorneeds_coordinator and not self.speakerspeaker.is_coordinator:
158  return cast(bool, getattr(self.speakerspeaker.coordinator, self.feature_typefeature_type))
159  return cast(bool, getattr(self.speakerspeaker, self.feature_typefeature_type))
160 
161  def turn_on(self, **kwargs: Any) -> None:
162  """Turn the entity on."""
163  self.send_commandsend_command(True)
164 
165  def turn_off(self, **kwargs: Any) -> None:
166  """Turn the entity off."""
167  self.send_commandsend_command(False)
168 
169  @soco_error()
170  def send_command(self, enable: bool) -> None:
171  """Enable or disable the feature on the device."""
172  if self.needs_coordinatorneeds_coordinator:
173  soco = self.socosoco.group.coordinator
174  else:
175  soco = self.socosoco
176  try:
177  setattr(soco, self.feature_typefeature_type, enable)
178  except SoCoUPnPException as exc:
179  _LOGGER.warning("Could not toggle %s: %s", self.entity_identity_id, exc)
180 
181 
183  """Representation of a Sonos Alarm entity."""
184 
185  _attr_entity_category = EntityCategory.CONFIG
186  _attr_icon = "mdi:alarm"
187 
188  def __init__(self, alarm_id: str, speaker: SonosSpeaker) -> None:
189  """Initialize the switch."""
190  super().__init__(speaker)
191  self._attr_unique_id_attr_unique_id = f"alarm-{speaker.household_id}:{alarm_id}"
192  self.alarm_idalarm_id = alarm_id
193  self.household_idhousehold_id = speaker.household_id
194  self.entity_identity_identity_id = ENTITY_ID_FORMAT.format(f"sonos_alarm_{self.alarm_id}")
195 
196  async def async_added_to_hass(self) -> None:
197  """Handle switch setup when added to hass."""
198  await super().async_added_to_hass()
199  self.async_on_removeasync_on_remove(
201  self.hasshass,
202  f"{SONOS_ALARMS_UPDATED}-{self.household_id}",
203  self.async_update_stateasync_update_state,
204  )
205  )
206 
207  async def async_write_state_daily(now: datetime.datetime) -> None:
208  """Update alarm state attributes each calendar day."""
209  _LOGGER.debug("Updating state attributes for %s", self.namenamename)
210  self.async_write_ha_stateasync_write_ha_state()
211 
212  self.async_on_removeasync_on_remove(
214  self.hasshass, async_write_state_daily, hour=0, minute=0, second=0
215  )
216  )
217 
218  @property
219  def alarm(self) -> Alarm:
220  """Return the alarm instance."""
221  return self.hasshass.data[DATA_SONOS].alarms[self.household_idhousehold_id].get(self.alarm_idalarm_id)
222 
223  @property
224  def name(self) -> str:
225  """Return the name of the sensor."""
226  return (
227  f"{self.alarm.recurrence.capitalize()} alarm"
228  f" {str(self.alarm.start_time)[:5]}"
229  )
230 
231  async def _async_fallback_poll(self) -> None:
232  """Call the central alarm polling method."""
233  await self.hasshass.data[DATA_SONOS].alarms[self.household_idhousehold_id].async_poll()
234 
235  @callback
236  def async_check_if_available(self) -> bool:
237  """Check if alarm exists and remove alarm entity if not available."""
238  if self.alarmalarm:
239  return True
240 
241  _LOGGER.debug("%s has been deleted", self.entity_identity_identity_id)
242 
243  entity_registry = er.async_get(self.hasshass)
244  if entity_registry.async_get(self.entity_identity_identity_id):
245  entity_registry.async_remove(self.entity_identity_identity_id)
246 
247  return False
248 
249  async def async_update_state(self) -> None:
250  """Poll the device for the current state."""
251  if not self.async_check_if_availableasync_check_if_available():
252  return
253 
254  if self.speakerspeakerspeaker.soco.uid != self.alarmalarm.zone.uid:
255  self.speakerspeakerspeaker = self.hasshass.data[DATA_SONOS].discovered.get(
256  self.alarmalarm.zone.uid
257  )
258  if self.speakerspeakerspeaker is None:
259  raise RuntimeError(
260  "No configured Sonos speaker has been found to match the alarm."
261  )
262 
263  self._async_update_device_async_update_device()
264 
265  self.async_write_ha_stateasync_write_ha_state()
266 
267  @callback
268  def _async_update_device(self) -> None:
269  """Update the device, since this alarm moved to a different player."""
270  device_registry = dr.async_get(self.hasshass)
271  entity_registry = er.async_get(self.hasshass)
272  entity = entity_registry.async_get(self.entity_identity_identity_id)
273 
274  if entity is None:
275  raise RuntimeError("Alarm has been deleted by accident.")
276 
277  new_device = device_registry.async_get_or_create(
278  config_entry_id=cast(str, entity.config_entry_id),
279  identifiers={(SONOS_DOMAIN, self.socosoco.uid)},
280  connections={(dr.CONNECTION_NETWORK_MAC, self.speakerspeakerspeaker.mac_address)},
281  )
282  if (
283  device := entity_registry.async_get(self.entity_identity_identity_id)
284  ) and device.device_id != new_device.id:
285  _LOGGER.debug("%s is moving to %s", self.entity_identity_identity_id, new_device.name)
286  entity_registry.async_update_entity(self.entity_identity_identity_id, device_id=new_device.id)
287 
288  @property
289  def _is_today(self) -> bool:
290  """Return whether this alarm is scheduled for today."""
291  recurrence = self.alarmalarm.recurrence
292  daynum = int(datetime.datetime.today().strftime("%w"))
293  return (
294  recurrence in ("DAILY", "ONCE")
295  or (recurrence == "WEEKENDS" and daynum in WEEKEND_DAYS)
296  or (recurrence == "WEEKDAYS" and daynum not in WEEKEND_DAYS)
297  or (recurrence.startswith("ON_") and str(daynum) in recurrence)
298  )
299 
300  @property
301  def available(self) -> bool:
302  """Return whether this alarm is available."""
303  return (self.alarmalarm is not None) and self.speakerspeakerspeaker.available
304 
305  @property
306  def is_on(self) -> bool:
307  """Return state of Sonos alarm switch."""
308  return self.alarmalarm.enabled
309 
310  @property
311  def extra_state_attributes(self) -> dict[str, Any]:
312  """Return attributes of Sonos alarm switch."""
313  return {
314  ATTR_ID: str(self.alarm_idalarm_id),
315  ATTR_TIME: str(self.alarmalarm.start_time),
316  ATTR_DURATION: str(self.alarmalarm.duration),
317  ATTR_RECURRENCE: str(self.alarmalarm.recurrence),
318  ATTR_VOLUME: self.alarmalarm.volume / 100,
319  ATTR_PLAY_MODE: str(self.alarmalarm.play_mode),
320  ATTR_SCHEDULED_TODAY: self._is_today_is_today,
321  ATTR_INCLUDE_LINKED_ZONES: self.alarmalarm.include_linked_zones,
322  }
323 
324  def turn_on(self, **kwargs: Any) -> None:
325  """Turn alarm switch on."""
326  self._handle_switch_on_off_handle_switch_on_off(turn_on=True)
327 
328  def turn_off(self, **kwargs: Any) -> None:
329  """Turn alarm switch off."""
330  self._handle_switch_on_off_handle_switch_on_off(turn_on=False)
331 
332  @soco_error()
333  def _handle_switch_on_off(self, turn_on: bool) -> None:
334  """Handle turn on/off of alarm switch."""
335  self.alarmalarm.enabled = turn_on
336  self.alarmalarm.save()
None __init__(self, str alarm_id, SonosSpeaker speaker)
Definition: switch.py:188
None __init__(self, str feature_type, SonosSpeaker speaker)
Definition: switch.py:130
None async_on_remove(self, CALLBACK_TYPE func)
Definition: entity.py:1331
str|UndefinedType|None name(self)
Definition: entity.py:738
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: switch.py:78
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
Definition: dispatcher.py:103
CALLBACK_TYPE async_track_time_change(HomeAssistant hass, Callable[[datetime], Coroutine[Any, Any, None]|None] action, Any|None hour=None, Any|None minute=None, Any|None second=None)
Definition: event.py:1904