Improve UniFi Protect Smart Sensor support (#64019)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Christopher Bailey 2022-01-12 22:54:22 -05:00 committed by GitHub
parent cafbcb634a
commit 20768172b1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 501 additions and 22 deletions

View file

@ -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."""

View file

@ -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

View file

@ -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:

View file

@ -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"

View file

@ -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):

View file

@ -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)

View file

@ -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,
)

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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
)

View file

@ -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"