"""Support for Ubiquiti's UniFi Protect NVR.""" from __future__ import annotations from dataclasses import dataclass import logging from typing import Final from pyunifiprotect.data import ProtectAdoptableDeviceModel, ProtectModelWithId from homeassistant.components.button import ( ButtonDeviceClass, ButtonEntity, ButtonEntityDescription, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DEVICES_THAT_ADOPT, DISPATCH_ADD, 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 _LOGGER = logging.getLogger(__name__) @dataclass class ProtectButtonEntityDescription( ProtectSetableKeysMixin[T], ButtonEntityDescription ): """Describes UniFi Protect Button entity.""" ufp_press: str | None = None DEVICE_CLASS_CHIME_BUTTON: Final = "unifiprotect__chime_button" KEY_ADOPT = "adopt" ALL_DEVICE_BUTTONS: tuple[ProtectButtonEntityDescription, ...] = ( ProtectButtonEntityDescription( key="reboot", entity_registry_enabled_default=False, device_class=ButtonDeviceClass.RESTART, name="Reboot Device", ufp_press="reboot", ufp_perm=PermRequired.WRITE, ), ProtectButtonEntityDescription( key="unadopt", entity_registry_enabled_default=False, name="Unadopt Device", icon="mdi:delete", ufp_press="unadopt", ufp_perm=PermRequired.DELETE, ), ) ADOPT_BUTTON = ProtectButtonEntityDescription[ProtectAdoptableDeviceModel]( key=KEY_ADOPT, name="Adopt Device", icon="mdi:plus-circle", ufp_press="adopt", ) SENSOR_BUTTONS: tuple[ProtectButtonEntityDescription, ...] = ( ProtectButtonEntityDescription( key="clear_tamper", name="Clear Tamper", icon="mdi:notification-clear-all", ufp_press="clear_tamper", ufp_perm=PermRequired.WRITE, ), ) CHIME_BUTTONS: tuple[ProtectButtonEntityDescription, ...] = ( ProtectButtonEntityDescription( key="play", name="Play Chime", device_class=DEVICE_CLASS_CHIME_BUTTON, icon="mdi:play", ufp_press="play", ), ProtectButtonEntityDescription( key="play_buzzer", name="Play Buzzer", icon="mdi:play", ufp_press="play_buzzer", ), ) @callback def _async_remove_adopt_button( hass: HomeAssistant, device: ProtectAdoptableDeviceModel ) -> None: entity_registry = er.async_get(hass) if entity_id := entity_registry.async_get_entity_id( Platform.BUTTON, DOMAIN, f"{device.mac}_adopt" ): entity_registry.async_remove(entity_id) async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Discover devices on a UniFi Protect NVR.""" data: ProtectData = hass.data[DOMAIN][entry.entry_id] async def _add_new_device(device: ProtectAdoptableDeviceModel) -> None: entities = async_all_device_entities( data, ProtectButton, all_descs=ALL_DEVICE_BUTTONS, unadopted_descs=[ADOPT_BUTTON], chime_descs=CHIME_BUTTONS, sense_descs=SENSOR_BUTTONS, ufp_device=device, ) async_add_entities(entities) _async_remove_adopt_button(hass, device) @callback def _async_add_unadopted_device(device: ProtectAdoptableDeviceModel) -> None: if not device.can_adopt or not device.can_create(data.api.bootstrap.auth_user): _LOGGER.debug("Device is not adoptable: %s", device.id) return entities = async_all_device_entities( data, ProtectButton, unadopted_descs=[ADOPT_BUTTON], ufp_device=device, ) async_add_entities(entities) entry.async_on_unload( async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) ) entry.async_on_unload( async_dispatcher_connect( hass, _ufpd(entry, DISPATCH_ADD), _async_add_unadopted_device ) ) entities: list[ProtectDeviceEntity] = async_all_device_entities( data, ProtectButton, all_descs=ALL_DEVICE_BUTTONS, unadopted_descs=[ADOPT_BUTTON], chime_descs=CHIME_BUTTONS, sense_descs=SENSOR_BUTTONS, ) async_add_entities(entities) for device in data.get_by_types(DEVICES_THAT_ADOPT): _async_remove_adopt_button(hass, device) class ProtectButton(ProtectDeviceEntity, ButtonEntity): """A Ubiquiti UniFi Protect Reboot button.""" entity_description: ProtectButtonEntityDescription def __init__( self, data: ProtectData, device: ProtectAdoptableDeviceModel, description: ProtectButtonEntityDescription, ) -> None: """Initialize an UniFi camera.""" super().__init__(data, device, description) self._attr_name = f"{self.device.display_name} {self.entity_description.name}" @callback def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: super()._async_update_device_from_protect(device) if self.entity_description.key == KEY_ADOPT: device = self.device self._attr_available = device.can_adopt and device.can_create( self.data.api.bootstrap.auth_user ) async def async_press(self) -> None: """Press the button.""" if self.entity_description.ufp_press is not None: await getattr(self.device, self.entity_description.ufp_press)() @callback def _async_updated_event(self, device: ProtectModelWithId) -> None: """Call back for incoming data that only writes when state has changed. Only available is 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_available = self._attr_available self._async_update_device_from_protect(device) if self._attr_available != previous_available: self.async_write_ha_state()