1 """Base class for protect data."""
3 from __future__
import annotations
5 from collections
import defaultdict
6 from collections.abc
import Callable, Generator, Iterable
7 from datetime
import datetime, timedelta
8 from functools
import partial
10 from typing
import TYPE_CHECKING, Any, cast
12 from uiprotect
import ProtectApiClient
13 from uiprotect.data
import (
19 ProtectAdoptableDeviceModel,
20 WSSubscriptionMessage,
22 from uiprotect.exceptions
import ClientError, NotAuthorized
23 from uiprotect.utils
import log_event
24 from uiprotect.websocket
import WebsocketState
30 async_dispatcher_connect,
31 async_dispatcher_send,
46 from .utils
import async_get_devices_by_type
48 _LOGGER = logging.getLogger(__name__)
49 type ProtectDeviceType = ProtectAdoptableDeviceModel | NVR
50 type UFPConfigEntry = ConfigEntry[ProtectData]
55 hass: HomeAssistant, entry: UFPConfigEntry
57 """Check if the last update was successful for a config entry."""
58 return hasattr(entry,
"runtime_data")
and entry.runtime_data.last_update_success
63 """Generate entry specific dispatch ID."""
64 return f
"{DOMAIN}.{entry.entry_id}.{dispatch}"
68 """Coordinate updates."""
73 protect: ProtectApiClient,
74 update_interval: timedelta,
75 entry: UFPConfigEntry,
77 """Initialize an subscriber."""
81 self._subscriptions: defaultdict[
82 str, set[Callable[[ProtectDeviceType],
None]]
84 self._pending_camera_ids: set[str] = set()
85 self.
_unsubs_unsubs: list[CALLBACK_TYPE] = []
95 """Check if RTSP is disabled."""
96 return self.
_entry_entry.options.get(CONF_DISABLE_RTSP,
False)
100 """Max number of events to load at once."""
101 return self.
_entry_entry.options.get(CONF_MAX_MEDIA, DEFAULT_MAX_MEDIA)
105 self, add_callback: Callable[[ProtectAdoptableDeviceModel],
None]
107 """Add an callback for on device adopt."""
108 self.
_entry_entry.async_on_unload(
113 self, device_types: Iterable[ModelType], ignore_unadopted: bool =
True
114 ) -> Generator[ProtectAdoptableDeviceModel]:
115 """Get all devices matching types."""
116 bootstrap = self.
apiapi.bootstrap
117 for device_type
in device_types:
119 if ignore_unadopted
and not device.is_adopted_by_us:
123 def get_cameras(self, ignore_unadopted: bool =
True) -> Generator[Camera]:
124 """Get all cameras."""
126 Generator[Camera], self.
get_by_typesget_by_types({ModelType.CAMERA}, ignore_unadopted)
131 """Subscribe and do the refresh."""
145 """Handle a change in the websocket state."""
151 force_update: bool =
False,
152 exception: Exception |
None =
None,
154 """Process a change in update success."""
159 level = logging.ERROR
if was_success
else logging.DEBUG
160 title = self.
_entry_entry.title
161 _LOGGER.log(level,
"%s: Connection lost", title, exc_info=exception)
167 _LOGGER.warning(
"%s: Connection restored", self.
_entry_entry.title)
173 """Stop processing data."""
174 for unsub
in self.
_unsubs_unsubs:
177 await self.
apiapi.async_disconnect_ws()
180 """Update the data."""
183 except NotAuthorized
as ex:
185 _LOGGER.exception(
"Auth error while updating")
189 _LOGGER.exception(
"Reauthentication required")
190 self.
_entry_entry.async_start_reauth(self.
_hass_hass)
192 except ClientError
as ex:
199 """Add pending camera.
201 A "pending camera" is one that has been adopted by not had its camera channels
202 initialized yet. Will cause Websocket code to check for channels to be
203 initialized for the camera and issue a dispatch once they do.
205 self._pending_camera_ids.
add(camera_id)
209 if device.is_adopted_by_us:
210 _LOGGER.debug(
"Device adopted: %s", device.id)
213 _LOGGER.debug(
"New device detected: %s", device.id)
218 registry = dr.async_get(self.
_hass_hass)
219 device_entry = registry.async_get_device(
220 connections={(dr.CONNECTION_NETWORK_MAC, device.mac)}
223 _LOGGER.debug(
"Device removed: %s", device.id)
224 registry.async_update_device(
225 device_entry.id, remove_config_entry_id=self.
_entry_entry.entry_id
230 self, device: ProtectAdoptableDeviceModel | NVR, changed_data: dict[str, Any]
234 device.model
is ModelType.CAMERA
235 and device.id
in self._pending_camera_ids
236 and "channels" in changed_data
238 self._pending_camera_ids.
remove(device.id)
242 if "doorbell_settings" in changed_data:
244 "Doorbell messages updated. Updating devices with LCD screens"
246 self.
apiapi.bootstrap.nvr.update_all_messages()
248 if camera.feature_flags.has_lcd_screen:
253 """Process a message from the websocket."""
254 if (new_obj := message.new_obj)
is None:
255 if isinstance(message.old_obj, ProtectAdoptableDeviceModel):
259 model_type = new_obj.model
260 if model_type
is ModelType.EVENT:
262 assert isinstance(new_obj, Event)
263 if _LOGGER.isEnabledFor(logging.DEBUG):
266 (new_obj.type
is EventType.DEVICE_ADOPTED)
267 and (metadata := new_obj.metadata)
268 and (device_id := metadata.device_id)
269 and (device := self.
apiapi.bootstrap.get_device_from_id(device_id))
272 elif camera := new_obj.camera:
274 elif light := new_obj.light:
276 elif sensor := new_obj.sensor:
280 if model_type
is ModelType.LIVEVIEW
and len(self.
apiapi.bootstrap.viewers) > 0:
283 "Liveviews updated. Restart Home Assistant to update Viewport select"
288 if message.old_obj
is None and isinstance(new_obj, ProtectAdoptableDeviceModel):
292 if getattr(new_obj,
"is_adopted_by_us",
True)
and hasattr(new_obj,
"mac"):
294 assert isinstance(new_obj, (ProtectAdoptableDeviceModel, NVR))
299 """Process update from the protect data."""
301 for device
in self.
get_by_typesget_by_types(DEVICES_THAT_ADOPT):
306 """Poll the Protect API."""
307 self.
_entry_entry.async_create_background_task(
310 name=f
"{DOMAIN} {self._entry.title} refresh",
316 self, mac: str, update_callback: Callable[[ProtectDeviceType],
None]
318 """Add an callback subscriber."""
319 self._subscriptions[mac].
add(update_callback)
324 self, mac: str, update_callback: Callable[[ProtectDeviceType],
None]
326 """Remove a callback subscriber."""
327 self._subscriptions[mac].
remove(update_callback)
328 if not self._subscriptions[mac]:
329 del self._subscriptions[mac]
333 """Call the callbacks for a device_id."""
335 if not (subscriptions := self._subscriptions.
get(mac)):
337 _LOGGER.debug(
"Updating device: %s (%s)", device.name, mac)
338 for update_callback
in subscriptions:
339 update_callback(device)
344 hass: HomeAssistant, config_entry_ids: set[str]
345 ) -> ProtectApiClient |
None:
346 """Find the UFP instance for the config entry ids."""
349 entry.runtime_data.api
350 for entry_id
in config_entry_ids
351 if (entry := hass.config_entries.async_get_entry(entry_id))
352 and entry.domain == DOMAIN
353 and hasattr(entry,
"runtime_data")
361 """Get all the UFP entries."""
363 list[UFPConfigEntry],
366 for entry
in hass.config_entries.async_entries(
367 DOMAIN, include_ignore=
True, include_disabled=
True
369 if hasattr(entry,
"runtime_data")
376 """Find the ProtectData instance for the NVR id."""
381 if entry.runtime_data.api.bootstrap.nvr.id == nvr_id
389 hass: HomeAssistant, entry_id: str
390 ) -> ProtectData |
None:
391 """Find the ProtectData instance for a config entry id."""
392 if (entry := hass.config_entries.async_get_entry(entry_id))
and hasattr(
393 entry,
"runtime_data"
395 entry = cast(UFPConfigEntry, entry)
396 return entry.runtime_data
None _async_remove_device(self, ProtectAdoptableDeviceModel device)
None _async_update_change(self, bool success, bool force_update=False, Exception|None exception=None)
None _async_update_device(self, ProtectAdoptableDeviceModel|NVR device, dict[str, Any] changed_data)
CALLBACK_TYPE async_subscribe(self, str mac, Callable[[ProtectDeviceType], None] update_callback)
None async_stop(self, *Any args)
None async_add_pending_camera_id(self, str camera_id)
Generator[Camera] get_cameras(self, bool ignore_unadopted=True)
None _async_add_device(self, ProtectAdoptableDeviceModel device)
None _async_process_ws_message(self, WSSubscriptionMessage message)
None _async_poll(self, datetime now)
None __init__(self, HomeAssistant hass, ProtectApiClient protect, timedelta update_interval, UFPConfigEntry entry)
None _async_process_updates(self)
Generator[ProtectAdoptableDeviceModel] get_by_types(self, Iterable[ModelType] device_types, bool ignore_unadopted=True)
None _async_signal_device_update(self, ProtectDeviceType device)
None _async_unsubscribe(self, str mac, Callable[[ProtectDeviceType], None] update_callback)
None _async_websocket_state_changed(self, WebsocketState state)
bool disable_stream(self)
None async_subscribe_adopt(self, Callable[[ProtectAdoptableDeviceModel], None] add_callback)
bool add(self, _T matcher)
bool remove(self, _T matcher)
web.Response get(self, web.Request request, str config_key)
IssData update(pyiss.ISS iss)
str _async_dispatch_id(UFPConfigEntry entry, str dispatch)
ProtectData|None async_get_data_for_entry_id(HomeAssistant hass, str entry_id)
ProtectApiClient|None async_ufp_instance_for_config_entry_ids(HomeAssistant hass, set[str] config_entry_ids)
bool async_last_update_was_successful(HomeAssistant hass, UFPConfigEntry entry)
ProtectData|None async_get_data_for_nvr_id(HomeAssistant hass, str nvr_id)
list[UFPConfigEntry] async_get_ufp_entries(HomeAssistant hass)
dict[str, ProtectAdoptableDeviceModel] async_get_devices_by_type(Bootstrap bootstrap, ModelType device_type)
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)
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)