Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Support to embed Sonos."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from collections import OrderedDict
7 from dataclasses import dataclass, field
8 import datetime
9 from functools import partial
10 import logging
11 import socket
12 from typing import Any, cast
13 from urllib.parse import urlparse
14 
15 from aiohttp import ClientError
16 from requests.exceptions import Timeout
17 from soco import events_asyncio, zonegroupstate
18 import soco.config as soco_config
19 from soco.core import SoCo
20 from soco.events_base import Event as SonosEvent, SubscriptionBase
21 from soco.exceptions import SoCoException
22 import voluptuous as vol
23 
24 from homeassistant import config_entries
25 from homeassistant.components import ssdp
26 from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
27 from homeassistant.config_entries import ConfigEntry
28 from homeassistant.const import CONF_HOSTS, EVENT_HOMEASSISTANT_STOP
29 from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback
30 from homeassistant.helpers import (
31  config_validation as cv,
32  device_registry as dr,
33  issue_registry as ir,
34 )
35 from homeassistant.helpers.dispatcher import async_dispatcher_send
36 from homeassistant.helpers.event import async_call_later, async_track_time_interval
37 from homeassistant.helpers.typing import ConfigType
38 from homeassistant.util.async_ import create_eager_task
39 
40 from .alarms import SonosAlarms
41 from .const import (
42  AVAILABILITY_CHECK_INTERVAL,
43  DATA_SONOS,
44  DATA_SONOS_DISCOVERY_MANAGER,
45  DISCOVERY_INTERVAL,
46  DOMAIN,
47  PLATFORMS,
48  SONOS_CHECK_ACTIVITY,
49  SONOS_REBOOTED,
50  SONOS_SPEAKER_ACTIVITY,
51  SONOS_VANISHED,
52  SUB_FAIL_ISSUE_ID,
53  SUB_FAIL_URL,
54  SUBSCRIPTION_TIMEOUT,
55  UPNP_ST,
56 )
57 from .exception import SonosUpdateError
58 from .favorites import SonosFavorites
59 from .helpers import sync_get_visible_zones
60 from .speaker import SonosSpeaker
61 
62 _LOGGER = logging.getLogger(__name__)
63 
64 CONF_ADVERTISE_ADDR = "advertise_addr"
65 CONF_INTERFACE_ADDR = "interface_addr"
66 DISCOVERY_IGNORED_MODELS = ["Sonos Boost"]
67 ZGS_SUBSCRIPTION_TIMEOUT = 2
68 
69 CONFIG_SCHEMA = vol.Schema(
70  {
71  DOMAIN: vol.Schema(
72  {
73  MP_DOMAIN: vol.All(
74  cv.deprecated(CONF_INTERFACE_ADDR),
75  vol.Schema(
76  {
77  vol.Optional(CONF_ADVERTISE_ADDR): cv.string,
78  vol.Optional(CONF_INTERFACE_ADDR): cv.string,
79  vol.Optional(CONF_HOSTS): vol.All(
80  cv.ensure_list_csv, [cv.string]
81  ),
82  }
83  ),
84  )
85  }
86  )
87  },
88  extra=vol.ALLOW_EXTRA,
89 )
90 
91 
92 @dataclass
93 class UnjoinData:
94  """Class to track data necessary for unjoin coalescing."""
95 
96  speakers: list[SonosSpeaker]
97  event: asyncio.Event = field(default_factory=asyncio.Event)
98 
99 
100 class SonosData:
101  """Storage class for platform global data."""
102 
103  def __init__(self) -> None:
104  """Initialize the data."""
105  # OrderedDict behavior used by SonosAlarms and SonosFavorites
106  self.discovered: OrderedDict[str, SonosSpeaker] = OrderedDict()
107  self.favorites: dict[str, SonosFavorites] = {}
108  self.alarms: dict[str, SonosAlarms] = {}
109  self.topology_conditiontopology_condition = asyncio.Condition()
110  self.hosts_heartbeat: CALLBACK_TYPE | None = None
111  self.discovery_known: set[str] = set()
112  self.boot_counts: dict[str, int] = {}
113  self.mdns_names: dict[str, str] = {}
114  self.entity_id_mappings: dict[str, SonosSpeaker] = {}
115  self.unjoin_data: dict[str, UnjoinData] = {}
116 
117 
118 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
119  """Set up the Sonos component."""
120  conf = config.get(DOMAIN)
121 
122  hass.data[DOMAIN] = conf or {}
123 
124  if conf is not None:
125  hass.async_create_task(
126  hass.config_entries.flow.async_init(
127  DOMAIN, context={"source": config_entries.SOURCE_IMPORT}
128  )
129  )
130 
131  return True
132 
133 
134 async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
135  """Set up Sonos from a config entry."""
136  soco_config.EVENTS_MODULE = events_asyncio
137  soco_config.REQUEST_TIMEOUT = 9.5
138  soco_config.ZGT_EVENT_FALLBACK = False
139  zonegroupstate.EVENT_CACHE_TIMEOUT = SUBSCRIPTION_TIMEOUT
140 
141  if DATA_SONOS not in hass.data:
142  hass.data[DATA_SONOS] = SonosData()
143 
144  data = hass.data[DATA_SONOS]
145  config = hass.data[DOMAIN].get("media_player", {})
146  hosts = config.get(CONF_HOSTS, [])
147  _LOGGER.debug("Reached async_setup_entry, config=%s", config)
148 
149  if advertise_addr := config.get(CONF_ADVERTISE_ADDR):
150  soco_config.EVENT_ADVERTISE_IP = advertise_addr
151 
152  if deprecated_address := config.get(CONF_INTERFACE_ADDR):
153  _LOGGER.warning(
154  (
155  "'%s' is deprecated, enable %s in the Network integration"
156  " (https://www.home-assistant.io/integrations/network/)"
157  ),
158  CONF_INTERFACE_ADDR,
159  deprecated_address,
160  )
161 
162  manager = hass.data[DATA_SONOS_DISCOVERY_MANAGER] = SonosDiscoveryManager(
163  hass, entry, data, hosts
164  )
165  await manager.setup_platforms_and_discovery()
166  return True
167 
168 
169 async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
170  """Unload a Sonos config entry."""
171  unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
172  await hass.data[DATA_SONOS_DISCOVERY_MANAGER].async_shutdown()
173  hass.data.pop(DATA_SONOS)
174  hass.data.pop(DATA_SONOS_DISCOVERY_MANAGER)
175  return unload_ok
176 
177 
179  """Manage sonos discovery."""
180 
181  def __init__(
182  self, hass: HomeAssistant, entry: ConfigEntry, data: SonosData, hosts: list[str]
183  ) -> None:
184  """Init discovery manager."""
185  self.hasshass = hass
186  self.entryentry = entry
187  self.datadata = data
188  self.hostshosts = set(hosts)
189  self.hosts_in_error: dict[str, bool] = {}
190  self.discovery_lockdiscovery_lock = asyncio.Lock()
191  self.creation_lockcreation_lock = asyncio.Lock()
192  self._known_invisible_known_invisible: set[SoCo] = set()
193  self._manual_config_required_manual_config_required = bool(hosts)
194 
195  async def async_shutdown(self) -> None:
196  """Stop all running tasks."""
197  await self._async_stop_event_listener_async_stop_event_listener()
198  self._stop_manual_heartbeat_stop_manual_heartbeat()
199 
200  def is_device_invisible(self, ip_address: str) -> bool:
201  """Check if device at provided IP is known to be invisible."""
202  return any(x for x in self._known_invisible_known_invisible if x.ip_address == ip_address)
203 
204  async def async_subscribe_to_zone_updates(self, ip_address: str) -> None:
205  """Test subscriptions and create SonosSpeakers based on results."""
206  soco = SoCo(ip_address)
207  # Cache now to avoid household ID lookup during first ZoneGroupState processing
208  await self.hasshass.async_add_executor_job(
209  getattr,
210  soco,
211  "household_id",
212  )
213  sub = await soco.zoneGroupTopology.subscribe()
214 
215  @callback
216  def _async_add_visible_zones(subscription_succeeded: bool = False) -> None:
217  """Determine visible zones and create SonosSpeaker instances."""
218  zones_to_add = set()
219  subscription = None
220  if subscription_succeeded:
221  subscription = sub
222 
223  visible_zones = soco.visible_zones
224  self._known_invisible_known_invisible = soco.all_zones - visible_zones
225  for zone in visible_zones:
226  if zone.uid not in self.datadata.discovered:
227  zones_to_add.add(zone)
228 
229  if not zones_to_add:
230  return
231 
232  self.hasshass.async_create_task(
233  self.async_add_speakersasync_add_speakers(zones_to_add, subscription, soco.uid),
234  eager_start=True,
235  )
236 
237  async def async_subscription_failed(now: datetime.datetime) -> None:
238  """Fallback logic if the subscription callback never arrives."""
239  addr, port = sub.event_listener.address
240  listener_address = f"{addr}:{port}"
241  if advertise_ip := soco_config.EVENT_ADVERTISE_IP:
242  listener_address += f" (advertising as {advertise_ip})"
243  ir.async_create_issue(
244  self.hasshass,
245  DOMAIN,
246  SUB_FAIL_ISSUE_ID,
247  is_fixable=False,
248  severity=ir.IssueSeverity.ERROR,
249  translation_key="subscriptions_failed",
250  translation_placeholders={
251  "device_ip": ip_address,
252  "listener_address": listener_address,
253  "sub_fail_url": SUB_FAIL_URL,
254  },
255  )
256 
257  _LOGGER.warning(
258  "Subscription to %s failed, attempting to poll directly", ip_address
259  )
260  try:
261  await sub.unsubscribe()
262  except (ClientError, OSError, Timeout) as ex:
263  _LOGGER.debug("Unsubscription from %s failed: %s", ip_address, ex)
264 
265  try:
266  await self.hasshass.async_add_executor_job(soco.zone_group_state.poll, soco)
267  except (OSError, SoCoException, Timeout) as ex:
268  _LOGGER.warning(
269  "Fallback pollling to %s failed, setup cannot continue: %s",
270  ip_address,
271  ex,
272  )
273  return
274  _LOGGER.debug("Fallback ZoneGroupState poll to %s succeeded", ip_address)
275  _async_add_visible_zones()
276 
277  cancel_failure_callback = async_call_later(
278  self.hasshass, ZGS_SUBSCRIPTION_TIMEOUT, async_subscription_failed
279  )
280 
281  @callback
282  def _async_subscription_succeeded(event: SonosEvent) -> None:
283  """Create SonosSpeakers when subscription callbacks successfully arrive."""
284  _LOGGER.debug("Subscription to %s succeeded", ip_address)
285  cancel_failure_callback()
286  ir.async_delete_issue(
287  self.hasshass,
288  DOMAIN,
289  SUB_FAIL_ISSUE_ID,
290  )
291  _async_add_visible_zones(subscription_succeeded=True)
292 
293  sub.callback = _async_subscription_succeeded
294  # Hold lock to prevent concurrent subscription attempts
295  await asyncio.sleep(ZGS_SUBSCRIPTION_TIMEOUT * 2)
296  try:
297  # Cancel this subscription as we create an autorenewing
298  # subscription when setting up the SonosSpeaker instance
299  await sub.unsubscribe()
300  except ClientError as ex:
301  # Will be rejected if already replaced by new subscription
302  _LOGGER.debug(
303  "Cleanup unsubscription from %s was rejected: %s", ip_address, ex
304  )
305  except (OSError, Timeout) as ex:
306  _LOGGER.error("Cleanup unsubscription from %s failed: %s", ip_address, ex)
307 
308  async def _async_stop_event_listener(self, event: Event | None = None) -> None:
309  for speaker in self.datadata.discovered.values():
310  speaker.activity_stats.log_report()
311  speaker.event_stats.log_report()
312  if zgs := next(
313  (
314  speaker.soco.zone_group_state
315  for speaker in self.datadata.discovered.values()
316  ),
317  None,
318  ):
319  _LOGGER.debug(
320  "ZoneGroupState stats: (%s/%s) processed",
321  zgs.processed_count,
322  zgs.total_requests,
323  )
324  await asyncio.gather(
325  *(
326  create_eager_task(speaker.async_offline())
327  for speaker in self.datadata.discovered.values()
328  )
329  )
330  if events_asyncio.event_listener:
331  await events_asyncio.event_listener.async_stop()
332 
333  @callback
334  def _stop_manual_heartbeat(self, event: Event | None = None) -> None:
335  if self.datadata.hosts_heartbeat:
336  self.datadata.hosts_heartbeat()
337  self.datadata.hosts_heartbeat = None
338 
340  self,
341  socos: set[SoCo],
342  zgs_subscription: SubscriptionBase | None,
343  zgs_subscription_uid: str | None,
344  ) -> None:
345  """Create and set up new SonosSpeaker instances."""
346 
347  def _add_speakers():
348  """Add all speakers in a single executor job."""
349  for soco in socos:
350  if soco.uid in self.datadata.discovered:
351  continue
352  sub = None
353  if soco.uid == zgs_subscription_uid and zgs_subscription:
354  sub = zgs_subscription
355  self._add_speaker_add_speaker(soco, sub)
356 
357  async with self.creation_lockcreation_lock:
358  await self.hasshass.async_add_executor_job(_add_speakers)
359 
361  self, soco: SoCo, zone_group_state_sub: SubscriptionBase | None
362  ) -> None:
363  """Create and set up a new SonosSpeaker instance."""
364  try:
365  speaker_info = soco.get_speaker_info(True, timeout=7)
366  if soco.uid not in self.datadata.boot_counts:
367  self.datadata.boot_counts[soco.uid] = soco.boot_seqnum
368  _LOGGER.debug("Adding new speaker: %s", speaker_info)
369  speaker = SonosSpeaker(self.hasshass, soco, speaker_info, zone_group_state_sub)
370  self.datadata.discovered[soco.uid] = speaker
371  for coordinator, coord_dict in (
372  (SonosAlarms, self.datadata.alarms),
373  (SonosFavorites, self.datadata.favorites),
374  ):
375  c_dict: dict[str, Any] = coord_dict
376  if soco.household_id not in c_dict:
377  new_coordinator = coordinator(self.hasshass, soco.household_id)
378  new_coordinator.setup(soco)
379  c_dict[soco.household_id] = new_coordinator
380  speaker.setup(self.entryentry)
381  except (OSError, SoCoException, Timeout) as ex:
382  _LOGGER.warning("Failed to add SonosSpeaker using %s: %s", soco, ex)
383 
385  self, now: datetime.datetime | None = None
386  ) -> None:
387  """Add and maintain Sonos devices from a manual configuration."""
388 
389  # Loop through each configured host and verify that Soco attributes are available for it.
390  for host in self.hostshosts.copy():
391  ip_addr = await self.hasshass.async_add_executor_job(socket.gethostbyname, host)
392  soco = SoCo(ip_addr)
393  try:
394  visible_zones = await self.hasshass.async_add_executor_job(
395  sync_get_visible_zones,
396  soco,
397  )
398  except (
399  OSError,
400  SoCoException,
401  Timeout,
402  TimeoutError,
403  ) as ex:
404  if not self.hosts_in_error.get(ip_addr):
405  _LOGGER.warning(
406  "Could not get visible Sonos devices from %s: %s", ip_addr, ex
407  )
408  self.hosts_in_error[ip_addr] = True
409  else:
410  _LOGGER.debug(
411  "Could not get visible Sonos devices from %s: %s", ip_addr, ex
412  )
413  continue
414 
415  if self.hosts_in_error.pop(ip_addr, None):
416  _LOGGER.warning("Connection reestablished to Sonos device %s", ip_addr)
417  # Each speaker has the topology for other online speakers, so add them in here if they were not
418  # configured. The metadata is already in Soco for these.
419  if new_hosts := {
420  x.ip_address for x in visible_zones if x.ip_address not in self.hostshosts
421  }:
422  _LOGGER.debug("Adding to manual hosts: %s", new_hosts)
423  self.hostshosts.update(new_hosts)
424 
425  if self.is_device_invisibleis_device_invisible(ip_addr):
426  _LOGGER.debug("Discarding %s from manual hosts", ip_addr)
427  self.hostshosts.discard(ip_addr)
428 
429  # Loop through each configured host that is not in error. Send a discovery message
430  # if a speaker does not already exist, or ping the speaker if it is unavailable.
431  for host in self.hostshosts.copy():
432  ip_addr = await self.hasshass.async_add_executor_job(socket.gethostbyname, host)
433  soco = SoCo(ip_addr)
434  # Skip hosts that are in error to avoid blocking call on soco.uuid in event loop
435  if self.hosts_in_error.get(ip_addr):
436  continue
437  known_speaker = next(
438  (
439  speaker
440  for speaker in self.datadata.discovered.values()
441  if speaker.soco.ip_address == ip_addr
442  ),
443  None,
444  )
445  if not known_speaker:
446  try:
447  await self._async_handle_discovery_message_async_handle_discovery_message(
448  soco.uid,
449  ip_addr,
450  "manual zone scan",
451  )
452  except (
453  OSError,
454  SoCoException,
455  Timeout,
456  TimeoutError,
457  ) as ex:
458  _LOGGER.warning("Discovery message failed to %s : %s", ip_addr, ex)
459  elif not known_speaker.available:
460  try:
461  await self.hasshass.async_add_executor_job(known_speaker.ping)
462  # Only send the message if the ping was successful.
464  self.hasshass,
465  f"{SONOS_SPEAKER_ACTIVITY}-{soco.uid}",
466  "manual zone scan",
467  )
468  except SonosUpdateError:
469  _LOGGER.debug(
470  "Manual poll to %s failed, keeping unavailable", ip_addr
471  )
472 
473  self.datadata.hosts_heartbeat = async_call_later(
474  self.hasshass, DISCOVERY_INTERVAL.total_seconds(), self.async_poll_manual_hostsasync_poll_manual_hosts
475  )
476 
478  self,
479  uid: str,
480  discovered_ip: str,
481  source: str,
482  boot_seqnum: int | None = None,
483  ) -> None:
484  """Handle discovered player creation and activity."""
485  async with self.discovery_lockdiscovery_lock:
486  if not self.datadata.discovered:
487  # Initial discovery, attempt to add all visible zones
488  await self.async_subscribe_to_zone_updatesasync_subscribe_to_zone_updates(discovered_ip)
489  elif uid not in self.datadata.discovered:
490  if self.is_device_invisibleis_device_invisible(discovered_ip):
491  return
492  await self.async_subscribe_to_zone_updatesasync_subscribe_to_zone_updates(discovered_ip)
493  elif boot_seqnum and boot_seqnum > self.datadata.boot_counts[uid]:
494  self.datadata.boot_counts[uid] = boot_seqnum
495  async_dispatcher_send(self.hasshass, f"{SONOS_REBOOTED}-{uid}")
496  else:
498  self.hasshass, f"{SONOS_SPEAKER_ACTIVITY}-{uid}", source
499  )
500 
501  @callback
503  self, info: ssdp.SsdpServiceInfo, change: ssdp.SsdpChange
504  ) -> None:
505  uid = info.upnp[ssdp.ATTR_UPNP_UDN]
506  if not uid.startswith("uuid:RINCON_"):
507  return
508  uid = uid[5:]
509 
510  if change == ssdp.SsdpChange.BYEBYE:
511  _LOGGER.debug(
512  "ssdp:byebye received from %s", info.upnp.get("friendlyName", uid)
513  )
514  reason = info.ssdp_headers.get("X-RINCON-REASON", "ssdp:byebye")
515  async_dispatcher_send(self.hasshass, f"{SONOS_VANISHED}-{uid}", reason)
516  return
517 
518  self.async_discovered_playerasync_discovered_player(
519  "SSDP",
520  info,
521  cast(str, urlparse(info.ssdp_location).hostname),
522  uid,
523  info.ssdp_headers.get("X-RINCON-BOOTSEQ"),
524  cast(str, info.upnp.get(ssdp.ATTR_UPNP_MODEL_NAME)),
525  None,
526  )
527 
528  @callback
530  self,
531  source: str,
532  info: ssdp.SsdpServiceInfo,
533  discovered_ip: str,
534  uid: str,
535  boot_seqnum: str | int | None,
536  model: str,
537  mdns_name: str | None,
538  ) -> None:
539  """Handle discovery via ssdp or zeroconf."""
540  if self._manual_config_required_manual_config_required:
541  _LOGGER.warning(
542  "Automatic discovery is working, Sonos hosts in configuration.yaml are"
543  " not needed"
544  )
545  self._manual_config_required_manual_config_required = False
546  if model in DISCOVERY_IGNORED_MODELS:
547  _LOGGER.debug("Ignoring device: %s", info)
548  return
549  if self.is_device_invisibleis_device_invisible(discovered_ip):
550  return
551 
552  if boot_seqnum:
553  boot_seqnum = int(boot_seqnum)
554  self.datadata.boot_counts.setdefault(uid, boot_seqnum)
555  if mdns_name:
556  self.datadata.mdns_names[uid] = mdns_name
557 
558  if uid not in self.datadata.discovery_known:
559  _LOGGER.debug("New %s discovery uid=%s: %s", source, uid, info)
560  self.datadata.discovery_known.add(uid)
561  self.entryentry.async_create_background_task(
562  self.hasshass,
563  self._async_handle_discovery_message_async_handle_discovery_message(
564  uid,
565  discovered_ip,
566  "discovery",
567  boot_seqnum=cast(int | None, boot_seqnum),
568  ),
569  "sonos-handle_discovery_message",
570  )
571 
572  async def setup_platforms_and_discovery(self) -> None:
573  """Set up platforms and discovery."""
574  await self.hasshass.config_entries.async_forward_entry_setups(self.entryentry, PLATFORMS)
575  self.entryentry.async_on_unload(
576  self.hasshass.bus.async_listen_once(
577  EVENT_HOMEASSISTANT_STOP,
578  self._async_stop_event_listener_async_stop_event_listener,
579  )
580  )
581  _LOGGER.debug("Adding discovery job")
582  if self.hostshosts:
583  self.entryentry.async_on_unload(
584  self.hasshass.bus.async_listen_once(
585  EVENT_HOMEASSISTANT_STOP,
586  self._stop_manual_heartbeat_stop_manual_heartbeat,
587  )
588  )
589  await self.async_poll_manual_hostsasync_poll_manual_hosts()
590 
591  self.entryentry.async_on_unload(
592  await ssdp.async_register_callback(
593  self.hasshass, self._async_ssdp_discovered_player_async_ssdp_discovered_player, {"st": UPNP_ST}
594  )
595  )
596 
597  self.entryentry.async_on_unload(
599  self.hasshass,
600  partial(
601  async_dispatcher_send,
602  self.hasshass,
603  SONOS_CHECK_ACTIVITY,
604  ),
605  AVAILABILITY_CHECK_INTERVAL,
606  )
607  )
608 
609 
611  hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry
612 ) -> bool:
613  """Remove Sonos config entry from a device."""
614  known_devices = hass.data[DATA_SONOS].discovered.keys()
615  for identifier in device_entry.identifiers:
616  if identifier[0] != DOMAIN:
617  continue
618  uid = identifier[1]
619  if uid not in known_devices:
620  return True
621  return False
bool is_device_invisible(self, str ip_address)
Definition: __init__.py:200
None _add_speaker(self, SoCo soco, SubscriptionBase|None zone_group_state_sub)
Definition: __init__.py:362
None async_add_speakers(self, set[SoCo] socos, SubscriptionBase|None zgs_subscription, str|None zgs_subscription_uid)
Definition: __init__.py:344
None _async_stop_event_listener(self, Event|None event=None)
Definition: __init__.py:308
None __init__(self, HomeAssistant hass, ConfigEntry entry, SonosData data, list[str] hosts)
Definition: __init__.py:183
None async_poll_manual_hosts(self, datetime.datetime|None now=None)
Definition: __init__.py:386
None async_discovered_player(self, str source, ssdp.SsdpServiceInfo info, str discovered_ip, str uid, str|int|None boot_seqnum, str model, str|None mdns_name)
Definition: __init__.py:538
None _async_ssdp_discovered_player(self, ssdp.SsdpServiceInfo info, ssdp.SsdpChange change)
Definition: __init__.py:504
None async_subscribe_to_zone_updates(self, str ip_address)
Definition: __init__.py:204
None _async_handle_discovery_message(self, str uid, str discovered_ip, str source, int|None boot_seqnum=None)
Definition: __init__.py:483
None _stop_manual_heartbeat(self, Event|None event=None)
Definition: __init__.py:334
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
IssData update(pyiss.ISS iss)
Definition: __init__.py:33
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:118
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:134
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:169
bool async_remove_config_entry_device(HomeAssistant hass, ConfigEntry config_entry, dr.DeviceEntry device_entry)
Definition: __init__.py:612
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)
Definition: dispatcher.py:193
CALLBACK_TYPE async_call_later(HomeAssistant hass, float|timedelta delay, HassJob[[datetime], Coroutine[Any, Any, None]|None]|Callable[[datetime], Coroutine[Any, Any, None]|None] action)
Definition: event.py:1597
CALLBACK_TYPE async_track_time_interval(HomeAssistant hass, Callable[[datetime], Coroutine[Any, Any, None]|None] action, timedelta interval, *str|None name=None, bool|None cancel_on_shutdown=None)
Definition: event.py:1679