Migrate Unifi Protect last tripped time attributes to their own entities (#68347)

This commit is contained in:
J. Nick Koston 2022-03-24 14:23:53 -10:00 committed by GitHub
parent 9a396c1d16
commit 63ca0e70be
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 139 additions and 42 deletions

View file

@ -13,7 +13,7 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_LAST_TRIP_TIME, ATTR_MODEL
from homeassistant.const import ATTR_MODEL
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@ -27,7 +27,6 @@ from .entity import (
async_all_device_entities,
)
from .models import ProtectRequiredKeysMixin
from .utils import get_nested_attr
_LOGGER = logging.getLogger(__name__)
_KEY_DOOR = "door"
@ -39,8 +38,6 @@ class ProtectBinaryEntityDescription(
):
"""Describes UniFi Protect Binary Sensor entity."""
ufp_last_trip_value: str | None = None
MOUNT_DEVICE_CLASS_MAP = {
MountType.GARAGE: BinarySensorDeviceClass.GARAGE_DOOR,
@ -57,7 +54,6 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = (
icon="mdi:doorbell-video",
ufp_required_field="feature_flags.has_chime",
ufp_value="is_ringing",
ufp_last_trip_value="last_ring",
),
ProtectBinaryEntityDescription(
key="dark",
@ -79,7 +75,6 @@ LIGHT_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = (
name="Motion Detected",
device_class=BinarySensorDeviceClass.MOTION,
ufp_value="is_pir_motion_detected",
ufp_last_trip_value="last_motion",
),
)
@ -89,7 +84,6 @@ SENSE_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = (
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(
@ -104,7 +98,6 @@ SENSE_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = (
name="Motion Detected",
device_class=BinarySensorDeviceClass.MOTION,
ufp_value="is_motion_detected",
ufp_last_trip_value="motion_detected_at",
ufp_enabled="is_motion_sensor_enabled",
),
ProtectBinaryEntityDescription(
@ -112,7 +105,6 @@ SENSE_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = (
name="Tampering Detected",
device_class=BinarySensorDeviceClass.TAMPER,
ufp_value="is_tampering_detected",
ufp_last_trip_value="tampering_detected_at",
),
)
@ -122,7 +114,6 @@ MOTION_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = (
name="Motion",
device_class=BinarySensorDeviceClass.MOTION,
ufp_value="is_motion_detected",
ufp_last_trip_value="last_motion",
),
)
@ -215,16 +206,6 @@ class ProtectDeviceBinarySensor(ProtectDeviceEntity, BinarySensorEntity):
super()._async_update_device_from_protect()
self._attr_is_on = self.entity_description.get_ufp_value(self.device)
if self.entity_description.ufp_last_trip_value is not None:
last_trip = get_nested_attr(
self.device, self.entity_description.ufp_last_trip_value
)
attrs = self.extra_state_attributes or {}
self._attr_extra_state_attributes = {
**attrs,
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(

View file

@ -186,6 +186,15 @@ CAMERA_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
ufp_required_field="voltage",
precision=2,
),
ProtectSensorEntityDescription(
key="doorbell_last_trip_time",
name="Last Doorbell Ring",
device_class=SensorDeviceClass.TIMESTAMP,
icon="mdi:doorbell-video",
ufp_required_field="feature_flags.has_chime",
ufp_value="last_ring",
entity_registry_enabled_default=False,
),
)
CAMERA_DISABLED_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
@ -252,6 +261,27 @@ SENSE_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
ufp_value_fn=_get_alarm_sound,
ufp_enabled="is_alarm_sensor_enabled",
),
ProtectSensorEntityDescription(
key="door_last_trip_time",
name="Last Open",
device_class=SensorDeviceClass.TIMESTAMP,
ufp_value="open_status_changed_at",
entity_registry_enabled_default=False,
),
ProtectSensorEntityDescription(
key="motion_last_trip_time",
name="Last Motion Detected",
device_class=SensorDeviceClass.TIMESTAMP,
ufp_value="motion_detected_at",
entity_registry_enabled_default=False,
),
ProtectSensorEntityDescription(
key="tampering_last_trip_time",
name="Last Tampering Detected",
device_class=SensorDeviceClass.TIMESTAMP,
ufp_value="tampering_detected_at",
entity_registry_enabled_default=False,
),
)
DOORLOCK_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
@ -399,6 +429,27 @@ MOTION_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
)
LIGHT_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
ProtectSensorEntityDescription(
key="motion_last_trip_time",
name="Last Motion Detected",
device_class=SensorDeviceClass.TIMESTAMP,
ufp_value="last_motion",
entity_registry_enabled_default=False,
),
)
MOTION_TRIP_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
ProtectSensorEntityDescription(
key="motion_last_trip_time",
name="Last Motion Detected",
device_class=SensorDeviceClass.TIMESTAMP,
ufp_value="last_motion",
entity_registry_enabled_default=False,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
@ -412,6 +463,7 @@ async def async_setup_entry(
all_descs=ALL_DEVICES_SENSORS,
camera_descs=CAMERA_SENSORS + CAMERA_DISABLED_SENSORS,
sense_descs=SENSE_SENSORS,
light_descs=LIGHT_SENSORS,
lock_descs=DOORLOCK_SENSORS,
)
entities += _async_motion_entities(data)
@ -426,6 +478,14 @@ def _async_motion_entities(
) -> list[ProtectDeviceEntity]:
entities: list[ProtectDeviceEntity] = []
for device in data.api.bootstrap.cameras.values():
for description in MOTION_TRIP_SENSORS:
entities.append(ProtectDeviceSensor(data, device, description))
_LOGGER.debug(
"Adding trip sensor entity %s for %s",
description.name,
device.name,
)
if not device.feature_flags.has_smart_detect:
continue

View file

@ -24,7 +24,6 @@ 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,
@ -230,7 +229,7 @@ async def test_binary_sensor_setup_light(
entity_registry = er.async_get(hass)
for index, description in enumerate(LIGHT_SENSORS):
for description in LIGHT_SENSORS:
unique_id, entity_id = ids_from_device_description(
Platform.BINARY_SENSOR, light, description
)
@ -244,9 +243,6 @@ async def test_binary_sensor_setup_light(
assert state.state == STATE_OFF
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
if index == 1:
assert state.attributes[ATTR_LAST_TRIP_TIME] == now - timedelta(hours=1)
async def test_binary_sensor_setup_camera_all(
hass: HomeAssistant, camera: Camera, now: datetime
@ -269,8 +265,6 @@ async def test_binary_sensor_setup_camera_all(
assert state.state == STATE_OFF
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
assert state.attributes[ATTR_LAST_TRIP_TIME] == now - timedelta(hours=1)
# Is Dark
description = CAMERA_SENSORS[1]
unique_id, entity_id = ids_from_device_description(
@ -333,8 +327,7 @@ async def test_binary_sensor_setup_sensor(
entity_registry = er.async_get(hass)
expected_trip_time = now - timedelta(hours=1)
for index, description in enumerate(SENSE_SENSORS):
for description in SENSE_SENSORS:
unique_id, entity_id = ids_from_device_description(
Platform.BINARY_SENSOR, sensor, description
)
@ -348,9 +341,6 @@ async def test_binary_sensor_setup_sensor(
assert state.state == STATE_OFF
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
if index != 1:
assert state.attributes[ATTR_LAST_TRIP_TIME] == expected_trip_time
async def test_binary_sensor_setup_sensor_none(
hass: HomeAssistant, sensor_none: Sensor

View file

@ -4,7 +4,7 @@ from __future__ import annotations
from copy import copy
from datetime import datetime, timedelta
from unittest.mock import Mock
from unittest.mock import AsyncMock, Mock
import pytest
from pyunifiprotect.data import NVR, Camera, Event, Sensor
@ -21,6 +21,7 @@ from homeassistant.components.unifiprotect.sensor import (
CAMERA_DISABLED_SENSORS,
CAMERA_SENSORS,
MOTION_SENSORS,
MOTION_TRIP_SENSORS,
NVR_DISABLED_SENSORS,
NVR_SENSORS,
OBJECT_TYPE_NONE,
@ -78,9 +79,6 @@ async def sensor_fixture(
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
Sensor.__config__.validate_assignment = True
@ -117,8 +115,8 @@ async def sensor_none_fixture(
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)
# 4 from all, 5 from sense, 12 NVR
assert_entity_counts(hass, Platform.SENSOR, 22, 14)
yield sensor_obj
@ -144,6 +142,7 @@ async def camera_fixture(
camera_obj.channels[2]._api = mock_entry.api
camera_obj.name = "Test Camera"
camera_obj.feature_flags.has_smart_detect = True
camera_obj.feature_flags.has_chime = True
camera_obj.is_smart_detected = False
camera_obj.wired_connection_state = WiredConnectionState(phy_rate=1000)
camera_obj.wifi_connection_state = WifiConnectionState(
@ -166,9 +165,6 @@ async def camera_fixture(
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
await hass.async_block_till_done()
# 3 from all, 6 from camera, 12 NVR
assert_entity_counts(hass, Platform.SENSOR, 22, 14)
yield camera_obj
Camera.__config__.validate_assignment = True
@ -178,11 +174,21 @@ async def test_sensor_setup_sensor(
hass: HomeAssistant, mock_entry: MockEntityFixture, sensor: Sensor
):
"""Test sensor entity setup for sensor devices."""
# 5 from all, 5 from sense, 12 NVR
assert_entity_counts(hass, Platform.SENSOR, 22, 14)
entity_registry = er.async_get(hass)
expected_values = ("10", "10.0", "10.0", "10.0", "none")
expected_values = (
"10",
"10.0",
"10.0",
"10.0",
"none",
)
for index, description in enumerate(SENSE_SENSORS):
if not description.entity_registry_enabled_default:
continue
unique_id, entity_id = ids_from_device_description(
Platform.SENSOR, sensor, description
)
@ -229,6 +235,8 @@ async def test_sensor_setup_sensor_none(
STATE_UNAVAILABLE,
)
for index, description in enumerate(SENSE_SENSORS):
if not description.entity_registry_enabled_default:
continue
unique_id, entity_id = ids_from_device_description(
Platform.SENSOR, sensor_none, description
)
@ -395,6 +403,8 @@ async def test_sensor_setup_camera(
hass: HomeAssistant, mock_entry: MockEntityFixture, camera: Camera, now: datetime
):
"""Test sensor entity setup for camera devices."""
# 3 from all, 8 from camera, 12 NVR
assert_entity_counts(hass, Platform.SENSOR, 24, 14)
entity_registry = er.async_get(hass)
@ -405,6 +415,8 @@ async def test_sensor_setup_camera(
"20.0",
)
for index, description in enumerate(CAMERA_SENSORS):
if not description.entity_registry_enabled_default:
continue
unique_id, entity_id = ids_from_device_description(
Platform.SENSOR, camera, description
)
@ -487,10 +499,37 @@ async def test_sensor_setup_camera(
assert state.attributes[ATTR_EVENT_SCORE] == 0
async def test_sensor_setup_camera_with_last_trip_time(
hass: HomeAssistant,
entity_registry_enabled_by_default: AsyncMock,
mock_entry: MockEntityFixture,
camera: Camera,
now: datetime,
):
"""Test sensor entity setup for camera devices with last trip time."""
entity_registry = er.async_get(hass)
# Last Trip Time
unique_id, entity_id = ids_from_device_description(
Platform.SENSOR, camera, MOTION_TRIP_SENSORS[0]
)
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 == "2021-12-20T17:26:53+00:00"
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
async def test_sensor_update_motion(
hass: HomeAssistant, mock_entry: MockEntityFixture, camera: Camera, now: datetime
):
"""Test sensor motion entity."""
# 3 from all, 8 from camera, 12 NVR
assert_entity_counts(hass, Platform.SENSOR, 24, 14)
_, entity_id = ids_from_device_description(
Platform.SENSOR, camera, MOTION_SENSORS[0]
@ -534,6 +573,8 @@ async def test_sensor_update_alarm(
hass: HomeAssistant, mock_entry: MockEntityFixture, sensor: Sensor, now: datetime
):
"""Test sensor motion entity."""
# 5 from all, 5 from sense, 12 NVR
assert_entity_counts(hass, Platform.SENSOR, 22, 14)
_, entity_id = ids_from_device_description(
Platform.SENSOR, sensor, SENSE_SENSORS[4]
@ -571,3 +612,28 @@ async def test_sensor_update_alarm(
assert state
assert state.state == "smoke"
await time_changed(hass, 10)
async def test_sensor_update_alarm_with_last_trip_time(
hass: HomeAssistant,
entity_registry_enabled_by_default: AsyncMock,
mock_entry: MockEntityFixture,
sensor: Sensor,
now: datetime,
):
"""Test sensor motion entity with last trip time."""
# Last Trip Time
unique_id, entity_id = ids_from_device_description(
Platform.SENSOR, sensor, SENSE_SENSORS[-3]
)
entity_registry = er.async_get(hass)
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 == "2022-01-04T04:03:56+00:00"
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION