diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index 91401700376..36b8d9a8a01 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -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,46 +196,13 @@ class ProtectDeviceBinarySensor(ProtectDeviceEntity, BinarySensorEntity): 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._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.""" diff --git a/homeassistant/components/unifiprotect/camera.py b/homeassistant/components/unifiprotect/camera.py index 95bc630e58d..e4598b76f44 100644 --- a/homeassistant/components/unifiprotect/camera.py +++ b/homeassistant/components/unifiprotect/camera.py @@ -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 = { diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 3ada51a110d..c69c0a4c950 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -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", diff --git a/homeassistant/components/unifiprotect/sensor.py b/homeassistant/components/unifiprotect/sensor.py index e46fdeb787b..cfce716f0b4 100644 --- a/homeassistant/components/unifiprotect/sensor.py +++ b/homeassistant/components/unifiprotect/sensor.py @@ -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) diff --git a/requirements_all.txt b/requirements_all.txt index c2948d12bda..f20abce1ffc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fdf05296115..f684cd2d476 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 diff --git a/tests/components/unifiprotect/test_binary_sensor.py b/tests/components/unifiprotect/test_binary_sensor.py index 52030ef768b..3414ec18848 100644 --- a/tests/components/unifiprotect/test_binary_sensor.py +++ b/tests/components/unifiprotect/test_binary_sensor.py @@ -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,22 +203,35 @@ async def test_binary_sensor_setup_camera_all( 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 - ) + description = CAMERA_SENSORS[0] + 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 + 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 + 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) + 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( @@ -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 diff --git a/tests/components/unifiprotect/test_sensor.py b/tests/components/unifiprotect/test_sensor.py index 49f6dfcd971..9d901cad3a0 100644 --- a/tests/components/unifiprotect/test_sensor.py +++ b/tests/components/unifiprotect/test_sensor.py @@ -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 ):