mirror of
https://github.com/home-assistant/core.git
synced 2025-07-18 10:47:10 +00:00
Add UniFi Protect binary_sensor platform (#63489)
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
00e7421e3a
commit
4e56217b89
273
homeassistant/components/unifiprotect/binary_sensor.py
Normal file
273
homeassistant/components/unifiprotect/binary_sensor.py
Normal file
@ -0,0 +1,273 @@
|
|||||||
|
"""This component provides binary sensors for UniFi Protect."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from copy import copy
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
|
import logging
|
||||||
|
from typing import Any, Final
|
||||||
|
|
||||||
|
from pyunifiprotect.data import NVR, Camera, Light, Sensor
|
||||||
|
|
||||||
|
from homeassistant.components.binary_sensor import (
|
||||||
|
DEVICE_CLASS_BATTERY,
|
||||||
|
DEVICE_CLASS_DOOR,
|
||||||
|
DEVICE_CLASS_MOTION,
|
||||||
|
DEVICE_CLASS_PROBLEM,
|
||||||
|
BinarySensorEntity,
|
||||||
|
BinarySensorEntityDescription,
|
||||||
|
)
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import ATTR_LAST_TRIP_TIME, ATTR_MODEL
|
||||||
|
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||||
|
from homeassistant.helpers.entity import EntityCategory
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.helpers.event import async_call_later
|
||||||
|
from homeassistant.util.dt import utcnow
|
||||||
|
|
||||||
|
from .const import DOMAIN, RING_INTERVAL
|
||||||
|
from .data import ProtectData
|
||||||
|
from .entity import ProtectDeviceEntity, ProtectNVREntity, async_all_device_entities
|
||||||
|
from .models import ProtectRequiredKeysMixin
|
||||||
|
from .utils import get_nested_attr
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ProtectBinaryEntityDescription(
|
||||||
|
ProtectRequiredKeysMixin, BinarySensorEntityDescription
|
||||||
|
):
|
||||||
|
"""Describes UniFi Protect Binary Sensor entity."""
|
||||||
|
|
||||||
|
|
||||||
|
_KEY_DOORBELL = "doorbell"
|
||||||
|
_KEY_MOTION = "motion"
|
||||||
|
_KEY_DOOR = "door"
|
||||||
|
_KEY_DARK = "dark"
|
||||||
|
_KEY_BATTERY_LOW = "battery_low"
|
||||||
|
_KEY_DISK_HEALTH = "disk_health"
|
||||||
|
|
||||||
|
DEVICE_CLASS_RING: Final = "unifiprotect__ring"
|
||||||
|
|
||||||
|
|
||||||
|
CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = (
|
||||||
|
ProtectBinaryEntityDescription(
|
||||||
|
key=_KEY_DOORBELL,
|
||||||
|
name="Doorbell Chime",
|
||||||
|
device_class=DEVICE_CLASS_RING,
|
||||||
|
icon="mdi:doorbell-video",
|
||||||
|
ufp_required_field="feature_flags.has_chime",
|
||||||
|
ufp_value="last_ring",
|
||||||
|
),
|
||||||
|
ProtectBinaryEntityDescription(
|
||||||
|
key=_KEY_DARK,
|
||||||
|
name="Is Dark",
|
||||||
|
icon="mdi:brightness-6",
|
||||||
|
ufp_value="is_dark",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
LIGHT_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = (
|
||||||
|
ProtectBinaryEntityDescription(
|
||||||
|
key=_KEY_DARK,
|
||||||
|
name="Is Dark",
|
||||||
|
icon="mdi:brightness-6",
|
||||||
|
ufp_value="is_dark",
|
||||||
|
),
|
||||||
|
ProtectBinaryEntityDescription(
|
||||||
|
key=_KEY_MOTION,
|
||||||
|
name="Motion Detected",
|
||||||
|
device_class=DEVICE_CLASS_MOTION,
|
||||||
|
ufp_value="is_pir_motion_detected",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
SENSE_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = (
|
||||||
|
ProtectBinaryEntityDescription(
|
||||||
|
key=_KEY_DOOR,
|
||||||
|
name="Door",
|
||||||
|
device_class=DEVICE_CLASS_DOOR,
|
||||||
|
ufp_value="is_opened",
|
||||||
|
),
|
||||||
|
ProtectBinaryEntityDescription(
|
||||||
|
key=_KEY_BATTERY_LOW,
|
||||||
|
name="Battery low",
|
||||||
|
device_class=DEVICE_CLASS_BATTERY,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
ufp_value="battery_status.is_low",
|
||||||
|
),
|
||||||
|
ProtectBinaryEntityDescription(
|
||||||
|
key=_KEY_MOTION,
|
||||||
|
name="Motion Detected",
|
||||||
|
device_class=DEVICE_CLASS_MOTION,
|
||||||
|
ufp_value="is_motion_detected",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
DISK_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = (
|
||||||
|
ProtectBinaryEntityDescription(
|
||||||
|
key=_KEY_DISK_HEALTH,
|
||||||
|
name="Disk {index} Health",
|
||||||
|
device_class=DEVICE_CLASS_PROBLEM,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up binary sensors for UniFi Protect integration."""
|
||||||
|
data: ProtectData = hass.data[DOMAIN][entry.entry_id]
|
||||||
|
entities: list[ProtectDeviceEntity] = async_all_device_entities(
|
||||||
|
data,
|
||||||
|
ProtectDeviceBinarySensor,
|
||||||
|
camera_descs=CAMERA_SENSORS,
|
||||||
|
light_descs=LIGHT_SENSORS,
|
||||||
|
sense_descs=SENSE_SENSORS,
|
||||||
|
)
|
||||||
|
entities += _async_nvr_entities(data)
|
||||||
|
|
||||||
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_nvr_entities(
|
||||||
|
data: ProtectData,
|
||||||
|
) -> list[ProtectDeviceEntity]:
|
||||||
|
entities: list[ProtectDeviceEntity] = []
|
||||||
|
device = data.api.bootstrap.nvr
|
||||||
|
for index, _ in enumerate(device.system_info.storage.devices):
|
||||||
|
for description in DISK_SENSORS:
|
||||||
|
entities.append(
|
||||||
|
ProtectDiskBinarySensor(data, device, description, index=index)
|
||||||
|
)
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Adding binary sensor entity %s",
|
||||||
|
(description.name or "{index}").format(index=index),
|
||||||
|
)
|
||||||
|
|
||||||
|
return entities
|
||||||
|
|
||||||
|
|
||||||
|
class ProtectDeviceBinarySensor(ProtectDeviceEntity, BinarySensorEntity):
|
||||||
|
"""A UniFi Protect Device Binary Sensor."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
data: ProtectData,
|
||||||
|
description: ProtectBinaryEntityDescription,
|
||||||
|
device: Camera | Light | Sensor | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the Binary Sensor."""
|
||||||
|
|
||||||
|
if device and not hasattr(self, "device"):
|
||||||
|
self.device: Camera | Light | Sensor = device
|
||||||
|
self.entity_description: ProtectBinaryEntityDescription = description
|
||||||
|
super().__init__(data)
|
||||||
|
self._doorbell_callback: CALLBACK_TYPE | None = None
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_update_extra_attrs_from_protect(self) -> dict[str, Any]:
|
||||||
|
attrs: dict[str, Any] = {}
|
||||||
|
key = self.entity_description.key
|
||||||
|
|
||||||
|
if key == _KEY_DARK:
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
if key == _KEY_DOORBELL:
|
||||||
|
assert isinstance(self.device, Camera)
|
||||||
|
attrs[ATTR_LAST_TRIP_TIME] = self.device.last_ring
|
||||||
|
elif isinstance(self.device, Sensor):
|
||||||
|
if key in (_KEY_MOTION, _KEY_DOOR):
|
||||||
|
if key == _KEY_MOTION:
|
||||||
|
last_trip = self.device.motion_detected_at
|
||||||
|
else:
|
||||||
|
last_trip = self.device.open_status_changed_at
|
||||||
|
|
||||||
|
attrs[ATTR_LAST_TRIP_TIME] = last_trip
|
||||||
|
elif isinstance(self.device, Light):
|
||||||
|
if key == _KEY_MOTION:
|
||||||
|
attrs[ATTR_LAST_TRIP_TIME] = self.device.last_motion
|
||||||
|
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_update_device_from_protect(self) -> None:
|
||||||
|
super()._async_update_device_from_protect()
|
||||||
|
|
||||||
|
assert self.entity_description.ufp_value is not None
|
||||||
|
|
||||||
|
self._attr_extra_state_attributes = (
|
||||||
|
self._async_update_extra_attrs_from_protect()
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.entity_description.key == _KEY_DOORBELL:
|
||||||
|
last_ring = get_nested_attr(self.device, self.entity_description.ufp_value)
|
||||||
|
now = utcnow()
|
||||||
|
|
||||||
|
is_ringing = (
|
||||||
|
False if last_ring is None else (now - last_ring) < RING_INTERVAL
|
||||||
|
)
|
||||||
|
_LOGGER.warning("%s, %s, %s", last_ring, now, is_ringing)
|
||||||
|
if is_ringing:
|
||||||
|
self._async_cancel_doorbell_callback()
|
||||||
|
self._doorbell_callback = async_call_later(
|
||||||
|
self.hass, RING_INTERVAL, self._async_reset_doorbell
|
||||||
|
)
|
||||||
|
self._attr_is_on = is_ringing
|
||||||
|
else:
|
||||||
|
self._attr_is_on = get_nested_attr(
|
||||||
|
self.device, self.entity_description.ufp_value
|
||||||
|
)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_cancel_doorbell_callback(self) -> None:
|
||||||
|
if self._doorbell_callback is not None:
|
||||||
|
_LOGGER.debug("Canceling doorbell ring callback")
|
||||||
|
self._doorbell_callback()
|
||||||
|
self._doorbell_callback = None
|
||||||
|
|
||||||
|
async def _async_reset_doorbell(self, now: datetime) -> None:
|
||||||
|
_LOGGER.debug("Doorbell ring ended")
|
||||||
|
self._doorbell_callback = None
|
||||||
|
self._async_updated_event()
|
||||||
|
|
||||||
|
async def async_will_remove_from_hass(self) -> None:
|
||||||
|
"""Run when entity will be removed from hass."""
|
||||||
|
self._async_cancel_doorbell_callback()
|
||||||
|
return await super().async_will_remove_from_hass()
|
||||||
|
|
||||||
|
|
||||||
|
class ProtectDiskBinarySensor(ProtectNVREntity, BinarySensorEntity):
|
||||||
|
"""A UniFi Protect NVR Disk Binary Sensor."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
data: ProtectData,
|
||||||
|
device: NVR,
|
||||||
|
description: ProtectBinaryEntityDescription,
|
||||||
|
index: int,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the Binary Sensor."""
|
||||||
|
description = copy(description)
|
||||||
|
description.key = f"{description.key}_{index}"
|
||||||
|
description.name = (description.name or "{index}").format(index=index)
|
||||||
|
self._index = index
|
||||||
|
self.entity_description: ProtectBinaryEntityDescription = description
|
||||||
|
super().__init__(data, device)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_update_device_from_protect(self) -> None:
|
||||||
|
super()._async_update_device_from_protect()
|
||||||
|
|
||||||
|
disks = self.device.system_info.storage.devices
|
||||||
|
disk_available = len(disks) > self._index
|
||||||
|
self._attr_available = self._attr_available and disk_available
|
||||||
|
if disk_available:
|
||||||
|
disk = disks[self._index]
|
||||||
|
self._attr_is_on = not disk.healthy
|
||||||
|
self._attr_extra_state_attributes = {ATTR_MODEL: disk.model}
|
@ -1,5 +1,7 @@
|
|||||||
"""Constant definitions for UniFi Protect Integration."""
|
"""Constant definitions for UniFi Protect Integration."""
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
from pyunifiprotect.data.types import ModelType, Version
|
from pyunifiprotect.data.types import ModelType, Version
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
@ -32,6 +34,8 @@ DEFAULT_BRAND = "Ubiquiti"
|
|||||||
DEFAULT_SCAN_INTERVAL = 5
|
DEFAULT_SCAN_INTERVAL = 5
|
||||||
DEFAULT_VERIFY_SSL = False
|
DEFAULT_VERIFY_SSL = False
|
||||||
|
|
||||||
|
RING_INTERVAL = timedelta(seconds=3)
|
||||||
|
|
||||||
DEVICE_TYPE_CAMERA = "camera"
|
DEVICE_TYPE_CAMERA = "camera"
|
||||||
DEVICES_THAT_ADOPT = {
|
DEVICES_THAT_ADOPT = {
|
||||||
ModelType.CAMERA,
|
ModelType.CAMERA,
|
||||||
@ -50,6 +54,7 @@ SERVICE_SET_DOORBELL_MESSAGE = "set_doorbell_message"
|
|||||||
TYPE_EMPTY_VALUE = ""
|
TYPE_EMPTY_VALUE = ""
|
||||||
|
|
||||||
PLATFORMS = [
|
PLATFORMS = [
|
||||||
|
Platform.BINARY_SENSOR,
|
||||||
Platform.BUTTON,
|
Platform.BUTTON,
|
||||||
Platform.CAMERA,
|
Platform.CAMERA,
|
||||||
Platform.LIGHT,
|
Platform.LIGHT,
|
||||||
|
@ -13,6 +13,7 @@ from pyunifiprotect.data import (
|
|||||||
StateType,
|
StateType,
|
||||||
Viewer,
|
Viewer,
|
||||||
)
|
)
|
||||||
|
from pyunifiprotect.data.nvr import NVR
|
||||||
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
import homeassistant.helpers.device_registry as dr
|
import homeassistant.helpers.device_registry as dr
|
||||||
@ -168,3 +169,37 @@ class ProtectDeviceEntity(Entity):
|
|||||||
self.device.id, self._async_updated_event
|
self.device.id, self._async_updated_event
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ProtectNVREntity(ProtectDeviceEntity):
|
||||||
|
"""Base class for unifi protect entities."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
entry: ProtectData,
|
||||||
|
device: NVR,
|
||||||
|
description: EntityDescription | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the entity."""
|
||||||
|
# ProtectNVREntity is intentionally a separate base class
|
||||||
|
self.device: NVR = device # type: ignore
|
||||||
|
super().__init__(entry, description=description)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_set_device_info(self) -> None:
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
connections={(dr.CONNECTION_NETWORK_MAC, self.device.mac)},
|
||||||
|
identifiers={(DOMAIN, self.device.mac)},
|
||||||
|
manufacturer=DEFAULT_BRAND,
|
||||||
|
name=self.device.name,
|
||||||
|
model=self.device.type,
|
||||||
|
sw_version=str(self.device.version),
|
||||||
|
configuration_url=self.device.api.base_url,
|
||||||
|
)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_update_device_from_protect(self) -> None:
|
||||||
|
if self.data.last_update_success:
|
||||||
|
self.device = self.data.api.bootstrap.nvr
|
||||||
|
|
||||||
|
self._attr_available = self.data.last_update_success
|
||||||
|
@ -177,6 +177,12 @@ def mock_sensor():
|
|||||||
return Sensor.from_unifi_dict(**data)
|
return Sensor.from_unifi_dict(**data)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def now():
|
||||||
|
"""Return datetime object that will be consistent throughout test."""
|
||||||
|
return dt_util.utcnow()
|
||||||
|
|
||||||
|
|
||||||
async def time_changed(hass: HomeAssistant, seconds: int) -> None:
|
async def time_changed(hass: HomeAssistant, seconds: int) -> None:
|
||||||
"""Trigger time changed."""
|
"""Trigger time changed."""
|
||||||
next_update = dt_util.utcnow() + timedelta(seconds)
|
next_update = dt_util.utcnow() + timedelta(seconds)
|
||||||
|
327
tests/components/unifiprotect/test_binary_sensor.py
Normal file
327
tests/components/unifiprotect/test_binary_sensor.py
Normal file
@ -0,0 +1,327 @@
|
|||||||
|
"""Test the UniFi Protect binary_sensor platform."""
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from copy import copy
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from unittest.mock import Mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from pyunifiprotect.data import Camera, Light
|
||||||
|
from pyunifiprotect.data.devices import Sensor
|
||||||
|
|
||||||
|
from homeassistant.components.unifiprotect.binary_sensor import (
|
||||||
|
CAMERA_SENSORS,
|
||||||
|
LIGHT_SENSORS,
|
||||||
|
SENSE_SENSORS,
|
||||||
|
)
|
||||||
|
from homeassistant.components.unifiprotect.const import (
|
||||||
|
DEFAULT_ATTRIBUTION,
|
||||||
|
RING_INTERVAL,
|
||||||
|
)
|
||||||
|
from homeassistant.const import (
|
||||||
|
ATTR_ATTRIBUTION,
|
||||||
|
ATTR_LAST_TRIP_TIME,
|
||||||
|
STATE_OFF,
|
||||||
|
STATE_ON,
|
||||||
|
Platform,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
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,
|
||||||
|
time_changed,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="camera")
|
||||||
|
async def camera_fixture(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_entry: MockEntityFixture,
|
||||||
|
mock_camera: Camera,
|
||||||
|
now: datetime,
|
||||||
|
):
|
||||||
|
"""Fixture for a single camera for testing the binary_sensor 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_chime = True
|
||||||
|
camera_obj.last_ring = now - timedelta(hours=1)
|
||||||
|
camera_obj.is_dark = False
|
||||||
|
|
||||||
|
mock_entry.api.bootstrap.reset_objects()
|
||||||
|
mock_entry.api.bootstrap.nvr.system_info.storage.devices = []
|
||||||
|
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.BINARY_SENSOR, 2, 2)
|
||||||
|
|
||||||
|
yield camera_obj
|
||||||
|
|
||||||
|
Camera.__config__.validate_assignment = True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="light")
|
||||||
|
async def light_fixture(
|
||||||
|
hass: HomeAssistant, mock_entry: MockEntityFixture, mock_light: Light, now: datetime
|
||||||
|
):
|
||||||
|
"""Fixture for a single light for testing the binary_sensor 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.is_dark = False
|
||||||
|
light_obj.is_pir_motion_detected = False
|
||||||
|
light_obj.last_motion = now - timedelta(hours=1)
|
||||||
|
|
||||||
|
mock_entry.api.bootstrap.reset_objects()
|
||||||
|
mock_entry.api.bootstrap.nvr.system_info.storage.devices = []
|
||||||
|
mock_entry.api.bootstrap.lights = {
|
||||||
|
light_obj.id: light_obj,
|
||||||
|
}
|
||||||
|
|
||||||
|
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert_entity_counts(hass, Platform.BINARY_SENSOR, 2, 2)
|
||||||
|
|
||||||
|
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 binary_sensor 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_chime = False
|
||||||
|
camera_obj.is_dark = False
|
||||||
|
|
||||||
|
mock_entry.api.bootstrap.reset_objects()
|
||||||
|
mock_entry.api.bootstrap.nvr.system_info.storage.devices = []
|
||||||
|
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.BINARY_SENSOR, 1, 1)
|
||||||
|
|
||||||
|
yield camera_obj
|
||||||
|
|
||||||
|
Camera.__config__.validate_assignment = True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="sensor")
|
||||||
|
async def sensor_fixture(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_entry: MockEntityFixture,
|
||||||
|
mock_sensor: Sensor,
|
||||||
|
now: datetime,
|
||||||
|
):
|
||||||
|
"""Fixture for a single sensor for testing the binary_sensor platform."""
|
||||||
|
|
||||||
|
# disable pydantic validation so mocking can happen
|
||||||
|
Sensor.__config__.validate_assignment = False
|
||||||
|
|
||||||
|
sensor_obj = mock_sensor.copy(deep=True)
|
||||||
|
sensor_obj._api = mock_entry.api
|
||||||
|
sensor_obj.name = "Test Sensor"
|
||||||
|
sensor_obj.is_opened = False
|
||||||
|
sensor_obj.battery_status.is_low = False
|
||||||
|
sensor_obj.is_motion_detected = False
|
||||||
|
sensor_obj.motion_detected_at = now - timedelta(hours=1)
|
||||||
|
sensor_obj.open_status_changed_at = now - timedelta(hours=1)
|
||||||
|
|
||||||
|
mock_entry.api.bootstrap.reset_objects()
|
||||||
|
mock_entry.api.bootstrap.nvr.system_info.storage.devices = []
|
||||||
|
mock_entry.api.bootstrap.sensors = {
|
||||||
|
sensor_obj.id: sensor_obj,
|
||||||
|
}
|
||||||
|
|
||||||
|
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert_entity_counts(hass, Platform.BINARY_SENSOR, 3, 3)
|
||||||
|
|
||||||
|
yield sensor_obj
|
||||||
|
|
||||||
|
Sensor.__config__.validate_assignment = True
|
||||||
|
|
||||||
|
|
||||||
|
async def test_binary_sensor_setup_light(
|
||||||
|
hass: HomeAssistant, light: Light, now: datetime
|
||||||
|
):
|
||||||
|
"""Test binary_sensor entity setup for light devices."""
|
||||||
|
|
||||||
|
entity_registry = er.async_get(hass)
|
||||||
|
|
||||||
|
for index, description in enumerate(LIGHT_SENSORS):
|
||||||
|
unique_id, entity_id = ids_from_device_description(
|
||||||
|
Platform.BINARY_SENSOR, 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 == STATE_OFF
|
||||||
|
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
|
||||||
|
|
||||||
|
if index == 1:
|
||||||
|
assert state.attributes[ATTR_LAST_TRIP_TIME] == now - timedelta(hours=1)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_binary_sensor_setup_camera_all(
|
||||||
|
hass: HomeAssistant, camera: Camera, now: datetime
|
||||||
|
):
|
||||||
|
"""Test binary_sensor entity setup for camera devices (all features)."""
|
||||||
|
|
||||||
|
entity_registry = er.async_get(hass)
|
||||||
|
|
||||||
|
for index, description in enumerate(CAMERA_SENSORS):
|
||||||
|
unique_id, entity_id = ids_from_device_description(
|
||||||
|
Platform.BINARY_SENSOR, 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 == STATE_OFF
|
||||||
|
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
|
||||||
|
|
||||||
|
if index == 0:
|
||||||
|
assert state.attributes[ATTR_LAST_TRIP_TIME] == now - timedelta(hours=1)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_binary_sensor_setup_camera_none(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
camera_none: Camera,
|
||||||
|
):
|
||||||
|
"""Test binary_sensor entity setup for camera devices (no features)."""
|
||||||
|
|
||||||
|
entity_registry = er.async_get(hass)
|
||||||
|
description = CAMERA_SENSORS[1]
|
||||||
|
|
||||||
|
unique_id, entity_id = ids_from_device_description(
|
||||||
|
Platform.BINARY_SENSOR, 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 == STATE_OFF
|
||||||
|
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
|
||||||
|
|
||||||
|
|
||||||
|
async def test_binary_sensor_setup_sensor(
|
||||||
|
hass: HomeAssistant, sensor: Sensor, now: datetime
|
||||||
|
):
|
||||||
|
"""Test binary_sensor entity setup for sensor devices."""
|
||||||
|
|
||||||
|
entity_registry = er.async_get(hass)
|
||||||
|
|
||||||
|
expected_trip_time = now - timedelta(hours=1)
|
||||||
|
for index, description in enumerate(SENSE_SENSORS):
|
||||||
|
unique_id, entity_id = ids_from_device_description(
|
||||||
|
Platform.BINARY_SENSOR, sensor, 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 == STATE_OFF
|
||||||
|
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
|
||||||
|
|
||||||
|
if index != 1:
|
||||||
|
assert state.attributes[ATTR_LAST_TRIP_TIME] == expected_trip_time
|
||||||
|
|
||||||
|
|
||||||
|
async def test_binary_sensor_update_doorbell(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_entry: MockEntityFixture,
|
||||||
|
camera: Camera,
|
||||||
|
):
|
||||||
|
"""Test select entity update (change doorbell message)."""
|
||||||
|
|
||||||
|
_, entity_id = ids_from_device_description(
|
||||||
|
Platform.BINARY_SENSOR, camera, CAMERA_SENSORS[0]
|
||||||
|
)
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state
|
||||||
|
assert state.state == STATE_OFF
|
||||||
|
|
||||||
|
new_bootstrap = copy(mock_entry.api.bootstrap)
|
||||||
|
new_camera = camera.copy()
|
||||||
|
new_camera.last_ring = utcnow()
|
||||||
|
|
||||||
|
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 == STATE_ON
|
||||||
|
|
||||||
|
# fire event a second time for code coverage (cancel existing)
|
||||||
|
mock_entry.api.ws_subscription(mock_msg)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state
|
||||||
|
assert state.state == STATE_ON
|
||||||
|
|
||||||
|
# since time is not really changing, switch the last ring back to allow turn off
|
||||||
|
new_camera.last_ring = utcnow() - RING_INTERVAL
|
||||||
|
await time_changed(hass, RING_INTERVAL.total_seconds())
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state
|
||||||
|
assert state.state == STATE_OFF
|
Loading…
x
Reference in New Issue
Block a user