Add new features from UniFi Protect 2.2.1-beta5 (#77391)

This commit is contained in:
Christopher Bailey 2022-08-28 13:31:07 -04:00 committed by GitHub
parent 441d7c0461
commit d29be2390b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 124 additions and 22 deletions

View File

@ -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(

View File

@ -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."""

View File

@ -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,
),

View File

@ -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(

View File

@ -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."""

View File

@ -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

View File

@ -56,6 +56,7 @@
"enableCrashReporting": true,
"disableAudio": false,
"analyticsData": "anonymous",
"isInsightsEnabled": true,
"anonymousDeviceId": "65257f7d-874c-498a-8f1b-00b2dd0a7ae1",
"cameraUtilization": 30,
"isRecycling": false,

View File

@ -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