1 """Shared Entity definition for UniFi Protect Integration."""
3 from __future__
import annotations
5 from collections.abc
import Callable, Coroutine, Sequence
6 from dataclasses
import dataclass
7 from datetime
import datetime
9 from functools
import partial
11 from operator
import attrgetter
12 from typing
import TYPE_CHECKING, Any, Generic, TypeVar
14 from uiprotect
import make_enabled_getter, make_required_getter, make_value_getter
15 from uiprotect.data
import (
19 ProtectAdoptableDeviceModel,
20 SmartDetectObjectType,
36 from .data
import ProtectData, ProtectDeviceType
38 _LOGGER = logging.getLogger(__name__)
40 T = TypeVar(
"T", bound=ProtectAdoptableDeviceModel | NVR)
44 """Type of permission level required for entity."""
54 klass: type[BaseProtectEntity],
55 model_type: ModelType,
56 descs: Sequence[ProtectEntityDescription],
57 unadopted_descs: Sequence[ProtectEntityDescription] |
None =
None,
58 ufp_device: ProtectAdoptableDeviceModel |
None =
None,
59 ) -> list[BaseProtectEntity]:
60 if not descs
and not unadopted_descs:
63 entities: list[BaseProtectEntity] = []
66 if ufp_device
is not None
67 else data.get_by_types({model_type}, ignore_unadopted=
False)
69 auth_user = data.api.bootstrap.auth_user
70 for device
in devices:
72 assert isinstance(device, ProtectAdoptableDeviceModel)
73 if not device.is_adopted_by_us:
75 for description
in unadopted_descs:
80 description=description,
84 "Adding %s entity %s for %s",
91 can_write = device.can_write(auth_user)
92 for description
in descs:
93 if (perms := description.ufp_perm)
is not None:
94 if perms
is PermRequired.WRITE
and not can_write:
96 if perms
is PermRequired.NO_WRITE
and can_write:
98 if perms
is PermRequired.DELETE
and not device.can_delete(auth_user):
101 if not description.has_required(device):
108 description=description,
112 "Adding %s entity %s for %s",
133 model_type: ModelType,
134 model_descriptions: dict[ModelType, Sequence[ProtectEntityDescription]] |
None,
135 all_descs: Sequence[ProtectEntityDescription] |
None,
136 ) -> list[ProtectEntityDescription]:
137 """Combine all the descriptions with descriptions a model type."""
138 descs: list[ProtectEntityDescription] =
list(all_descs)
if all_descs
else []
139 if model_descriptions
and (model_descs := model_descriptions.get(model_type)):
140 descs.extend(model_descs)
147 klass: type[BaseProtectEntity],
148 model_descriptions: dict[ModelType, Sequence[ProtectEntityDescription]]
150 all_descs: Sequence[ProtectEntityDescription] |
None =
None,
151 unadopted_descs: list[ProtectEntityDescription] |
None =
None,
152 ufp_device: ProtectAdoptableDeviceModel |
None =
None,
153 ) -> list[BaseProtectEntity]:
154 """Generate a list of all the device entities."""
155 if ufp_device
is None:
156 entities: list[BaseProtectEntity] = []
157 for model_type
in _ALL_MODEL_TYPES:
164 device_model_type = ufp_device.model
165 assert device_model_type
is not None
168 data, klass, device_model_type, descs, unadopted_descs, ufp_device
173 """Base class for UniFi protect entities."""
175 device: ProtectDeviceType
177 _attr_should_poll =
False
178 _attr_attribution = DEFAULT_ATTRIBUTION
179 _state_attrs: tuple[str, ...] = (
"_attr_available",)
180 _attr_has_entity_name =
True
181 _async_get_ufp_enabled: Callable[[ProtectAdoptableDeviceModel], bool] |
None =
None
186 device: ProtectDeviceType,
187 description: EntityDescription |
None =
None,
189 """Initialize the entity."""
194 if description
is None:
199 self.
_attr_unique_id_attr_unique_id = f
"{self.device.mac}_{description.key}"
200 if isinstance(description, ProtectEntityDescription):
205 partial(attrgetter(attr), self)
for attr
in self._state_attrs
209 """Update the entity.
211 Only used by the generic entity update service.
213 await self.
datadata.async_refresh()
217 """Set device info."""
221 """Update Entity object from Protect device."""
223 if last_updated_success := self.
datadata.last_update_success:
224 self.
devicedevice = device
226 if device.model
is ModelType.NVR:
227 available = last_updated_success
230 assert isinstance(device, ProtectAdoptableDeviceModel)
231 connected = device.state
is StateType.CONNECTED
or (
232 not device.is_adopted_by_us
and device.can_adopt
235 enabled =
not async_get_ufp_enabled
or async_get_ufp_enabled(device)
236 available = last_updated_success
and connected
and enabled
238 if available != was_available:
243 """When device is updated from Protect."""
244 previous_attrs = [getter()
for getter
in self.
_state_getters_state_getters]
248 if previous_attrs[idx] != getter():
253 if _LOGGER.isEnabledFor(logging.DEBUG):
254 device_name = device.name
or ""
255 if hasattr(self,
"entity_description")
and self.
entity_descriptionentity_description.name:
256 device_name += f
" {self.entity_description.name}"
259 "Updating state [%s (%s)] %s -> %s",
263 tuple((getattr(self, attr))
for attr
in self._state_attrs),
268 """When entity is added to hass."""
277 """Base class for entities with is_on property."""
279 _state_attrs: tuple[str, ...] = (
"_attr_available",
"_attr_is_on")
280 _attr_is_on: bool |
None
281 entity_description: ProtectEntityDescription
284 self, device: ProtectAdoptableDeviceModel | NVR
288 if was_on != (is_on := self.
entity_descriptionentity_description.get_ufp_value(device)
is True):
293 """Base class for UniFi protect entities."""
298 name=self.
devicedevice.display_name,
299 manufacturer=DEFAULT_BRAND,
300 model=self.
devicedevice.market_name
or self.
devicedevice.type,
301 model_id=self.
devicedevice.type,
302 via_device=(DOMAIN, self.
datadata.api.bootstrap.nvr.mac),
303 sw_version=self.
devicedevice.firmware_version,
304 connections={(dr.CONNECTION_NETWORK_MAC, self.
devicedevice.mac)},
305 configuration_url=self.
devicedevice.protect_url,
310 """Base class for unifi protect entities."""
317 connections={(dr.CONNECTION_NETWORK_MAC, self.
devicedevice.mac)},
318 identifiers={(DOMAIN, self.
devicedevice.mac)},
319 manufacturer=DEFAULT_BRAND,
320 name=self.
devicedevice.display_name,
321 model=self.
devicedevice.type,
322 sw_version=
str(self.
devicedevice.version),
323 configuration_url=self.
devicedevice.api.base_url,
328 """Adds motion event attributes to sensor."""
330 entity_description: ProtectEventMixin
331 _unrecorded_attributes = frozenset({ATTR_EVENT_ID, ATTR_EVENT_SCORE})
332 _event: Event |
None =
None
333 _event_end: datetime |
None =
None
337 """Clear the event and state."""
341 """Set event attrs."""
343 ATTR_EVENT_ID: event.id,
344 ATTR_EVENT_SCORE: event.score,
358 self, prev_event: Event |
None, prev_event_end: datetime |
None
360 """Determine if the event has already ended.
362 The event_end time is passed because the prev_event and event object
363 may be the same object, and the uiprotect code will mutate the
364 event object so we need to check the datetime object that was
365 saved from the last time the entity was updated.
368 (event := self._event)
372 and prev_event.id == event.id
376 @dataclass(frozen=True, kw_only=True)
378 """Base class for protect entity descriptions."""
380 ufp_required_field: str |
None =
None
381 ufp_value: str |
None =
None
382 ufp_value_fn: Callable[[T], Any] |
None =
None
383 ufp_enabled: str |
None =
None
384 ufp_perm: PermRequired |
None =
None
387 has_required: Callable[[T], bool] = bool
388 get_ufp_enabled: Callable[[T], bool] |
None =
None
391 """Return value from UniFi Protect device; overridden in __post_init__."""
396 f
"`ufp_value` or `ufp_value_fn` is required for {self}"
400 """Override get_ufp_value, has_required, and get_ufp_enabled if required."""
401 _setter = partial(object.__setattr__, self)
403 if (ufp_value := self.ufp_value)
is not None:
404 _setter(
"get_ufp_value", make_value_getter(ufp_value))
405 elif (ufp_value_fn := self.ufp_value_fn)
is not None:
406 _setter(
"get_ufp_value", ufp_value_fn)
408 if (ufp_enabled := self.ufp_enabled)
is not None:
409 _setter(
"get_ufp_enabled", make_enabled_getter(ufp_enabled))
411 if (ufp_required_field := self.ufp_required_field)
is not None:
412 _setter(
"has_required", make_required_getter(ufp_required_field))
415 @dataclass(frozen=True, kw_only=True)
417 """Mixin for events."""
419 ufp_event_obj: str |
None =
None
420 ufp_obj_type: SmartDetectObjectType |
None =
None
423 """Return value from UniFi Protect device."""
427 """Determine if the detection type is a match."""
429 not (obj_type := self.ufp_obj_type)
or obj_type
in event.smart_detect_types
433 """Override get_event_obj if ufp_event_obj is set."""
434 if (_ufp_event_obj := self.ufp_event_obj)
is not None:
435 object.__setattr__(self,
"get_event_obj", attrgetter(_ufp_event_obj))
439 @dataclass(frozen=True, kw_only=True)
441 """Mixin for settable values."""
443 ufp_set_method: str |
None =
None
444 ufp_set_method_fn: Callable[[T, Any], Coroutine[Any, Any,
None]] |
None =
None
446 async
def ufp_set(self, obj: T, value: Any) ->
None:
447 """Set value for UniFi Protect device."""
448 _LOGGER.debug(
"Setting %s to %s for %s", self.name, value, obj.display_name)
449 if self.ufp_set_method
is not None:
450 await getattr(obj, self.ufp_set_method)(value)
451 elif self.ufp_set_method_fn
is not None:
452 await self.ufp_set_method_fn(obj, value)
None _async_set_device_info(self)
None async_added_to_hass(self)
None __init__(self, ProtectData data, ProtectDeviceType device, EntityDescription|None description=None)
None _async_update_device_from_protect(self, ProtectDeviceType device)
None _async_updated_event(self, ProtectDeviceType device)
None _async_event_with_immediate_end(self)
_attr_extra_state_attributes
None _set_event_done(self)
bool _event_already_ended(self, Event|None prev_event, datetime|None prev_event_end)
None _set_event_attrs(self, Event event)
None _async_set_device_info(self)
Any get_ufp_value(self, T obj)
bool has_matching_smart(self, Event event)
Event|None get_event_obj(self, T obj)
None _async_update_device_from_protect(self, ProtectAdoptableDeviceModel|NVR device)
None _async_set_device_info(self)
None ufp_set(self, T obj, Any value)
None async_write_ha_state(self)
None async_on_remove(self, CALLBACK_TYPE func)
CALLBACK_TYPE async_subscribe(HomeAssistant hass, str topic, Callable[[ReceiveMessage], Coroutine[Any, Any, None]|None] msg_callback, int qos=DEFAULT_QOS, str|None encoding=DEFAULT_ENCODING)
list[BaseProtectEntity] async_all_device_entities(ProtectData data, type[BaseProtectEntity] klass, dict[ModelType, Sequence[ProtectEntityDescription]]|None model_descriptions=None, Sequence[ProtectEntityDescription]|None all_descs=None, list[ProtectEntityDescription]|None unadopted_descs=None, ProtectAdoptableDeviceModel|None ufp_device=None)
list[BaseProtectEntity] _async_device_entities(ProtectData data, type[BaseProtectEntity] klass, ModelType model_type, Sequence[ProtectEntityDescription] descs, Sequence[ProtectEntityDescription]|None unadopted_descs=None, ProtectAdoptableDeviceModel|None ufp_device=None)
list[ProtectEntityDescription] _combine_model_descs(ModelType model_type, dict[ModelType, Sequence[ProtectEntityDescription]]|None model_descriptions, Sequence[ProtectEntityDescription]|None all_descs)