Migrate Unifi Protect last tripped time attributes to their own entities (#68347)
This commit is contained in:
parent
9a396c1d16
commit
63ca0e70be
4 changed files with 139 additions and 42 deletions
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue