Add new features from new UniFi Protect (#82892)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Christopher Bailey 2022-11-29 04:44:31 -05:00 committed by GitHub
parent a5890b2374
commit 596016c2ac
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 166 additions and 17 deletions

View file

@ -14,6 +14,7 @@ from pyunifiprotect.data import (
ProtectAdoptableDeviceModel, ProtectAdoptableDeviceModel,
ProtectModelWithId, ProtectModelWithId,
Sensor, Sensor,
SmartDetectAudioType,
SmartDetectObjectType, SmartDetectObjectType,
) )
from pyunifiprotect.data.nvr import UOSDisk from pyunifiprotect.data.nvr import UOSDisk
@ -201,6 +202,24 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = (
ufp_value="is_package_detection_on", ufp_value="is_package_detection_on",
ufp_perm=PermRequired.NO_WRITE, ufp_perm=PermRequired.NO_WRITE,
), ),
ProtectBinaryEntityDescription(
key="smart_licenseplate",
name="Detections: License Plate",
icon="mdi:car",
entity_category=EntityCategory.DIAGNOSTIC,
ufp_required_field="can_detect_license_plate",
ufp_value="is_license_plate_detection_on",
ufp_perm=PermRequired.NO_WRITE,
),
ProtectBinaryEntityDescription(
key="smart_smoke",
name="Detections: Smoke/CO",
icon="mdi:fire",
entity_category=EntityCategory.DIAGNOSTIC,
ufp_required_field="can_detect_smoke",
ufp_value="is_smoke_detection_on",
ufp_perm=PermRequired.NO_WRITE,
),
) )
LIGHT_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( LIGHT_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = (
@ -381,6 +400,37 @@ MOTION_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
ufp_event_obj="last_smart_detect_event", ufp_event_obj="last_smart_detect_event",
ufp_smart_type=SmartDetectObjectType.PACKAGE, ufp_smart_type=SmartDetectObjectType.PACKAGE,
), ),
ProtectBinaryEventEntityDescription(
key="smart_audio_any",
name="Audio 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_audio_detect_event",
),
ProtectBinaryEventEntityDescription(
key="smart_audio_smoke",
name="Smoke Alarm Detected",
device_class=DEVICE_CLASS_DETECTION,
icon="mdi:fire",
ufp_value="is_smart_detected",
ufp_required_field="can_detect_smoke",
ufp_enabled="is_smoke_detection_on",
ufp_event_obj="last_smart_audio_detect_event",
ufp_smart_type=SmartDetectAudioType.SMOKE,
),
ProtectBinaryEventEntityDescription(
key="smart_audio_cmonx",
name="CO Alarm Detected",
device_class=DEVICE_CLASS_DETECTION,
icon="mdi:fire",
ufp_value="is_smart_detected",
ufp_required_field="can_detect_smoke",
ufp_enabled="is_smoke_detection_on",
ufp_event_obj="last_smart_audio_detect_event",
ufp_smart_type=SmartDetectAudioType.CMONX,
),
) )
DOORLOCK_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( DOORLOCK_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = (

View file

@ -406,10 +406,13 @@ class ProtectMediaSource(MediaSource):
event_text = "Motion Event" event_text = "Motion Event"
elif event_type == EventType.SMART_DETECT.value: elif event_type == EventType.SMART_DETECT.value:
if isinstance(event, Event): if isinstance(event, Event):
smart_type = event.smart_detect_types[0] smart_types = event.smart_detect_types
else: else:
smart_type = SmartDetectObjectType(event["smartDetectTypes"][0]) smart_types = [
event_text = f"Smart Detection - {smart_type.name.title()}" SmartDetectObjectType(e) for e in event["smartDetectTypes"]
]
smart_type_names = [s.name.title().replace("_", " ") for s in smart_types]
event_text = f"Smart Detection - {','.join(smart_type_names)}"
title += f" {event_text}" title += f" {event_text}"
nvr = data.api.bootstrap.nvr nvr = data.api.bootstrap.nvr

View file

@ -15,6 +15,7 @@ from pyunifiprotect.data import (
ProtectDeviceModel, ProtectDeviceModel,
ProtectModelWithId, ProtectModelWithId,
Sensor, Sensor,
SmartDetectObjectType,
) )
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
@ -527,6 +528,15 @@ MOTION_SENSORS: tuple[ProtectSensorEventEntityDescription, ...] = (
ufp_value="is_smart_detected", ufp_value="is_smart_detected",
ufp_event_obj="last_smart_detect_event", ufp_event_obj="last_smart_detect_event",
), ),
ProtectSensorEventEntityDescription(
key="smart_obj_licenseplate",
name="License Plate Detected",
icon="mdi:car",
device_class=DEVICE_CLASS_DETECTION,
ufp_value="is_smart_detected",
ufp_event_obj="last_smart_detect_event",
ufp_smart_type=SmartDetectObjectType.LICENSE_PLATE,
),
) )
@ -756,7 +766,20 @@ class ProtectEventSensor(EventEntityMixin, SensorEntity):
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
EventEntityMixin._async_update_device_from_protect(self, device) EventEntityMixin._async_update_device_from_protect(self, device)
if self._event is None: if (
self._attr_native_value = OBJECT_TYPE_NONE self.entity_description.ufp_smart_type
== SmartDetectObjectType.LICENSE_PLATE
):
if (
self._event is None
or self._event.metadata is None
or self._event.metadata.license_plate is None
):
self._attr_native_value = OBJECT_TYPE_NONE
else:
self._attr_native_value = self._event.metadata.license_plate.name
else: else:
self._attr_native_value = self._event.smart_detect_types[0].value if self._event is None:
self._attr_native_value = OBJECT_TYPE_NONE
else:
self._attr_native_value = self._event.smart_detect_types[0].value

View file

@ -126,7 +126,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
), ),
ProtectSwitchEntityDescription( ProtectSwitchEntityDescription(
key="osd_bitrate", key="osd_bitrate",
name="Overlay: Show Bitrate", name="Overlay: Show Nerd Mode",
icon="mdi:fullscreen", icon="mdi:fullscreen",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
ufp_value="osd_settings.is_debug_enabled", ufp_value="osd_settings.is_debug_enabled",
@ -182,6 +182,26 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
ufp_set_method="set_package_detection", ufp_set_method="set_package_detection",
ufp_perm=PermRequired.WRITE, ufp_perm=PermRequired.WRITE,
), ),
ProtectSwitchEntityDescription(
key="smart_licenseplate",
name="Detections: License Plate",
icon="mdi:car",
entity_category=EntityCategory.CONFIG,
ufp_required_field="can_detect_license_plate",
ufp_value="is_license_plate_detection_on",
ufp_set_method="set_license_plate_detection",
ufp_perm=PermRequired.WRITE,
),
ProtectSwitchEntityDescription(
key="smart_smoke",
name="Detections: Smoke/CO",
icon="mdi:fire",
entity_category=EntityCategory.CONFIG,
ufp_required_field="can_detect_smoke",
ufp_value="is_smoke_detection_on",
ufp_set_method="set_smoke_detection",
ufp_perm=PermRequired.WRITE,
),
) )
PRIVACY_MODE_SWITCH = ProtectSwitchEntityDescription[Camera]( PRIVACY_MODE_SWITCH = ProtectSwitchEntityDescription[Camera](

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, 6, 6) assert_entity_counts(hass, Platform.BINARY_SENSOR, 7, 7)
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, 6, 6) assert_entity_counts(hass, Platform.BINARY_SENSOR, 7, 7)
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, 6, 6) assert_entity_counts(hass, Platform.BINARY_SENSOR, 7, 7)
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
@ -262,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, 12, 12) assert_entity_counts(hass, Platform.BINARY_SENSOR, 13, 13)
_, 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

@ -13,7 +13,7 @@ from pyunifiprotect.data import (
Sensor, Sensor,
SmartDetectObjectType, SmartDetectObjectType,
) )
from pyunifiprotect.data.nvr import EventMetadata from pyunifiprotect.data.nvr import EventMetadata, LicensePlateMetadata
from homeassistant.components.unifiprotect.const import ( from homeassistant.components.unifiprotect.const import (
ATTR_EVENT_SCORE, ATTR_EVENT_SCORE,
@ -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, 12) assert_entity_counts(hass, Platform.SENSOR, 26, 13)
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, 12) assert_entity_counts(hass, Platform.SENSOR, 26, 13)
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, 12) assert_entity_counts(hass, Platform.SENSOR, 26, 13)
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
@ -424,7 +424,7 @@ async def test_sensor_setup_camera_with_last_trip_time(
"""Test sensor entity setup for camera devices with last trip time.""" """Test sensor entity setup for camera devices with last trip time."""
await init_entry(hass, ufp, [doorbell]) await init_entry(hass, ufp, [doorbell])
assert_entity_counts(hass, Platform.SENSOR, 25, 25) assert_entity_counts(hass, Platform.SENSOR, 26, 26)
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
@ -452,7 +452,7 @@ 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, 12) assert_entity_counts(hass, Platform.SENSOR, 26, 13)
_, entity_id = ids_from_device_description( _, entity_id = ids_from_device_description(
Platform.SENSOR, doorbell, MOTION_SENSORS[0] Platform.SENSOR, doorbell, MOTION_SENSORS[0]
@ -565,3 +565,54 @@ async def test_sensor_update_alarm_with_last_trip_time(
== (fixed_now - timedelta(hours=1)).replace(microsecond=0).isoformat() == (fixed_now - timedelta(hours=1)).replace(microsecond=0).isoformat()
) )
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
async def test_camera_update_licenseplate(
hass: HomeAssistant, ufp: MockUFPFixture, camera: Camera, fixed_now: datetime
):
"""Test sensor motion entity."""
camera.feature_flags.smart_detect_types.append(SmartDetectObjectType.LICENSE_PLATE)
camera.feature_flags.has_smart_detect = True
camera.smart_detect_settings.object_types.append(
SmartDetectObjectType.LICENSE_PLATE
)
await init_entry(hass, ufp, [camera])
assert_entity_counts(hass, Platform.SENSOR, 24, 13)
_, entity_id = ids_from_device_description(
Platform.SENSOR, camera, MOTION_SENSORS[1]
)
event_metadata = EventMetadata(
license_plate=LicensePlateMetadata(name="ABCD1234", confidence_level=95)
)
event = Event(
id="test_event_id",
type=EventType.SMART_DETECT,
start=fixed_now - timedelta(seconds=1),
end=None,
score=100,
smart_detect_types=[SmartDetectObjectType.LICENSE_PLATE],
smart_detect_event_ids=[],
metadata=event_metadata,
api=ufp.api,
)
new_camera = camera.copy()
new_camera.is_smart_detected = True
new_camera.last_smart_detect_event_id = event.id
mock_msg = Mock()
mock_msg.changed_data = {}
mock_msg.new_obj = new_camera
ufp.api.bootstrap.cameras = {new_camera.id: new_camera}
ufp.api.bootstrap.events = {event.id: event}
ufp.ws_msg(mock_msg)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state
assert state.state == "ABCD1234"

View file

@ -35,6 +35,8 @@ CAMERA_SWITCHES_BASIC = [
for d in CAMERA_SWITCHES for d in CAMERA_SWITCHES
if d.name != "Detections: Face" if d.name != "Detections: Face"
and d.name != "Detections: Package" and d.name != "Detections: Package"
and d.name != "Detections: License Plate"
and d.name != "Detections: Smoke/CO"
and d.name != "SSH Enabled" and d.name != "SSH Enabled"
] ]
CAMERA_SWITCHES_NO_EXTRA = [ CAMERA_SWITCHES_NO_EXTRA = [