Split UniFi Protect object sensor into multiple (#82595)

This commit is contained in:
Christopher Bailey 2022-11-28 14:07:53 -05:00 committed by GitHub
parent 892be99ca0
commit b842e26d36
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 274 additions and 87 deletions

View File

@ -8,13 +8,13 @@ import logging
from pyunifiprotect.data import ( from pyunifiprotect.data import (
NVR, NVR,
Camera, Camera,
Event,
Light, Light,
ModelType, ModelType,
MountType, MountType,
ProtectAdoptableDeviceModel, ProtectAdoptableDeviceModel,
ProtectModelWithId, ProtectModelWithId,
Sensor, Sensor,
SmartDetectObjectType,
) )
from pyunifiprotect.data.nvr import UOSDisk from pyunifiprotect.data.nvr import UOSDisk
@ -29,15 +29,15 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DISPATCH_ADOPT, DOMAIN from .const import DEVICE_CLASS_DETECTION, DISPATCH_ADOPT, DOMAIN
from .data import ProtectData from .data import ProtectData
from .entity import ( from .entity import (
EventThumbnailMixin, EventEntityMixin,
ProtectDeviceEntity, ProtectDeviceEntity,
ProtectNVREntity, ProtectNVREntity,
async_all_device_entities, async_all_device_entities,
) )
from .models import PermRequired, ProtectRequiredKeysMixin from .models import PermRequired, ProtectEventMixin, ProtectRequiredKeysMixin
from .utils import async_dispatch_id as _ufpd from .utils import async_dispatch_id as _ufpd
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -51,6 +51,13 @@ class ProtectBinaryEntityDescription(
"""Describes UniFi Protect Binary Sensor entity.""" """Describes UniFi Protect Binary Sensor entity."""
@dataclass
class ProtectBinaryEventEntityDescription(
ProtectEventMixin, BinarySensorEntityDescription
):
"""Describes UniFi Protect Binary Sensor entity."""
MOUNT_DEVICE_CLASS_MAP = { MOUNT_DEVICE_CLASS_MAP = {
MountType.GARAGE: BinarySensorDeviceClass.GARAGE_DOOR, MountType.GARAGE: BinarySensorDeviceClass.GARAGE_DOOR,
MountType.WINDOW: BinarySensorDeviceClass.WINDOW, MountType.WINDOW: BinarySensorDeviceClass.WINDOW,
@ -179,7 +186,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = (
ProtectBinaryEntityDescription( ProtectBinaryEntityDescription(
key="smart_face", key="smart_face",
name="Detections: Face", name="Detections: Face",
icon="mdi:human-greeting", icon="mdi:mdi-face",
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
ufp_required_field="can_detect_face", ufp_required_field="can_detect_face",
ufp_value="is_face_detection_on", ufp_value="is_face_detection_on",
@ -313,12 +320,66 @@ SENSE_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = (
), ),
) )
MOTION_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( MOTION_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
ProtectBinaryEntityDescription( ProtectBinaryEventEntityDescription(
key="motion", key="motion",
name="Motion", name="Motion",
device_class=BinarySensorDeviceClass.MOTION, device_class=BinarySensorDeviceClass.MOTION,
ufp_value="is_motion_detected", ufp_value="is_motion_detected",
ufp_event_obj="last_motion_event",
),
ProtectBinaryEventEntityDescription(
key="smart_obj_any",
name="Object Detected",
icon="mdi:eye",
device_class=DEVICE_CLASS_DETECTION,
ufp_value="is_smart_detected",
ufp_required_field="feature_flags.has_smart_detect",
ufp_event_obj="last_smart_detect_event",
),
ProtectBinaryEventEntityDescription(
key="smart_obj_person",
name="Person Detected",
icon="mdi:walk",
device_class=DEVICE_CLASS_DETECTION,
ufp_value="is_smart_detected",
ufp_required_field="can_detect_person",
ufp_enabled="is_person_detection_on",
ufp_event_obj="last_smart_detect_event",
ufp_smart_type=SmartDetectObjectType.PERSON,
),
ProtectBinaryEventEntityDescription(
key="smart_obj_vehicle",
name="Vehicle Detected",
icon="mdi:car",
device_class=DEVICE_CLASS_DETECTION,
ufp_value="is_smart_detected",
ufp_required_field="can_detect_vehicle",
ufp_enabled="is_vehicle_detection_on",
ufp_event_obj="last_smart_detect_event",
ufp_smart_type=SmartDetectObjectType.VEHICLE,
),
ProtectBinaryEventEntityDescription(
key="smart_obj_face",
name="Face Detected",
device_class=DEVICE_CLASS_DETECTION,
icon="mdi:mdi-face",
ufp_value="is_smart_detected",
ufp_required_field="can_detect_face",
ufp_enabled="is_face_detection_on",
ufp_event_obj="last_smart_detect_event",
ufp_smart_type=SmartDetectObjectType.FACE,
),
ProtectBinaryEventEntityDescription(
key="smart_obj_package",
name="Package Detected",
device_class=DEVICE_CLASS_DETECTION,
icon="mdi:package-variant-closed",
ufp_value="is_smart_detected",
ufp_required_field="can_detect_package",
ufp_enabled="is_package_detection_on",
ufp_event_obj="last_smart_detect_event",
ufp_smart_type=SmartDetectObjectType.PACKAGE,
), ),
) )
@ -415,6 +476,8 @@ def _async_motion_entities(
) )
for device in devices: for device in devices:
for description in MOTION_SENSORS: for description in MOTION_SENSORS:
if not description.has_required(device):
continue
entities.append(ProtectEventBinarySensor(data, device, description)) entities.append(ProtectEventBinarySensor(data, device, description))
_LOGGER.debug( _LOGGER.debug(
"Adding binary sensor entity %s for %s", "Adding binary sensor entity %s for %s",
@ -508,17 +571,12 @@ class ProtectDiskBinarySensor(ProtectNVREntity, BinarySensorEntity):
self._attr_is_on = not self._disk.is_healthy self._attr_is_on = not self._disk.is_healthy
class ProtectEventBinarySensor(EventThumbnailMixin, ProtectDeviceBinarySensor): class ProtectEventBinarySensor(EventEntityMixin, BinarySensorEntity):
"""A UniFi Protect Device Binary Sensor with access tokens.""" """A UniFi Protect Device Binary Sensor for events."""
device: Camera entity_description: ProtectBinaryEventEntityDescription
@callback @callback
def _async_get_event(self) -> Event | None: def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
"""Get event from Protect device.""" super()._async_update_device_from_protect(device)
self._attr_is_on = self.entity_description.get_ufp_value(self.device)
event: Event | None = None
if self.device.is_motion_detected and self.device.last_motion_event is not None:
event = self.device.last_motion_event
return event

View File

@ -7,6 +7,7 @@ from homeassistant.const import Platform
DOMAIN = "unifiprotect" DOMAIN = "unifiprotect"
ATTR_EVENT_SCORE = "event_score" ATTR_EVENT_SCORE = "event_score"
ATTR_EVENT_ID = "event_id"
ATTR_WIDTH = "width" ATTR_WIDTH = "width"
ATTR_HEIGHT = "height" ATTR_HEIGHT = "height"
ATTR_FPS = "fps" ATTR_FPS = "fps"
@ -67,3 +68,5 @@ PLATFORMS = [
DISPATCH_ADD = "add_device" DISPATCH_ADD = "add_device"
DISPATCH_ADOPT = "adopt_device" DISPATCH_ADOPT = "adopt_device"
DISPATCH_CHANNELS = "new_camera_channels" DISPATCH_CHANNELS = "new_camera_channels"
DEVICE_CLASS_DETECTION = "unifiprotect__detection"

View File

@ -24,10 +24,15 @@ from homeassistant.core import callback
import homeassistant.helpers.device_registry as dr import homeassistant.helpers.device_registry as dr
from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription
from .const import ATTR_EVENT_SCORE, DEFAULT_ATTRIBUTION, DEFAULT_BRAND, DOMAIN from .const import (
ATTR_EVENT_ID,
ATTR_EVENT_SCORE,
DEFAULT_ATTRIBUTION,
DEFAULT_BRAND,
DOMAIN,
)
from .data import ProtectData from .data import ProtectData
from .models import PermRequired, ProtectRequiredKeysMixin from .models import PermRequired, ProtectEventMixin, ProtectRequiredKeysMixin
from .utils import get_nested_attr
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -82,10 +87,8 @@ def _async_device_entities(
): ):
continue continue
if description.ufp_required_field: if not description.has_required(device):
required_field = get_nested_attr(device, description.ufp_required_field) continue
if not required_field:
continue
entities.append( entities.append(
klass( klass(
@ -294,42 +297,39 @@ class ProtectNVREntity(ProtectDeviceEntity):
self._attr_available = self.data.last_update_success self._attr_available = self.data.last_update_success
class EventThumbnailMixin(ProtectDeviceEntity): class EventEntityMixin(ProtectDeviceEntity):
"""Adds motion event attributes to sensor.""" """Adds motion event attributes to sensor."""
def __init__(self, *args: Any, **kwarg: Any) -> None: entity_description: ProtectEventMixin
def __init__(
self,
*args: Any,
**kwarg: Any,
) -> None:
"""Init an sensor that has event thumbnails.""" """Init an sensor that has event thumbnails."""
super().__init__(*args, **kwarg) super().__init__(*args, **kwarg)
self._event: Event | None = None self._event: Event | None = None
@callback @callback
def _async_get_event(self) -> Event | None: def _async_event_extra_attrs(self) -> dict[str, Any]:
"""Get event from Protect device. attrs: dict[str, Any] = {}
To be overridden by child classes.
"""
raise NotImplementedError()
@callback
def _async_thumbnail_extra_attrs(self) -> dict[str, Any]:
# Camera motion sensors with object detection
attrs: dict[str, Any] = {
ATTR_EVENT_SCORE: 0,
}
if self._event is None: if self._event is None:
return attrs return attrs
attrs[ATTR_EVENT_ID] = self._event.id
attrs[ATTR_EVENT_SCORE] = self._event.score attrs[ATTR_EVENT_SCORE] = self._event.score
return attrs return attrs
@callback @callback
def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
super()._async_update_device_from_protect(device) super()._async_update_device_from_protect(device)
self._event = self._async_get_event() self._attr_is_on: bool | None = self.entity_description.get_is_on(device)
self._event = self.entity_description.get_event_obj(device)
attrs = self.extra_state_attributes or {} attrs = self.extra_state_attributes or {}
self._attr_extra_state_attributes = { self._attr_extra_state_attributes = {
**attrs, **attrs,
**self._async_thumbnail_extra_attrs(), **self._async_event_extra_attrs(),
} }

View File

@ -12,7 +12,10 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform from homeassistant.const import Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er, issue_registry as ir
from homeassistant.helpers.issue_registry import IssueSeverity
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -30,6 +33,23 @@ async def async_migrate_data(
await async_migrate_device_ids(hass, entry, protect) await async_migrate_device_ids(hass, entry, protect)
_LOGGER.debug("Completed Migrate: async_migrate_device_ids") _LOGGER.debug("Completed Migrate: async_migrate_device_ids")
entity_registry = er.async_get(hass)
for entity in er.async_entries_for_config_entry(entity_registry, entry.entry_id):
if (
entity.domain == Platform.SENSOR
and entity.disabled_by is None
and "detected_object" in entity.unique_id
):
ir.async_create_issue(
hass,
DOMAIN,
"deprecate_smart_sensor",
is_fixable=False,
breaks_in_ha_version="2023.2.0",
severity=IssueSeverity.WARNING,
translation_key="deprecate_smart_sensor",
)
async def async_get_bootstrap(protect: ProtectApiClient) -> Bootstrap: async def async_get_bootstrap(protect: ProtectApiClient) -> Bootstrap:
"""Get UniFi Protect bootstrap or raise appropriate HA error.""" """Get UniFi Protect bootstrap or raise appropriate HA error."""

View File

@ -5,9 +5,9 @@ from collections.abc import Callable, Coroutine
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum from enum import Enum
import logging import logging
from typing import Any, Generic, TypeVar, Union from typing import Any, Generic, TypeVar, Union, cast
from pyunifiprotect.data import NVR, ProtectAdoptableDeviceModel from pyunifiprotect.data import NVR, Event, ProtectAdoptableDeviceModel
from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.entity import EntityDescription
@ -54,6 +54,41 @@ class ProtectRequiredKeysMixin(EntityDescription, Generic[T]):
return bool(get_nested_attr(obj, self.ufp_enabled)) return bool(get_nested_attr(obj, self.ufp_enabled))
return True return True
def has_required(self, obj: T) -> bool:
"""Return if has required field."""
if self.ufp_required_field is None:
return True
return bool(get_nested_attr(obj, self.ufp_required_field))
@dataclass
class ProtectEventMixin(ProtectRequiredKeysMixin[T]):
"""Mixin for events."""
ufp_event_obj: str | None = None
ufp_smart_type: str | None = None
def get_event_obj(self, obj: T) -> Event | None:
"""Return value from UniFi Protect device."""
if self.ufp_event_obj is not None:
return cast(Event, get_nested_attr(obj, self.ufp_event_obj))
return None
def get_is_on(self, obj: T) -> bool:
"""Return value if event is active."""
value = bool(self.get_ufp_value(obj))
if value:
event = self.get_event_obj(obj)
value = event is not None
if event is not None and self.ufp_smart_type is not None:
value = self.ufp_smart_type in event.smart_detect_types
return value
@dataclass @dataclass
class ProtectSetableKeysMixin(ProtectRequiredKeysMixin[T]): class ProtectSetableKeysMixin(ProtectRequiredKeysMixin[T]):

View File

@ -9,7 +9,6 @@ from typing import Any, cast
from pyunifiprotect.data import ( from pyunifiprotect.data import (
NVR, NVR,
Camera, Camera,
Event,
Light, Light,
ModelType, ModelType,
ProtectAdoptableDeviceModel, ProtectAdoptableDeviceModel,
@ -41,20 +40,19 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DISPATCH_ADOPT, DOMAIN from .const import DEVICE_CLASS_DETECTION, DISPATCH_ADOPT, DOMAIN
from .data import ProtectData from .data import ProtectData
from .entity import ( from .entity import (
EventThumbnailMixin, EventEntityMixin,
ProtectDeviceEntity, ProtectDeviceEntity,
ProtectNVREntity, ProtectNVREntity,
async_all_device_entities, async_all_device_entities,
) )
from .models import PermRequired, ProtectRequiredKeysMixin, T from .models import PermRequired, ProtectEventMixin, ProtectRequiredKeysMixin, T
from .utils import async_dispatch_id as _ufpd, async_get_light_motion_current from .utils import async_dispatch_id as _ufpd, async_get_light_motion_current
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
OBJECT_TYPE_NONE = "none" OBJECT_TYPE_NONE = "none"
DEVICE_CLASS_DETECTION = "unifiprotect__detection"
@dataclass @dataclass
@ -74,6 +72,13 @@ class ProtectSensorEntityDescription(
return value return value
@dataclass
class ProtectSensorEventEntityDescription(
ProtectEventMixin[T], SensorEntityDescription
):
"""Describes UniFi Protect Sensor entity."""
def _get_uptime(obj: ProtectDeviceModel) -> datetime | None: def _get_uptime(obj: ProtectDeviceModel) -> datetime | None:
if obj.up_since is None: if obj.up_since is None:
return None return None
@ -513,11 +518,14 @@ NVR_DISABLED_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
), ),
) )
MOTION_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( MOTION_SENSORS: tuple[ProtectSensorEventEntityDescription, ...] = (
ProtectSensorEntityDescription( ProtectSensorEventEntityDescription(
key="detected_object", key="detected_object",
name="Detected Object", name="Detected Object",
device_class=DEVICE_CLASS_DETECTION, device_class=DEVICE_CLASS_DETECTION,
entity_registry_enabled_default=False,
ufp_value="is_smart_detected",
ufp_event_obj="last_smart_detect_event",
), ),
) )
@ -666,8 +674,8 @@ def _async_motion_entities(
if not device.feature_flags.has_smart_detect: if not device.feature_flags.has_smart_detect:
continue continue
for description in MOTION_SENSORS: for event_desc in MOTION_SENSORS:
entities.append(ProtectEventSensor(data, device, description)) entities.append(ProtectEventSensor(data, device, event_desc))
_LOGGER.debug( _LOGGER.debug(
"Adding sensor entity %s for %s", "Adding sensor entity %s for %s",
description.name, description.name,
@ -730,29 +738,24 @@ class ProtectNVRSensor(ProtectNVREntity, SensorEntity):
self._attr_native_value = self.entity_description.get_ufp_value(self.device) self._attr_native_value = self.entity_description.get_ufp_value(self.device)
class ProtectEventSensor(ProtectDeviceSensor, EventThumbnailMixin): class ProtectEventSensor(EventEntityMixin, SensorEntity):
"""A UniFi Protect Device Sensor with access tokens.""" """A UniFi Protect Device Sensor with access tokens."""
device: Camera entity_description: ProtectSensorEventEntityDescription
@callback def __init__(
def _async_get_event(self) -> Event | None: self,
"""Get event from Protect device.""" data: ProtectData,
device: ProtectAdoptableDeviceModel,
event: Event | None = None description: ProtectSensorEventEntityDescription,
if ( ) -> None:
self.device.is_smart_detected """Initialize an UniFi Protect sensor."""
and self.device.last_smart_detect_event is not None super().__init__(data, device, description)
and len(self.device.last_smart_detect_event.smart_detect_types) > 0
):
event = self.device.last_smart_detect_event
return event
@callback @callback
def _async_update_device_from_protect(self, device: ProtectModelWithId) -> 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, device) EventEntityMixin._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:

View File

@ -75,6 +75,10 @@
"ea_setup_failed": { "ea_setup_failed": {
"title": "Setup error using Early Access version", "title": "Setup error using Early Access version",
"description": "You are using v{version} of UniFi Protect which is an Early Access version. An unrecoverable error occurred while trying to load the integration. Please [downgrade to a stable version](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) of UniFi Protect to continue using the integration.\n\nError: {error}" "description": "You are using v{version} of UniFi Protect which is an Early Access version. An unrecoverable error occurred while trying to load the integration. Please [downgrade to a stable version](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) of UniFi Protect to continue using the integration.\n\nError: {error}"
},
"deprecate_smart_sensor": {
"title": "Smart Detection Sensor Deprecated",
"description": "The unified \"Detected Object\" sensor for smart detections is now deprecated. It has been replaced with individual smart detection binary sensors for each smart detection type. Please update any templates or automations accordingly."
} }
} }
} }

View File

@ -42,6 +42,10 @@
} }
}, },
"issues": { "issues": {
"deprecate_smart_sensor": {
"description": "The unified \"Detected Object\" sensor for smart detections is now deprecated. It has been replaced with individual smart detection binary sensors for each smart detection type. Please update any templates or automations accordingly.",
"title": "Smart Detection Sensor Deprecated"
},
"ea_setup_failed": { "ea_setup_failed": {
"description": "You are using v{version} of UniFi Protect which is an Early Access version. An unrecoverable error occurred while trying to load the integration. Please [downgrade to a stable version](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) of UniFi Protect to continue using the integration.\n\nError: {error}", "description": "You are using v{version} of UniFi Protect which is an Early Access version. An unrecoverable error occurred while trying to load the integration. Please [downgrade to a stable version](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) of UniFi Protect to continue using the integration.\n\nError: {error}",
"title": "Setup error using Early Access version" "title": "Setup error using Early Access version"

View File

@ -50,11 +50,11 @@ async def test_binary_sensor_camera_remove(
ufp.api.bootstrap.nvr.system_info.ustorage = None ufp.api.bootstrap.nvr.system_info.ustorage = None
await init_entry(hass, ufp, [doorbell, unadopted_camera]) await init_entry(hass, ufp, [doorbell, unadopted_camera])
assert_entity_counts(hass, Platform.BINARY_SENSOR, 3, 3) assert_entity_counts(hass, Platform.BINARY_SENSOR, 6, 6)
await remove_entities(hass, ufp, [doorbell, unadopted_camera]) await remove_entities(hass, ufp, [doorbell, unadopted_camera])
assert_entity_counts(hass, Platform.BINARY_SENSOR, 0, 0) assert_entity_counts(hass, Platform.BINARY_SENSOR, 0, 0)
await adopt_devices(hass, ufp, [doorbell, unadopted_camera]) await adopt_devices(hass, ufp, [doorbell, unadopted_camera])
assert_entity_counts(hass, Platform.BINARY_SENSOR, 3, 3) assert_entity_counts(hass, Platform.BINARY_SENSOR, 6, 6)
async def test_binary_sensor_light_remove( async def test_binary_sensor_light_remove(
@ -120,7 +120,7 @@ async def test_binary_sensor_setup_camera_all(
ufp.api.bootstrap.nvr.system_info.ustorage = None ufp.api.bootstrap.nvr.system_info.ustorage = None
await init_entry(hass, ufp, [doorbell, unadopted_camera]) await init_entry(hass, ufp, [doorbell, unadopted_camera])
assert_entity_counts(hass, Platform.BINARY_SENSOR, 3, 3) assert_entity_counts(hass, Platform.BINARY_SENSOR, 6, 6)
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
@ -167,7 +167,6 @@ async def test_binary_sensor_setup_camera_all(
assert state assert state
assert state.state == STATE_OFF assert state.state == STATE_OFF
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
assert state.attributes[ATTR_EVENT_SCORE] == 0
async def test_binary_sensor_setup_camera_none( async def test_binary_sensor_setup_camera_none(
@ -263,7 +262,7 @@ async def test_binary_sensor_update_motion(
"""Test binary_sensor motion entity.""" """Test binary_sensor motion entity."""
await init_entry(hass, ufp, [doorbell, unadopted_camera]) await init_entry(hass, ufp, [doorbell, unadopted_camera])
assert_entity_counts(hass, Platform.BINARY_SENSOR, 9, 9) assert_entity_counts(hass, Platform.BINARY_SENSOR, 12, 12)
_, entity_id = ids_from_device_description( _, entity_id = ids_from_device_description(
Platform.BINARY_SENSOR, doorbell, MOTION_SENSORS[0] Platform.BINARY_SENSOR, doorbell, MOTION_SENSORS[0]

View File

@ -6,7 +6,7 @@ from copy import copy
from http import HTTPStatus from http import HTTPStatus
from unittest.mock import Mock from unittest.mock import Mock
from pyunifiprotect.data import Version from pyunifiprotect.data import Camera, Version
from homeassistant.components.repairs.issue_handler import ( from homeassistant.components.repairs.issue_handler import (
async_process_repairs_platforms, async_process_repairs_platforms,
@ -16,7 +16,9 @@ from homeassistant.components.repairs.websocket_api import (
RepairsFlowResourceView, RepairsFlowResourceView,
) )
from homeassistant.components.unifiprotect.const import DOMAIN from homeassistant.components.unifiprotect.const import DOMAIN
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from .utils import MockUFPFixture, init_entry from .utils import MockUFPFixture, init_entry
@ -40,9 +42,12 @@ async def test_ea_warning_ignore(
msg = await ws_client.receive_json() msg = await ws_client.receive_json()
assert msg["success"] assert msg["success"]
assert len(msg["result"]["issues"]) == 1 assert len(msg["result"]["issues"]) > 0
issue = msg["result"]["issues"][0] issue = None
assert issue["issue_id"] == "ea_warning" for i in msg["result"]["issues"]:
if i["issue_id"] == "ea_warning":
issue = i
assert issue is not None
url = RepairsFlowIndexView.url url = RepairsFlowIndexView.url
resp = await client.post(url, json={"handler": DOMAIN, "issue_id": "ea_warning"}) resp = await client.post(url, json={"handler": DOMAIN, "issue_id": "ea_warning"})
@ -89,9 +94,12 @@ async def test_ea_warning_fix(
msg = await ws_client.receive_json() msg = await ws_client.receive_json()
assert msg["success"] assert msg["success"]
assert len(msg["result"]["issues"]) == 1 assert len(msg["result"]["issues"]) > 0
issue = msg["result"]["issues"][0] issue = None
assert issue["issue_id"] == "ea_warning" for i in msg["result"]["issues"]:
if i["issue_id"] == "ea_warning":
issue = i
assert issue is not None
url = RepairsFlowIndexView.url url = RepairsFlowIndexView.url
resp = await client.post(url, json={"handler": DOMAIN, "issue_id": "ea_warning"}) resp = await client.post(url, json={"handler": DOMAIN, "issue_id": "ea_warning"})
@ -118,3 +126,53 @@ async def test_ea_warning_fix(
data = await resp.json() data = await resp.json()
assert data["type"] == "create_entry" assert data["type"] == "create_entry"
async def test_deprecate_smart_default(
hass: HomeAssistant, ufp: MockUFPFixture, hass_ws_client, doorbell: Camera
):
"""Test Deprecate Sensor repair does not exist by default (new installs)."""
await init_entry(hass, ufp, [doorbell])
await async_process_repairs_platforms(hass)
ws_client = await hass_ws_client(hass)
await ws_client.send_json({"id": 1, "type": "repairs/list_issues"})
msg = await ws_client.receive_json()
assert msg["success"]
issue = None
for i in msg["result"]["issues"]:
if i["issue_id"] == "deprecate_smart_sensor":
issue = i
assert issue is None
async def test_deprecate_smart_active(
hass: HomeAssistant, ufp: MockUFPFixture, hass_ws_client, doorbell: Camera
):
"""Test Deprecate Sensor repair exists for existing installs."""
registry = er.async_get(hass)
registry.async_get_or_create(
Platform.SENSOR,
DOMAIN,
f"{doorbell.mac}_detected_object",
config_entry=ufp.entry,
)
await init_entry(hass, ufp, [doorbell])
await async_process_repairs_platforms(hass)
ws_client = await hass_ws_client(hass)
await ws_client.send_json({"id": 1, "type": "repairs/list_issues"})
msg = await ws_client.receive_json()
assert msg["success"]
issue = None
for i in msg["result"]["issues"]:
if i["issue_id"] == "deprecate_smart_sensor":
issue = i
assert issue is not None

View File

@ -62,11 +62,11 @@ async def test_sensor_camera_remove(
ufp.api.bootstrap.nvr.system_info.ustorage = None ufp.api.bootstrap.nvr.system_info.ustorage = None
await init_entry(hass, ufp, [doorbell, unadopted_camera]) await init_entry(hass, ufp, [doorbell, unadopted_camera])
assert_entity_counts(hass, Platform.SENSOR, 25, 13) assert_entity_counts(hass, Platform.SENSOR, 25, 12)
await remove_entities(hass, ufp, [doorbell, unadopted_camera]) await remove_entities(hass, ufp, [doorbell, unadopted_camera])
assert_entity_counts(hass, Platform.SENSOR, 12, 9) assert_entity_counts(hass, Platform.SENSOR, 12, 9)
await adopt_devices(hass, ufp, [doorbell, unadopted_camera]) await adopt_devices(hass, ufp, [doorbell, unadopted_camera])
assert_entity_counts(hass, Platform.SENSOR, 25, 13) assert_entity_counts(hass, Platform.SENSOR, 25, 12)
async def test_sensor_sensor_remove( async def test_sensor_sensor_remove(
@ -318,7 +318,7 @@ async def test_sensor_setup_camera(
"""Test sensor entity setup for camera devices.""" """Test sensor entity setup for camera devices."""
await init_entry(hass, ufp, [doorbell]) await init_entry(hass, ufp, [doorbell])
assert_entity_counts(hass, Platform.SENSOR, 25, 13) assert_entity_counts(hass, Platform.SENSOR, 25, 12)
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
@ -406,11 +406,12 @@ async def test_sensor_setup_camera(
assert entity assert entity
assert entity.unique_id == unique_id assert entity.unique_id == unique_id
await enable_entity(hass, ufp.entry.entry_id, entity_id)
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
assert state assert state
assert state.state == OBJECT_TYPE_NONE assert state.state == OBJECT_TYPE_NONE
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
assert state.attributes[ATTR_EVENT_SCORE] == 0
async def test_sensor_setup_camera_with_last_trip_time( async def test_sensor_setup_camera_with_last_trip_time(
@ -451,12 +452,14 @@ async def test_sensor_update_motion(
"""Test sensor motion entity.""" """Test sensor motion entity."""
await init_entry(hass, ufp, [doorbell]) await init_entry(hass, ufp, [doorbell])
assert_entity_counts(hass, Platform.SENSOR, 25, 13) assert_entity_counts(hass, Platform.SENSOR, 25, 12)
_, entity_id = ids_from_device_description( _, entity_id = ids_from_device_description(
Platform.SENSOR, doorbell, MOTION_SENSORS[0] Platform.SENSOR, doorbell, MOTION_SENSORS[0]
) )
await enable_entity(hass, ufp.entry.entry_id, entity_id)
event = Event( event = Event(
id="test_event_id", id="test_event_id",
type=EventType.SMART_DETECT, type=EventType.SMART_DETECT,