Improve UniFi Protect Smart Sensor support (#64019)
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
cafbcb634a
commit
20768172b1
13 changed files with 501 additions and 22 deletions
|
@ -5,7 +5,7 @@ from copy import copy
|
|||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
from pyunifiprotect.data import NVR, Camera, Event, Light, Sensor
|
||||
from pyunifiprotect.data import NVR, Camera, Event, Light, MountType, Sensor
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
|
@ -30,6 +30,7 @@ from .models import ProtectRequiredKeysMixin
|
|||
from .utils import get_nested_attr
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_KEY_DOOR = "door"
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -41,6 +42,13 @@ class ProtectBinaryEntityDescription(
|
|||
ufp_last_trip_value: str | None = None
|
||||
|
||||
|
||||
MOUNT_DEVICE_CLASS_MAP = {
|
||||
MountType.GARAGE: BinarySensorDeviceClass.GARAGE_DOOR,
|
||||
MountType.WINDOW: BinarySensorDeviceClass.WINDOW,
|
||||
MountType.DOOR: BinarySensorDeviceClass.DOOR,
|
||||
}
|
||||
|
||||
|
||||
CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = (
|
||||
ProtectBinaryEntityDescription(
|
||||
key="doorbell",
|
||||
|
@ -77,11 +85,12 @@ LIGHT_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = (
|
|||
|
||||
SENSE_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = (
|
||||
ProtectBinaryEntityDescription(
|
||||
key="door",
|
||||
name="Door",
|
||||
key=_KEY_DOOR,
|
||||
name="Contact",
|
||||
device_class=BinarySensorDeviceClass.DOOR,
|
||||
ufp_value="is_opened",
|
||||
ufp_last_trip_value="open_status_changed_at",
|
||||
ufp_enabled="is_contact_sensor_enabled",
|
||||
),
|
||||
ProtectBinaryEntityDescription(
|
||||
key="battery_low",
|
||||
|
@ -96,6 +105,14 @@ SENSE_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = (
|
|||
device_class=BinarySensorDeviceClass.MOTION,
|
||||
ufp_value="is_motion_detected",
|
||||
ufp_last_trip_value="motion_detected_at",
|
||||
ufp_enabled="is_motion_sensor_enabled",
|
||||
),
|
||||
ProtectBinaryEntityDescription(
|
||||
key="tampering",
|
||||
name="Tampering Detected",
|
||||
device_class=BinarySensorDeviceClass.TAMPER,
|
||||
ufp_value="is_tampering_detected",
|
||||
ufp_last_trip_value="tampering_detected_at",
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -197,6 +214,12 @@ class ProtectDeviceBinarySensor(ProtectDeviceEntity, BinarySensorEntity):
|
|||
ATTR_LAST_TRIP_TIME: last_trip,
|
||||
}
|
||||
|
||||
# UP Sense can be any of the 3 contact sensor device classes
|
||||
if self.entity_description.key == _KEY_DOOR and isinstance(self.device, Sensor):
|
||||
self.entity_description.device_class = MOUNT_DEVICE_CLASS_MAP.get(
|
||||
self.device.mount_type, BinarySensorDeviceClass.DOOR
|
||||
)
|
||||
|
||||
|
||||
class ProtectDiskBinarySensor(ProtectNVREntity, BinarySensorEntity):
|
||||
"""A UniFi Protect NVR Disk Binary Sensor."""
|
||||
|
|
|
@ -7,9 +7,14 @@ import logging
|
|||
from typing import Any
|
||||
|
||||
from pyunifiprotect import NotAuthorized, NvrError, ProtectApiClient
|
||||
from pyunifiprotect.data import Bootstrap, ModelType, WSSubscriptionMessage
|
||||
from pyunifiprotect.data import (
|
||||
Bootstrap,
|
||||
Event,
|
||||
Liveview,
|
||||
ModelType,
|
||||
WSSubscriptionMessage,
|
||||
)
|
||||
from pyunifiprotect.data.base import ProtectAdoptableDeviceModel, ProtectDeviceModel
|
||||
from pyunifiprotect.data.nvr import Liveview
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
|
@ -115,6 +120,14 @@ class ProtectData:
|
|||
for camera in self.api.bootstrap.cameras.values():
|
||||
if camera.feature_flags.has_lcd_screen:
|
||||
self.async_signal_device_id_update(camera.id)
|
||||
# trigger updates for camera that the event references
|
||||
elif isinstance(message.new_obj, Event):
|
||||
if message.new_obj.camera is not None:
|
||||
self.async_signal_device_id_update(message.new_obj.camera.id)
|
||||
elif message.new_obj.light is not None:
|
||||
self.async_signal_device_id_update(message.new_obj.light.id)
|
||||
elif message.new_obj.sensor is not None:
|
||||
self.async_signal_device_id_update(message.new_obj.sensor.id)
|
||||
# alert user viewport needs restart so voice clients can get new options
|
||||
elif len(self.api.bootstrap.viewers) > 0 and isinstance(
|
||||
message.new_obj, Liveview
|
||||
|
|
|
@ -149,9 +149,19 @@ class ProtectDeviceEntity(Entity):
|
|||
devices = getattr(self.data.api.bootstrap, f"{self.device.model.value}s")
|
||||
self.device = devices[self.device.id]
|
||||
|
||||
self._attr_available = (
|
||||
is_connected = (
|
||||
self.data.last_update_success and self.device.state == StateType.CONNECTED
|
||||
)
|
||||
if (
|
||||
hasattr(self, "entity_description")
|
||||
and self.entity_description is not None
|
||||
and hasattr(self.entity_description, "get_ufp_enabled")
|
||||
):
|
||||
assert isinstance(self.entity_description, ProtectRequiredKeysMixin)
|
||||
is_connected = is_connected and self.entity_description.get_ufp_enabled(
|
||||
self.device
|
||||
)
|
||||
self._attr_available = is_connected
|
||||
|
||||
@callback
|
||||
def _async_updated_event(self) -> None:
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/unifiprotect",
|
||||
"requirements": [
|
||||
"pyunifiprotect==1.6.2"
|
||||
"pyunifiprotect==1.6.3"
|
||||
],
|
||||
"dependencies": [
|
||||
"http"
|
||||
|
|
|
@ -22,6 +22,7 @@ class ProtectRequiredKeysMixin:
|
|||
ufp_required_field: str | None = None
|
||||
ufp_value: str | None = None
|
||||
ufp_value_fn: Callable[[ProtectAdoptableDeviceModel | NVR], Any] | None = None
|
||||
ufp_enabled: str | None = None
|
||||
|
||||
def get_ufp_value(self, obj: ProtectAdoptableDeviceModel | NVR) -> Any:
|
||||
"""Return value from UniFi Protect device."""
|
||||
|
@ -35,6 +36,12 @@ class ProtectRequiredKeysMixin:
|
|||
"`ufp_value` or `ufp_value_fn` is required"
|
||||
)
|
||||
|
||||
def get_ufp_enabled(self, obj: ProtectAdoptableDeviceModel | NVR) -> bool:
|
||||
"""Return value from UniFi Protect device."""
|
||||
if self.ufp_enabled is not None:
|
||||
return bool(get_nested_attr(obj, self.ufp_enabled))
|
||||
return True
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProtectSetableKeysMixin(ProtectRequiredKeysMixin):
|
||||
|
|
|
@ -111,6 +111,21 @@ LIGHT_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = (
|
|||
),
|
||||
)
|
||||
|
||||
SENSE_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = (
|
||||
ProtectNumberEntityDescription(
|
||||
key="sensitivity",
|
||||
name="Motion Sensitivity",
|
||||
icon="mdi:walk",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
ufp_min=0,
|
||||
ufp_max=100,
|
||||
ufp_step=1,
|
||||
ufp_required_field=None,
|
||||
ufp_value="motion_settings.sensitivity",
|
||||
ufp_set_method="set_motion_sensitivity",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
|
@ -124,6 +139,7 @@ async def async_setup_entry(
|
|||
ProtectNumbers,
|
||||
camera_descs=CAMERA_NUMBERS,
|
||||
light_descs=LIGHT_NUMBERS,
|
||||
sense_descs=SENSE_NUMBERS,
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
|
|
@ -19,7 +19,10 @@ from pyunifiprotect.data import (
|
|||
RecordingMode,
|
||||
Viewer,
|
||||
)
|
||||
from pyunifiprotect.data.types import ChimeType
|
||||
from pyunifiprotect.data.base import ProtectAdoptableDeviceModel
|
||||
from pyunifiprotect.data.devices import Sensor
|
||||
from pyunifiprotect.data.nvr import NVR
|
||||
from pyunifiprotect.data.types import ChimeType, MountType
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||
|
@ -52,6 +55,14 @@ CHIME_TYPES = [
|
|||
{"id": ChimeType.DIGITAL.value, "name": "Digital"},
|
||||
]
|
||||
|
||||
MOUNT_TYPES = [
|
||||
{"id": MountType.NONE.value, "name": "None"},
|
||||
{"id": MountType.DOOR.value, "name": "Door"},
|
||||
{"id": MountType.WINDOW.value, "name": "Window"},
|
||||
{"id": MountType.GARAGE.value, "name": "Garage"},
|
||||
{"id": MountType.LEAK.value, "name": "Leak"},
|
||||
]
|
||||
|
||||
LIGHT_MODE_MOTION = "On Motion - Always"
|
||||
LIGHT_MODE_MOTION_DARK = "On Motion - When Dark"
|
||||
LIGHT_MODE_DARK = "When Dark"
|
||||
|
@ -161,8 +172,10 @@ async def _set_light_mode(obj: Any, mode: str) -> None:
|
|||
)
|
||||
|
||||
|
||||
async def _set_paired_camera(obj: Any, camera_id: str) -> None:
|
||||
assert isinstance(obj, Light)
|
||||
async def _set_paired_camera(
|
||||
obj: ProtectAdoptableDeviceModel | NVR, camera_id: str
|
||||
) -> None:
|
||||
assert isinstance(obj, (Sensor, Light))
|
||||
if camera_id == TYPE_EMPTY_VALUE:
|
||||
camera: Camera | None = None
|
||||
else:
|
||||
|
@ -253,6 +266,28 @@ LIGHT_SELECTS: tuple[ProtectSelectEntityDescription, ...] = (
|
|||
),
|
||||
)
|
||||
|
||||
SENSE_SELECTS: tuple[ProtectSelectEntityDescription, ...] = (
|
||||
ProtectSelectEntityDescription(
|
||||
key="mount_type",
|
||||
name="Mount Type",
|
||||
icon="mdi:screwdriver",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
ufp_options=MOUNT_TYPES,
|
||||
ufp_enum_type=MountType,
|
||||
ufp_value="mount_type",
|
||||
ufp_set_method="set_mount_type",
|
||||
),
|
||||
ProtectSelectEntityDescription(
|
||||
key="paired_camera",
|
||||
name="Paired Camera",
|
||||
icon="mdi:cctv",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
ufp_value="camera_id",
|
||||
ufp_options_callable=_get_paired_camera_options,
|
||||
ufp_set_method_fn=_set_paired_camera,
|
||||
),
|
||||
)
|
||||
|
||||
VIEWER_SELECTS: tuple[ProtectSelectEntityDescription, ...] = (
|
||||
ProtectSelectEntityDescription(
|
||||
key="viewer",
|
||||
|
@ -278,6 +313,7 @@ async def async_setup_entry(
|
|||
ProtectSelects,
|
||||
camera_descs=CAMERA_SELECTS,
|
||||
light_descs=LIGHT_SELECTS,
|
||||
sense_descs=SENSE_SELECTS,
|
||||
viewer_descs=VIEWER_SELECTS,
|
||||
)
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ from typing import Any
|
|||
|
||||
from pyunifiprotect.data import NVR, Camera, Event
|
||||
from pyunifiprotect.data.base import ProtectAdoptableDeviceModel
|
||||
from pyunifiprotect.data.devices import Sensor
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
|
@ -42,7 +43,7 @@ from .entity import (
|
|||
from .models import ProtectRequiredKeysMixin
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DETECTED_OBJECT_NONE = "none"
|
||||
OBJECT_TYPE_NONE = "none"
|
||||
DEVICE_CLASS_DETECTION = "unifiprotect__detection"
|
||||
|
||||
|
||||
|
@ -88,6 +89,19 @@ def _get_nvr_memory(obj: Any) -> float | None:
|
|||
return (1 - memory.available / memory.total) * 100
|
||||
|
||||
|
||||
def _get_alarm_sound(obj: ProtectAdoptableDeviceModel | NVR) -> str:
|
||||
assert isinstance(obj, Sensor)
|
||||
|
||||
alarm_type = OBJECT_TYPE_NONE
|
||||
if (
|
||||
obj.is_alarm_detected
|
||||
and obj.last_alarm_event is not None
|
||||
and obj.last_alarm_event.metadata is not None
|
||||
):
|
||||
alarm_type = obj.last_alarm_event.metadata.alarm_type or OBJECT_TYPE_NONE
|
||||
return alarm_type.lower()
|
||||
|
||||
|
||||
ALL_DEVICES_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
|
||||
ProtectSensorEntityDescription(
|
||||
key="uptime",
|
||||
|
@ -210,6 +224,7 @@ SENSE_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
|
|||
device_class=SensorDeviceClass.ILLUMINANCE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
ufp_value="stats.light.value",
|
||||
ufp_enabled="is_light_sensor_enabled",
|
||||
),
|
||||
ProtectSensorEntityDescription(
|
||||
key="humidity_level",
|
||||
|
@ -218,6 +233,7 @@ SENSE_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
|
|||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
ufp_value="stats.humidity.value",
|
||||
ufp_enabled="is_humidity_sensor_enabled",
|
||||
),
|
||||
ProtectSensorEntityDescription(
|
||||
key="temperature_level",
|
||||
|
@ -226,6 +242,13 @@ SENSE_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
|
|||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
ufp_value="stats.temperature.value",
|
||||
ufp_enabled="is_temperature_sensor_enabled",
|
||||
),
|
||||
ProtectSensorEntityDescription(
|
||||
key="alarm_sound",
|
||||
name="Alarm Sound Detected",
|
||||
ufp_value_fn=_get_alarm_sound,
|
||||
ufp_enabled="is_alarm_sensor_enabled",
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -479,6 +502,6 @@ class ProtectEventSensor(ProtectDeviceSensor, EventThumbnailMixin):
|
|||
# do not call ProtectDeviceSensor method since we want event to get value here
|
||||
EventThumbnailMixin._async_update_device_from_protect(self)
|
||||
if self._event is None:
|
||||
self._attr_native_value = DETECTED_OBJECT_NONE
|
||||
self._attr_native_value = OBJECT_TYPE_NONE
|
||||
else:
|
||||
self._attr_native_value = self._event.smart_detect_types[0].value
|
||||
|
|
|
@ -152,6 +152,56 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
|
|||
),
|
||||
)
|
||||
|
||||
SENSE_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
|
||||
ProtectSwitchEntityDescription(
|
||||
key="status_light",
|
||||
name="Status Light On",
|
||||
icon="mdi:led-on",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
ufp_value="led_settings.is_enabled",
|
||||
ufp_set_method="set_status_light",
|
||||
),
|
||||
ProtectSwitchEntityDescription(
|
||||
key="motion",
|
||||
name="Motion Detection",
|
||||
icon="mdi:walk",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
ufp_value="motion_settings.is_enabled",
|
||||
ufp_set_method="set_motion_status",
|
||||
),
|
||||
ProtectSwitchEntityDescription(
|
||||
key="temperature",
|
||||
name="Temperature Sensor",
|
||||
icon="mdi:thermometer",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
ufp_value="temperature_settings.is_enabled",
|
||||
ufp_set_method="set_temperature_status",
|
||||
),
|
||||
ProtectSwitchEntityDescription(
|
||||
key="humidity",
|
||||
name="Humidity Sensor",
|
||||
icon="mdi:water-percent",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
ufp_value="humidity_settings.is_enabled",
|
||||
ufp_set_method="set_humidity_status",
|
||||
),
|
||||
ProtectSwitchEntityDescription(
|
||||
key="light",
|
||||
name="Light Sensor",
|
||||
icon="mdi:brightness-5",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
ufp_value="light_settings.is_enabled",
|
||||
ufp_set_method="set_light_status",
|
||||
),
|
||||
ProtectSwitchEntityDescription(
|
||||
key="alarm",
|
||||
name="Alarm Sound Detection",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
ufp_value="alarm_settings.is_enabled",
|
||||
ufp_set_method="set_alarm_status",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
LIGHT_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
|
||||
ProtectSwitchEntityDescription(
|
||||
|
@ -178,6 +228,7 @@ async def async_setup_entry(
|
|||
all_descs=ALL_DEVICES_SWITCHES,
|
||||
camera_descs=CAMERA_SWITCHES,
|
||||
light_descs=LIGHT_SWITCHES,
|
||||
sense_descs=SENSE_SWITCHES,
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
|
|
@ -2015,7 +2015,7 @@ pytrafikverket==0.1.6.2
|
|||
pyudev==0.22.0
|
||||
|
||||
# homeassistant.components.unifiprotect
|
||||
pyunifiprotect==1.6.2
|
||||
pyunifiprotect==1.6.3
|
||||
|
||||
# homeassistant.components.uptimerobot
|
||||
pyuptimerobot==21.11.0
|
||||
|
|
|
@ -1240,7 +1240,7 @@ pytrafikverket==0.1.6.2
|
|||
pyudev==0.22.0
|
||||
|
||||
# homeassistant.components.unifiprotect
|
||||
pyunifiprotect==1.6.2
|
||||
pyunifiprotect==1.6.3
|
||||
|
||||
# homeassistant.components.uptimerobot
|
||||
pyuptimerobot==21.11.0
|
||||
|
|
|
@ -7,8 +7,10 @@ from datetime import datetime, timedelta
|
|||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
from pyunifiprotect.data import Camera, Event, EventType, Light, Sensor
|
||||
from pyunifiprotect.data import Camera, Event, EventType, Light, MountType, Sensor
|
||||
from pyunifiprotect.data.nvr import EventMetadata
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
|
||||
from homeassistant.components.unifiprotect.binary_sensor import (
|
||||
CAMERA_SENSORS,
|
||||
LIGHT_SENSORS,
|
||||
|
@ -21,9 +23,11 @@ from homeassistant.components.unifiprotect.const import (
|
|||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION,
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_LAST_TRIP_TIME,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
STATE_UNAVAILABLE,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
@ -157,11 +161,15 @@ async def sensor_fixture(
|
|||
sensor_obj = mock_sensor.copy(deep=True)
|
||||
sensor_obj._api = mock_entry.api
|
||||
sensor_obj.name = "Test Sensor"
|
||||
sensor_obj.mount_type = MountType.DOOR
|
||||
sensor_obj.is_opened = False
|
||||
sensor_obj.battery_status.is_low = False
|
||||
sensor_obj.is_motion_detected = False
|
||||
sensor_obj.alarm_settings.is_enabled = True
|
||||
sensor_obj.motion_detected_at = now - timedelta(hours=1)
|
||||
sensor_obj.open_status_changed_at = now - timedelta(hours=1)
|
||||
sensor_obj.alarm_triggered_at = now - timedelta(hours=1)
|
||||
sensor_obj.tampering_detected_at = now - timedelta(hours=1)
|
||||
|
||||
mock_entry.api.bootstrap.reset_objects()
|
||||
mock_entry.api.bootstrap.nvr.system_info.storage.devices = []
|
||||
|
@ -172,7 +180,43 @@ async def sensor_fixture(
|
|||
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert_entity_counts(hass, Platform.BINARY_SENSOR, 3, 3)
|
||||
assert_entity_counts(hass, Platform.BINARY_SENSOR, 4, 4)
|
||||
|
||||
yield sensor_obj
|
||||
|
||||
Sensor.__config__.validate_assignment = True
|
||||
|
||||
|
||||
@pytest.fixture(name="sensor_none")
|
||||
async def sensor_none_fixture(
|
||||
hass: HomeAssistant,
|
||||
mock_entry: MockEntityFixture,
|
||||
mock_sensor: Sensor,
|
||||
now: datetime,
|
||||
):
|
||||
"""Fixture for a single sensor for testing the binary_sensor platform."""
|
||||
|
||||
# disable pydantic validation so mocking can happen
|
||||
Sensor.__config__.validate_assignment = False
|
||||
|
||||
sensor_obj = mock_sensor.copy(deep=True)
|
||||
sensor_obj._api = mock_entry.api
|
||||
sensor_obj.name = "Test Sensor"
|
||||
sensor_obj.mount_type = MountType.LEAK
|
||||
sensor_obj.battery_status.is_low = False
|
||||
sensor_obj.alarm_settings.is_enabled = False
|
||||
sensor_obj.tampering_detected_at = now - timedelta(hours=1)
|
||||
|
||||
mock_entry.api.bootstrap.reset_objects()
|
||||
mock_entry.api.bootstrap.nvr.system_info.storage.devices = []
|
||||
mock_entry.api.bootstrap.sensors = {
|
||||
sensor_obj.id: sensor_obj,
|
||||
}
|
||||
|
||||
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert_entity_counts(hass, Platform.BINARY_SENSOR, 4, 4)
|
||||
|
||||
yield sensor_obj
|
||||
|
||||
|
@ -308,6 +352,35 @@ async def test_binary_sensor_setup_sensor(
|
|||
assert state.attributes[ATTR_LAST_TRIP_TIME] == expected_trip_time
|
||||
|
||||
|
||||
async def test_binary_sensor_setup_sensor_none(
|
||||
hass: HomeAssistant, sensor_none: Sensor
|
||||
):
|
||||
"""Test binary_sensor entity setup for sensor with most sensors disabled."""
|
||||
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
expected = [
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_OFF,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_OFF,
|
||||
]
|
||||
for index, description in enumerate(SENSE_SENSORS):
|
||||
unique_id, entity_id = ids_from_device_description(
|
||||
Platform.BINARY_SENSOR, sensor_none, description
|
||||
)
|
||||
|
||||
entity = entity_registry.async_get(entity_id)
|
||||
assert entity
|
||||
assert entity.unique_id == unique_id
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
print(entity_id)
|
||||
assert state.state == expected[index]
|
||||
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
|
||||
|
||||
|
||||
async def test_binary_sensor_update_motion(
|
||||
hass: HomeAssistant, mock_entry: MockEntityFixture, camera: Camera, now: datetime
|
||||
):
|
||||
|
@ -348,3 +421,109 @@ async def test_binary_sensor_update_motion(
|
|||
assert state.state == STATE_ON
|
||||
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
|
||||
assert state.attributes[ATTR_EVENT_SCORE] == 100
|
||||
|
||||
|
||||
async def test_binary_sensor_update_light_motion(
|
||||
hass: HomeAssistant, mock_entry: MockEntityFixture, light: Light, now: datetime
|
||||
):
|
||||
"""Test binary_sensor motion entity."""
|
||||
|
||||
_, entity_id = ids_from_device_description(
|
||||
Platform.BINARY_SENSOR, light, LIGHT_SENSORS[1]
|
||||
)
|
||||
|
||||
event_metadata = EventMetadata(light_id=light.id)
|
||||
event = Event(
|
||||
id="test_event_id",
|
||||
type=EventType.MOTION_LIGHT,
|
||||
start=now - timedelta(seconds=1),
|
||||
end=None,
|
||||
score=100,
|
||||
smart_detect_types=[],
|
||||
smart_detect_event_ids=[],
|
||||
metadata=event_metadata,
|
||||
api=mock_entry.api,
|
||||
)
|
||||
|
||||
new_bootstrap = copy(mock_entry.api.bootstrap)
|
||||
new_light = light.copy()
|
||||
new_light.is_pir_motion_detected = True
|
||||
new_light.last_motion_event_id = event.id
|
||||
|
||||
mock_msg = Mock()
|
||||
mock_msg.changed_data = {}
|
||||
mock_msg.new_obj = event
|
||||
|
||||
new_bootstrap.lights = {new_light.id: new_light}
|
||||
new_bootstrap.events = {event.id: event}
|
||||
mock_entry.api.bootstrap = new_bootstrap
|
||||
mock_entry.api.ws_subscription(mock_msg)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == STATE_ON
|
||||
|
||||
|
||||
async def test_binary_sensor_update_mount_type_window(
|
||||
hass: HomeAssistant, mock_entry: MockEntityFixture, sensor: Sensor
|
||||
):
|
||||
"""Test binary_sensor motion entity."""
|
||||
|
||||
_, entity_id = ids_from_device_description(
|
||||
Platform.BINARY_SENSOR, sensor, SENSE_SENSORS[0]
|
||||
)
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.attributes[ATTR_DEVICE_CLASS] == BinarySensorDeviceClass.DOOR.value
|
||||
|
||||
new_bootstrap = copy(mock_entry.api.bootstrap)
|
||||
new_sensor = sensor.copy()
|
||||
new_sensor.mount_type = MountType.WINDOW
|
||||
|
||||
mock_msg = Mock()
|
||||
mock_msg.changed_data = {}
|
||||
mock_msg.new_obj = new_sensor
|
||||
|
||||
new_bootstrap.sensors = {new_sensor.id: new_sensor}
|
||||
mock_entry.api.bootstrap = new_bootstrap
|
||||
mock_entry.api.ws_subscription(mock_msg)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.attributes[ATTR_DEVICE_CLASS] == BinarySensorDeviceClass.WINDOW.value
|
||||
|
||||
|
||||
async def test_binary_sensor_update_mount_type_garage(
|
||||
hass: HomeAssistant, mock_entry: MockEntityFixture, sensor: Sensor
|
||||
):
|
||||
"""Test binary_sensor motion entity."""
|
||||
|
||||
_, entity_id = ids_from_device_description(
|
||||
Platform.BINARY_SENSOR, sensor, SENSE_SENSORS[0]
|
||||
)
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.attributes[ATTR_DEVICE_CLASS] == BinarySensorDeviceClass.DOOR.value
|
||||
|
||||
new_bootstrap = copy(mock_entry.api.bootstrap)
|
||||
new_sensor = sensor.copy()
|
||||
new_sensor.mount_type = MountType.GARAGE
|
||||
|
||||
mock_msg = Mock()
|
||||
mock_msg.changed_data = {}
|
||||
mock_msg.new_obj = new_sensor
|
||||
|
||||
new_bootstrap.sensors = {new_sensor.id: new_sensor}
|
||||
mock_entry.api.bootstrap = new_bootstrap
|
||||
mock_entry.api.ws_subscription(mock_msg)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert (
|
||||
state.attributes[ATTR_DEVICE_CLASS] == BinarySensorDeviceClass.GARAGE_DOOR.value
|
||||
)
|
||||
|
|
|
@ -9,6 +9,7 @@ from unittest.mock import Mock
|
|||
import pytest
|
||||
from pyunifiprotect.data import NVR, Camera, Event, Sensor
|
||||
from pyunifiprotect.data.base import WifiConnectionState, WiredConnectionState
|
||||
from pyunifiprotect.data.nvr import EventMetadata
|
||||
from pyunifiprotect.data.types import EventType, SmartDetectObjectType
|
||||
|
||||
from homeassistant.components.unifiprotect.const import (
|
||||
|
@ -19,13 +20,18 @@ from homeassistant.components.unifiprotect.sensor import (
|
|||
ALL_DEVICES_SENSORS,
|
||||
CAMERA_DISABLED_SENSORS,
|
||||
CAMERA_SENSORS,
|
||||
DETECTED_OBJECT_NONE,
|
||||
MOTION_SENSORS,
|
||||
NVR_DISABLED_SENSORS,
|
||||
NVR_SENSORS,
|
||||
OBJECT_TYPE_NONE,
|
||||
SENSE_SENSORS,
|
||||
)
|
||||
from homeassistant.const import ATTR_ATTRIBUTION, STATE_UNKNOWN, Platform
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
|
@ -53,6 +59,10 @@ async def sensor_fixture(
|
|||
sensor_obj._api = mock_entry.api
|
||||
sensor_obj.name = "Test Sensor"
|
||||
sensor_obj.battery_status.percentage = 10.0
|
||||
sensor_obj.light_settings.is_enabled = True
|
||||
sensor_obj.humidity_settings.is_enabled = True
|
||||
sensor_obj.temperature_settings.is_enabled = True
|
||||
sensor_obj.alarm_settings.is_enabled = True
|
||||
sensor_obj.stats.light.value = 10.0
|
||||
sensor_obj.stats.humidity.value = 10.0
|
||||
sensor_obj.stats.temperature.value = 10.0
|
||||
|
@ -68,7 +78,46 @@ async def sensor_fixture(
|
|||
await hass.async_block_till_done()
|
||||
|
||||
# 2 from all, 4 from sense, 12 NVR
|
||||
assert_entity_counts(hass, Platform.SENSOR, 18, 13)
|
||||
assert_entity_counts(hass, Platform.SENSOR, 19, 14)
|
||||
|
||||
yield sensor_obj
|
||||
|
||||
Sensor.__config__.validate_assignment = True
|
||||
|
||||
|
||||
@pytest.fixture(name="sensor_none")
|
||||
async def sensor_none_fixture(
|
||||
hass: HomeAssistant,
|
||||
mock_entry: MockEntityFixture,
|
||||
mock_sensor: Sensor,
|
||||
now: datetime,
|
||||
):
|
||||
"""Fixture for a single sensor for testing the sensor platform."""
|
||||
|
||||
# disable pydantic validation so mocking can happen
|
||||
Sensor.__config__.validate_assignment = False
|
||||
|
||||
sensor_obj = mock_sensor.copy(deep=True)
|
||||
sensor_obj._api = mock_entry.api
|
||||
sensor_obj.name = "Test Sensor"
|
||||
sensor_obj.battery_status.percentage = 10.0
|
||||
sensor_obj.light_settings.is_enabled = False
|
||||
sensor_obj.humidity_settings.is_enabled = False
|
||||
sensor_obj.temperature_settings.is_enabled = False
|
||||
sensor_obj.alarm_settings.is_enabled = False
|
||||
sensor_obj.up_since = now
|
||||
sensor_obj.bluetooth_connection_state.signal_strength = -50.0
|
||||
|
||||
mock_entry.api.bootstrap.reset_objects()
|
||||
mock_entry.api.bootstrap.sensors = {
|
||||
sensor_obj.id: sensor_obj,
|
||||
}
|
||||
|
||||
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# 2 from all, 4 from sense, 12 NVR
|
||||
assert_entity_counts(hass, Platform.SENSOR, 19, 14)
|
||||
|
||||
yield sensor_obj
|
||||
|
||||
|
@ -131,7 +180,7 @@ async def test_sensor_setup_sensor(
|
|||
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
expected_values = ("10", "10.0", "10.0", "10.0")
|
||||
expected_values = ("10", "10.0", "10.0", "10.0", "none")
|
||||
for index, description in enumerate(SENSE_SENSORS):
|
||||
unique_id, entity_id = ids_from_device_description(
|
||||
Platform.SENSOR, sensor, description
|
||||
|
@ -164,6 +213,35 @@ async def test_sensor_setup_sensor(
|
|||
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
|
||||
|
||||
|
||||
async def test_sensor_setup_sensor_none(
|
||||
hass: HomeAssistant, mock_entry: MockEntityFixture, sensor_none: Sensor
|
||||
):
|
||||
"""Test sensor entity setup for sensor devices with no sensors enabled."""
|
||||
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
expected_values = (
|
||||
"10",
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNAVAILABLE,
|
||||
)
|
||||
for index, description in enumerate(SENSE_SENSORS):
|
||||
unique_id, entity_id = ids_from_device_description(
|
||||
Platform.SENSOR, sensor_none, description
|
||||
)
|
||||
|
||||
entity = entity_registry.async_get(entity_id)
|
||||
assert entity
|
||||
assert entity.unique_id == unique_id
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == expected_values[index]
|
||||
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
|
||||
|
||||
|
||||
async def test_sensor_setup_nvr(
|
||||
hass: HomeAssistant, mock_entry: MockEntityFixture, now: datetime
|
||||
):
|
||||
|
@ -403,7 +481,7 @@ async def test_sensor_setup_camera(
|
|||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == DETECTED_OBJECT_NONE
|
||||
assert state.state == OBJECT_TYPE_NONE
|
||||
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
|
||||
assert state.attributes[ATTR_EVENT_SCORE] == 0
|
||||
|
||||
|
@ -426,6 +504,7 @@ async def test_sensor_update_motion(
|
|||
smart_detect_types=[SmartDetectObjectType.PERSON],
|
||||
smart_detect_event_ids=[],
|
||||
camera_id=camera.id,
|
||||
api=mock_entry.api,
|
||||
)
|
||||
|
||||
new_bootstrap = copy(mock_entry.api.bootstrap)
|
||||
|
@ -435,7 +514,7 @@ async def test_sensor_update_motion(
|
|||
|
||||
mock_msg = Mock()
|
||||
mock_msg.changed_data = {}
|
||||
mock_msg.new_obj = new_camera
|
||||
mock_msg.new_obj = event
|
||||
|
||||
new_bootstrap.cameras = {new_camera.id: new_camera}
|
||||
new_bootstrap.events = {event.id: event}
|
||||
|
@ -448,3 +527,45 @@ async def test_sensor_update_motion(
|
|||
assert state.state == SmartDetectObjectType.PERSON.value
|
||||
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
|
||||
assert state.attributes[ATTR_EVENT_SCORE] == 100
|
||||
|
||||
|
||||
async def test_sensor_update_alarm(
|
||||
hass: HomeAssistant, mock_entry: MockEntityFixture, sensor: Sensor, now: datetime
|
||||
):
|
||||
"""Test sensor motion entity."""
|
||||
|
||||
_, entity_id = ids_from_device_description(
|
||||
Platform.SENSOR, sensor, SENSE_SENSORS[4]
|
||||
)
|
||||
|
||||
event_metadata = EventMetadata(sensor_id=sensor.id, alarm_type="smoke")
|
||||
event = Event(
|
||||
id="test_event_id",
|
||||
type=EventType.SENSOR_ALARM,
|
||||
start=now - timedelta(seconds=1),
|
||||
end=None,
|
||||
score=100,
|
||||
smart_detect_types=[],
|
||||
smart_detect_event_ids=[],
|
||||
metadata=event_metadata,
|
||||
api=mock_entry.api,
|
||||
)
|
||||
|
||||
new_bootstrap = copy(mock_entry.api.bootstrap)
|
||||
new_sensor = sensor.copy()
|
||||
new_sensor.set_alarm_timeout()
|
||||
new_sensor.last_alarm_event_id = event.id
|
||||
|
||||
mock_msg = Mock()
|
||||
mock_msg.changed_data = {}
|
||||
mock_msg.new_obj = event
|
||||
|
||||
new_bootstrap.sensors = {new_sensor.id: new_sensor}
|
||||
new_bootstrap.events = {event.id: event}
|
||||
mock_entry.api.bootstrap = new_bootstrap
|
||||
mock_entry.api.ws_subscription(mock_msg)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == "smoke"
|
||||
|
|
Loading…
Add table
Reference in a new issue