Minor bugfixes for UniFi Protect (#63475)

This commit is contained in:
Christopher Bailey 2022-01-08 11:49:55 -05:00 committed by GitHub
parent 51754f796b
commit 8860549ef2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 84 additions and 123 deletions

View File

@ -3,9 +3,8 @@ from __future__ import annotations
from copy import copy from copy import copy
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, timedelta
import logging import logging
from typing import Any, Final from typing import Any
from pyunifiprotect.data import NVR, Camera, Light, Sensor from pyunifiprotect.data import NVR, Camera, Light, Sensor
@ -13,17 +12,16 @@ from homeassistant.components.binary_sensor import (
DEVICE_CLASS_BATTERY, DEVICE_CLASS_BATTERY,
DEVICE_CLASS_DOOR, DEVICE_CLASS_DOOR,
DEVICE_CLASS_MOTION, DEVICE_CLASS_MOTION,
DEVICE_CLASS_OCCUPANCY,
DEVICE_CLASS_PROBLEM, DEVICE_CLASS_PROBLEM,
BinarySensorEntity, BinarySensorEntity,
BinarySensorEntityDescription, BinarySensorEntityDescription,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_LAST_TRIP_TIME, ATTR_MODEL from homeassistant.const import ATTR_LAST_TRIP_TIME, ATTR_MODEL
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback 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 from .const import DOMAIN
from .data import ProtectData from .data import ProtectData
@ -48,18 +46,15 @@ _KEY_DARK = "dark"
_KEY_BATTERY_LOW = "battery_low" _KEY_BATTERY_LOW = "battery_low"
_KEY_DISK_HEALTH = "disk_health" _KEY_DISK_HEALTH = "disk_health"
DEVICE_CLASS_RING: Final = "unifiprotect__ring"
RING_INTERVAL = timedelta(seconds=3)
CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = (
ProtectBinaryEntityDescription( ProtectBinaryEntityDescription(
key=_KEY_DOORBELL, key=_KEY_DOORBELL,
name="Doorbell Chime", name="Doorbell",
device_class=DEVICE_CLASS_RING, device_class=DEVICE_CLASS_OCCUPANCY,
icon="mdi:doorbell-video", icon="mdi:doorbell-video",
ufp_required_field="feature_flags.has_chime", ufp_required_field="feature_flags.has_chime",
ufp_value="last_ring", ufp_value="is_ringing",
), ),
ProtectBinaryEntityDescription( ProtectBinaryEntityDescription(
key=_KEY_DARK, key=_KEY_DARK,
@ -169,7 +164,6 @@ class ProtectDeviceBinarySensor(ProtectDeviceEntity, BinarySensorEntity):
self.device: Camera | Light | Sensor = device self.device: Camera | Light | Sensor = device
self.entity_description: ProtectBinaryEntityDescription = description self.entity_description: ProtectBinaryEntityDescription = description
super().__init__(data) super().__init__(data)
self._doorbell_callback: CALLBACK_TYPE | None = None
@callback @callback
def _async_update_extra_attrs_from_protect(self) -> dict[str, Any]: def _async_update_extra_attrs_from_protect(self) -> dict[str, Any]:
@ -202,46 +196,13 @@ class ProtectDeviceBinarySensor(ProtectDeviceEntity, BinarySensorEntity):
assert self.entity_description.ufp_value is not None assert self.entity_description.ufp_value is not None
self._attr_is_on = get_nested_attr(
self.device, self.entity_description.ufp_value
)
self._attr_extra_state_attributes = ( self._attr_extra_state_attributes = (
self._async_update_extra_attrs_from_protect() 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): class ProtectDiskBinarySensor(ProtectNVREntity, BinarySensorEntity):
"""A UniFi Protect NVR Disk Binary Sensor.""" """A UniFi Protect NVR Disk Binary Sensor."""

View File

@ -5,7 +5,7 @@ from collections.abc import Generator
import logging import logging
from pyunifiprotect.api import ProtectApiClient from pyunifiprotect.api import ProtectApiClient
from pyunifiprotect.data import Camera as UFPCamera from pyunifiprotect.data import Camera as UFPCamera, StateType
from pyunifiprotect.data.devices import CameraChannel from pyunifiprotect.data.devices import CameraChannel
from homeassistant.components.camera import SUPPORT_STREAM, Camera from homeassistant.components.camera import SUPPORT_STREAM, Camera
@ -137,9 +137,12 @@ class ProtectCamera(ProtectDeviceEntity, Camera):
super()._async_update_device_from_protect() super()._async_update_device_from_protect()
self.channel = self.device.channels[self.channel.id] self.channel = self.device.channels[self.channel.id]
self._attr_motion_detection_enabled = ( self._attr_motion_detection_enabled = (
self.device.is_connected and self.device.feature_flags.has_motion_zones self.device.state == StateType.CONNECTED
and self.device.feature_flags.has_motion_zones
)
self._attr_is_recording = (
self.device.state == StateType.CONNECTED and self.device.is_recording
) )
self._attr_is_recording = self.device.is_connected and self.device.is_recording
self._async_set_stream_source() self._async_set_stream_source()
self._attr_extra_state_attributes = { self._attr_extra_state_attributes = {

View File

@ -4,7 +4,7 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/unifiprotect", "documentation": "https://www.home-assistant.io/integrations/unifiprotect",
"requirements": [ "requirements": [
"pyunifiprotect==1.4.8" "pyunifiprotect==1.5.3"
], ],
"codeowners": [ "codeowners": [
"@briis", "@briis",

View File

@ -443,6 +443,9 @@ class ProtectNVRSensor(SensorValueMixin, ProtectNVREntity, SensorEntity):
# _KEY_MEMORY # _KEY_MEMORY
if self.entity_description.ufp_value is None: if self.entity_description.ufp_value is None:
memory = self.device.system_info.memory memory = self.device.system_info.memory
if memory.available is None or memory.total is None:
self._attr_available = False
return
value = (1 - memory.available / memory.total) * 100 value = (1 - memory.available / memory.total) * 100
else: else:
value = get_nested_attr(self.device, self.entity_description.ufp_value) value = get_nested_attr(self.device, self.entity_description.ufp_value)

View File

@ -2009,7 +2009,7 @@ pytrafikverket==0.1.6.2
pyudev==0.22.0 pyudev==0.22.0
# homeassistant.components.unifiprotect # homeassistant.components.unifiprotect
pyunifiprotect==1.4.8 pyunifiprotect==1.5.3
# homeassistant.components.uptimerobot # homeassistant.components.uptimerobot
pyuptimerobot==21.11.0 pyuptimerobot==21.11.0

View File

@ -1234,7 +1234,7 @@ pytrafikverket==0.1.6.2
pyudev==0.22.0 pyudev==0.22.0
# homeassistant.components.unifiprotect # homeassistant.components.unifiprotect
pyunifiprotect==1.4.8 pyunifiprotect==1.5.3
# homeassistant.components.uptimerobot # homeassistant.components.uptimerobot
pyuptimerobot==21.11.0 pyuptimerobot==21.11.0

View File

@ -2,9 +2,7 @@
# pylint: disable=protected-access # pylint: disable=protected-access
from __future__ import annotations from __future__ import annotations
from copy import copy
from datetime import datetime, timedelta from datetime import datetime, timedelta
from unittest.mock import Mock
import pytest import pytest
from pyunifiprotect.data import Camera, Light from pyunifiprotect.data import Camera, Light
@ -13,7 +11,6 @@ from pyunifiprotect.data.devices import Sensor
from homeassistant.components.unifiprotect.binary_sensor import ( from homeassistant.components.unifiprotect.binary_sensor import (
CAMERA_SENSORS, CAMERA_SENSORS,
LIGHT_SENSORS, LIGHT_SENSORS,
RING_INTERVAL,
SENSE_SENSORS, SENSE_SENSORS,
) )
from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION
@ -21,18 +18,15 @@ from homeassistant.const import (
ATTR_ATTRIBUTION, ATTR_ATTRIBUTION,
ATTR_LAST_TRIP_TIME, ATTR_LAST_TRIP_TIME,
STATE_OFF, STATE_OFF,
STATE_ON,
Platform, Platform,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.util.dt import utcnow
from .conftest import ( from .conftest import (
MockEntityFixture, MockEntityFixture,
assert_entity_counts, assert_entity_counts,
ids_from_device_description, ids_from_device_description,
time_changed,
) )
@ -209,22 +203,35 @@ async def test_binary_sensor_setup_camera_all(
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
for index, description in enumerate(CAMERA_SENSORS): description = CAMERA_SENSORS[0]
unique_id, entity_id = ids_from_device_description( unique_id, entity_id = ids_from_device_description(
Platform.BINARY_SENSOR, camera, description Platform.BINARY_SENSOR, camera, description
) )
entity = entity_registry.async_get(entity_id) entity = entity_registry.async_get(entity_id)
assert entity assert entity
assert entity.unique_id == unique_id assert entity.unique_id == unique_id
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
assert state assert state
assert state.state == STATE_OFF assert state.state == STATE_OFF
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
if index == 0: assert state.attributes[ATTR_LAST_TRIP_TIME] == now - timedelta(hours=1)
assert state.attributes[ATTR_LAST_TRIP_TIME] == now - timedelta(hours=1)
description = CAMERA_SENSORS[1]
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
async def test_binary_sensor_setup_camera_none( async def test_binary_sensor_setup_camera_none(
@ -274,52 +281,3 @@ async def test_binary_sensor_setup_sensor(
if index != 1: if index != 1:
assert state.attributes[ATTR_LAST_TRIP_TIME] == expected_trip_time 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

View File

@ -18,7 +18,7 @@ from homeassistant.components.unifiprotect.sensor import (
NVR_SENSORS, NVR_SENSORS,
SENSE_SENSORS, SENSE_SENSORS,
) )
from homeassistant.const import ATTR_ATTRIBUTION, Platform from homeassistant.const import ATTR_ATTRIBUTION, STATE_UNAVAILABLE, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
@ -232,6 +232,42 @@ async def test_sensor_setup_nvr(
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
async def test_sensor_nvr_memory_unavaiable(
hass: HomeAssistant, mock_entry: MockEntityFixture, now: datetime
):
"""Test memory sensor for NVR if no data available."""
mock_entry.api.bootstrap.reset_objects()
nvr: NVR = mock_entry.api.bootstrap.nvr
nvr.system_info.memory.available = None
nvr.system_info.memory.total = None
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
await hass.async_block_till_done()
# 2 from all, 4 from sense, 12 NVR
assert_entity_counts(hass, Platform.SENSOR, 12, 9)
entity_registry = er.async_get(hass)
description = NVR_DISABLED_SENSORS[2]
unique_id, entity_id = ids_from_device_description(
Platform.SENSOR, nvr, description
)
entity = entity_registry.async_get(entity_id)
assert entity
assert entity.disabled is not description.entity_registry_enabled_default
assert entity.unique_id == unique_id
await enable_entity(hass, mock_entry.entry.entry_id, entity_id)
state = hass.states.get(entity_id)
assert state
assert state.state == STATE_UNAVAILABLE
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
async def test_sensor_setup_camera( async def test_sensor_setup_camera(
hass: HomeAssistant, mock_entry: MockEntityFixture, camera: Camera, now: datetime hass: HomeAssistant, mock_entry: MockEntityFixture, camera: Camera, now: datetime
): ):