mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
Minor bugfixes for UniFi Protect (#63475)
This commit is contained in:
parent
51754f796b
commit
8860549ef2
@ -3,9 +3,8 @@ from __future__ import annotations
|
||||
|
||||
from copy import copy
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from typing import Any, Final
|
||||
from typing import Any
|
||||
|
||||
from pyunifiprotect.data import NVR, Camera, Light, Sensor
|
||||
|
||||
@ -13,17 +12,16 @@ from homeassistant.components.binary_sensor import (
|
||||
DEVICE_CLASS_BATTERY,
|
||||
DEVICE_CLASS_DOOR,
|
||||
DEVICE_CLASS_MOTION,
|
||||
DEVICE_CLASS_OCCUPANCY,
|
||||
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.core import 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
|
||||
from .data import ProtectData
|
||||
@ -48,18 +46,15 @@ _KEY_DARK = "dark"
|
||||
_KEY_BATTERY_LOW = "battery_low"
|
||||
_KEY_DISK_HEALTH = "disk_health"
|
||||
|
||||
DEVICE_CLASS_RING: Final = "unifiprotect__ring"
|
||||
RING_INTERVAL = timedelta(seconds=3)
|
||||
|
||||
|
||||
CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = (
|
||||
ProtectBinaryEntityDescription(
|
||||
key=_KEY_DOORBELL,
|
||||
name="Doorbell Chime",
|
||||
device_class=DEVICE_CLASS_RING,
|
||||
name="Doorbell",
|
||||
device_class=DEVICE_CLASS_OCCUPANCY,
|
||||
icon="mdi:doorbell-video",
|
||||
ufp_required_field="feature_flags.has_chime",
|
||||
ufp_value="last_ring",
|
||||
ufp_value="is_ringing",
|
||||
),
|
||||
ProtectBinaryEntityDescription(
|
||||
key=_KEY_DARK,
|
||||
@ -169,7 +164,6 @@ class ProtectDeviceBinarySensor(ProtectDeviceEntity, BinarySensorEntity):
|
||||
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]:
|
||||
@ -202,45 +196,12 @@ class ProtectDeviceBinarySensor(ProtectDeviceEntity, BinarySensorEntity):
|
||||
|
||||
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()
|
||||
self._attr_extra_state_attributes = (
|
||||
self._async_update_extra_attrs_from_protect()
|
||||
)
|
||||
|
||||
|
||||
class ProtectDiskBinarySensor(ProtectNVREntity, BinarySensorEntity):
|
||||
|
@ -5,7 +5,7 @@ from collections.abc import Generator
|
||||
import logging
|
||||
|
||||
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 homeassistant.components.camera import SUPPORT_STREAM, Camera
|
||||
@ -137,9 +137,12 @@ class ProtectCamera(ProtectDeviceEntity, Camera):
|
||||
super()._async_update_device_from_protect()
|
||||
self.channel = self.device.channels[self.channel.id]
|
||||
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._attr_extra_state_attributes = {
|
||||
|
@ -4,7 +4,7 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/unifiprotect",
|
||||
"requirements": [
|
||||
"pyunifiprotect==1.4.8"
|
||||
"pyunifiprotect==1.5.3"
|
||||
],
|
||||
"codeowners": [
|
||||
"@briis",
|
||||
|
@ -443,6 +443,9 @@ class ProtectNVRSensor(SensorValueMixin, ProtectNVREntity, SensorEntity):
|
||||
# _KEY_MEMORY
|
||||
if self.entity_description.ufp_value is None:
|
||||
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
|
||||
else:
|
||||
value = get_nested_attr(self.device, self.entity_description.ufp_value)
|
||||
|
@ -2009,7 +2009,7 @@ pytrafikverket==0.1.6.2
|
||||
pyudev==0.22.0
|
||||
|
||||
# homeassistant.components.unifiprotect
|
||||
pyunifiprotect==1.4.8
|
||||
pyunifiprotect==1.5.3
|
||||
|
||||
# homeassistant.components.uptimerobot
|
||||
pyuptimerobot==21.11.0
|
||||
|
@ -1234,7 +1234,7 @@ pytrafikverket==0.1.6.2
|
||||
pyudev==0.22.0
|
||||
|
||||
# homeassistant.components.unifiprotect
|
||||
pyunifiprotect==1.4.8
|
||||
pyunifiprotect==1.5.3
|
||||
|
||||
# homeassistant.components.uptimerobot
|
||||
pyuptimerobot==21.11.0
|
||||
|
@ -2,9 +2,7 @@
|
||||
# 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
|
||||
@ -13,7 +11,6 @@ from pyunifiprotect.data.devices import Sensor
|
||||
from homeassistant.components.unifiprotect.binary_sensor import (
|
||||
CAMERA_SENSORS,
|
||||
LIGHT_SENSORS,
|
||||
RING_INTERVAL,
|
||||
SENSE_SENSORS,
|
||||
)
|
||||
from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION
|
||||
@ -21,18 +18,15 @@ 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,
|
||||
)
|
||||
|
||||
|
||||
@ -209,7 +203,7 @@ async def test_binary_sensor_setup_camera_all(
|
||||
|
||||
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(
|
||||
Platform.BINARY_SENSOR, camera, description
|
||||
)
|
||||
@ -223,9 +217,22 @@ async def test_binary_sensor_setup_camera_all(
|
||||
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)
|
||||
|
||||
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(
|
||||
hass: HomeAssistant,
|
||||
@ -274,52 +281,3 @@ async def test_binary_sensor_setup_sensor(
|
||||
|
||||
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
|
||||
|
@ -18,7 +18,7 @@ from homeassistant.components.unifiprotect.sensor import (
|
||||
NVR_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.helpers import entity_registry as er
|
||||
|
||||
@ -232,6 +232,42 @@ async def test_sensor_setup_nvr(
|
||||
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(
|
||||
hass: HomeAssistant, mock_entry: MockEntityFixture, camera: Camera, now: datetime
|
||||
):
|
||||
|
Loading…
x
Reference in New Issue
Block a user