Cleanup device callbacks in unifiprotect (#73463)

This commit is contained in:
J. Nick Koston 2022-06-20 22:52:41 -05:00 committed by GitHub
parent 0007178d63
commit 9d13252142
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 74 additions and 60 deletions

View File

@ -5,7 +5,15 @@ from copy import copy
from dataclasses import dataclass from dataclasses import dataclass
import logging import logging
from pyunifiprotect.data import NVR, Camera, Event, Light, MountType, Sensor from pyunifiprotect.data import (
NVR,
Camera,
Event,
Light,
MountType,
ProtectModelWithId,
Sensor,
)
from pyunifiprotect.data.nvr import UOSDisk from pyunifiprotect.data.nvr import UOSDisk
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
@ -205,8 +213,8 @@ class ProtectDeviceBinarySensor(ProtectDeviceEntity, BinarySensorEntity):
entity_description: ProtectBinaryEntityDescription entity_description: ProtectBinaryEntityDescription
@callback @callback
def _async_update_device_from_protect(self) -> None: def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
super()._async_update_device_from_protect() super()._async_update_device_from_protect(device)
self._attr_is_on = self.entity_description.get_ufp_value(self.device) self._attr_is_on = self.entity_description.get_ufp_value(self.device)
# UP Sense can be any of the 3 contact sensor device classes # UP Sense can be any of the 3 contact sensor device classes
@ -240,8 +248,8 @@ class ProtectDiskBinarySensor(ProtectNVREntity, BinarySensorEntity):
super().__init__(data, device, description) super().__init__(data, device, description)
@callback @callback
def _async_update_device_from_protect(self) -> None: def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
super()._async_update_device_from_protect() super()._async_update_device_from_protect(device)
slot = self._disk.slot slot = self._disk.slot
self._attr_available = False self._attr_available = False

View File

@ -5,7 +5,12 @@ from collections.abc import Generator
import logging import logging
from pyunifiprotect.api import ProtectApiClient from pyunifiprotect.api import ProtectApiClient
from pyunifiprotect.data import Camera as UFPCamera, CameraChannel, StateType from pyunifiprotect.data import (
Camera as UFPCamera,
CameraChannel,
ProtectModelWithId,
StateType,
)
from homeassistant.components.camera import Camera, CameraEntityFeature from homeassistant.components.camera import Camera, CameraEntityFeature
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@ -137,8 +142,8 @@ class ProtectCamera(ProtectDeviceEntity, Camera):
) )
@callback @callback
def _async_update_device_from_protect(self) -> None: def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
super()._async_update_device_from_protect() super()._async_update_device_from_protect(device)
self.channel = self.device.channels[self.channel.id] self.channel = self.device.channels[self.channel.id]
motion_enabled = self.device.recording_settings.enable_motion_detection motion_enabled = self.device.recording_settings.enable_motion_detection
self._attr_motion_detection_enabled = ( self._attr_motion_detection_enabled = (

View File

@ -1,7 +1,7 @@
"""Base class for protect data.""" """Base class for protect data."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Generator, Iterable from collections.abc import Callable, Generator, Iterable
from datetime import timedelta from datetime import timedelta
import logging import logging
from typing import Any from typing import Any
@ -12,9 +12,10 @@ from pyunifiprotect.data import (
Event, Event,
Liveview, Liveview,
ModelType, ModelType,
ProtectAdoptableDeviceModel,
ProtectModelWithId,
WSSubscriptionMessage, WSSubscriptionMessage,
) )
from pyunifiprotect.data.base import ProtectAdoptableDeviceModel
from pyunifiprotect.exceptions import ClientError, NotAuthorized from pyunifiprotect.exceptions import ClientError, NotAuthorized
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@ -54,7 +55,7 @@ class ProtectData:
self._entry = entry self._entry = entry
self._hass = hass self._hass = hass
self._update_interval = update_interval self._update_interval = update_interval
self._subscriptions: dict[str, list[CALLBACK_TYPE]] = {} self._subscriptions: dict[str, list[Callable[[ProtectModelWithId], None]]] = {}
self._unsub_interval: CALLBACK_TYPE | None = None self._unsub_interval: CALLBACK_TYPE | None = None
self._unsub_websocket: CALLBACK_TYPE | None = None self._unsub_websocket: CALLBACK_TYPE | None = None
@ -123,7 +124,7 @@ class ProtectData:
return return
if message.new_obj.model in DEVICES_WITH_ENTITIES: if message.new_obj.model in DEVICES_WITH_ENTITIES:
self.async_signal_device_id_update(message.new_obj.id) self._async_signal_device_update(message.new_obj)
# trigger update for all Cameras with LCD screens when NVR Doorbell settings updates # trigger update for all Cameras with LCD screens when NVR Doorbell settings updates
if "doorbell_settings" in message.changed_data: if "doorbell_settings" in message.changed_data:
_LOGGER.debug( _LOGGER.debug(
@ -132,15 +133,15 @@ class ProtectData:
self.api.bootstrap.nvr.update_all_messages() self.api.bootstrap.nvr.update_all_messages()
for camera in self.api.bootstrap.cameras.values(): for camera in self.api.bootstrap.cameras.values():
if camera.feature_flags.has_lcd_screen: if camera.feature_flags.has_lcd_screen:
self.async_signal_device_id_update(camera.id) self._async_signal_device_update(camera)
# trigger updates for camera that the event references # trigger updates for camera that the event references
elif isinstance(message.new_obj, Event): elif isinstance(message.new_obj, Event):
if message.new_obj.camera is not None: if message.new_obj.camera is not None:
self.async_signal_device_id_update(message.new_obj.camera.id) self._async_signal_device_update(message.new_obj.camera)
elif message.new_obj.light is not None: elif message.new_obj.light is not None:
self.async_signal_device_id_update(message.new_obj.light.id) self._async_signal_device_update(message.new_obj.light)
elif message.new_obj.sensor is not None: elif message.new_obj.sensor is not None:
self.async_signal_device_id_update(message.new_obj.sensor.id) self._async_signal_device_update(message.new_obj.sensor)
# alert user viewport needs restart so voice clients can get new options # alert user viewport needs restart so voice clients can get new options
elif len(self.api.bootstrap.viewers) > 0 and isinstance( elif len(self.api.bootstrap.viewers) > 0 and isinstance(
message.new_obj, Liveview message.new_obj, Liveview
@ -157,13 +158,13 @@ class ProtectData:
if updates is None: if updates is None:
return return
self.async_signal_device_id_update(self.api.bootstrap.nvr.id) self._async_signal_device_update(self.api.bootstrap.nvr)
for device in async_get_devices(self.api.bootstrap, DEVICES_THAT_ADOPT): for device in async_get_devices(self.api.bootstrap, DEVICES_THAT_ADOPT):
self.async_signal_device_id_update(device.id) self._async_signal_device_update(device)
@callback @callback
def async_subscribe_device_id( def async_subscribe_device_id(
self, device_id: str, update_callback: CALLBACK_TYPE self, device_id: str, update_callback: Callable[[ProtectModelWithId], None]
) -> CALLBACK_TYPE: ) -> CALLBACK_TYPE:
"""Add an callback subscriber.""" """Add an callback subscriber."""
if not self._subscriptions: if not self._subscriptions:
@ -179,7 +180,7 @@ class ProtectData:
@callback @callback
def async_unsubscribe_device_id( def async_unsubscribe_device_id(
self, device_id: str, update_callback: CALLBACK_TYPE self, device_id: str, update_callback: Callable[[ProtectModelWithId], None]
) -> None: ) -> None:
"""Remove a callback subscriber.""" """Remove a callback subscriber."""
self._subscriptions[device_id].remove(update_callback) self._subscriptions[device_id].remove(update_callback)
@ -190,14 +191,15 @@ class ProtectData:
self._unsub_interval = None self._unsub_interval = None
@callback @callback
def async_signal_device_id_update(self, device_id: str) -> None: def _async_signal_device_update(self, device: ProtectModelWithId) -> None:
"""Call the callbacks for a device_id.""" """Call the callbacks for a device_id."""
device_id = device.id
if not self._subscriptions.get(device_id): if not self._subscriptions.get(device_id):
return return
_LOGGER.debug("Updating device: %s", device_id) _LOGGER.debug("Updating device: %s", device_id)
for update_callback in self._subscriptions[device_id]: for update_callback in self._subscriptions[device_id]:
update_callback() update_callback(device)
@callback @callback

View File

@ -14,6 +14,7 @@ from pyunifiprotect.data import (
Light, Light,
ModelType, ModelType,
ProtectAdoptableDeviceModel, ProtectAdoptableDeviceModel,
ProtectModelWithId,
Sensor, Sensor,
StateType, StateType,
Viewer, Viewer,
@ -26,7 +27,7 @@ from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription
from .const import ATTR_EVENT_SCORE, DEFAULT_ATTRIBUTION, DEFAULT_BRAND, DOMAIN from .const import ATTR_EVENT_SCORE, DEFAULT_ATTRIBUTION, DEFAULT_BRAND, DOMAIN
from .data import ProtectData from .data import ProtectData
from .models import ProtectRequiredKeysMixin from .models import ProtectRequiredKeysMixin
from .utils import async_device_by_id, get_nested_attr from .utils import get_nested_attr
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -127,7 +128,7 @@ class ProtectDeviceEntity(Entity):
self._attr_attribution = DEFAULT_ATTRIBUTION self._attr_attribution = DEFAULT_ATTRIBUTION
self._async_set_device_info() self._async_set_device_info()
self._async_update_device_from_protect() self._async_update_device_from_protect(device)
async def async_update(self) -> None: async def async_update(self) -> None:
"""Update the entity. """Update the entity.
@ -149,14 +150,10 @@ class ProtectDeviceEntity(Entity):
) )
@callback @callback
def _async_update_device_from_protect(self) -> None: def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
"""Update Entity object from Protect device.""" """Update Entity object from Protect device."""
if self.data.last_update_success: if self.data.last_update_success:
assert self.device.model assert isinstance(device, ProtectAdoptableDeviceModel)
device = async_device_by_id(
self.data.api.bootstrap, self.device.id, device_type=self.device.model
)
assert device is not None
self.device = device self.device = device
is_connected = ( is_connected = (
@ -174,9 +171,9 @@ class ProtectDeviceEntity(Entity):
self._attr_available = is_connected self._attr_available = is_connected
@callback @callback
def _async_updated_event(self) -> None: def _async_updated_event(self, device: ProtectModelWithId) -> None:
"""Call back for incoming data.""" """Call back for incoming data."""
self._async_update_device_from_protect() self._async_update_device_from_protect(device)
self.async_write_ha_state() self.async_write_ha_state()
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
@ -217,7 +214,7 @@ class ProtectNVREntity(ProtectDeviceEntity):
) )
@callback @callback
def _async_update_device_from_protect(self) -> None: def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
if self.data.last_update_success: if self.data.last_update_success:
self.device = self.data.api.bootstrap.nvr self.device = self.data.api.bootstrap.nvr
@ -254,8 +251,8 @@ class EventThumbnailMixin(ProtectDeviceEntity):
return attrs return attrs
@callback @callback
def _async_update_device_from_protect(self) -> None: def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
super()._async_update_device_from_protect() super()._async_update_device_from_protect(device)
self._event = self._async_get_event() self._event = self._async_get_event()
attrs = self.extra_state_attributes or {} attrs = self.extra_state_attributes or {}

View File

@ -4,7 +4,7 @@ from __future__ import annotations
import logging import logging
from typing import Any from typing import Any
from pyunifiprotect.data import Light from pyunifiprotect.data import Light, ProtectModelWithId
from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@ -59,8 +59,8 @@ class ProtectLight(ProtectDeviceEntity, LightEntity):
_attr_supported_color_modes = {ColorMode.BRIGHTNESS} _attr_supported_color_modes = {ColorMode.BRIGHTNESS}
@callback @callback
def _async_update_device_from_protect(self) -> None: def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
super()._async_update_device_from_protect() super()._async_update_device_from_protect(device)
self._attr_is_on = self.device.is_light_on self._attr_is_on = self.device.is_light_on
self._attr_brightness = unifi_brightness_to_hass( self._attr_brightness = unifi_brightness_to_hass(
self.device.light_device_settings.led_level self.device.light_device_settings.led_level

View File

@ -4,7 +4,7 @@ from __future__ import annotations
import logging import logging
from typing import Any from typing import Any
from pyunifiprotect.data import Doorlock, LockStatusType from pyunifiprotect.data import Doorlock, LockStatusType, ProtectModelWithId
from homeassistant.components.lock import LockEntity, LockEntityDescription from homeassistant.components.lock import LockEntity, LockEntityDescription
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@ -56,8 +56,8 @@ class ProtectLock(ProtectDeviceEntity, LockEntity):
self._attr_name = f"{self.device.name} Lock" self._attr_name = f"{self.device.name} Lock"
@callback @callback
def _async_update_device_from_protect(self) -> None: def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
super()._async_update_device_from_protect() super()._async_update_device_from_protect(device)
self._attr_is_locked = False self._attr_is_locked = False
self._attr_is_locking = False self._attr_is_locking = False

View File

@ -4,7 +4,7 @@ from __future__ import annotations
import logging import logging
from typing import Any from typing import Any
from pyunifiprotect.data import Camera from pyunifiprotect.data import Camera, ProtectModelWithId
from pyunifiprotect.exceptions import StreamError from pyunifiprotect.exceptions import StreamError
from homeassistant.components import media_source from homeassistant.components import media_source
@ -83,8 +83,8 @@ class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity):
self._attr_media_content_type = MEDIA_TYPE_MUSIC self._attr_media_content_type = MEDIA_TYPE_MUSIC
@callback @callback
def _async_update_device_from_protect(self) -> None: def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
super()._async_update_device_from_protect() super()._async_update_device_from_protect(device)
self._attr_volume_level = float(self.device.speaker_settings.volume / 100) self._attr_volume_level = float(self.device.speaker_settings.volume / 100)
if ( if (
@ -110,7 +110,7 @@ class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity):
): ):
_LOGGER.debug("Stopping playback for %s Speaker", self.device.name) _LOGGER.debug("Stopping playback for %s Speaker", self.device.name)
await self.device.stop_audio() await self.device.stop_audio()
self._async_updated_event() self._async_updated_event(self.device)
async def async_play_media( async def async_play_media(
self, media_type: str, media_id: str, **kwargs: Any self, media_type: str, media_id: str, **kwargs: Any
@ -134,11 +134,11 @@ class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity):
raise HomeAssistantError(err) from err raise HomeAssistantError(err) from err
else: else:
# update state after starting player # update state after starting player
self._async_updated_event() self._async_updated_event(self.device)
# wait until player finishes to update state again # wait until player finishes to update state again
await self.device.wait_until_audio_completes() await self.device.wait_until_audio_completes()
self._async_updated_event() self._async_updated_event(self.device)
async def async_browse_media( async def async_browse_media(
self, media_content_type: str | None = None, media_content_id: str | None = None self, media_content_type: str | None = None, media_content_id: str | None = None

View File

@ -147,12 +147,12 @@ async def async_migrate_device_ids(
try: try:
registry.async_update_entity(entity.entity_id, new_unique_id=new_unique_id) registry.async_update_entity(entity.entity_id, new_unique_id=new_unique_id)
except ValueError as err: except ValueError as err:
print(err)
_LOGGER.warning( _LOGGER.warning(
"Could not migrate entity %s (old unique_id: %s, new unique_id: %s)", "Could not migrate entity %s (old unique_id: %s, new unique_id: %s): %s",
entity.entity_id, entity.entity_id,
entity.unique_id, entity.unique_id,
new_unique_id, new_unique_id,
err,
) )
else: else:
count += 1 count += 1

View File

@ -4,7 +4,7 @@ from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from datetime import timedelta from datetime import timedelta
from pyunifiprotect.data import Camera, Doorlock, Light from pyunifiprotect.data import Camera, Doorlock, Light, ProtectModelWithId
from homeassistant.components.number import NumberEntity, NumberEntityDescription from homeassistant.components.number import NumberEntity, NumberEntityDescription
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@ -203,8 +203,8 @@ class ProtectNumbers(ProtectDeviceEntity, NumberEntity):
self._attr_native_step = self.entity_description.ufp_step self._attr_native_step = self.entity_description.ufp_step
@callback @callback
def _async_update_device_from_protect(self) -> None: def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
super()._async_update_device_from_protect() super()._async_update_device_from_protect(device)
self._attr_native_value = self.entity_description.get_ufp_value(self.device) self._attr_native_value = self.entity_description.get_ufp_value(self.device)
async def async_set_native_value(self, value: float) -> None: async def async_set_native_value(self, value: float) -> None:

View File

@ -19,6 +19,7 @@ from pyunifiprotect.data import (
LightModeEnableType, LightModeEnableType,
LightModeType, LightModeType,
MountType, MountType,
ProtectModelWithId,
RecordingMode, RecordingMode,
Sensor, Sensor,
Viewer, Viewer,
@ -355,8 +356,8 @@ class ProtectSelects(ProtectDeviceEntity, SelectEntity):
self._async_set_options() self._async_set_options()
@callback @callback
def _async_update_device_from_protect(self) -> None: def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
super()._async_update_device_from_protect() super()._async_update_device_from_protect(device)
# entities with categories are not exposed for voice and safe to update dynamically # entities with categories are not exposed for voice and safe to update dynamically
if ( if (

View File

@ -12,6 +12,7 @@ from pyunifiprotect.data import (
Event, Event,
ProtectAdoptableDeviceModel, ProtectAdoptableDeviceModel,
ProtectDeviceModel, ProtectDeviceModel,
ProtectModelWithId,
Sensor, Sensor,
) )
@ -540,8 +541,8 @@ class ProtectDeviceSensor(ProtectDeviceEntity, SensorEntity):
super().__init__(data, device, description) super().__init__(data, device, description)
@callback @callback
def _async_update_device_from_protect(self) -> None: def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
super()._async_update_device_from_protect() super()._async_update_device_from_protect(device)
self._attr_native_value = self.entity_description.get_ufp_value(self.device) self._attr_native_value = self.entity_description.get_ufp_value(self.device)
@ -560,8 +561,8 @@ class ProtectNVRSensor(ProtectNVREntity, SensorEntity):
super().__init__(data, device, description) super().__init__(data, device, description)
@callback @callback
def _async_update_device_from_protect(self) -> None: def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
super()._async_update_device_from_protect() super()._async_update_device_from_protect(device)
self._attr_native_value = self.entity_description.get_ufp_value(self.device) self._attr_native_value = self.entity_description.get_ufp_value(self.device)
@ -585,9 +586,9 @@ class ProtectEventSensor(ProtectDeviceSensor, EventThumbnailMixin):
return event return event
@callback @callback
def _async_update_device_from_protect(self) -> None: def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
# do not call ProtectDeviceSensor method since we want event to get value here # do not call ProtectDeviceSensor method since we want event to get value here
EventThumbnailMixin._async_update_device_from_protect(self) EventThumbnailMixin._async_update_device_from_protect(self, device)
if self._event is None: if self._event is None:
self._attr_native_value = OBJECT_TYPE_NONE self._attr_native_value = OBJECT_TYPE_NONE
else: else: