Home Assistant Unofficial Reference 2024.12.1
entity.py
Go to the documentation of this file.
1 """Support for SimpliSafe alarm systems."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Iterable
6 
7 from simplipy.device import Device, DeviceTypes
8 from simplipy.system.v3 import SystemV3
9 from simplipy.websocket import (
10  EVENT_CONNECTION_LOST,
11  EVENT_CONNECTION_RESTORED,
12  EVENT_LOCK_LOCKED,
13  EVENT_LOCK_UNLOCKED,
14  EVENT_POWER_OUTAGE,
15  EVENT_POWER_RESTORED,
16  WebsocketEvent,
17 )
18 
19 from homeassistant.core import callback
20 from homeassistant.helpers.device_registry import DeviceInfo
21 from homeassistant.helpers.dispatcher import async_dispatcher_connect
23  CoordinatorEntity,
24  DataUpdateCoordinator,
25 )
26 
27 from . import SimpliSafe
28 from .const import (
29  ATTR_LAST_EVENT_INFO,
30  ATTR_LAST_EVENT_SENSOR_NAME,
31  ATTR_LAST_EVENT_SENSOR_TYPE,
32  ATTR_LAST_EVENT_TIMESTAMP,
33  ATTR_SYSTEM_ID,
34  DISPATCHER_TOPIC_WEBSOCKET_EVENT,
35  DOMAIN,
36  LOGGER,
37 )
38 from .typing import SystemType
39 
40 DEFAULT_CONFIG_URL = "https://webapp.simplisafe.com/new/#/dashboard"
41 DEFAULT_ENTITY_MODEL = "Alarm control panel"
42 DEFAULT_ERROR_THRESHOLD = 2
43 
44 WEBSOCKET_EVENTS_REQUIRING_SERIAL = [EVENT_LOCK_LOCKED, EVENT_LOCK_UNLOCKED]
45 
46 
47 class SimpliSafeEntity(CoordinatorEntity[DataUpdateCoordinator[None]]):
48  """Define a base SimpliSafe entity."""
49 
50  _attr_has_entity_name = True
51 
52  def __init__(
53  self,
54  simplisafe: SimpliSafe,
55  system: SystemType,
56  *,
57  device: Device | None = None,
58  additional_websocket_events: Iterable[str] | None = None,
59  ) -> None:
60  """Initialize."""
61  assert simplisafe.coordinator
62  super().__init__(simplisafe.coordinator)
63 
64  # SimpliSafe can incorrectly return an error state when there isn't any
65  # error. This can lead to entities having an unknown state frequently.
66  # To protect against that, we measure an error count for each entity and only
67  # mark the state as unavailable if we detect a few in a row:
68  self._error_count_error_count = 0
69 
70  if device:
71  model = device.type.name.capitalize().replace("_", " ")
72  device_name = f"{device.name.capitalize()} {model}"
73  serial = device.serial
74  else:
75  model = device_name = DEFAULT_ENTITY_MODEL
76  serial = system.serial
77 
78  event = simplisafe.initial_event_to_use[system.system_id]
79 
80  if raw_type := event.get("sensorType"):
81  try:
82  device_type = DeviceTypes(raw_type)
83  except ValueError:
84  device_type = DeviceTypes.UNKNOWN
85  else:
86  device_type = DeviceTypes.UNKNOWN
87 
88  self._attr_extra_state_attributes_attr_extra_state_attributes = {
89  ATTR_LAST_EVENT_INFO: event.get("info"),
90  ATTR_LAST_EVENT_SENSOR_NAME: event.get("sensorName"),
91  ATTR_LAST_EVENT_SENSOR_TYPE: device_type.name.lower(),
92  ATTR_LAST_EVENT_TIMESTAMP: event.get("eventTimestamp"),
93  ATTR_SYSTEM_ID: system.system_id,
94  }
95 
96  self._attr_device_info_attr_device_info = DeviceInfo(
97  configuration_url=DEFAULT_CONFIG_URL,
98  identifiers={(DOMAIN, serial)},
99  manufacturer="SimpliSafe",
100  model=model,
101  name=device_name,
102  via_device=(DOMAIN, str(system.system_id)),
103  )
104 
105  self._attr_unique_id_attr_unique_id = serial
106  self._device_device = device
107  self._online_online = True
108  self._simplisafe_simplisafe = simplisafe
109  self._system_system = system
110  self._websocket_events_to_listen_for_websocket_events_to_listen_for = [
111  EVENT_CONNECTION_LOST,
112  EVENT_CONNECTION_RESTORED,
113  EVENT_POWER_OUTAGE,
114  EVENT_POWER_RESTORED,
115  ]
116  if additional_websocket_events:
117  self._websocket_events_to_listen_for_websocket_events_to_listen_for += additional_websocket_events
118 
119  @property
120  def available(self) -> bool:
121  """Return whether the entity is available."""
122  # We can easily detect if the V3 system is offline, but no simple check exists
123  # for the V2 system. Therefore, assuming the coordinator hasn't failed, we mark
124  # the entity as available if:
125  # 1. We can verify that the system is online (assuming True if we can't)
126  # 2. We can verify that the entity is online
127  if isinstance(self._system_system, SystemV3):
128  system_offline = self._system_system.offline
129  else:
130  system_offline = False
131 
132  return (
133  self._error_count_error_count < DEFAULT_ERROR_THRESHOLD
134  and self._online_online
135  and not system_offline
136  )
137 
138  @callback
139  def _handle_coordinator_update(self) -> None:
140  """Update the entity with new REST API data."""
141  if self.coordinator.last_update_success:
142  self.async_reset_error_countasync_reset_error_count()
143  else:
144  self.async_increment_error_countasync_increment_error_count()
145 
146  self.async_update_from_rest_apiasync_update_from_rest_api()
147  self.async_write_ha_state()
148 
149  @callback
150  def _handle_websocket_update(self, event: WebsocketEvent) -> None:
151  """Update the entity with new websocket data."""
152  # Ignore this event if it belongs to a system other than this one:
153  if event.system_id != self._system_system.system_id:
154  return
155 
156  # Ignore this event if this entity hasn't expressed interest in its type:
157  if event.event_type not in self._websocket_events_to_listen_for_websocket_events_to_listen_for:
158  return
159 
160  # Ignore this event if it belongs to a entity with a different serial
161  # number from this one's:
162  if (
163  self._device_device
164  and event.event_type in WEBSOCKET_EVENTS_REQUIRING_SERIAL
165  and event.sensor_serial != self._device_device.serial
166  ):
167  return
168 
169  sensor_type: str | None
170  if event.sensor_type:
171  sensor_type = event.sensor_type.name
172  else:
173  sensor_type = None
174 
175  self._attr_extra_state_attributes_attr_extra_state_attributes.update(
176  {
177  ATTR_LAST_EVENT_INFO: event.info,
178  ATTR_LAST_EVENT_SENSOR_NAME: event.sensor_name,
179  ATTR_LAST_EVENT_SENSOR_TYPE: sensor_type,
180  ATTR_LAST_EVENT_TIMESTAMP: event.timestamp,
181  }
182  )
183 
184  # It's unknown whether these events reach the base station (since the connection
185  # is lost); we include this for completeness and coverage:
186  if event.event_type in (EVENT_CONNECTION_LOST, EVENT_POWER_OUTAGE):
187  self._online_online = False
188  return
189 
190  # If the base station comes back online, set entities to available, but don't
191  # instruct the entities to update their state (since there won't be anything new
192  # until the next websocket event or REST API update:
193  if event.event_type in (EVENT_CONNECTION_RESTORED, EVENT_POWER_RESTORED):
194  self._online_online = True
195  return
196 
197  self.async_update_from_websocket_eventasync_update_from_websocket_event(event)
198  self.async_write_ha_state()
199 
200  async def async_added_to_hass(self) -> None:
201  """Register callbacks."""
202  await super().async_added_to_hass()
203 
204  self.async_on_remove(
206  self.hasshass,
207  DISPATCHER_TOPIC_WEBSOCKET_EVENT.format(self._system_system.system_id),
208  self._handle_websocket_update_handle_websocket_update,
209  )
210  )
211 
212  self.async_update_from_rest_apiasync_update_from_rest_api()
213 
214  @callback
215  def async_increment_error_count(self) -> None:
216  """Increment this entity's error count."""
217  LOGGER.debug('Error for entity "%s" (total: %s)', self.namename, self._error_count_error_count)
218  self._error_count_error_count += 1
219 
220  @callback
221  def async_reset_error_count(self) -> None:
222  """Reset this entity's error count."""
223  if self._error_count_error_count == 0:
224  return
225 
226  LOGGER.debug('Resetting error count for "%s"', self.namename)
227  self._error_count_error_count = 0
228 
229  @callback
230  def async_update_from_rest_api(self) -> None:
231  """Update the entity when new data comes from the REST API."""
232 
233  @callback
234  def async_update_from_websocket_event(self, event: WebsocketEvent) -> None:
235  """Update the entity when new data comes from the websocket."""
None _handle_websocket_update(self, WebsocketEvent event)
Definition: entity.py:150
None __init__(self, SimpliSafe simplisafe, SystemType system, *Device|None device=None, Iterable[str]|None additional_websocket_events=None)
Definition: entity.py:59
None async_update_from_websocket_event(self, WebsocketEvent event)
Definition: entity.py:234
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
Definition: dispatcher.py:103