From ec47f7b6ffd299a541ebb68ec52849c8a52e4454 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Mon, 12 Dec 2022 02:30:24 -0500 Subject: [PATCH] Add text platform for UniFi Protect (#83674) --- .../components/unifiprotect/const.py | 1 + homeassistant/components/unifiprotect/text.py | 107 ++++++++++++++++++ tests/components/unifiprotect/test_text.py | 92 +++++++++++++++ 3 files changed, 200 insertions(+) create mode 100644 homeassistant/components/unifiprotect/text.py create mode 100644 tests/components/unifiprotect/test_text.py diff --git a/homeassistant/components/unifiprotect/const.py b/homeassistant/components/unifiprotect/const.py index df4d8f77d99..de9911e7d7b 100644 --- a/homeassistant/components/unifiprotect/const.py +++ b/homeassistant/components/unifiprotect/const.py @@ -63,6 +63,7 @@ PLATFORMS = [ Platform.SELECT, Platform.SENSOR, Platform.SWITCH, + Platform.TEXT, ] DISPATCH_ADD = "add_device" diff --git a/homeassistant/components/unifiprotect/text.py b/homeassistant/components/unifiprotect/text.py new file mode 100644 index 00000000000..49df4e3d907 --- /dev/null +++ b/homeassistant/components/unifiprotect/text.py @@ -0,0 +1,107 @@ +"""Text entities for UniFi Protect.""" +from __future__ import annotations + +from dataclasses import dataclass + +from pyunifiprotect.data import ( + Camera, + DoorbellMessageType, + ProtectAdoptableDeviceModel, + ProtectModelWithId, +) + +from homeassistant.components.text import TextEntity, TextEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import EntityCategory +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 ProtectTextEntityDescription(ProtectSetableKeysMixin[T], TextEntityDescription): + """Describes UniFi Protect Text entity.""" + + +def _get_doorbell_current(obj: Camera) -> str | None: + if obj.lcd_message is None: + return obj.api.bootstrap.nvr.doorbell_settings.default_message_text + return obj.lcd_message.text + + +async def _set_doorbell_message(obj: Camera, message: str) -> None: + await obj.set_lcd_text(DoorbellMessageType.CUSTOM_MESSAGE, text=message) + + +CAMERA: tuple[ProtectTextEntityDescription, ...] = ( + ProtectTextEntityDescription( + key="doorbell", + name="Doorbell", + entity_category=EntityCategory.CONFIG, + ufp_value_fn=_get_doorbell_current, + ufp_set_method_fn=_set_doorbell_message, + ufp_required_field="feature_flags.has_lcd_screen", + ufp_perm=PermRequired.WRITE, + ), +) + + +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, + ProtectDeviceText, + camera_descs=CAMERA, + 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, + ProtectDeviceText, + camera_descs=CAMERA, + ) + + async_add_entities(entities) + + +class ProtectDeviceText(ProtectDeviceEntity, TextEntity): + """A Ubiquiti UniFi Protect Sensor.""" + + entity_description: ProtectTextEntityDescription + + def __init__( + self, + data: ProtectData, + device: ProtectAdoptableDeviceModel, + description: ProtectTextEntityDescription, + ) -> None: + """Initialize an UniFi Protect sensor.""" + super().__init__(data, device, description) + + @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_value(self, value: str) -> None: + """Change the value.""" + + await self.entity_description.ufp_set(self.device, value) diff --git a/tests/components/unifiprotect/test_text.py b/tests/components/unifiprotect/test_text.py new file mode 100644 index 00000000000..17fe3ee7bc2 --- /dev/null +++ b/tests/components/unifiprotect/test_text.py @@ -0,0 +1,92 @@ +"""Test the UniFi Protect text platform.""" +# pylint: disable=protected-access +from __future__ import annotations + +from unittest.mock import AsyncMock, Mock + +from pyunifiprotect.data import Camera, DoorbellMessageType, LCDMessage + +from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION +from homeassistant.components.unifiprotect.text import CAMERA +from homeassistant.const import ATTR_ATTRIBUTION, ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from .utils import ( + MockUFPFixture, + adopt_devices, + assert_entity_counts, + ids_from_device_description, + init_entry, + remove_entities, +) + + +async def test_text_camera_remove( + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, unadopted_camera: Camera +): + """Test removing and re-adding a camera device.""" + + ufp.api.bootstrap.nvr.system_info.ustorage = None + await init_entry(hass, ufp, [doorbell, unadopted_camera]) + assert_entity_counts(hass, Platform.TEXT, 1, 1) + await remove_entities(hass, ufp, [doorbell, unadopted_camera]) + assert_entity_counts(hass, Platform.TEXT, 0, 0) + await adopt_devices(hass, ufp, [doorbell, unadopted_camera]) + assert_entity_counts(hass, Platform.TEXT, 1, 1) + + +async def test_text_camera_setup( + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera +): + """Test text entity setup for camera devices.""" + + doorbell.lcd_message = LCDMessage( + type=DoorbellMessageType.CUSTOM_MESSAGE, text="Test" + ) + await init_entry(hass, ufp, [doorbell]) + assert_entity_counts(hass, Platform.TEXT, 1, 1) + + entity_registry = er.async_get(hass) + + description = CAMERA[0] + unique_id, entity_id = ids_from_device_description( + Platform.TEXT, doorbell, description + ) + + entity = entity_registry.async_get(entity_id) + assert entity + assert entity.unique_id == unique_id + + state = hass.states.get(entity_id) + assert state + assert state.state == "Test" + assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION + + +async def test_text_camera_set( + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera +): + """Test text entity setting value camera devices.""" + + await init_entry(hass, ufp, [doorbell]) + assert_entity_counts(hass, Platform.TEXT, 1, 1) + + description = CAMERA[0] + unique_id, entity_id = ids_from_device_description( + Platform.TEXT, doorbell, description + ) + + doorbell.__fields__["set_lcd_text"] = Mock(final=False) + doorbell.set_lcd_text = AsyncMock() + + await hass.services.async_call( + "text", + "set_value", + {ATTR_ENTITY_ID: entity_id, "value": "Test test"}, + blocking=True, + ) + + doorbell.set_lcd_text.assert_called_once_with( + DoorbellMessageType.CUSTOM_MESSAGE, text="Test test" + )