1
0
mirror of https://github.com/home-assistant/core synced 2024-07-01 07:15:04 +00:00

Add event platform to unifiprotect (#120681)

* Add event platform to unifiprotect

* Add event platform to unifiprotect

* Add event platform to unifiprotect

* Add event platform to unifiprotect

* adjust

* tweaks

* translations

* coverage

* coverage

* Update tests/components/unifiprotect/test_event.py
This commit is contained in:
J. Nick Koston 2024-06-29 00:49:14 -05:00 committed by GitHub
parent 0dfb5bd7d9
commit 2cfd6d53bd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 268 additions and 0 deletions

View File

@ -61,6 +61,7 @@ PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.CAMERA,
Platform.EVENT,
Platform.LIGHT,
Platform.LOCK,
Platform.MEDIA_PLAYER,

View File

@ -0,0 +1,102 @@
"""Platform providing event entities for UniFi Protect."""
from __future__ import annotations
import dataclasses
from uiprotect.data import (
Camera,
EventType,
ProtectAdoptableDeviceModel,
ProtectModelWithId,
)
from homeassistant.components.event import (
EventDeviceClass,
EventEntity,
EventEntityDescription,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import ATTR_EVENT_ID
from .data import ProtectData, UFPConfigEntry
from .entity import EventEntityMixin, ProtectDeviceEntity
from .models import ProtectEventMixin
@dataclasses.dataclass(frozen=True, kw_only=True)
class ProtectEventEntityDescription(ProtectEventMixin, EventEntityDescription):
"""Describes UniFi Protect event entity."""
EVENT_DESCRIPTIONS: tuple[ProtectEventEntityDescription, ...] = (
ProtectEventEntityDescription(
key="doorbell",
translation_key="doorbell",
name="Doorbell",
device_class=EventDeviceClass.DOORBELL,
icon="mdi:doorbell-video",
ufp_required_field="feature_flags.is_doorbell",
ufp_event_obj="last_ring_event",
event_types=[EventType.RING],
),
)
class ProtectDeviceEventEntity(EventEntityMixin, ProtectDeviceEntity, EventEntity):
"""A UniFi Protect event entity."""
entity_description: ProtectEventEntityDescription
@callback
def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
description = self.entity_description
prev_event = self._event
prev_event_end = self._event_end
super()._async_update_device_from_protect(device)
if event := description.get_event_obj(device):
self._event = event
self._event_end = event.end if event else None
if (
event
and not self._event_already_ended(prev_event, prev_event_end)
and (event_types := description.event_types)
and (event_type := event.type) in event_types
):
self._trigger_event(event_type, {ATTR_EVENT_ID: event.id})
self.async_write_ha_state()
@callback
def _async_event_entities(
data: ProtectData,
ufp_device: ProtectAdoptableDeviceModel | None = None,
) -> list[ProtectDeviceEntity]:
entities: list[ProtectDeviceEntity] = []
for device in data.get_cameras() if ufp_device is None else [ufp_device]:
entities.extend(
ProtectDeviceEventEntity(data, device, description)
for description in EVENT_DESCRIPTIONS
if description.has_required(device)
)
return entities
async def async_setup_entry(
hass: HomeAssistant,
entry: UFPConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up event entities for UniFi Protect integration."""
data = entry.runtime_data
@callback
def _add_new_device(device: ProtectAdoptableDeviceModel) -> None:
if device.is_adopted and isinstance(device, Camera):
async_add_entities(_async_event_entities(data, ufp_device=device))
data.async_subscribe_adopt(_add_new_device)
async_add_entities(_async_event_entities(data))

View File

@ -137,6 +137,17 @@
"none": "Clear"
}
}
},
"event": {
"doorbell": {
"state_attributes": {
"event_type": {
"state": {
"ring": "Ring"
}
}
}
}
}
},
"services": {

View File

@ -0,0 +1,154 @@
"""Test the UniFi Protect event platform."""
from __future__ import annotations
from datetime import datetime, timedelta
from unittest.mock import Mock
from uiprotect.data import Camera, Event, EventType, ModelType, SmartDetectObjectType
from homeassistant.components.unifiprotect.const import (
ATTR_EVENT_ID,
DEFAULT_ATTRIBUTION,
)
from homeassistant.components.unifiprotect.event import EVENT_DESCRIPTIONS
from homeassistant.const import ATTR_ATTRIBUTION, Platform
from homeassistant.core import Event as HAEvent, HomeAssistant, callback
from homeassistant.helpers.event import async_track_state_change_event
from .utils import (
MockUFPFixture,
adopt_devices,
assert_entity_counts,
ids_from_device_description,
init_entry,
remove_entities,
)
async def test_camera_remove(
hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, unadopted_camera: Camera
) -> None:
"""Test removing and re-adding a camera device."""
ufp.api.bootstrap.nvr.system_info.ustorage = None
await init_entry(hass, ufp, [doorbell, unadopted_camera])
assert_entity_counts(hass, Platform.EVENT, 1, 1)
await remove_entities(hass, ufp, [doorbell, unadopted_camera])
assert_entity_counts(hass, Platform.EVENT, 0, 0)
await adopt_devices(hass, ufp, [doorbell, unadopted_camera])
assert_entity_counts(hass, Platform.EVENT, 1, 1)
async def test_doorbell_ring(
hass: HomeAssistant,
ufp: MockUFPFixture,
doorbell: Camera,
unadopted_camera: Camera,
fixed_now: datetime,
) -> None:
"""Test a doorbell ring event."""
await init_entry(hass, ufp, [doorbell, unadopted_camera])
assert_entity_counts(hass, Platform.EVENT, 1, 1)
events: list[HAEvent] = []
@callback
def _capture_event(event: HAEvent) -> None:
events.append(event)
_, entity_id = ids_from_device_description(
Platform.EVENT, doorbell, EVENT_DESCRIPTIONS[0]
)
unsub = async_track_state_change_event(hass, entity_id, _capture_event)
event = Event(
model=ModelType.EVENT,
id="test_event_id",
type=EventType.RING,
start=fixed_now - timedelta(seconds=1),
end=None,
score=100,
smart_detect_types=[],
smart_detect_event_ids=[],
camera_id=doorbell.id,
api=ufp.api,
)
new_camera = doorbell.copy()
new_camera.last_ring_event_id = "test_event_id"
ufp.api.bootstrap.cameras = {new_camera.id: new_camera}
ufp.api.bootstrap.events = {event.id: event}
mock_msg = Mock()
mock_msg.changed_data = {}
mock_msg.new_obj = event
ufp.ws_msg(mock_msg)
await hass.async_block_till_done()
assert len(events) == 1
state = events[0].data["new_state"]
assert state
timestamp = state.state
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
assert state.attributes[ATTR_EVENT_ID] == "test_event_id"
event = Event(
model=ModelType.EVENT,
id="test_event_id",
type=EventType.RING,
start=fixed_now - timedelta(seconds=1),
end=fixed_now + timedelta(seconds=1),
score=50,
smart_detect_types=[],
smart_detect_event_ids=[],
camera_id=doorbell.id,
api=ufp.api,
)
new_camera = doorbell.copy()
ufp.api.bootstrap.cameras = {new_camera.id: new_camera}
ufp.api.bootstrap.events = {event.id: event}
mock_msg = Mock()
mock_msg.changed_data = {}
mock_msg.new_obj = event
ufp.ws_msg(mock_msg)
await hass.async_block_till_done()
# Event is already seen and has end, should now be off
state = hass.states.get(entity_id)
assert state
assert state.state == timestamp
# Now send an event that has an end right away
event = Event(
model=ModelType.EVENT,
id="new_event_id",
type=EventType.RING,
start=fixed_now - timedelta(seconds=1),
end=fixed_now + timedelta(seconds=1),
score=80,
smart_detect_types=[SmartDetectObjectType.PACKAGE],
smart_detect_event_ids=[],
camera_id=doorbell.id,
api=ufp.api,
)
new_camera = doorbell.copy()
ufp.api.bootstrap.cameras = {new_camera.id: new_camera}
ufp.api.bootstrap.events = {event.id: event}
mock_msg = Mock()
mock_msg.changed_data = {}
mock_msg.new_obj = event
ufp.ws_msg(mock_msg)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state
assert state.state == timestamp
unsub()