ha-core/homeassistant/components/unifiprotect/switch.py

556 lines
18 KiB
Python

"""Component providing Switches for UniFi Protect."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from pyunifiprotect.data import (
NVR,
Camera,
ProtectAdoptableDeviceModel,
ProtectModelWithId,
RecordingMode,
VideoMode,
)
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
from .const import DISPATCH_ADOPT, DOMAIN
from .data import ProtectData
from .entity import ProtectDeviceEntity, ProtectNVREntity, async_all_device_entities
from .models import PermRequired, ProtectSetableKeysMixin, T
from .utils import async_dispatch_id as _ufpd
ATTR_PREV_MIC = "prev_mic_level"
ATTR_PREV_RECORD = "prev_record_mode"
@dataclass
class ProtectSwitchEntityDescription(
ProtectSetableKeysMixin[T], SwitchEntityDescription
):
"""Describes UniFi Protect Switch entity."""
async def _set_highfps(obj: Camera, value: bool) -> None:
if value:
await obj.set_video_mode(VideoMode.HIGH_FPS)
else:
await obj.set_video_mode(VideoMode.DEFAULT)
CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
ProtectSwitchEntityDescription(
key="ssh",
name="SSH Enabled",
icon="mdi:lock",
entity_registry_enabled_default=False,
entity_category=EntityCategory.CONFIG,
ufp_value="is_ssh_enabled",
ufp_set_method="set_ssh",
ufp_perm=PermRequired.WRITE,
),
ProtectSwitchEntityDescription(
key="status_light",
name="Status Light On",
icon="mdi:led-on",
entity_category=EntityCategory.CONFIG,
ufp_required_field="feature_flags.has_led_status",
ufp_value="led_settings.is_enabled",
ufp_set_method="set_status_light",
ufp_perm=PermRequired.WRITE,
),
ProtectSwitchEntityDescription(
key="hdr_mode",
name="HDR Mode",
icon="mdi:brightness-7",
entity_category=EntityCategory.CONFIG,
ufp_required_field="feature_flags.has_hdr",
ufp_value="hdr_mode",
ufp_set_method="set_hdr",
ufp_perm=PermRequired.WRITE,
),
ProtectSwitchEntityDescription[Camera](
key="high_fps",
name="High FPS",
icon="mdi:video-high-definition",
entity_category=EntityCategory.CONFIG,
ufp_required_field="feature_flags.has_highfps",
ufp_value="is_high_fps_enabled",
ufp_set_method_fn=_set_highfps,
ufp_perm=PermRequired.WRITE,
),
ProtectSwitchEntityDescription(
key="system_sounds",
name="System Sounds",
icon="mdi:speaker",
entity_category=EntityCategory.CONFIG,
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,
),
ProtectSwitchEntityDescription(
key="osd_name",
name="Overlay: Show Name",
icon="mdi:fullscreen",
entity_category=EntityCategory.CONFIG,
ufp_value="osd_settings.is_name_enabled",
ufp_set_method="set_osd_name",
ufp_perm=PermRequired.WRITE,
),
ProtectSwitchEntityDescription(
key="osd_date",
name="Overlay: Show Date",
icon="mdi:fullscreen",
entity_category=EntityCategory.CONFIG,
ufp_value="osd_settings.is_date_enabled",
ufp_set_method="set_osd_date",
ufp_perm=PermRequired.WRITE,
),
ProtectSwitchEntityDescription(
key="osd_logo",
name="Overlay: Show Logo",
icon="mdi:fullscreen",
entity_category=EntityCategory.CONFIG,
ufp_value="osd_settings.is_logo_enabled",
ufp_set_method="set_osd_logo",
ufp_perm=PermRequired.WRITE,
),
ProtectSwitchEntityDescription(
key="osd_bitrate",
name="Overlay: Show Nerd Mode",
icon="mdi:fullscreen",
entity_category=EntityCategory.CONFIG,
ufp_value="osd_settings.is_debug_enabled",
ufp_set_method="set_osd_bitrate",
ufp_perm=PermRequired.WRITE,
),
ProtectSwitchEntityDescription(
key="motion",
name="Detections: Motion",
icon="mdi:run-fast",
entity_category=EntityCategory.CONFIG,
ufp_value="recording_settings.enable_motion_detection",
ufp_enabled="is_recording_enabled",
ufp_set_method="set_motion_detection",
ufp_perm=PermRequired.WRITE,
),
ProtectSwitchEntityDescription(
key="smart_person",
name="Detections: Person",
icon="mdi:walk",
entity_category=EntityCategory.CONFIG,
ufp_required_field="can_detect_person",
ufp_value="is_person_detection_on",
ufp_enabled="is_recording_enabled",
ufp_set_method="set_person_detection",
ufp_perm=PermRequired.WRITE,
),
ProtectSwitchEntityDescription(
key="smart_vehicle",
name="Detections: Vehicle",
icon="mdi:car",
entity_category=EntityCategory.CONFIG,
ufp_required_field="can_detect_vehicle",
ufp_value="is_vehicle_detection_on",
ufp_enabled="is_recording_enabled",
ufp_set_method="set_vehicle_detection",
ufp_perm=PermRequired.WRITE,
),
ProtectSwitchEntityDescription(
key="smart_face",
name="Detections: Face",
icon="mdi:human-greeting",
entity_category=EntityCategory.CONFIG,
ufp_required_field="can_detect_face",
ufp_value="is_face_detection_on",
ufp_enabled="is_recording_enabled",
ufp_set_method="set_face_detection",
ufp_perm=PermRequired.WRITE,
),
ProtectSwitchEntityDescription(
key="smart_package",
name="Detections: Package",
icon="mdi:package-variant-closed",
entity_category=EntityCategory.CONFIG,
ufp_required_field="can_detect_package",
ufp_value="is_package_detection_on",
ufp_enabled="is_recording_enabled",
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_enabled="is_recording_enabled",
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_enabled="is_recording_enabled",
ufp_set_method="set_smoke_detection",
ufp_perm=PermRequired.WRITE,
),
)
PRIVACY_MODE_SWITCH = ProtectSwitchEntityDescription[Camera](
key="privacy_mode",
name="Privacy Mode",
icon="mdi:eye-settings",
entity_category=EntityCategory.CONFIG,
ufp_required_field="feature_flags.has_privacy_mask",
ufp_value="is_privacy_on",
ufp_perm=PermRequired.WRITE,
)
SENSE_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
ProtectSwitchEntityDescription(
key="status_light",
name="Status Light On",
icon="mdi:led-on",
entity_category=EntityCategory.CONFIG,
ufp_value="led_settings.is_enabled",
ufp_set_method="set_status_light",
ufp_perm=PermRequired.WRITE,
),
ProtectSwitchEntityDescription(
key="motion",
name="Motion Detection",
icon="mdi:walk",
entity_category=EntityCategory.CONFIG,
ufp_value="motion_settings.is_enabled",
ufp_set_method="set_motion_status",
ufp_perm=PermRequired.WRITE,
),
ProtectSwitchEntityDescription(
key="temperature",
name="Temperature Sensor",
icon="mdi:thermometer",
entity_category=EntityCategory.CONFIG,
ufp_value="temperature_settings.is_enabled",
ufp_set_method="set_temperature_status",
ufp_perm=PermRequired.WRITE,
),
ProtectSwitchEntityDescription(
key="humidity",
name="Humidity Sensor",
icon="mdi:water-percent",
entity_category=EntityCategory.CONFIG,
ufp_value="humidity_settings.is_enabled",
ufp_set_method="set_humidity_status",
ufp_perm=PermRequired.WRITE,
),
ProtectSwitchEntityDescription(
key="light",
name="Light Sensor",
icon="mdi:brightness-5",
entity_category=EntityCategory.CONFIG,
ufp_value="light_settings.is_enabled",
ufp_set_method="set_light_status",
ufp_perm=PermRequired.WRITE,
),
ProtectSwitchEntityDescription(
key="alarm",
name="Alarm Sound Detection",
entity_category=EntityCategory.CONFIG,
ufp_value="alarm_settings.is_enabled",
ufp_set_method="set_alarm_status",
ufp_perm=PermRequired.WRITE,
),
)
LIGHT_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
ProtectSwitchEntityDescription(
key="ssh",
name="SSH Enabled",
icon="mdi:lock",
entity_registry_enabled_default=False,
entity_category=EntityCategory.CONFIG,
ufp_value="is_ssh_enabled",
ufp_set_method="set_ssh",
ufp_perm=PermRequired.WRITE,
),
ProtectSwitchEntityDescription(
key="status_light",
name="Status Light On",
icon="mdi:led-on",
entity_category=EntityCategory.CONFIG,
ufp_value="light_device_settings.is_indicator_enabled",
ufp_set_method="set_status_light",
ufp_perm=PermRequired.WRITE,
),
)
DOORLOCK_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
ProtectSwitchEntityDescription(
key="status_light",
name="Status Light On",
icon="mdi:led-on",
entity_category=EntityCategory.CONFIG,
ufp_value="led_settings.is_enabled",
ufp_set_method="set_status_light",
ufp_perm=PermRequired.WRITE,
),
)
VIEWER_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
ProtectSwitchEntityDescription(
key="ssh",
name="SSH Enabled",
icon="mdi:lock",
entity_registry_enabled_default=False,
entity_category=EntityCategory.CONFIG,
ufp_value="is_ssh_enabled",
ufp_set_method="set_ssh",
ufp_perm=PermRequired.WRITE,
),
)
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,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up sensors for UniFi Protect integration."""
data: ProtectData = hass.data[DOMAIN][entry.entry_id]
async def _add_new_device(device: ProtectAdoptableDeviceModel) -> None:
entities = async_all_device_entities(
data,
ProtectSwitch,
camera_descs=CAMERA_SWITCHES,
light_descs=LIGHT_SWITCHES,
sense_descs=SENSE_SWITCHES,
lock_descs=DOORLOCK_SWITCHES,
viewer_descs=VIEWER_SWITCHES,
ufp_device=device,
)
entities += async_all_device_entities(
data,
ProtectPrivacyModeSwitch,
camera_descs=[PRIVACY_MODE_SWITCH],
ufp_device=device,
)
async_add_entities(entities)
entry.async_on_unload(
async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device)
)
entities: list[ProtectDeviceEntity] = async_all_device_entities(
data,
ProtectSwitch,
camera_descs=CAMERA_SWITCHES,
light_descs=LIGHT_SWITCHES,
sense_descs=SENSE_SWITCHES,
lock_descs=DOORLOCK_SWITCHES,
viewer_descs=VIEWER_SWITCHES,
)
entities += async_all_device_entities(
data,
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)
class ProtectSwitch(ProtectDeviceEntity, SwitchEntity):
"""A UniFi Protect Switch."""
entity_description: ProtectSwitchEntityDescription
def __init__(
self,
data: ProtectData,
device: ProtectAdoptableDeviceModel,
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}"
self._switch_type = self.entity_description.key
def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
super()._async_update_device_from_protect(device)
self._attr_is_on = 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)
@callback
def _async_updated_event(self, device: ProtectModelWithId) -> None:
"""Call back for incoming data that only writes when state has changed.
Only the is_on and available are ever updated for these
entities, and since the websocket update for the device will trigger
an update for all entities connected to the device, we want to avoid
writing state unless something has actually changed.
"""
previous_is_on = self._attr_is_on
previous_available = self._attr_available
self._async_update_device_from_protect(device)
if (
self._attr_is_on != previous_is_on
or self._attr_available != previous_available
):
self.async_write_ha_state()
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."""
device: Camera
def __init__(
self,
data: ProtectData,
device: ProtectAdoptableDeviceModel,
description: ProtectSwitchEntityDescription,
) -> None:
"""Initialize an UniFi Protect Switch."""
super().__init__(data, device, description)
if self.device.is_privacy_on:
extra_state = self.extra_state_attributes or {}
self._previous_mic_level = extra_state.get(ATTR_PREV_MIC, 100)
self._previous_record_mode = extra_state.get(
ATTR_PREV_RECORD, RecordingMode.ALWAYS
)
else:
self._previous_mic_level = self.device.mic_volume
self._previous_record_mode = self.device.recording_settings.mode
@callback
def _update_previous_attr(self) -> None:
if self.is_on:
self._attr_extra_state_attributes = {
ATTR_PREV_MIC: self._previous_mic_level,
ATTR_PREV_RECORD: self._previous_record_mode,
}
else:
self._attr_extra_state_attributes = {}
@callback
def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
super()._async_update_device_from_protect(device)
# do not add extra state attribute on initialize
if self.entity_id:
self._update_previous_attr()
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the device on."""
self._previous_mic_level = self.device.mic_volume
self._previous_record_mode = self.device.recording_settings.mode
await self.device.set_privacy(True, 0, RecordingMode.NEVER)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the device off."""
extra_state = self.extra_state_attributes or {}
prev_mic = extra_state.get(ATTR_PREV_MIC, self._previous_mic_level)
prev_record = extra_state.get(ATTR_PREV_RECORD, self._previous_record_mode)
await self.device.set_privacy(False, prev_mic, prev_record)
async def async_added_to_hass(self) -> None:
"""Restore extra state attributes on startp up."""
await super().async_added_to_hass()
if not (last_state := await self.async_get_last_state()):
return
self._previous_mic_level = last_state.attributes.get(
ATTR_PREV_MIC, self._previous_mic_level
)
self._previous_record_mode = last_state.attributes.get(
ATTR_PREV_RECORD, self._previous_record_mode
)
self._update_previous_attr()