core/homeassistant/components/unifiprotect/select.py

426 lines
14 KiB
Python

"""Component providing select entities for UniFi Protect."""
from __future__ import annotations
from collections.abc import Callable, Sequence
from dataclasses import dataclass
from enum import Enum
import logging
from typing import Any, Final
from uiprotect.api import ProtectApiClient
from uiprotect.data import (
Camera,
ChimeType,
DoorbellMessageType,
Doorlock,
IRLEDMode,
Light,
LightModeEnableType,
LightModeType,
ModelType,
MountType,
ProtectAdoptableDeviceModel,
RecordingMode,
Sensor,
Viewer,
)
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import TYPE_EMPTY_VALUE
from .data import ProtectData, ProtectDeviceType, UFPConfigEntry
from .entity import (
PermRequired,
ProtectDeviceEntity,
ProtectEntityDescription,
ProtectSetableKeysMixin,
T,
async_all_device_entities,
)
from .utils import async_get_light_motion_current
_LOGGER = logging.getLogger(__name__)
_KEY_LIGHT_MOTION = "light_motion"
HDR_MODES = [
{"id": "always", "name": "Always On"},
{"id": "off", "name": "Always Off"},
{"id": "auto", "name": "Auto"},
]
INFRARED_MODES = [
{"id": IRLEDMode.AUTO.value, "name": "Auto"},
{"id": IRLEDMode.ON.value, "name": "Always Enable"},
{"id": IRLEDMode.AUTO_NO_LED.value, "name": "Auto (Filter Only, no LED's)"},
{"id": IRLEDMode.CUSTOM.value, "name": "Auto (Custom Lux)"},
{"id": IRLEDMode.OFF.value, "name": "Always Disable"},
]
CHIME_TYPES = [
{"id": ChimeType.NONE.value, "name": "None"},
{"id": ChimeType.MECHANICAL.value, "name": "Mechanical"},
{"id": ChimeType.DIGITAL.value, "name": "Digital"},
]
MOUNT_TYPES = [
{"id": MountType.NONE.value, "name": "None"},
{"id": MountType.DOOR.value, "name": "Door"},
{"id": MountType.WINDOW.value, "name": "Window"},
{"id": MountType.GARAGE.value, "name": "Garage"},
{"id": MountType.LEAK.value, "name": "Leak"},
]
LIGHT_MODE_MOTION = "On Motion - Always"
LIGHT_MODE_MOTION_DARK = "On Motion - When Dark"
LIGHT_MODE_DARK = "When Dark"
LIGHT_MODE_OFF = "Manual"
LIGHT_MODES = [LIGHT_MODE_MOTION, LIGHT_MODE_DARK, LIGHT_MODE_OFF]
LIGHT_MODE_TO_SETTINGS = {
LIGHT_MODE_MOTION: (LightModeType.MOTION.value, LightModeEnableType.ALWAYS.value),
LIGHT_MODE_MOTION_DARK: (
LightModeType.MOTION.value,
LightModeEnableType.DARK.value,
),
LIGHT_MODE_DARK: (LightModeType.WHEN_DARK.value, LightModeEnableType.DARK.value),
LIGHT_MODE_OFF: (LightModeType.MANUAL.value, None),
}
MOTION_MODE_TO_LIGHT_MODE = [
{"id": LightModeType.MOTION.value, "name": LIGHT_MODE_MOTION},
{"id": f"{LightModeType.MOTION.value}Dark", "name": LIGHT_MODE_MOTION_DARK},
{"id": LightModeType.WHEN_DARK.value, "name": LIGHT_MODE_DARK},
{"id": LightModeType.MANUAL.value, "name": LIGHT_MODE_OFF},
]
DEVICE_RECORDING_MODES = [
{"id": mode.value, "name": mode.value.title()} for mode in list(RecordingMode)
]
DEVICE_CLASS_LCD_MESSAGE: Final = "unifiprotect__lcd_message"
@dataclass(frozen=True, kw_only=True)
class ProtectSelectEntityDescription(
ProtectSetableKeysMixin[T], SelectEntityDescription
):
"""Describes UniFi Protect Select entity."""
ufp_options: list[dict[str, Any]] | None = None
ufp_options_fn: Callable[[ProtectApiClient], list[dict[str, Any]]] | None = None
ufp_enum_type: type[Enum] | None = None
def _get_viewer_options(api: ProtectApiClient) -> list[dict[str, Any]]:
return [
{"id": item.id, "name": item.name} for item in api.bootstrap.liveviews.values()
]
def _get_doorbell_options(api: ProtectApiClient) -> list[dict[str, Any]]:
default_message = api.bootstrap.nvr.doorbell_settings.default_message_text
messages = api.bootstrap.nvr.doorbell_settings.all_messages
built_messages: list[dict[str, str]] = []
for item in messages:
msg_type = item.type.value
if item.type is DoorbellMessageType.CUSTOM_MESSAGE:
msg_type = f"{DoorbellMessageType.CUSTOM_MESSAGE.value}:{item.text}"
built_messages.append({"id": msg_type, "name": item.text})
return [
{"id": "", "name": f"Default Message ({default_message})"},
*built_messages,
]
def _get_paired_camera_options(api: ProtectApiClient) -> list[dict[str, Any]]:
options = [{"id": TYPE_EMPTY_VALUE, "name": "Not Paired"}]
options.extend(
{"id": camera.id, "name": camera.display_name or camera.type}
for camera in api.bootstrap.cameras.values()
)
return options
def _get_viewer_current(obj: Viewer) -> str:
return obj.liveview_id
def _get_doorbell_current(obj: Camera) -> str | None:
if obj.lcd_message is None:
return None
return obj.lcd_message.text
async def _set_light_mode(obj: Light, mode: str) -> None:
lightmode, timing = LIGHT_MODE_TO_SETTINGS[mode]
await obj.set_light_settings(
LightModeType(lightmode),
enable_at=None if timing is None else LightModeEnableType(timing),
)
async def _set_paired_camera(obj: Light | Sensor | Doorlock, camera_id: str) -> None:
if camera_id == TYPE_EMPTY_VALUE:
camera: Camera | None = None
else:
camera = obj.api.bootstrap.cameras.get(camera_id)
await obj.set_paired_camera(camera)
async def _set_doorbell_message(obj: Camera, message: str) -> None:
if message.startswith(DoorbellMessageType.CUSTOM_MESSAGE.value):
message = message.split(":")[-1]
await obj.set_lcd_text(DoorbellMessageType.CUSTOM_MESSAGE, text=message)
elif message == TYPE_EMPTY_VALUE:
await obj.set_lcd_text(None)
else:
await obj.set_lcd_text(DoorbellMessageType(message))
async def _set_liveview(obj: Viewer, liveview_id: str) -> None:
liveview = obj.api.bootstrap.liveviews[liveview_id]
await obj.set_liveview(liveview)
CAMERA_SELECTS: tuple[ProtectSelectEntityDescription, ...] = (
ProtectSelectEntityDescription(
key="recording_mode",
name="Recording mode",
icon="mdi:video-outline",
entity_category=EntityCategory.CONFIG,
ufp_options=DEVICE_RECORDING_MODES,
ufp_enum_type=RecordingMode,
ufp_value="recording_settings.mode",
ufp_set_method="set_recording_mode",
ufp_perm=PermRequired.WRITE,
),
ProtectSelectEntityDescription(
key="infrared",
name="Infrared mode",
icon="mdi:circle-opacity",
entity_category=EntityCategory.CONFIG,
ufp_required_field="feature_flags.has_led_ir",
ufp_options=INFRARED_MODES,
ufp_enum_type=IRLEDMode,
ufp_value="isp_settings.ir_led_mode",
ufp_set_method="set_ir_led_model",
ufp_perm=PermRequired.WRITE,
),
ProtectSelectEntityDescription[Camera](
key="doorbell_text",
name="Doorbell text",
icon="mdi:card-text",
entity_category=EntityCategory.CONFIG,
device_class=DEVICE_CLASS_LCD_MESSAGE,
ufp_required_field="feature_flags.has_lcd_screen",
ufp_value_fn=_get_doorbell_current,
ufp_options_fn=_get_doorbell_options,
ufp_set_method_fn=_set_doorbell_message,
ufp_perm=PermRequired.WRITE,
),
ProtectSelectEntityDescription(
key="chime_type",
name="Chime type",
icon="mdi:bell",
entity_category=EntityCategory.CONFIG,
ufp_required_field="feature_flags.has_chime",
ufp_options=CHIME_TYPES,
ufp_enum_type=ChimeType,
ufp_value="chime_type",
ufp_set_method="set_chime_type",
ufp_perm=PermRequired.WRITE,
),
ProtectSelectEntityDescription(
key="hdr_mode",
name="HDR mode",
icon="mdi:brightness-7",
entity_category=EntityCategory.CONFIG,
ufp_required_field="feature_flags.has_hdr",
ufp_options=HDR_MODES,
ufp_value="hdr_mode_display",
ufp_set_method="set_hdr_mode",
ufp_perm=PermRequired.WRITE,
),
)
LIGHT_SELECTS: tuple[ProtectSelectEntityDescription, ...] = (
ProtectSelectEntityDescription[Light](
key=_KEY_LIGHT_MOTION,
name="Light mode",
icon="mdi:spotlight",
entity_category=EntityCategory.CONFIG,
ufp_options=MOTION_MODE_TO_LIGHT_MODE,
ufp_value_fn=async_get_light_motion_current,
ufp_set_method_fn=_set_light_mode,
ufp_perm=PermRequired.WRITE,
),
ProtectSelectEntityDescription[Light](
key="paired_camera",
name="Paired camera",
icon="mdi:cctv",
entity_category=EntityCategory.CONFIG,
ufp_value="camera_id",
ufp_options_fn=_get_paired_camera_options,
ufp_set_method_fn=_set_paired_camera,
ufp_perm=PermRequired.WRITE,
),
)
SENSE_SELECTS: tuple[ProtectSelectEntityDescription, ...] = (
ProtectSelectEntityDescription(
key="mount_type",
name="Mount type",
icon="mdi:screwdriver",
entity_category=EntityCategory.CONFIG,
ufp_options=MOUNT_TYPES,
ufp_enum_type=MountType,
ufp_value="mount_type",
ufp_set_method="set_mount_type",
ufp_perm=PermRequired.WRITE,
),
ProtectSelectEntityDescription[Sensor](
key="paired_camera",
name="Paired camera",
icon="mdi:cctv",
entity_category=EntityCategory.CONFIG,
ufp_value="camera_id",
ufp_options_fn=_get_paired_camera_options,
ufp_set_method_fn=_set_paired_camera,
ufp_perm=PermRequired.WRITE,
),
)
DOORLOCK_SELECTS: tuple[ProtectSelectEntityDescription, ...] = (
ProtectSelectEntityDescription[Doorlock](
key="paired_camera",
name="Paired camera",
icon="mdi:cctv",
entity_category=EntityCategory.CONFIG,
ufp_value="camera_id",
ufp_options_fn=_get_paired_camera_options,
ufp_set_method_fn=_set_paired_camera,
ufp_perm=PermRequired.WRITE,
),
)
VIEWER_SELECTS: tuple[ProtectSelectEntityDescription, ...] = (
ProtectSelectEntityDescription[Viewer](
key="viewer",
name="Liveview",
icon="mdi:view-dashboard",
entity_category=None,
ufp_options_fn=_get_viewer_options,
ufp_value_fn=_get_viewer_current,
ufp_set_method_fn=_set_liveview,
ufp_perm=PermRequired.WRITE,
),
)
_MODEL_DESCRIPTIONS: dict[ModelType, Sequence[ProtectEntityDescription]] = {
ModelType.CAMERA: CAMERA_SELECTS,
ModelType.LIGHT: LIGHT_SELECTS,
ModelType.SENSOR: SENSE_SELECTS,
ModelType.VIEWPORT: VIEWER_SELECTS,
ModelType.DOORLOCK: DOORLOCK_SELECTS,
}
async def async_setup_entry(
hass: HomeAssistant, entry: UFPConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up number entities for UniFi Protect integration."""
data = entry.runtime_data
@callback
def _add_new_device(device: ProtectAdoptableDeviceModel) -> None:
async_add_entities(
async_all_device_entities(
data,
ProtectSelects,
model_descriptions=_MODEL_DESCRIPTIONS,
ufp_device=device,
)
)
data.async_subscribe_adopt(_add_new_device)
async_add_entities(
async_all_device_entities(
data, ProtectSelects, model_descriptions=_MODEL_DESCRIPTIONS
)
)
class ProtectSelects(ProtectDeviceEntity, SelectEntity):
"""A UniFi Protect Select Entity."""
device: Camera | Light | Viewer
entity_description: ProtectSelectEntityDescription
_state_attrs = ("_attr_available", "_attr_options", "_attr_current_option")
def __init__(
self,
data: ProtectData,
device: Camera | Light | Viewer,
description: ProtectSelectEntityDescription,
) -> None:
"""Initialize the unifi protect select entity."""
self._async_set_options(data, description)
super().__init__(data, device, description)
@callback
def _async_update_device_from_protect(self, device: ProtectDeviceType) -> None:
super()._async_update_device_from_protect(device)
entity_description = self.entity_description
# entities with categories are not exposed for voice
# and safe to update dynamically
if (
entity_description.entity_category is not None
and entity_description.ufp_options_fn is not None
):
_LOGGER.debug(
"Updating dynamic select options for %s", entity_description.name
)
self._async_set_options(self.data, entity_description)
if (unifi_value := entity_description.get_ufp_value(device)) is None:
unifi_value = TYPE_EMPTY_VALUE
self._attr_current_option = self._unifi_to_hass_options.get(
unifi_value, unifi_value
)
@callback
def _async_set_options(
self, data: ProtectData, description: ProtectSelectEntityDescription
) -> None:
"""Set options attributes from UniFi Protect device."""
if (ufp_options := description.ufp_options) is not None:
options = ufp_options
else:
assert description.ufp_options_fn is not None
options = description.ufp_options_fn(data.api)
self._attr_options = [item["name"] for item in options]
self._hass_to_unifi_options = {item["name"]: item["id"] for item in options}
self._unifi_to_hass_options = {item["id"]: item["name"] for item in options}
async def async_select_option(self, option: str) -> None:
"""Change the Select Entity Option."""
# Light Motion is a bit different
if self.entity_description.key == _KEY_LIGHT_MOTION:
assert self.entity_description.ufp_set_method_fn is not None
await self.entity_description.ufp_set_method_fn(self.device, option)
return
unifi_value = self._hass_to_unifi_options[option]
if self.entity_description.ufp_enum_type is not None:
unifi_value = self.entity_description.ufp_enum_type(unifi_value)
await self.entity_description.ufp_set(self.device, unifi_value)