Add UniFi Protect select platform (#63337)
This commit is contained in:
parent
48057e1dfb
commit
a2677983a2
14 changed files with 1284 additions and 13 deletions
|
@ -1,8 +1,10 @@
|
|||
"""Constant definitions for UniFi Protect Integration."""
|
||||
|
||||
from pyunifiprotect.data.types import ModelType, Version
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.const import ATTR_ENTITY_ID, Platform
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
DOMAIN = "unifiprotect"
|
||||
|
||||
|
@ -11,6 +13,8 @@ ATTR_HEIGHT = "height"
|
|||
ATTR_FPS = "fps"
|
||||
ATTR_BITRATE = "bitrate"
|
||||
ATTR_CHANNEL_ID = "channel_id"
|
||||
ATTR_MESSAGE = "message"
|
||||
ATTR_DURATION = "duration"
|
||||
|
||||
CONF_DISABLE_RTSP = "disable_rtsp"
|
||||
CONF_ALL_UPDATES = "all_updates"
|
||||
|
@ -41,11 +45,24 @@ DEVICES_FOR_SUBSCRIBE = DEVICES_WITH_ENTITIES | {ModelType.EVENT}
|
|||
MIN_REQUIRED_PROTECT_V = Version("1.20.0")
|
||||
OUTDATED_LOG_MESSAGE = "You are running v%s of UniFi Protect. Minimum required version is v%s. Please upgrade UniFi Protect and then retry"
|
||||
|
||||
SERVICE_SET_DOORBELL_MESSAGE = "set_doorbell_message"
|
||||
|
||||
TYPE_EMPTY_VALUE = ""
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.BUTTON,
|
||||
Platform.CAMERA,
|
||||
Platform.LIGHT,
|
||||
Platform.MEDIA_PLAYER,
|
||||
Platform.NUMBER,
|
||||
Platform.SELECT,
|
||||
Platform.SWITCH,
|
||||
]
|
||||
|
||||
SET_DOORBELL_LCD_MESSAGE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Required(ATTR_MESSAGE): cv.string,
|
||||
vol.Optional(ATTR_DURATION, default=""): cv.string,
|
||||
}
|
||||
)
|
||||
|
|
|
@ -10,6 +10,7 @@ from typing import Any
|
|||
from pyunifiprotect import NotAuthorized, NvrError, ProtectApiClient
|
||||
from pyunifiprotect.data import Bootstrap, ModelType, WSSubscriptionMessage
|
||||
from pyunifiprotect.data.base import ProtectAdoptableDeviceModel, ProtectDeviceModel
|
||||
from pyunifiprotect.data.nvr import Liveview
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
|
@ -107,6 +108,22 @@ class ProtectData:
|
|||
def _async_process_ws_message(self, message: WSSubscriptionMessage) -> None:
|
||||
if message.new_obj.model in DEVICES_WITH_ENTITIES:
|
||||
self.async_signal_device_id_update(message.new_obj.id)
|
||||
# trigger update for all Cameras with LCD screens when NVR Doorbell settings updates
|
||||
if "doorbell_settings" in message.changed_data:
|
||||
_LOGGER.debug(
|
||||
"Doorbell messages updated. Updating devices with LCD screens"
|
||||
)
|
||||
self.api.bootstrap.nvr.update_all_messages()
|
||||
for camera in self.api.bootstrap.cameras.values():
|
||||
if camera.feature_flags.has_lcd_screen:
|
||||
self.async_signal_device_id_update(camera.id)
|
||||
# alert user viewport needs restart so voice clients can get new options
|
||||
elif len(self.api.bootstrap.viewers) > 0 and isinstance(
|
||||
message.new_obj, Liveview
|
||||
):
|
||||
_LOGGER.warning(
|
||||
"Liveviews updated. Restart Home Assistant to update Viewport select options"
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_process_updates(self, updates: Bootstrap | None) -> None:
|
||||
|
@ -157,5 +174,6 @@ class ProtectData:
|
|||
if not self._subscriptions.get(device_id):
|
||||
return
|
||||
|
||||
_LOGGER.debug("Updating device: %s", device_id)
|
||||
for update_callback in self._subscriptions[device_id]:
|
||||
update_callback()
|
||||
|
|
|
@ -70,7 +70,7 @@ def async_all_device_entities(
|
|||
camera_descs: Sequence[ProtectRequiredKeysMixin] | None = None,
|
||||
light_descs: Sequence[ProtectRequiredKeysMixin] | None = None,
|
||||
sense_descs: Sequence[ProtectRequiredKeysMixin] | None = None,
|
||||
viewport_descs: Sequence[ProtectRequiredKeysMixin] | None = None,
|
||||
viewer_descs: Sequence[ProtectRequiredKeysMixin] | None = None,
|
||||
all_descs: Sequence[ProtectRequiredKeysMixin] | None = None,
|
||||
) -> list[ProtectDeviceEntity]:
|
||||
"""Generate a list of all the device entities."""
|
||||
|
@ -78,13 +78,13 @@ def async_all_device_entities(
|
|||
camera_descs = list(camera_descs or []) + all_descs
|
||||
light_descs = list(light_descs or []) + all_descs
|
||||
sense_descs = list(sense_descs or []) + all_descs
|
||||
viewport_descs = list(viewport_descs or []) + all_descs
|
||||
viewer_descs = list(viewer_descs or []) + all_descs
|
||||
|
||||
return (
|
||||
_async_device_entities(data, klass, ModelType.CAMERA, camera_descs)
|
||||
+ _async_device_entities(data, klass, ModelType.LIGHT, light_descs)
|
||||
+ _async_device_entities(data, klass, ModelType.SENSOR, sense_descs)
|
||||
+ _async_device_entities(data, klass, ModelType.VIEWPORT, viewport_descs)
|
||||
+ _async_device_entities(data, klass, ModelType.VIEWPORT, viewer_descs)
|
||||
)
|
||||
|
||||
|
||||
|
|
351
homeassistant/components/unifiprotect/select.py
Normal file
351
homeassistant/components/unifiprotect/select.py
Normal file
|
@ -0,0 +1,351 @@
|
|||
"""This component provides select entities for UniFi Protect."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from enum import Enum
|
||||
import logging
|
||||
from typing import Any, Final
|
||||
|
||||
from pyunifiprotect.data import (
|
||||
Camera,
|
||||
DoorbellMessageType,
|
||||
IRLEDMode,
|
||||
Light,
|
||||
LightModeEnableType,
|
||||
LightModeType,
|
||||
Liveview,
|
||||
RecordingMode,
|
||||
Viewer,
|
||||
)
|
||||
from pyunifiprotect.data.devices import LCDMessage
|
||||
|
||||
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_platform
|
||||
from homeassistant.helpers.entity import EntityCategory
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
SERVICE_SET_DOORBELL_MESSAGE,
|
||||
SET_DOORBELL_LCD_MESSAGE_SCHEMA,
|
||||
TYPE_EMPTY_VALUE,
|
||||
)
|
||||
from .data import ProtectData
|
||||
from .entity import ProtectDeviceEntity, async_all_device_entities
|
||||
from .models import ProtectRequiredKeysMixin
|
||||
from .utils import get_nested_attr
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_KEY_IR = "infrared"
|
||||
_KEY_REC_MODE = "recording_mode"
|
||||
_KEY_VIEWER = "viewer"
|
||||
_KEY_LIGHT_MOTION = "light_motion"
|
||||
_KEY_DOORBELL_TEXT = "doorbell_text"
|
||||
_KEY_PAIRED_CAMERA = "paired_camera"
|
||||
|
||||
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.OFF.value, "name": "Always Disable"},
|
||||
]
|
||||
|
||||
|
||||
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
|
||||
class ProtectSelectEntityDescription(ProtectRequiredKeysMixin, SelectEntityDescription):
|
||||
"""Describes UniFi Protect Select entity."""
|
||||
|
||||
ufp_options: list[dict[str, Any]] | None = None
|
||||
ufp_enum_type: type[Enum] | None = None
|
||||
ufp_set_function: str | None = None
|
||||
|
||||
|
||||
CAMERA_SELECTS: tuple[ProtectSelectEntityDescription, ...] = (
|
||||
ProtectSelectEntityDescription(
|
||||
key=_KEY_REC_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_function="set_recording_mode",
|
||||
),
|
||||
ProtectSelectEntityDescription(
|
||||
key=_KEY_IR,
|
||||
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_function="set_ir_led_model",
|
||||
),
|
||||
ProtectSelectEntityDescription(
|
||||
key=_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="lcd_message",
|
||||
),
|
||||
)
|
||||
|
||||
LIGHT_SELECTS: tuple[ProtectSelectEntityDescription, ...] = (
|
||||
ProtectSelectEntityDescription(
|
||||
key=_KEY_LIGHT_MOTION,
|
||||
name="Light Mode",
|
||||
icon="mdi:spotlight",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
ufp_options=MOTION_MODE_TO_LIGHT_MODE,
|
||||
ufp_value="light_mode_settings.mode",
|
||||
),
|
||||
ProtectSelectEntityDescription(
|
||||
key=_KEY_PAIRED_CAMERA,
|
||||
name="Paired Camera",
|
||||
icon="mdi:cctv",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
ufp_value="camera_id",
|
||||
),
|
||||
)
|
||||
|
||||
VIEWER_SELECTS: tuple[ProtectSelectEntityDescription, ...] = (
|
||||
ProtectSelectEntityDescription(
|
||||
key=_KEY_VIEWER,
|
||||
name="Liveview",
|
||||
icon="mdi:view-dashboard",
|
||||
entity_category=None,
|
||||
ufp_value="liveview",
|
||||
ufp_set_function="set_liveview",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: entity_platform.AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up number entities for UniFi Protect integration."""
|
||||
data: ProtectData = hass.data[DOMAIN][entry.entry_id]
|
||||
entities: list[ProtectDeviceEntity] = async_all_device_entities(
|
||||
data,
|
||||
ProtectSelects,
|
||||
camera_descs=CAMERA_SELECTS,
|
||||
light_descs=LIGHT_SELECTS,
|
||||
viewer_descs=VIEWER_SELECTS,
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_SET_DOORBELL_MESSAGE,
|
||||
SET_DOORBELL_LCD_MESSAGE_SCHEMA,
|
||||
"async_set_doorbell_message",
|
||||
)
|
||||
|
||||
|
||||
class ProtectSelects(ProtectDeviceEntity, SelectEntity):
|
||||
"""A UniFi Protect Select Entity."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
data: ProtectData,
|
||||
device: Camera | Light | Viewer,
|
||||
description: ProtectSelectEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the unifi protect select entity."""
|
||||
assert description.ufp_value is not None
|
||||
|
||||
self.device: Camera | Light | Viewer = device
|
||||
self.entity_description: ProtectSelectEntityDescription = description
|
||||
super().__init__(data)
|
||||
self._attr_name = f"{self.device.name} {self.entity_description.name}"
|
||||
|
||||
options = description.ufp_options
|
||||
if options is not None:
|
||||
self._attr_options = [item["name"] for item in options]
|
||||
self._hass_to_unifi_options: dict[str, Any] = {
|
||||
item["name"]: item["id"] for item in options
|
||||
}
|
||||
self._unifi_to_hass_options: dict[Any, str] = {
|
||||
item["id"]: item["name"] for item in options
|
||||
}
|
||||
self._async_set_dynamic_options()
|
||||
|
||||
@callback
|
||||
def _async_update_device_from_protect(self) -> None:
|
||||
super()._async_update_device_from_protect()
|
||||
|
||||
# entities with categories are not exposed for voice and safe to update dynamically
|
||||
if self.entity_description.entity_category is not None:
|
||||
_LOGGER.debug(
|
||||
"Updating dynamic select options for %s", self.entity_description.name
|
||||
)
|
||||
self._async_set_dynamic_options()
|
||||
|
||||
@callback
|
||||
def _async_set_dynamic_options(self) -> None:
|
||||
"""Options that do not actually update dynamically.
|
||||
|
||||
This is due to possible downstream platforms dependencies on these options.
|
||||
"""
|
||||
if self.entity_description.ufp_options is not None:
|
||||
return
|
||||
|
||||
if self.entity_description.key == _KEY_VIEWER:
|
||||
options = [
|
||||
{"id": item.id, "name": item.name}
|
||||
for item in self.data.api.bootstrap.liveviews.values()
|
||||
]
|
||||
elif self.entity_description.key == _KEY_DOORBELL_TEXT:
|
||||
default_message = (
|
||||
self.data.api.bootstrap.nvr.doorbell_settings.default_message_text
|
||||
)
|
||||
messages = self.data.api.bootstrap.nvr.doorbell_settings.all_messages
|
||||
built_messages = (
|
||||
{"id": item.type.value, "name": item.text} for item in messages
|
||||
)
|
||||
|
||||
options = [
|
||||
{"id": "", "name": f"Default Message ({default_message})"},
|
||||
*built_messages,
|
||||
]
|
||||
elif self.entity_description.key == _KEY_PAIRED_CAMERA:
|
||||
options = [{"id": TYPE_EMPTY_VALUE, "name": "Not Paired"}]
|
||||
for camera in self.data.api.bootstrap.cameras.values():
|
||||
options.append({"id": camera.id, "name": camera.name})
|
||||
|
||||
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}
|
||||
|
||||
@property
|
||||
def current_option(self) -> str:
|
||||
"""Return the current selected option."""
|
||||
|
||||
assert self.entity_description.ufp_value is not None
|
||||
unifi_value = get_nested_attr(self.device, self.entity_description.ufp_value)
|
||||
|
||||
if unifi_value is None:
|
||||
unifi_value = TYPE_EMPTY_VALUE
|
||||
elif isinstance(unifi_value, Liveview):
|
||||
unifi_value = unifi_value.id
|
||||
elif self.entity_description.key == _KEY_LIGHT_MOTION:
|
||||
assert isinstance(self.device, Light)
|
||||
|
||||
# a bit of extra to allow On Motion Always/Dark
|
||||
if (
|
||||
self.device.light_mode_settings.mode == LightModeType.MOTION
|
||||
and self.device.light_mode_settings.enable_at
|
||||
== LightModeEnableType.DARK
|
||||
):
|
||||
unifi_value = f"{LightModeType.MOTION.value}Dark"
|
||||
elif self.entity_description.key == _KEY_DOORBELL_TEXT:
|
||||
assert isinstance(unifi_value, LCDMessage)
|
||||
return unifi_value.text
|
||||
return self._unifi_to_hass_options.get(unifi_value, unifi_value)
|
||||
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Change the Select Entity Option."""
|
||||
|
||||
if isinstance(self.device, Light):
|
||||
if self.entity_description.key == _KEY_LIGHT_MOTION:
|
||||
lightmode, timing = LIGHT_MODE_TO_SETTINGS[option]
|
||||
_LOGGER.debug("Changing Light Mode to %s", option)
|
||||
await self.device.set_light_settings(
|
||||
LightModeType(lightmode),
|
||||
enable_at=None if timing is None else LightModeEnableType(timing),
|
||||
)
|
||||
return
|
||||
|
||||
unifi_value = self._hass_to_unifi_options[option]
|
||||
if self.entity_description.key == _KEY_PAIRED_CAMERA:
|
||||
if unifi_value == TYPE_EMPTY_VALUE:
|
||||
unifi_value = None
|
||||
camera = self.data.api.bootstrap.cameras.get(unifi_value)
|
||||
await self.device.set_paired_camera(camera)
|
||||
_LOGGER.debug("Changed Paired Camera to to: %s", option)
|
||||
return
|
||||
|
||||
unifi_value = self._hass_to_unifi_options[option]
|
||||
if isinstance(self.device, Camera):
|
||||
if self.entity_description.key == _KEY_DOORBELL_TEXT:
|
||||
if unifi_value.startswith(DoorbellMessageType.CUSTOM_MESSAGE.value):
|
||||
await self.device.set_lcd_text(
|
||||
DoorbellMessageType.CUSTOM_MESSAGE, text=option
|
||||
)
|
||||
elif unifi_value == TYPE_EMPTY_VALUE:
|
||||
await self.device.set_lcd_text(None)
|
||||
else:
|
||||
await self.device.set_lcd_text(DoorbellMessageType(unifi_value))
|
||||
|
||||
_LOGGER.debug("Changed Doorbell LCD Text to: %s", option)
|
||||
return
|
||||
|
||||
if self.entity_description.ufp_enum_type is not None:
|
||||
unifi_value = self.entity_description.ufp_enum_type(unifi_value)
|
||||
elif self.entity_description.key == _KEY_VIEWER:
|
||||
unifi_value = self.data.api.bootstrap.liveviews[unifi_value]
|
||||
|
||||
_LOGGER.debug("%s set to: %s", self.entity_description.key, option)
|
||||
assert self.entity_description.ufp_set_function
|
||||
coro = getattr(self.device, self.entity_description.ufp_set_function)
|
||||
await coro(unifi_value)
|
||||
|
||||
async def async_set_doorbell_message(self, message: str, duration: str) -> None:
|
||||
"""Set LCD Message on Doorbell display."""
|
||||
|
||||
if self.entity_description.key != _KEY_DOORBELL_TEXT:
|
||||
raise HomeAssistantError("Not a doorbell text select entity")
|
||||
|
||||
assert isinstance(self.device, Camera)
|
||||
reset_at = None
|
||||
timeout_msg = ""
|
||||
if duration.isnumeric():
|
||||
reset_at = utcnow() + timedelta(minutes=int(duration))
|
||||
timeout_msg = f" with timeout of {duration} minute(s)"
|
||||
|
||||
_LOGGER.debug(
|
||||
'Setting message for %s to "%s"%s', self.device.name, message, timeout_msg
|
||||
)
|
||||
await self.device.set_lcd_text(
|
||||
DoorbellMessageType.CUSTOM_MESSAGE, message, reset_at=reset_at
|
||||
)
|
34
homeassistant/components/unifiprotect/services.yaml
Normal file
34
homeassistant/components/unifiprotect/services.yaml
Normal file
|
@ -0,0 +1,34 @@
|
|||
set_doorbell_message:
|
||||
name: Set Doorbell message
|
||||
description: >
|
||||
Use to dynamically set the message on a Doorbell LCD screen. Should only be used to set dynamic messages
|
||||
(i.e. setting the current outdoor temperature on your Doorbell). Static messages should still using the Select entity and the
|
||||
add_doorbell_text / remove_doorbell_text services.
|
||||
fields:
|
||||
entity_id:
|
||||
name: Doorbell Text
|
||||
description: (Required) Doorbell to display message on
|
||||
example: "select.front_doorbell_camera_doorbell_text"
|
||||
required: true
|
||||
selector:
|
||||
entity:
|
||||
integration: unifiprotect
|
||||
domain: select
|
||||
device_class: unifiprotect__lcd_message
|
||||
message:
|
||||
name: Message to display
|
||||
description: (Required) Message to display on LCD Panel. Max 30 characters
|
||||
example: "Welcome | 09:23 | 25°C"
|
||||
required: true
|
||||
selector:
|
||||
text:
|
||||
duration:
|
||||
name: Duration (minutes)
|
||||
description: "(Optional) Number of minutes to display message, before returning to default. Leave blank to display always"
|
||||
example: 5
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
max: 120
|
||||
step: 1
|
||||
mode: slider
|
|
@ -12,6 +12,9 @@ from unittest.mock import AsyncMock, Mock, patch
|
|||
import pytest
|
||||
from pyunifiprotect.data import Camera, Light, Version, WSSubscriptionMessage
|
||||
from pyunifiprotect.data.base import ProtectAdoptableDeviceModel
|
||||
from pyunifiprotect.data.devices import Viewer
|
||||
from pyunifiprotect.data.nvr import DoorbellMessage, Liveview
|
||||
from pyunifiprotect.data.types import DoorbellMessageType, ModelType
|
||||
|
||||
from homeassistant.components.unifiprotect.const import DOMAIN, MIN_REQUIRED_PROTECT_V
|
||||
from homeassistant.const import Platform
|
||||
|
@ -33,6 +36,27 @@ class MockPortData:
|
|||
rtsps: int = 7447
|
||||
|
||||
|
||||
@dataclass
|
||||
class MockDoorbellSettings:
|
||||
"""Mock Port information."""
|
||||
|
||||
default_message_text = "Welcome"
|
||||
all_messages = [
|
||||
DoorbellMessage(
|
||||
type=DoorbellMessageType.LEAVE_PACKAGE_AT_DOOR,
|
||||
text=DoorbellMessageType.LEAVE_PACKAGE_AT_DOOR.value.replace("_", " "),
|
||||
),
|
||||
DoorbellMessage(
|
||||
type=DoorbellMessageType.DO_NOT_DISTURB,
|
||||
text=DoorbellMessageType.DO_NOT_DISTURB.value.replace("_", " "),
|
||||
),
|
||||
DoorbellMessage(
|
||||
type=DoorbellMessageType.CUSTOM_MESSAGE,
|
||||
text="Test",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class MockNvrData:
|
||||
"""Mock for NVR."""
|
||||
|
@ -42,6 +66,9 @@ class MockNvrData:
|
|||
name: str
|
||||
id: str
|
||||
ports: MockPortData = MockPortData()
|
||||
doorbell_settings = MockDoorbellSettings()
|
||||
update_all_messages = Mock()
|
||||
model: ModelType = ModelType.NVR
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -53,6 +80,15 @@ class MockBootstrap:
|
|||
lights: dict[str, Any]
|
||||
sensors: dict[str, Any]
|
||||
viewers: dict[str, Any]
|
||||
liveviews: dict[str, Any]
|
||||
|
||||
def reset_objects(self) -> None:
|
||||
"""Reset all devices on bootstrap for tests."""
|
||||
self.cameras = {}
|
||||
self.lights = {}
|
||||
self.sensors = {}
|
||||
self.viewers = {}
|
||||
self.liveviews = {}
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -71,7 +107,7 @@ MOCK_OLD_NVR_DATA = MockNvrData(
|
|||
)
|
||||
|
||||
MOCK_BOOTSTRAP = MockBootstrap(
|
||||
nvr=MOCK_NVR_DATA, cameras={}, lights={}, sensors={}, viewers={}
|
||||
nvr=MOCK_NVR_DATA, cameras={}, lights={}, sensors={}, viewers={}, liveviews={}
|
||||
)
|
||||
|
||||
|
||||
|
@ -122,6 +158,17 @@ def mock_entry(
|
|||
yield MockEntityFixture(mock_config, mock_client)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_liveview():
|
||||
"""Mock UniFi Protect Camera device."""
|
||||
|
||||
path = Path(__file__).parent / "sample_data" / "sample_liveview.json"
|
||||
with open(path, encoding="utf-8") as json_file:
|
||||
data = json.load(json_file)
|
||||
|
||||
yield Liveview.from_unifi_dict(**data)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_camera():
|
||||
"""Mock UniFi Protect Camera device."""
|
||||
|
@ -144,6 +191,17 @@ def mock_light():
|
|||
yield Light.from_unifi_dict(**data)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_viewer():
|
||||
"""Mock UniFi Protect Viewport device."""
|
||||
|
||||
path = Path(__file__).parent / "sample_data" / "sample_viewport.json"
|
||||
with open(path, encoding="utf-8") as json_file:
|
||||
data = json.load(json_file)
|
||||
|
||||
yield Viewer.from_unifi_dict(**data)
|
||||
|
||||
|
||||
async def time_changed(hass: HomeAssistant, seconds: int) -> None:
|
||||
"""Trigger time changed."""
|
||||
next_update = dt_util.utcnow() + timedelta(seconds)
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
{
|
||||
"name": "Default",
|
||||
"isDefault": true,
|
||||
"isGlobal": true,
|
||||
"layout": 9,
|
||||
"slots": [
|
||||
{
|
||||
"cameras": [
|
||||
"0488c1538efb5cc9f73f77ca"
|
||||
],
|
||||
"cycleMode": "time",
|
||||
"cycleInterval": 10
|
||||
},
|
||||
{
|
||||
"cameras": [
|
||||
"0de062b4f6922d489d3b312d"
|
||||
],
|
||||
"cycleMode": "time",
|
||||
"cycleInterval": 10
|
||||
},
|
||||
{
|
||||
"cameras": [
|
||||
"193be66559c03ec5629f54cd"
|
||||
],
|
||||
"cycleMode": "time",
|
||||
"cycleInterval": 10
|
||||
},
|
||||
{
|
||||
"cameras": [
|
||||
"16b0c551e36d872806f2806b"
|
||||
],
|
||||
"cycleMode": "time",
|
||||
"cycleInterval": 10
|
||||
},
|
||||
{
|
||||
"cameras": [
|
||||
"5becd64d90f1cae3a4146a0f"
|
||||
],
|
||||
"cycleMode": "time",
|
||||
"cycleInterval": 10
|
||||
},
|
||||
{
|
||||
"cameras": [
|
||||
"4f5fab885aca3f7c226b22b9"
|
||||
],
|
||||
"cycleMode": "time",
|
||||
"cycleInterval": 10
|
||||
},
|
||||
{
|
||||
"cameras": [
|
||||
"cc7a572a0a8677baae933873"
|
||||
],
|
||||
"cycleMode": "time",
|
||||
"cycleInterval": 10
|
||||
},
|
||||
{
|
||||
"cameras": [
|
||||
"f4e9f4421209908c51284e67"
|
||||
],
|
||||
"cycleMode": "time",
|
||||
"cycleInterval": 10
|
||||
},
|
||||
{
|
||||
"cameras": [],
|
||||
"cycleMode": "time",
|
||||
"cycleInterval": 10
|
||||
}
|
||||
],
|
||||
"owner": "5a839670ad0a929bf8271c26",
|
||||
"id": "ecb21f15e6d8fae65fea82f8",
|
||||
"modelKey": "liveview"
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"mac": "4EDC1B6D2F76",
|
||||
"host": "192.168.34.145",
|
||||
"connectionHost": "192.168.178.217",
|
||||
"type": "UP Viewport",
|
||||
"name": "Yfptv Ttklkw",
|
||||
"upSince": 1639845760126,
|
||||
"uptime": 178121,
|
||||
"lastSeen": 1640023881126,
|
||||
"connectedSince": 1640020660049,
|
||||
"state": "CONNECTED",
|
||||
"hardwareRevision": null,
|
||||
"firmwareVersion": "1.2.54",
|
||||
"latestFirmwareVersion": "1.2.54",
|
||||
"firmwareBuild": "dcfb16f3.210907.625",
|
||||
"isUpdating": false,
|
||||
"isAdopting": false,
|
||||
"isAdopted": true,
|
||||
"isAdoptedByOther": false,
|
||||
"isProvisioned": false,
|
||||
"isRebooting": false,
|
||||
"isSshEnabled": false,
|
||||
"canAdopt": false,
|
||||
"isAttemptingToConnect": false,
|
||||
"streamLimit": 16,
|
||||
"softwareVersion": "1.2.54",
|
||||
"wiredConnectionState": {
|
||||
"phyRate": 1000
|
||||
},
|
||||
"liveview": "ecb21f15e6d8fae65fea82f8",
|
||||
"id": "5ec2a22846047eeb6e976922",
|
||||
"isConnected": true,
|
||||
"marketName": "UP ViewPort",
|
||||
"modelKey": "viewer"
|
||||
}
|
|
@ -436,6 +436,7 @@ async def test_camera_ws_update(
|
|||
new_camera.is_recording = True
|
||||
|
||||
mock_msg = Mock()
|
||||
mock_msg.changed_data = {}
|
||||
mock_msg.new_obj = new_camera
|
||||
|
||||
new_bootstrap.cameras = {new_camera.id: new_camera}
|
||||
|
@ -463,6 +464,7 @@ async def test_camera_ws_update_offline(
|
|||
new_camera.state = StateType.DISCONNECTED
|
||||
|
||||
mock_msg = Mock()
|
||||
mock_msg.changed_data = {}
|
||||
mock_msg.new_obj = new_camera
|
||||
|
||||
new_bootstrap.cameras = {new_camera.id: new_camera}
|
||||
|
@ -477,6 +479,7 @@ async def test_camera_ws_update_offline(
|
|||
new_camera.state = StateType.CONNECTED
|
||||
|
||||
mock_msg = Mock()
|
||||
mock_msg.changed_data = {}
|
||||
mock_msg.new_obj = new_camera
|
||||
|
||||
new_bootstrap.cameras = {new_camera.id: new_camera}
|
||||
|
|
|
@ -84,6 +84,7 @@ async def test_light_update(
|
|||
new_light.light_device_settings.led_level = 3
|
||||
|
||||
mock_msg = Mock()
|
||||
mock_msg.changed_data = {}
|
||||
mock_msg.new_obj = new_light
|
||||
|
||||
new_bootstrap.lights = {new_light.id: new_light}
|
||||
|
|
|
@ -98,6 +98,7 @@ async def test_media_player_update(
|
|||
new_camera.talkback_stream.is_running = True
|
||||
|
||||
mock_msg = Mock()
|
||||
mock_msg.changed_data = {}
|
||||
mock_msg.new_obj = new_camera
|
||||
|
||||
new_bootstrap.cameras = {new_camera.id: new_camera}
|
||||
|
@ -142,6 +143,7 @@ async def test_media_player_stop(
|
|||
new_camera.talkback_stream.is_running = True
|
||||
|
||||
mock_msg = Mock()
|
||||
mock_msg.changed_data = {}
|
||||
mock_msg.new_obj = new_camera
|
||||
|
||||
new_bootstrap.cameras = {new_camera.id: new_camera}
|
||||
|
|
|
@ -41,7 +41,7 @@ async def light_fixture(
|
|||
light_obj.light_device_settings.pir_sensitivity = 45
|
||||
light_obj.light_device_settings.pir_duration = timedelta(seconds=45)
|
||||
|
||||
mock_entry.api.bootstrap.cameras = {}
|
||||
mock_entry.api.bootstrap.reset_objects()
|
||||
mock_entry.api.bootstrap.lights = {
|
||||
light_obj.id: light_obj,
|
||||
}
|
||||
|
@ -81,7 +81,7 @@ async def camera_fixture(
|
|||
camera_obj.isp_settings.zoom_position = 0
|
||||
camera_obj.chime_duration = timedelta(seconds=0)
|
||||
|
||||
mock_entry.api.bootstrap.lights = {}
|
||||
mock_entry.api.bootstrap.reset_objects()
|
||||
mock_entry.api.bootstrap.cameras = {
|
||||
camera_obj.id: camera_obj,
|
||||
}
|
||||
|
@ -159,7 +159,7 @@ async def test_number_setup_camera_none(
|
|||
# has_wdr is an the inverse of has HDR
|
||||
camera_obj.feature_flags.has_hdr = True
|
||||
|
||||
mock_entry.api.bootstrap.lights = {}
|
||||
mock_entry.api.bootstrap.reset_objects()
|
||||
mock_entry.api.bootstrap.cameras = {
|
||||
camera_obj.id: camera_obj,
|
||||
}
|
||||
|
@ -188,7 +188,7 @@ async def test_number_setup_camera_missing_attr(
|
|||
|
||||
Camera.__config__.validate_assignment = True
|
||||
|
||||
mock_entry.api.bootstrap.lights = {}
|
||||
mock_entry.api.bootstrap.reset_objects()
|
||||
mock_entry.api.bootstrap.cameras = {
|
||||
camera_obj.id: camera_obj,
|
||||
}
|
||||
|
|
680
tests/components/unifiprotect/test_select.py
Normal file
680
tests/components/unifiprotect/test_select.py
Normal file
|
@ -0,0 +1,680 @@
|
|||
"""Test the UniFi Protect number platform."""
|
||||
# pylint: disable=protected-access
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import copy
|
||||
from datetime import timedelta
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
from pyunifiprotect.data import Camera, Light
|
||||
from pyunifiprotect.data.devices import LCDMessage, Viewer
|
||||
from pyunifiprotect.data.nvr import DoorbellMessage, Liveview
|
||||
from pyunifiprotect.data.types import (
|
||||
DoorbellMessageType,
|
||||
IRLEDMode,
|
||||
LightModeEnableType,
|
||||
LightModeType,
|
||||
RecordingMode,
|
||||
)
|
||||
|
||||
from homeassistant.components.select.const import ATTR_OPTIONS
|
||||
from homeassistant.components.unifiprotect.const import (
|
||||
ATTR_DURATION,
|
||||
ATTR_MESSAGE,
|
||||
DEFAULT_ATTRIBUTION,
|
||||
SERVICE_SET_DOORBELL_MESSAGE,
|
||||
)
|
||||
from homeassistant.components.unifiprotect.select import (
|
||||
CAMERA_SELECTS,
|
||||
LIGHT_MODE_OFF,
|
||||
LIGHT_SELECTS,
|
||||
VIEWER_SELECTS,
|
||||
)
|
||||
from homeassistant.const import ATTR_ATTRIBUTION, ATTR_ENTITY_ID, ATTR_OPTION, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from .conftest import (
|
||||
MockEntityFixture,
|
||||
assert_entity_counts,
|
||||
ids_from_device_description,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(name="viewer")
|
||||
async def viewer_fixture(
|
||||
hass: HomeAssistant,
|
||||
mock_entry: MockEntityFixture,
|
||||
mock_viewer: Viewer,
|
||||
mock_liveview: Liveview,
|
||||
):
|
||||
"""Fixture for a single viewport for testing the number platform."""
|
||||
|
||||
# disable pydantic validation so mocking can happen
|
||||
Viewer.__config__.validate_assignment = False
|
||||
|
||||
viewer_obj = mock_viewer.copy(deep=True)
|
||||
viewer_obj._api = mock_entry.api
|
||||
viewer_obj.name = "Test Viewer"
|
||||
viewer_obj.liveview_id = mock_liveview.id
|
||||
|
||||
mock_entry.api.bootstrap.reset_objects()
|
||||
mock_entry.api.bootstrap.viewers = {
|
||||
viewer_obj.id: viewer_obj,
|
||||
}
|
||||
mock_entry.api.bootstrap.liveviews = {mock_liveview.id: mock_liveview}
|
||||
|
||||
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert_entity_counts(hass, Platform.SELECT, 1, 1)
|
||||
|
||||
yield viewer_obj
|
||||
|
||||
Viewer.__config__.validate_assignment = True
|
||||
|
||||
|
||||
@pytest.fixture(name="camera")
|
||||
async def camera_fixture(
|
||||
hass: HomeAssistant, mock_entry: MockEntityFixture, mock_camera: Camera
|
||||
):
|
||||
"""Fixture for a single camera for testing the switch platform."""
|
||||
|
||||
# disable pydantic validation so mocking can happen
|
||||
Camera.__config__.validate_assignment = False
|
||||
|
||||
camera_obj = mock_camera.copy(deep=True)
|
||||
camera_obj._api = mock_entry.api
|
||||
camera_obj.channels[0]._api = mock_entry.api
|
||||
camera_obj.channels[1]._api = mock_entry.api
|
||||
camera_obj.channels[2]._api = mock_entry.api
|
||||
camera_obj.name = "Test Camera"
|
||||
camera_obj.feature_flags.has_lcd_screen = True
|
||||
camera_obj.recording_settings.mode = RecordingMode.ALWAYS
|
||||
camera_obj.isp_settings.ir_led_mode = IRLEDMode.AUTO
|
||||
camera_obj.lcd_message = None
|
||||
|
||||
mock_entry.api.bootstrap.reset_objects()
|
||||
mock_entry.api.bootstrap.cameras = {
|
||||
camera_obj.id: camera_obj,
|
||||
}
|
||||
|
||||
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert_entity_counts(hass, Platform.SELECT, 3, 3)
|
||||
|
||||
yield camera_obj
|
||||
|
||||
Camera.__config__.validate_assignment = True
|
||||
|
||||
|
||||
@pytest.fixture(name="light")
|
||||
async def light_fixture(
|
||||
hass: HomeAssistant,
|
||||
mock_entry: MockEntityFixture,
|
||||
mock_light: Light,
|
||||
camera: Camera,
|
||||
):
|
||||
"""Fixture for a single light for testing the number platform."""
|
||||
|
||||
# disable pydantic validation so mocking can happen
|
||||
Light.__config__.validate_assignment = False
|
||||
|
||||
light_obj = mock_light.copy(deep=True)
|
||||
light_obj._api = mock_entry.api
|
||||
light_obj.name = "Test Light"
|
||||
light_obj.camera_id = None
|
||||
light_obj.light_mode_settings.mode = LightModeType.MOTION
|
||||
light_obj.light_mode_settings.enable_at = LightModeEnableType.DARK
|
||||
|
||||
mock_entry.api.bootstrap.reset_objects()
|
||||
mock_entry.api.bootstrap.cameras = {camera.id: camera}
|
||||
mock_entry.api.bootstrap.lights = {
|
||||
light_obj.id: light_obj,
|
||||
}
|
||||
|
||||
await hass.config_entries.async_reload(mock_entry.entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert_entity_counts(hass, Platform.SELECT, 5, 5)
|
||||
|
||||
yield light_obj
|
||||
|
||||
Light.__config__.validate_assignment = True
|
||||
|
||||
|
||||
@pytest.fixture(name="camera_none")
|
||||
async def camera_none_fixture(
|
||||
hass: HomeAssistant, mock_entry: MockEntityFixture, mock_camera: Camera
|
||||
):
|
||||
"""Fixture for a single camera for testing the switch platform."""
|
||||
|
||||
# disable pydantic validation so mocking can happen
|
||||
Camera.__config__.validate_assignment = False
|
||||
|
||||
camera_obj = mock_camera.copy(deep=True)
|
||||
camera_obj._api = mock_entry.api
|
||||
camera_obj.channels[0]._api = mock_entry.api
|
||||
camera_obj.channels[1]._api = mock_entry.api
|
||||
camera_obj.channels[2]._api = mock_entry.api
|
||||
camera_obj.name = "Test Camera"
|
||||
camera_obj.feature_flags.has_lcd_screen = False
|
||||
camera_obj.recording_settings.mode = RecordingMode.ALWAYS
|
||||
camera_obj.isp_settings.ir_led_mode = IRLEDMode.AUTO
|
||||
|
||||
mock_entry.api.bootstrap.reset_objects()
|
||||
mock_entry.api.bootstrap.cameras = {
|
||||
camera_obj.id: camera_obj,
|
||||
}
|
||||
|
||||
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert_entity_counts(hass, Platform.SELECT, 2, 2)
|
||||
|
||||
yield camera_obj
|
||||
|
||||
Camera.__config__.validate_assignment = True
|
||||
|
||||
|
||||
async def test_select_setup_light(
|
||||
hass: HomeAssistant,
|
||||
light: Light,
|
||||
):
|
||||
"""Test select entity setup for light devices."""
|
||||
|
||||
entity_registry = er.async_get(hass)
|
||||
expected_values = ("On Motion - When Dark", "Not Paired")
|
||||
|
||||
for index, description in enumerate(LIGHT_SELECTS):
|
||||
unique_id, entity_id = ids_from_device_description(
|
||||
Platform.SELECT, light, 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 == expected_values[index]
|
||||
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
|
||||
|
||||
|
||||
async def test_select_setup_viewer(
|
||||
hass: HomeAssistant,
|
||||
viewer: Viewer,
|
||||
):
|
||||
"""Test select entity setup for light devices."""
|
||||
|
||||
entity_registry = er.async_get(hass)
|
||||
description = VIEWER_SELECTS[0]
|
||||
|
||||
unique_id, entity_id = ids_from_device_description(
|
||||
Platform.SELECT, viewer, 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 == viewer.liveview.name
|
||||
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
|
||||
|
||||
|
||||
async def test_number_setup_camera_all(
|
||||
hass: HomeAssistant,
|
||||
camera: Camera,
|
||||
):
|
||||
"""Test number entity setup for camera devices (all features)."""
|
||||
|
||||
entity_registry = er.async_get(hass)
|
||||
expected_values = ("Always", "Auto", "Default Message (Welcome)")
|
||||
|
||||
for index, description in enumerate(CAMERA_SELECTS):
|
||||
unique_id, entity_id = ids_from_device_description(
|
||||
Platform.SELECT, camera, 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 == expected_values[index]
|
||||
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
|
||||
|
||||
|
||||
async def test_number_setup_camera_none(
|
||||
hass: HomeAssistant,
|
||||
camera_none: Camera,
|
||||
):
|
||||
"""Test number entity setup for camera devices (no features)."""
|
||||
|
||||
entity_registry = er.async_get(hass)
|
||||
expected_values = ("Always", "Auto", "Default Message (Welcome)")
|
||||
|
||||
for index, description in enumerate(CAMERA_SELECTS):
|
||||
if index == 2:
|
||||
return
|
||||
|
||||
unique_id, entity_id = ids_from_device_description(
|
||||
Platform.SELECT, camera_none, 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 == expected_values[index]
|
||||
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
|
||||
|
||||
|
||||
async def test_select_update_liveview(
|
||||
hass: HomeAssistant,
|
||||
mock_entry: MockEntityFixture,
|
||||
viewer: Viewer,
|
||||
mock_liveview: Liveview,
|
||||
):
|
||||
"""Test select entity update (new Liveview)."""
|
||||
|
||||
_, entity_id = ids_from_device_description(
|
||||
Platform.SELECT, viewer, VIEWER_SELECTS[0]
|
||||
)
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
expected_options = state.attributes[ATTR_OPTIONS]
|
||||
|
||||
new_bootstrap = copy(mock_entry.api.bootstrap)
|
||||
new_liveview = copy(mock_liveview)
|
||||
new_liveview.id = "test_id"
|
||||
|
||||
mock_msg = Mock()
|
||||
mock_msg.changed_data = {}
|
||||
mock_msg.new_obj = new_liveview
|
||||
|
||||
new_bootstrap.liveviews = {**new_bootstrap.liveviews, new_liveview.id: new_liveview}
|
||||
mock_entry.api.bootstrap = new_bootstrap
|
||||
mock_entry.api.ws_subscription(mock_msg)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.attributes[ATTR_OPTIONS] == expected_options
|
||||
|
||||
|
||||
async def test_select_update_doorbell_settings(
|
||||
hass: HomeAssistant, mock_entry: MockEntityFixture, camera: Camera
|
||||
):
|
||||
"""Test select entity update (new Doorbell Message)."""
|
||||
|
||||
expected_length = (
|
||||
len(mock_entry.api.bootstrap.nvr.doorbell_settings.all_messages) + 1
|
||||
)
|
||||
|
||||
_, entity_id = ids_from_device_description(
|
||||
Platform.SELECT, camera, CAMERA_SELECTS[2]
|
||||
)
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert len(state.attributes[ATTR_OPTIONS]) == expected_length
|
||||
|
||||
expected_length += 1
|
||||
new_nvr = copy(mock_entry.api.bootstrap.nvr)
|
||||
new_nvr.doorbell_settings.all_messages = [
|
||||
*new_nvr.doorbell_settings.all_messages,
|
||||
DoorbellMessage(
|
||||
type=DoorbellMessageType.CUSTOM_MESSAGE,
|
||||
text="Test2",
|
||||
),
|
||||
]
|
||||
|
||||
mock_msg = Mock()
|
||||
mock_msg.changed_data = {"doorbell_settings": {}}
|
||||
mock_msg.new_obj = new_nvr
|
||||
|
||||
mock_entry.api.bootstrap.nvr = new_nvr
|
||||
mock_entry.api.ws_subscription(mock_msg)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
new_nvr.update_all_messages.assert_called_once()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert len(state.attributes[ATTR_OPTIONS]) == expected_length
|
||||
|
||||
|
||||
async def test_select_update_doorbell_message(
|
||||
hass: HomeAssistant,
|
||||
mock_entry: MockEntityFixture,
|
||||
camera: Camera,
|
||||
):
|
||||
"""Test select entity update (change doorbell message)."""
|
||||
|
||||
_, entity_id = ids_from_device_description(
|
||||
Platform.SELECT, camera, CAMERA_SELECTS[2]
|
||||
)
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == "Default Message (Welcome)"
|
||||
|
||||
new_bootstrap = copy(mock_entry.api.bootstrap)
|
||||
new_camera = camera.copy()
|
||||
new_camera.lcd_message = LCDMessage(
|
||||
type=DoorbellMessageType.CUSTOM_MESSAGE, text="Test"
|
||||
)
|
||||
|
||||
mock_msg = Mock()
|
||||
mock_msg.changed_data = {}
|
||||
mock_msg.new_obj = new_camera
|
||||
|
||||
new_bootstrap.cameras = {new_camera.id: new_camera}
|
||||
mock_entry.api.bootstrap = new_bootstrap
|
||||
mock_entry.api.ws_subscription(mock_msg)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == "Test"
|
||||
|
||||
|
||||
async def test_select_set_option_light_motion(
|
||||
hass: HomeAssistant,
|
||||
light: Light,
|
||||
):
|
||||
"""Test Light Mode select."""
|
||||
_, entity_id = ids_from_device_description(Platform.SELECT, light, LIGHT_SELECTS[0])
|
||||
|
||||
light.__fields__["set_light_settings"] = Mock()
|
||||
light.set_light_settings = AsyncMock()
|
||||
|
||||
await hass.services.async_call(
|
||||
"select",
|
||||
"select_option",
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_OPTION: LIGHT_MODE_OFF},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
light.set_light_settings.assert_called_once_with(
|
||||
LightModeType.MANUAL, enable_at=None
|
||||
)
|
||||
|
||||
|
||||
async def test_select_set_option_light_camera(
|
||||
hass: HomeAssistant,
|
||||
light: Light,
|
||||
):
|
||||
"""Test Paired Camera select."""
|
||||
_, entity_id = ids_from_device_description(Platform.SELECT, light, LIGHT_SELECTS[1])
|
||||
|
||||
light.__fields__["set_paired_camera"] = Mock()
|
||||
light.set_paired_camera = AsyncMock()
|
||||
|
||||
camera = list(light.api.bootstrap.cameras.values())[0]
|
||||
|
||||
await hass.services.async_call(
|
||||
"select",
|
||||
"select_option",
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_OPTION: camera.name},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
light.set_paired_camera.assert_called_once_with(camera)
|
||||
|
||||
await hass.services.async_call(
|
||||
"select",
|
||||
"select_option",
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "Not Paired"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
light.set_paired_camera.assert_called_with(None)
|
||||
|
||||
|
||||
async def test_select_set_option_camera_recording(
|
||||
hass: HomeAssistant,
|
||||
camera: Camera,
|
||||
):
|
||||
"""Test Recording Mode select."""
|
||||
_, entity_id = ids_from_device_description(
|
||||
Platform.SELECT, camera, CAMERA_SELECTS[0]
|
||||
)
|
||||
|
||||
camera.__fields__["set_recording_mode"] = Mock()
|
||||
camera.set_recording_mode = AsyncMock()
|
||||
|
||||
await hass.services.async_call(
|
||||
"select",
|
||||
"select_option",
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "Never"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
camera.set_recording_mode.assert_called_once_with(RecordingMode.NEVER)
|
||||
|
||||
|
||||
async def test_select_set_option_camera_ir(
|
||||
hass: HomeAssistant,
|
||||
camera: Camera,
|
||||
):
|
||||
"""Test Infrared Mode select."""
|
||||
_, entity_id = ids_from_device_description(
|
||||
Platform.SELECT, camera, CAMERA_SELECTS[1]
|
||||
)
|
||||
|
||||
camera.__fields__["set_ir_led_model"] = Mock()
|
||||
camera.set_ir_led_model = AsyncMock()
|
||||
|
||||
await hass.services.async_call(
|
||||
"select",
|
||||
"select_option",
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "Always Enable"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
camera.set_ir_led_model.assert_called_once_with(IRLEDMode.ON)
|
||||
|
||||
|
||||
async def test_select_set_option_camera_doorbell_custom(
|
||||
hass: HomeAssistant,
|
||||
camera: Camera,
|
||||
):
|
||||
"""Test Doorbell Text select (user defined message)."""
|
||||
_, entity_id = ids_from_device_description(
|
||||
Platform.SELECT, camera, CAMERA_SELECTS[2]
|
||||
)
|
||||
|
||||
camera.__fields__["set_lcd_text"] = Mock()
|
||||
camera.set_lcd_text = AsyncMock()
|
||||
|
||||
await hass.services.async_call(
|
||||
"select",
|
||||
"select_option",
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "Test"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_select_set_option_camera_doorbell_unifi(
|
||||
hass: HomeAssistant,
|
||||
camera: Camera,
|
||||
):
|
||||
"""Test Doorbell Text select (unifi message)."""
|
||||
_, entity_id = ids_from_device_description(
|
||||
Platform.SELECT, camera, CAMERA_SELECTS[2]
|
||||
)
|
||||
|
||||
camera.__fields__["set_lcd_text"] = Mock()
|
||||
camera.set_lcd_text = AsyncMock()
|
||||
|
||||
await hass.services.async_call(
|
||||
"select",
|
||||
"select_option",
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
ATTR_OPTION: "LEAVE PACKAGE AT DOOR",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
camera.set_lcd_text.assert_called_once_with(
|
||||
DoorbellMessageType.LEAVE_PACKAGE_AT_DOOR
|
||||
)
|
||||
|
||||
await hass.services.async_call(
|
||||
"select",
|
||||
"select_option",
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
ATTR_OPTION: "Default Message (Welcome)",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
camera.set_lcd_text.assert_called_with(None)
|
||||
|
||||
|
||||
async def test_select_set_option_camera_doorbell_default(
|
||||
hass: HomeAssistant,
|
||||
camera: Camera,
|
||||
):
|
||||
"""Test Doorbell Text select (default message)."""
|
||||
_, entity_id = ids_from_device_description(
|
||||
Platform.SELECT, camera, CAMERA_SELECTS[2]
|
||||
)
|
||||
|
||||
camera.__fields__["set_lcd_text"] = Mock()
|
||||
camera.set_lcd_text = AsyncMock()
|
||||
|
||||
await hass.services.async_call(
|
||||
"select",
|
||||
"select_option",
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
ATTR_OPTION: "Default Message (Welcome)",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
camera.set_lcd_text.assert_called_once_with(None)
|
||||
|
||||
|
||||
async def test_select_set_option_viewer(
|
||||
hass: HomeAssistant,
|
||||
viewer: Viewer,
|
||||
):
|
||||
"""Test Liveview select."""
|
||||
_, entity_id = ids_from_device_description(
|
||||
Platform.SELECT, viewer, VIEWER_SELECTS[0]
|
||||
)
|
||||
|
||||
viewer.__fields__["set_liveview"] = Mock()
|
||||
viewer.set_liveview = AsyncMock()
|
||||
|
||||
liveview = list(viewer.api.bootstrap.liveviews.values())[0]
|
||||
|
||||
await hass.services.async_call(
|
||||
"select",
|
||||
"select_option",
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_OPTION: liveview.name},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
viewer.set_liveview.assert_called_once_with(liveview)
|
||||
|
||||
|
||||
async def test_select_service_doorbell_invalid(
|
||||
hass: HomeAssistant,
|
||||
camera: Camera,
|
||||
):
|
||||
"""Test Doorbell Text service (invalid)."""
|
||||
_, entity_id = ids_from_device_description(
|
||||
Platform.SELECT, camera, CAMERA_SELECTS[1]
|
||||
)
|
||||
|
||||
camera.__fields__["set_lcd_text"] = Mock()
|
||||
camera.set_lcd_text = AsyncMock()
|
||||
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
"unifiprotect",
|
||||
SERVICE_SET_DOORBELL_MESSAGE,
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_MESSAGE: "Test"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
camera.set_lcd_text.assert_not_called
|
||||
|
||||
|
||||
async def test_select_service_doorbell_success(
|
||||
hass: HomeAssistant,
|
||||
camera: Camera,
|
||||
):
|
||||
"""Test Doorbell Text service (success)."""
|
||||
_, entity_id = ids_from_device_description(
|
||||
Platform.SELECT, camera, CAMERA_SELECTS[2]
|
||||
)
|
||||
|
||||
camera.__fields__["set_lcd_text"] = Mock()
|
||||
camera.set_lcd_text = AsyncMock()
|
||||
|
||||
await hass.services.async_call(
|
||||
"unifiprotect",
|
||||
SERVICE_SET_DOORBELL_MESSAGE,
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
ATTR_MESSAGE: "Test",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
camera.set_lcd_text.assert_called_once_with(
|
||||
DoorbellMessageType.CUSTOM_MESSAGE, "Test", reset_at=None
|
||||
)
|
||||
|
||||
|
||||
@patch("homeassistant.components.unifiprotect.select.utcnow")
|
||||
async def test_select_service_doorbell_with_reset(
|
||||
mock_now,
|
||||
hass: HomeAssistant,
|
||||
camera: Camera,
|
||||
):
|
||||
"""Test Doorbell Text service (success with reset time)."""
|
||||
now = utcnow()
|
||||
mock_now.return_value = now
|
||||
|
||||
_, entity_id = ids_from_device_description(
|
||||
Platform.SELECT, camera, CAMERA_SELECTS[2]
|
||||
)
|
||||
|
||||
camera.__fields__["set_lcd_text"] = Mock()
|
||||
camera.set_lcd_text = AsyncMock()
|
||||
|
||||
await hass.services.async_call(
|
||||
"unifiprotect",
|
||||
SERVICE_SET_DOORBELL_MESSAGE,
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
ATTR_MESSAGE: "Test",
|
||||
ATTR_DURATION: 60,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
camera.set_lcd_text.assert_called_once_with(
|
||||
DoorbellMessageType.CUSTOM_MESSAGE,
|
||||
"Test",
|
||||
reset_at=now + timedelta(minutes=60),
|
||||
)
|
|
@ -42,7 +42,7 @@ async def light_fixture(
|
|||
light_obj.is_ssh_enabled = False
|
||||
light_obj.light_device_settings.is_indicator_enabled = False
|
||||
|
||||
mock_entry.api.bootstrap.cameras = {}
|
||||
mock_entry.api.bootstrap.reset_objects()
|
||||
mock_entry.api.bootstrap.lights = {
|
||||
light_obj.id: light_obj,
|
||||
}
|
||||
|
@ -91,7 +91,7 @@ async def camera_fixture(
|
|||
camera_obj.osd_settings.is_debug_enabled = False
|
||||
camera_obj.smart_detect_settings.object_types = []
|
||||
|
||||
mock_entry.api.bootstrap.lights = {}
|
||||
mock_entry.api.bootstrap.reset_objects()
|
||||
mock_entry.api.bootstrap.cameras = {
|
||||
camera_obj.id: camera_obj,
|
||||
}
|
||||
|
@ -134,7 +134,7 @@ async def camera_none_fixture(
|
|||
camera_obj.osd_settings.is_logo_enabled = False
|
||||
camera_obj.osd_settings.is_debug_enabled = False
|
||||
|
||||
mock_entry.api.bootstrap.lights = {}
|
||||
mock_entry.api.bootstrap.reset_objects()
|
||||
mock_entry.api.bootstrap.cameras = {
|
||||
camera_obj.id: camera_obj,
|
||||
}
|
||||
|
@ -178,7 +178,7 @@ async def camera_privacy_fixture(
|
|||
camera_obj.osd_settings.is_logo_enabled = False
|
||||
camera_obj.osd_settings.is_debug_enabled = False
|
||||
|
||||
mock_entry.api.bootstrap.lights = {}
|
||||
mock_entry.api.bootstrap.reset_objects()
|
||||
mock_entry.api.bootstrap.cameras = {
|
||||
camera_obj.id: camera_obj,
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue