mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 03:07:37 +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 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."""
|
||||||
|
@ -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 = {
|
||||||
|
@ -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",
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
|
||||||
|
@ -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
|
||||||
):
|
):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user