mirror of
https://github.com/home-assistant/core.git
synced 2025-04-29 11:47:50 +00:00
Add new features from UniFi Protect 2.2.1-beta5 (#77391)
This commit is contained in:
parent
441d7c0461
commit
d29be2390b
@ -114,8 +114,9 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = (
|
|||||||
name="System Sounds",
|
name="System Sounds",
|
||||||
icon="mdi:speaker",
|
icon="mdi:speaker",
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
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_value="speaker_settings.are_system_sounds_enabled",
|
||||||
|
ufp_enabled="feature_flags.has_speaker",
|
||||||
ufp_perm=PermRequired.NO_WRITE,
|
ufp_perm=PermRequired.NO_WRITE,
|
||||||
),
|
),
|
||||||
ProtectBinaryEntityDescription(
|
ProtectBinaryEntityDescription(
|
||||||
|
@ -9,6 +9,7 @@ from pyunifiprotect.data import (
|
|||||||
ModelType,
|
ModelType,
|
||||||
ProtectAdoptableDeviceModel,
|
ProtectAdoptableDeviceModel,
|
||||||
ProtectModelWithId,
|
ProtectModelWithId,
|
||||||
|
StateType,
|
||||||
)
|
)
|
||||||
from pyunifiprotect.exceptions import StreamError
|
from pyunifiprotect.exceptions import StreamError
|
||||||
|
|
||||||
@ -48,7 +49,9 @@ async def async_setup_entry(
|
|||||||
data: ProtectData = hass.data[DOMAIN][entry.entry_id]
|
data: ProtectData = hass.data[DOMAIN][entry.entry_id]
|
||||||
|
|
||||||
async def _add_new_device(device: ProtectAdoptableDeviceModel) -> None:
|
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)])
|
async_add_entities([ProtectMediaPlayer(data, device)])
|
||||||
|
|
||||||
entry.async_on_unload(
|
entry.async_on_unload(
|
||||||
@ -58,7 +61,7 @@ async def async_setup_entry(
|
|||||||
entities = []
|
entities = []
|
||||||
for device in data.get_by_types({ModelType.CAMERA}):
|
for device in data.get_by_types({ModelType.CAMERA}):
|
||||||
device = cast(Camera, device)
|
device = cast(Camera, device)
|
||||||
if device.feature_flags.has_speaker:
|
if device.has_speaker or device.has_removable_speaker:
|
||||||
entities.append(ProtectMediaPlayer(data, device))
|
entities.append(ProtectMediaPlayer(data, device))
|
||||||
|
|
||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
@ -107,6 +110,12 @@ class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity):
|
|||||||
else:
|
else:
|
||||||
self._attr_state = STATE_IDLE
|
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:
|
async def async_set_volume_level(self, volume: float) -> None:
|
||||||
"""Set volume level, range 0..1."""
|
"""Set volume level, range 0..1."""
|
||||||
|
|
||||||
|
@ -82,8 +82,9 @@ CAMERA_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = (
|
|||||||
ufp_min=0,
|
ufp_min=0,
|
||||||
ufp_max=100,
|
ufp_max=100,
|
||||||
ufp_step=1,
|
ufp_step=1,
|
||||||
ufp_required_field="feature_flags.has_mic",
|
ufp_required_field="has_mic",
|
||||||
ufp_value="mic_volume",
|
ufp_value="mic_volume",
|
||||||
|
ufp_enabled="feature_flags.has_mic",
|
||||||
ufp_set_method="set_mic_volume",
|
ufp_set_method="set_mic_volume",
|
||||||
ufp_perm=PermRequired.WRITE,
|
ufp_perm=PermRequired.WRITE,
|
||||||
),
|
),
|
||||||
|
@ -215,8 +215,9 @@ CAMERA_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
|
|||||||
icon="mdi:microphone",
|
icon="mdi:microphone",
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
ufp_required_field="feature_flags.has_mic",
|
ufp_required_field="has_mic",
|
||||||
ufp_value="mic_volume",
|
ufp_value="mic_volume",
|
||||||
|
ufp_enabled="feature_flags.has_mic",
|
||||||
ufp_perm=PermRequired.NO_WRITE,
|
ufp_perm=PermRequired.NO_WRITE,
|
||||||
),
|
),
|
||||||
ProtectSensorEntityDescription(
|
ProtectSensorEntityDescription(
|
||||||
|
@ -5,6 +5,7 @@ from dataclasses import dataclass
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from pyunifiprotect.data import (
|
from pyunifiprotect.data import (
|
||||||
|
NVR,
|
||||||
Camera,
|
Camera,
|
||||||
ProtectAdoptableDeviceModel,
|
ProtectAdoptableDeviceModel,
|
||||||
ProtectModelWithId,
|
ProtectModelWithId,
|
||||||
@ -22,7 +23,7 @@ from homeassistant.helpers.restore_state import RestoreEntity
|
|||||||
|
|
||||||
from .const import DISPATCH_ADOPT, DOMAIN
|
from .const import DISPATCH_ADOPT, DOMAIN
|
||||||
from .data import ProtectData
|
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 .models import PermRequired, ProtectSetableKeysMixin, T
|
||||||
from .utils import async_dispatch_id as _ufpd
|
from .utils import async_dispatch_id as _ufpd
|
||||||
|
|
||||||
@ -90,8 +91,9 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
|
|||||||
name="System Sounds",
|
name="System Sounds",
|
||||||
icon="mdi:speaker",
|
icon="mdi:speaker",
|
||||||
entity_category=EntityCategory.CONFIG,
|
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_value="speaker_settings.are_system_sounds_enabled",
|
||||||
|
ufp_enabled="feature_flags.has_speaker",
|
||||||
ufp_set_method="set_system_sounds",
|
ufp_set_method="set_system_sounds",
|
||||||
ufp_perm=PermRequired.WRITE,
|
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(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@ -342,6 +363,17 @@ async def async_setup_entry(
|
|||||||
ProtectPrivacyModeSwitch,
|
ProtectPrivacyModeSwitch,
|
||||||
camera_descs=[PRIVACY_MODE_SWITCH],
|
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)
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
@ -377,6 +409,37 @@ class ProtectSwitch(ProtectDeviceEntity, SwitchEntity):
|
|||||||
await self.entity_description.ufp_set(self.device, False)
|
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):
|
class ProtectPrivacyModeSwitch(RestoreEntity, ProtectSwitch):
|
||||||
"""A UniFi Protect Switch."""
|
"""A UniFi Protect Switch."""
|
||||||
|
|
||||||
|
@ -209,6 +209,7 @@ def doorbell_fixture(camera: Camera, fixed_now: datetime):
|
|||||||
SmartDetectObjectType.PERSON,
|
SmartDetectObjectType.PERSON,
|
||||||
SmartDetectObjectType.VEHICLE,
|
SmartDetectObjectType.VEHICLE,
|
||||||
]
|
]
|
||||||
|
doorbell.has_speaker = True
|
||||||
doorbell.feature_flags.has_hdr = True
|
doorbell.feature_flags.has_hdr = True
|
||||||
doorbell.feature_flags.has_lcd_screen = True
|
doorbell.feature_flags.has_lcd_screen = True
|
||||||
doorbell.feature_flags.has_speaker = True
|
doorbell.feature_flags.has_speaker = True
|
||||||
|
@ -56,6 +56,7 @@
|
|||||||
"enableCrashReporting": true,
|
"enableCrashReporting": true,
|
||||||
"disableAudio": false,
|
"disableAudio": false,
|
||||||
"analyticsData": "anonymous",
|
"analyticsData": "anonymous",
|
||||||
|
"isInsightsEnabled": true,
|
||||||
"anonymousDeviceId": "65257f7d-874c-498a-8f1b-00b2dd0a7ae1",
|
"anonymousDeviceId": "65257f7d-874c-498a-8f1b-00b2dd0a7ae1",
|
||||||
"cameraUtilization": 30,
|
"cameraUtilization": 30,
|
||||||
"isRecycling": false,
|
"isRecycling": false,
|
||||||
|
@ -49,11 +49,11 @@ async def test_switch_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.SWITCH, 13, 12)
|
assert_entity_counts(hass, Platform.SWITCH, 15, 14)
|
||||||
await remove_entities(hass, ufp, [doorbell, unadopted_camera])
|
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])
|
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(
|
async def test_switch_light_remove(
|
||||||
@ -63,11 +63,36 @@ async def test_switch_light_remove(
|
|||||||
|
|
||||||
ufp.api.bootstrap.nvr.system_info.ustorage = None
|
ufp.api.bootstrap.nvr.system_info.ustorage = None
|
||||||
await init_entry(hass, ufp, [light])
|
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])
|
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])
|
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(
|
async def test_switch_setup_no_perm(
|
||||||
@ -95,7 +120,7 @@ async def test_switch_setup_light(
|
|||||||
"""Test switch entity setup for light devices."""
|
"""Test switch entity setup for light devices."""
|
||||||
|
|
||||||
await init_entry(hass, ufp, [light])
|
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)
|
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)."""
|
"""Test switch entity setup for camera devices (all enabled feature flags)."""
|
||||||
|
|
||||||
await init_entry(hass, ufp, [doorbell])
|
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)
|
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)."""
|
"""Test switch entity setup for camera devices (no enabled feature flags)."""
|
||||||
|
|
||||||
await init_entry(hass, ufp, [camera])
|
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)
|
entity_registry = er.async_get(hass)
|
||||||
|
|
||||||
@ -235,7 +260,7 @@ async def test_switch_light_status(
|
|||||||
"""Tests status light switch for lights."""
|
"""Tests status light switch for lights."""
|
||||||
|
|
||||||
await init_entry(hass, ufp, [light])
|
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]
|
description = LIGHT_SWITCHES[1]
|
||||||
|
|
||||||
@ -263,7 +288,7 @@ async def test_switch_camera_ssh(
|
|||||||
"""Tests SSH switch for cameras."""
|
"""Tests SSH switch for cameras."""
|
||||||
|
|
||||||
await init_entry(hass, ufp, [doorbell])
|
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]
|
description = CAMERA_SWITCHES[0]
|
||||||
|
|
||||||
@ -296,7 +321,7 @@ async def test_switch_camera_simple(
|
|||||||
"""Tests all simple switches for cameras."""
|
"""Tests all simple switches for cameras."""
|
||||||
|
|
||||||
await init_entry(hass, ufp, [doorbell])
|
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
|
assert description.ufp_set_method is not None
|
||||||
|
|
||||||
@ -325,7 +350,7 @@ async def test_switch_camera_highfps(
|
|||||||
"""Tests High FPS switch for cameras."""
|
"""Tests High FPS switch for cameras."""
|
||||||
|
|
||||||
await init_entry(hass, ufp, [doorbell])
|
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]
|
description = CAMERA_SWITCHES[3]
|
||||||
|
|
||||||
@ -356,7 +381,7 @@ async def test_switch_camera_privacy(
|
|||||||
previous_record = doorbell.recording_settings.mode = RecordingMode.DETECTIONS
|
previous_record = doorbell.recording_settings.mode = RecordingMode.DETECTIONS
|
||||||
|
|
||||||
await init_entry(hass, ufp, [doorbell])
|
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
|
description = PRIVACY_MODE_SWITCH
|
||||||
|
|
||||||
@ -408,7 +433,7 @@ async def test_switch_camera_privacy_already_on(
|
|||||||
|
|
||||||
doorbell.add_privacy_zone()
|
doorbell.add_privacy_zone()
|
||||||
await init_entry(hass, ufp, [doorbell])
|
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
|
description = PRIVACY_MODE_SWITCH
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user