diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index 0d127a55554..05f7b37d6c8 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -114,8 +114,9 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( name="System Sounds", icon="mdi:speaker", entity_category=EntityCategory.DIAGNOSTIC, - ufp_required_field="feature_flags.has_speaker", + ufp_required_field="has_speaker", ufp_value="speaker_settings.are_system_sounds_enabled", + ufp_enabled="feature_flags.has_speaker", ufp_perm=PermRequired.NO_WRITE, ), ProtectBinaryEntityDescription( diff --git a/homeassistant/components/unifiprotect/media_player.py b/homeassistant/components/unifiprotect/media_player.py index 34686f52519..d8edc7fe4e9 100644 --- a/homeassistant/components/unifiprotect/media_player.py +++ b/homeassistant/components/unifiprotect/media_player.py @@ -9,6 +9,7 @@ from pyunifiprotect.data import ( ModelType, ProtectAdoptableDeviceModel, ProtectModelWithId, + StateType, ) from pyunifiprotect.exceptions import StreamError @@ -48,7 +49,9 @@ async def async_setup_entry( data: ProtectData = hass.data[DOMAIN][entry.entry_id] async def _add_new_device(device: ProtectAdoptableDeviceModel) -> None: - if isinstance(device, Camera) and device.feature_flags.has_speaker: + if isinstance(device, Camera) and ( + device.has_speaker or device.has_removable_speaker + ): async_add_entities([ProtectMediaPlayer(data, device)]) entry.async_on_unload( @@ -58,7 +61,7 @@ async def async_setup_entry( entities = [] for device in data.get_by_types({ModelType.CAMERA}): device = cast(Camera, device) - if device.feature_flags.has_speaker: + if device.has_speaker or device.has_removable_speaker: entities.append(ProtectMediaPlayer(data, device)) async_add_entities(entities) @@ -107,6 +110,12 @@ class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity): else: self._attr_state = STATE_IDLE + is_connected = self.data.last_update_success and ( + self.device.state == StateType.CONNECTED + or (not self.device.is_adopted_by_us and self.device.can_adopt) + ) + self._attr_available = is_connected and self.device.feature_flags.has_speaker + async def async_set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" diff --git a/homeassistant/components/unifiprotect/number.py b/homeassistant/components/unifiprotect/number.py index ed9faf4da40..3b80532a32c 100644 --- a/homeassistant/components/unifiprotect/number.py +++ b/homeassistant/components/unifiprotect/number.py @@ -82,8 +82,9 @@ CAMERA_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( ufp_min=0, ufp_max=100, ufp_step=1, - ufp_required_field="feature_flags.has_mic", + ufp_required_field="has_mic", ufp_value="mic_volume", + ufp_enabled="feature_flags.has_mic", ufp_set_method="set_mic_volume", ufp_perm=PermRequired.WRITE, ), diff --git a/homeassistant/components/unifiprotect/sensor.py b/homeassistant/components/unifiprotect/sensor.py index c74bd00e055..57bd4fc7230 100644 --- a/homeassistant/components/unifiprotect/sensor.py +++ b/homeassistant/components/unifiprotect/sensor.py @@ -215,8 +215,9 @@ CAMERA_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( icon="mdi:microphone", native_unit_of_measurement=PERCENTAGE, entity_category=EntityCategory.DIAGNOSTIC, - ufp_required_field="feature_flags.has_mic", + ufp_required_field="has_mic", ufp_value="mic_volume", + ufp_enabled="feature_flags.has_mic", ufp_perm=PermRequired.NO_WRITE, ), ProtectSensorEntityDescription( diff --git a/homeassistant/components/unifiprotect/switch.py b/homeassistant/components/unifiprotect/switch.py index fa53cc3b87b..65de9f52913 100644 --- a/homeassistant/components/unifiprotect/switch.py +++ b/homeassistant/components/unifiprotect/switch.py @@ -5,6 +5,7 @@ from dataclasses import dataclass from typing import Any from pyunifiprotect.data import ( + NVR, Camera, ProtectAdoptableDeviceModel, ProtectModelWithId, @@ -22,7 +23,7 @@ from homeassistant.helpers.restore_state import RestoreEntity from .const import DISPATCH_ADOPT, DOMAIN from .data import ProtectData -from .entity import ProtectDeviceEntity, async_all_device_entities +from .entity import ProtectDeviceEntity, ProtectNVREntity, async_all_device_entities from .models import PermRequired, ProtectSetableKeysMixin, T from .utils import async_dispatch_id as _ufpd @@ -90,8 +91,9 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( name="System Sounds", icon="mdi:speaker", entity_category=EntityCategory.CONFIG, - ufp_required_field="feature_flags.has_speaker", + ufp_required_field="has_speaker", ufp_value="speaker_settings.are_system_sounds_enabled", + ufp_enabled="feature_flags.has_speaker", ufp_set_method="set_system_sounds", ufp_perm=PermRequired.WRITE, ), @@ -296,6 +298,25 @@ VIEWER_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ) +NVR_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( + ProtectSwitchEntityDescription( + key="analytics_enabled", + name="Analytics Enabled", + icon="mdi:google-analytics", + entity_category=EntityCategory.CONFIG, + ufp_value="is_analytics_enabled", + ufp_set_method="set_anonymous_analytics", + ), + ProtectSwitchEntityDescription( + key="insights_enabled", + name="Insights Enabled", + icon="mdi:magnify", + entity_category=EntityCategory.CONFIG, + ufp_value="is_insights_enabled", + ufp_set_method="set_insights", + ), +) + async def async_setup_entry( hass: HomeAssistant, @@ -342,6 +363,17 @@ async def async_setup_entry( ProtectPrivacyModeSwitch, camera_descs=[PRIVACY_MODE_SWITCH], ) + + if ( + data.api.bootstrap.nvr.can_write(data.api.bootstrap.auth_user) + and data.api.bootstrap.nvr.is_insights_enabled is not None + ): + for switch in NVR_SWITCHES: + entities.append( + ProtectNVRSwitch( + data, device=data.api.bootstrap.nvr, description=switch + ) + ) async_add_entities(entities) @@ -377,6 +409,37 @@ class ProtectSwitch(ProtectDeviceEntity, SwitchEntity): await self.entity_description.ufp_set(self.device, False) +class ProtectNVRSwitch(ProtectNVREntity, SwitchEntity): + """A UniFi Protect NVR Switch.""" + + entity_description: ProtectSwitchEntityDescription + + def __init__( + self, + data: ProtectData, + device: NVR, + description: ProtectSwitchEntityDescription, + ) -> None: + """Initialize an UniFi Protect Switch.""" + super().__init__(data, device, description) + self._attr_name = f"{self.device.display_name} {self.entity_description.name}" + + @property + def is_on(self) -> bool: + """Return true if device is on.""" + return self.entity_description.get_ufp_value(self.device) is True + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the device on.""" + + await self.entity_description.ufp_set(self.device, True) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the device off.""" + + await self.entity_description.ufp_set(self.device, False) + + class ProtectPrivacyModeSwitch(RestoreEntity, ProtectSwitch): """A UniFi Protect Switch.""" diff --git a/tests/components/unifiprotect/conftest.py b/tests/components/unifiprotect/conftest.py index 2a9edb605e7..b006dfbd004 100644 --- a/tests/components/unifiprotect/conftest.py +++ b/tests/components/unifiprotect/conftest.py @@ -209,6 +209,7 @@ def doorbell_fixture(camera: Camera, fixed_now: datetime): SmartDetectObjectType.PERSON, SmartDetectObjectType.VEHICLE, ] + doorbell.has_speaker = True doorbell.feature_flags.has_hdr = True doorbell.feature_flags.has_lcd_screen = True doorbell.feature_flags.has_speaker = True diff --git a/tests/components/unifiprotect/fixtures/sample_nvr.json b/tests/components/unifiprotect/fixtures/sample_nvr.json index 507e75fec09..8777e3ce945 100644 --- a/tests/components/unifiprotect/fixtures/sample_nvr.json +++ b/tests/components/unifiprotect/fixtures/sample_nvr.json @@ -56,6 +56,7 @@ "enableCrashReporting": true, "disableAudio": false, "analyticsData": "anonymous", + "isInsightsEnabled": true, "anonymousDeviceId": "65257f7d-874c-498a-8f1b-00b2dd0a7ae1", "cameraUtilization": 30, "isRecycling": false, diff --git a/tests/components/unifiprotect/test_switch.py b/tests/components/unifiprotect/test_switch.py index 7bf9e3f8f83..50f82736ee5 100644 --- a/tests/components/unifiprotect/test_switch.py +++ b/tests/components/unifiprotect/test_switch.py @@ -49,11 +49,11 @@ async def test_switch_camera_remove( ufp.api.bootstrap.nvr.system_info.ustorage = None await init_entry(hass, ufp, [doorbell, unadopted_camera]) - assert_entity_counts(hass, Platform.SWITCH, 13, 12) + assert_entity_counts(hass, Platform.SWITCH, 15, 14) await remove_entities(hass, ufp, [doorbell, unadopted_camera]) - assert_entity_counts(hass, Platform.SWITCH, 0, 0) + assert_entity_counts(hass, Platform.SWITCH, 2, 2) await adopt_devices(hass, ufp, [doorbell, unadopted_camera]) - assert_entity_counts(hass, Platform.SWITCH, 13, 12) + assert_entity_counts(hass, Platform.SWITCH, 15, 14) async def test_switch_light_remove( @@ -63,11 +63,36 @@ async def test_switch_light_remove( ufp.api.bootstrap.nvr.system_info.ustorage = None await init_entry(hass, ufp, [light]) - assert_entity_counts(hass, Platform.SWITCH, 2, 1) + assert_entity_counts(hass, Platform.SWITCH, 4, 3) await remove_entities(hass, ufp, [light]) - assert_entity_counts(hass, Platform.SWITCH, 0, 0) + assert_entity_counts(hass, Platform.SWITCH, 2, 2) await adopt_devices(hass, ufp, [light]) - assert_entity_counts(hass, Platform.SWITCH, 2, 1) + assert_entity_counts(hass, Platform.SWITCH, 4, 3) + + +async def test_switch_nvr(hass: HomeAssistant, ufp: MockUFPFixture): + """Test switch entity setup for light devices.""" + + await init_entry(hass, ufp, []) + + assert_entity_counts(hass, Platform.SWITCH, 2, 2) + + nvr = ufp.api.bootstrap.nvr + nvr.__fields__["set_insights"] = Mock() + nvr.set_insights = AsyncMock() + entity_id = "switch.unifiprotect_insights_enabled" + + await hass.services.async_call( + "switch", "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + + nvr.set_insights.assert_called_once_with(True) + + await hass.services.async_call( + "switch", "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + + nvr.set_insights.assert_called_with(False) async def test_switch_setup_no_perm( @@ -95,7 +120,7 @@ async def test_switch_setup_light( """Test switch entity setup for light devices.""" await init_entry(hass, ufp, [light]) - assert_entity_counts(hass, Platform.SWITCH, 2, 1) + assert_entity_counts(hass, Platform.SWITCH, 4, 3) entity_registry = er.async_get(hass) @@ -140,7 +165,7 @@ async def test_switch_setup_camera_all( """Test switch entity setup for camera devices (all enabled feature flags).""" await init_entry(hass, ufp, [doorbell]) - assert_entity_counts(hass, Platform.SWITCH, 13, 12) + assert_entity_counts(hass, Platform.SWITCH, 15, 14) entity_registry = er.async_get(hass) @@ -187,7 +212,7 @@ async def test_switch_setup_camera_none( """Test switch entity setup for camera devices (no enabled feature flags).""" await init_entry(hass, ufp, [camera]) - assert_entity_counts(hass, Platform.SWITCH, 6, 5) + assert_entity_counts(hass, Platform.SWITCH, 8, 7) entity_registry = er.async_get(hass) @@ -235,7 +260,7 @@ async def test_switch_light_status( """Tests status light switch for lights.""" await init_entry(hass, ufp, [light]) - assert_entity_counts(hass, Platform.SWITCH, 2, 1) + assert_entity_counts(hass, Platform.SWITCH, 4, 3) description = LIGHT_SWITCHES[1] @@ -263,7 +288,7 @@ async def test_switch_camera_ssh( """Tests SSH switch for cameras.""" await init_entry(hass, ufp, [doorbell]) - assert_entity_counts(hass, Platform.SWITCH, 13, 12) + assert_entity_counts(hass, Platform.SWITCH, 15, 14) description = CAMERA_SWITCHES[0] @@ -296,7 +321,7 @@ async def test_switch_camera_simple( """Tests all simple switches for cameras.""" await init_entry(hass, ufp, [doorbell]) - assert_entity_counts(hass, Platform.SWITCH, 13, 12) + assert_entity_counts(hass, Platform.SWITCH, 15, 14) assert description.ufp_set_method is not None @@ -325,7 +350,7 @@ async def test_switch_camera_highfps( """Tests High FPS switch for cameras.""" await init_entry(hass, ufp, [doorbell]) - assert_entity_counts(hass, Platform.SWITCH, 13, 12) + assert_entity_counts(hass, Platform.SWITCH, 15, 14) description = CAMERA_SWITCHES[3] @@ -356,7 +381,7 @@ async def test_switch_camera_privacy( previous_record = doorbell.recording_settings.mode = RecordingMode.DETECTIONS await init_entry(hass, ufp, [doorbell]) - assert_entity_counts(hass, Platform.SWITCH, 13, 12) + assert_entity_counts(hass, Platform.SWITCH, 15, 14) description = PRIVACY_MODE_SWITCH @@ -408,7 +433,7 @@ async def test_switch_camera_privacy_already_on( doorbell.add_privacy_zone() await init_entry(hass, ufp, [doorbell]) - assert_entity_counts(hass, Platform.SWITCH, 13, 12) + assert_entity_counts(hass, Platform.SWITCH, 15, 14) description = PRIVACY_MODE_SWITCH