core/homeassistant/components/unifiprotect/number.py

289 lines
9.0 KiB
Python

"""Component providing number entities for UniFi Protect."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import timedelta
from pyunifiprotect.data import (
Camera,
Doorlock,
Light,
ProtectAdoptableDeviceModel,
ProtectModelWithId,
)
from homeassistant.components.number import NumberEntity, NumberEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTime
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DISPATCH_ADOPT, DOMAIN
from .data import ProtectData
from .entity import ProtectDeviceEntity, async_all_device_entities
from .models import PermRequired, ProtectSetableKeysMixin, T
from .utils import async_dispatch_id as _ufpd
@dataclass
class NumberKeysMixin:
"""Mixin for required keys."""
ufp_max: int | float
ufp_min: int | float
ufp_step: int | float
@dataclass
class ProtectNumberEntityDescription(
ProtectSetableKeysMixin[T], NumberEntityDescription, NumberKeysMixin
):
"""Describes UniFi Protect Number entity."""
def _get_pir_duration(obj: Light) -> int:
return int(obj.light_device_settings.pir_duration.total_seconds())
async def _set_pir_duration(obj: Light, value: float) -> None:
await obj.set_duration(timedelta(seconds=value))
def _get_auto_close(obj: Doorlock) -> int:
return int(obj.auto_close_time.total_seconds())
async def _set_auto_close(obj: Doorlock, value: float) -> None:
await obj.set_auto_close_time(timedelta(seconds=value))
def _get_chime_duration(obj: Camera) -> int:
return int(obj.chime_duration.total_seconds())
CAMERA_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = (
ProtectNumberEntityDescription(
key="wdr_value",
name="Wide Dynamic Range",
icon="mdi:state-machine",
entity_category=EntityCategory.CONFIG,
ufp_min=0,
ufp_max=3,
ufp_step=1,
ufp_required_field="feature_flags.has_wdr",
ufp_value="isp_settings.wdr",
ufp_set_method="set_wdr_level",
ufp_perm=PermRequired.WRITE,
),
ProtectNumberEntityDescription(
key="mic_level",
name="Microphone Level",
icon="mdi:microphone",
entity_category=EntityCategory.CONFIG,
native_unit_of_measurement=PERCENTAGE,
ufp_min=0,
ufp_max=100,
ufp_step=1,
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,
),
ProtectNumberEntityDescription(
key="zoom_position",
name="Zoom Level",
icon="mdi:magnify-plus-outline",
entity_category=EntityCategory.CONFIG,
native_unit_of_measurement=PERCENTAGE,
ufp_min=0,
ufp_max=100,
ufp_step=1,
ufp_required_field="feature_flags.can_optical_zoom",
ufp_value="isp_settings.zoom_position",
ufp_set_method="set_camera_zoom",
ufp_perm=PermRequired.WRITE,
),
ProtectNumberEntityDescription(
key="chime_duration",
name="Chime Duration",
icon="mdi:bell",
entity_category=EntityCategory.CONFIG,
native_unit_of_measurement=UnitOfTime.SECONDS,
ufp_min=1,
ufp_max=10,
ufp_step=0.1,
ufp_required_field="feature_flags.has_chime",
ufp_enabled="is_digital_chime",
ufp_value_fn=_get_chime_duration,
ufp_set_method="set_chime_duration",
ufp_perm=PermRequired.WRITE,
),
)
LIGHT_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = (
ProtectNumberEntityDescription(
key="sensitivity",
name="Motion Sensitivity",
icon="mdi:walk",
entity_category=EntityCategory.CONFIG,
native_unit_of_measurement=PERCENTAGE,
ufp_min=0,
ufp_max=100,
ufp_step=1,
ufp_required_field=None,
ufp_value="light_device_settings.pir_sensitivity",
ufp_set_method="set_sensitivity",
ufp_perm=PermRequired.WRITE,
),
ProtectNumberEntityDescription[Light](
key="duration",
name="Auto-shutoff Duration",
icon="mdi:camera-timer",
entity_category=EntityCategory.CONFIG,
native_unit_of_measurement=UnitOfTime.SECONDS,
ufp_min=15,
ufp_max=900,
ufp_step=15,
ufp_required_field=None,
ufp_value_fn=_get_pir_duration,
ufp_set_method_fn=_set_pir_duration,
ufp_perm=PermRequired.WRITE,
),
)
SENSE_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = (
ProtectNumberEntityDescription(
key="sensitivity",
name="Motion Sensitivity",
icon="mdi:walk",
entity_category=EntityCategory.CONFIG,
native_unit_of_measurement=PERCENTAGE,
ufp_min=0,
ufp_max=100,
ufp_step=1,
ufp_required_field=None,
ufp_value="motion_settings.sensitivity",
ufp_set_method="set_motion_sensitivity",
ufp_perm=PermRequired.WRITE,
),
)
DOORLOCK_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = (
ProtectNumberEntityDescription[Doorlock](
key="auto_lock_time",
name="Auto-lock Timeout",
icon="mdi:walk",
entity_category=EntityCategory.CONFIG,
native_unit_of_measurement=UnitOfTime.SECONDS,
ufp_min=0,
ufp_max=3600,
ufp_step=15,
ufp_required_field=None,
ufp_value_fn=_get_auto_close,
ufp_set_method_fn=_set_auto_close,
ufp_perm=PermRequired.WRITE,
),
)
CHIME_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = (
ProtectNumberEntityDescription(
key="volume",
name="Volume",
icon="mdi:speaker",
entity_category=EntityCategory.CONFIG,
native_unit_of_measurement=PERCENTAGE,
ufp_min=0,
ufp_max=100,
ufp_step=1,
ufp_value="volume",
ufp_set_method="set_volume",
ufp_perm=PermRequired.WRITE,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up number entities 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,
ProtectNumbers,
camera_descs=CAMERA_NUMBERS,
light_descs=LIGHT_NUMBERS,
sense_descs=SENSE_NUMBERS,
lock_descs=DOORLOCK_NUMBERS,
chime_descs=CHIME_NUMBERS,
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,
ProtectNumbers,
camera_descs=CAMERA_NUMBERS,
light_descs=LIGHT_NUMBERS,
sense_descs=SENSE_NUMBERS,
lock_descs=DOORLOCK_NUMBERS,
chime_descs=CHIME_NUMBERS,
)
async_add_entities(entities)
class ProtectNumbers(ProtectDeviceEntity, NumberEntity):
"""A UniFi Protect Number Entity."""
device: Camera | Light
entity_description: ProtectNumberEntityDescription
def __init__(
self,
data: ProtectData,
device: Camera | Light,
description: ProtectNumberEntityDescription,
) -> None:
"""Initialize the Number Entities."""
super().__init__(data, device, description)
self._attr_native_max_value = self.entity_description.ufp_max
self._attr_native_min_value = self.entity_description.ufp_min
self._attr_native_step = self.entity_description.ufp_step
@callback
def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
super()._async_update_device_from_protect(device)
self._attr_native_value = self.entity_description.get_ufp_value(self.device)
async def async_set_native_value(self, value: float) -> None:
"""Set new value."""
await self.entity_description.ufp_set(self.device, value)
@callback
def _async_updated_event(self, device: ProtectModelWithId) -> None:
"""Call back for incoming data that only writes when state has changed.
Only the native value 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_value = self._attr_native_value
previous_available = self._attr_available
self._async_update_device_from_protect(device)
if (
self._attr_native_value != previous_value
or self._attr_available != previous_available
):
self.async_write_ha_state()