diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index 3d018d6eee1..d38fe8582ab 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -14,6 +14,7 @@ from pyunifiprotect.data import ( ProtectAdoptableDeviceModel, ProtectModelWithId, Sensor, + SmartDetectAudioType, SmartDetectObjectType, ) from pyunifiprotect.data.nvr import UOSDisk @@ -201,6 +202,24 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ufp_value="is_package_detection_on", 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, ...] = ( @@ -381,6 +400,37 @@ MOTION_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( ufp_event_obj="last_smart_detect_event", 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, ...] = ( diff --git a/homeassistant/components/unifiprotect/media_source.py b/homeassistant/components/unifiprotect/media_source.py index 6ebb36c11c5..81054d9aff5 100644 --- a/homeassistant/components/unifiprotect/media_source.py +++ b/homeassistant/components/unifiprotect/media_source.py @@ -406,10 +406,13 @@ class ProtectMediaSource(MediaSource): event_text = "Motion Event" elif event_type == EventType.SMART_DETECT.value: if isinstance(event, Event): - smart_type = event.smart_detect_types[0] + smart_types = event.smart_detect_types else: - smart_type = SmartDetectObjectType(event["smartDetectTypes"][0]) - event_text = f"Smart Detection - {smart_type.name.title()}" + smart_types = [ + 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}" nvr = data.api.bootstrap.nvr diff --git a/homeassistant/components/unifiprotect/sensor.py b/homeassistant/components/unifiprotect/sensor.py index 168faebe8ac..621fad2a1a5 100644 --- a/homeassistant/components/unifiprotect/sensor.py +++ b/homeassistant/components/unifiprotect/sensor.py @@ -15,6 +15,7 @@ from pyunifiprotect.data import ( ProtectDeviceModel, ProtectModelWithId, Sensor, + SmartDetectObjectType, ) from homeassistant.components.sensor import ( @@ -527,6 +528,15 @@ MOTION_SENSORS: tuple[ProtectSensorEventEntityDescription, ...] = ( ufp_value="is_smart_detected", 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: # do not call ProtectDeviceSensor method since we want event to get value here EventEntityMixin._async_update_device_from_protect(self, device) - if self._event is None: - self._attr_native_value = OBJECT_TYPE_NONE + if ( + 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: - 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 diff --git a/homeassistant/components/unifiprotect/switch.py b/homeassistant/components/unifiprotect/switch.py index 65de9f52913..fa501f6a364 100644 --- a/homeassistant/components/unifiprotect/switch.py +++ b/homeassistant/components/unifiprotect/switch.py @@ -126,7 +126,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="osd_bitrate", - name="Overlay: Show Bitrate", + name="Overlay: Show Nerd Mode", icon="mdi:fullscreen", entity_category=EntityCategory.CONFIG, ufp_value="osd_settings.is_debug_enabled", @@ -182,6 +182,26 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ufp_set_method="set_package_detection", 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]( diff --git a/tests/components/unifiprotect/test_binary_sensor.py b/tests/components/unifiprotect/test_binary_sensor.py index cb8d7bb659c..b2935552855 100644 --- a/tests/components/unifiprotect/test_binary_sensor.py +++ b/tests/components/unifiprotect/test_binary_sensor.py @@ -50,11 +50,11 @@ async def test_binary_sensor_camera_remove( ufp.api.bootstrap.nvr.system_info.ustorage = None 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]) assert_entity_counts(hass, Platform.BINARY_SENSOR, 0, 0) 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( @@ -120,7 +120,7 @@ async def test_binary_sensor_setup_camera_all( ufp.api.bootstrap.nvr.system_info.ustorage = None 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) @@ -262,7 +262,7 @@ async def test_binary_sensor_update_motion( """Test binary_sensor motion entity.""" 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( Platform.BINARY_SENSOR, doorbell, MOTION_SENSORS[0] diff --git a/tests/components/unifiprotect/test_sensor.py b/tests/components/unifiprotect/test_sensor.py index f095260c38e..0ec50398e7a 100644 --- a/tests/components/unifiprotect/test_sensor.py +++ b/tests/components/unifiprotect/test_sensor.py @@ -13,7 +13,7 @@ from pyunifiprotect.data import ( Sensor, SmartDetectObjectType, ) -from pyunifiprotect.data.nvr import EventMetadata +from pyunifiprotect.data.nvr import EventMetadata, LicensePlateMetadata from homeassistant.components.unifiprotect.const import ( ATTR_EVENT_SCORE, @@ -62,11 +62,11 @@ async def test_sensor_camera_remove( ufp.api.bootstrap.nvr.system_info.ustorage = None 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]) assert_entity_counts(hass, Platform.SENSOR, 12, 9) 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( @@ -318,7 +318,7 @@ async def test_sensor_setup_camera( """Test sensor entity setup for camera devices.""" 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) @@ -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.""" 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) @@ -452,7 +452,7 @@ async def test_sensor_update_motion( """Test sensor motion entity.""" 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( 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() ) 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" diff --git a/tests/components/unifiprotect/test_switch.py b/tests/components/unifiprotect/test_switch.py index 82bf90eefd4..2ede00e60f2 100644 --- a/tests/components/unifiprotect/test_switch.py +++ b/tests/components/unifiprotect/test_switch.py @@ -35,6 +35,8 @@ CAMERA_SWITCHES_BASIC = [ for d in CAMERA_SWITCHES if d.name != "Detections: Face" and d.name != "Detections: Package" + and d.name != "Detections: License Plate" + and d.name != "Detections: Smoke/CO" and d.name != "SSH Enabled" ] CAMERA_SWITCHES_NO_EXTRA = [