Axis use entity descripton binary sensor platform (#113705)

This commit is contained in:
Robert Svensson 2024-03-22 15:49:51 +01:00 committed by GitHub
parent 5b6361080c
commit a9e857202d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 292 additions and 77 deletions

View file

@ -2,10 +2,12 @@
from __future__ import annotations
from collections.abc import Callable
from collections.abc import Callable, Iterable
from dataclasses import dataclass
from datetime import datetime, timedelta
from functools import partial
from axis.models.event import Event, EventGroup, EventOperation, EventTopic
from axis.models.event import Event, EventOperation, EventTopic
from axis.vapix.interfaces.applications.fence_guard import FenceGuardHandler
from axis.vapix.interfaces.applications.loitering_guard import LoiteringGuardHandler
from axis.vapix.interfaces.applications.motion_guard import MotionGuardHandler
@ -14,6 +16,7 @@ from axis.vapix.interfaces.applications.vmd4 import Vmd4Handler
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
@ -23,26 +26,160 @@ from homeassistant.helpers.event import async_call_later
from .entity import AxisEventEntity
from .hub import AxisHub
DEVICE_CLASS = {
EventGroup.INPUT: BinarySensorDeviceClass.CONNECTIVITY,
EventGroup.LIGHT: BinarySensorDeviceClass.LIGHT,
EventGroup.MOTION: BinarySensorDeviceClass.MOTION,
EventGroup.SOUND: BinarySensorDeviceClass.SOUND,
}
EVENT_TOPICS = (
EventTopic.DAY_NIGHT_VISION,
EventTopic.FENCE_GUARD,
EventTopic.LOITERING_GUARD,
EventTopic.MOTION_DETECTION,
EventTopic.MOTION_DETECTION_3,
EventTopic.MOTION_DETECTION_4,
EventTopic.MOTION_GUARD,
EventTopic.OBJECT_ANALYTICS,
EventTopic.PIR,
EventTopic.PORT_INPUT,
EventTopic.PORT_SUPERVISED_INPUT,
EventTopic.SOUND_TRIGGER_LEVEL,
@dataclass(frozen=True, kw_only=True)
class AxisBinarySensorDescription(BinarySensorEntityDescription):
"""Axis binary sensor entity description."""
event_topic: tuple[EventTopic, ...] | EventTopic
"""Event topic that provides state updates."""
name_fn: Callable[[AxisHub, Event], str] = lambda hub, event: ""
"""Function providing the corresponding name to the event ID."""
supported_fn: Callable[[AxisHub, Event], bool] = lambda hub, event: True
"""Function validating if event is supported."""
@callback
def event_id_is_int(event_id: str) -> bool:
"""Make sure event ID is int."""
try:
_ = int(event_id)
except ValueError:
return False
return True
@callback
def guard_suite_supported_fn(hub: AxisHub, event: Event) -> bool:
"""Validate event ID is int."""
_, _, profile_id = event.id.partition("Profile")
return event_id_is_int(profile_id)
@callback
def object_analytics_supported_fn(hub: AxisHub, event: Event) -> bool:
"""Validate event ID is int."""
_, _, profile_id = event.id.partition("Scenario")
return event_id_is_int(profile_id)
@callback
def guard_suite_name_fn(
handler: FenceGuardHandler
| LoiteringGuardHandler
| MotionGuardHandler
| Vmd4Handler,
event: Event,
event_type: str,
) -> str:
"""Get guard suite item name."""
if handler.initialized and (profiles := handler["0"].profiles):
for profile_id, profile in profiles.items():
camera_id = profile.camera
if event.id == f"Camera{camera_id}Profile{profile_id}":
return f"{event_type} {profile.name}"
return ""
@callback
def fence_guard_name_fn(hub: AxisHub, event: Event) -> str:
"""Fence guard name."""
return guard_suite_name_fn(hub.api.vapix.fence_guard, event, "Fence Guard")
@callback
def loitering_guard_name_fn(hub: AxisHub, event: Event) -> str:
"""Loitering guard name."""
return guard_suite_name_fn(hub.api.vapix.loitering_guard, event, "Loitering Guard")
@callback
def motion_guard_name_fn(hub: AxisHub, event: Event) -> str:
"""Motion guard name."""
return guard_suite_name_fn(hub.api.vapix.motion_guard, event, "Motion Guard")
@callback
def motion_detection_4_name_fn(hub: AxisHub, event: Event) -> str:
"""Motion detection 4 name."""
return guard_suite_name_fn(hub.api.vapix.vmd4, event, "VMD4")
@callback
def object_analytics_name_fn(hub: AxisHub, event: Event) -> str:
"""Get object analytics name."""
if hub.api.vapix.object_analytics.initialized and (
scenarios := hub.api.vapix.object_analytics["0"].scenarios
):
for scenario_id, scenario in scenarios.items():
device_id = scenario.devices[0]["id"]
if event.id == f"Device{device_id}Scenario{scenario_id}":
return f"Object Analytics {scenario.name}"
return ""
ENTITY_DESCRIPTIONS = (
AxisBinarySensorDescription(
key="Input port state",
device_class=BinarySensorDeviceClass.CONNECTIVITY,
event_topic=(EventTopic.PORT_INPUT, EventTopic.PORT_SUPERVISED_INPUT),
name_fn=lambda hub, event: hub.api.vapix.ports[event.id].name,
supported_fn=lambda hub, event: event_id_is_int(event.id),
),
AxisBinarySensorDescription(
key="Day/Night vision state",
device_class=BinarySensorDeviceClass.LIGHT,
event_topic=EventTopic.DAY_NIGHT_VISION,
),
AxisBinarySensorDescription(
key="Sound trigger state",
device_class=BinarySensorDeviceClass.SOUND,
event_topic=EventTopic.SOUND_TRIGGER_LEVEL,
),
AxisBinarySensorDescription(
key="Motion sensors state",
device_class=BinarySensorDeviceClass.MOTION,
event_topic=(
EventTopic.PIR,
EventTopic.MOTION_DETECTION,
EventTopic.MOTION_DETECTION_3,
),
),
AxisBinarySensorDescription(
key="Motion detection 4 state",
device_class=BinarySensorDeviceClass.MOTION,
event_topic=EventTopic.MOTION_DETECTION_4,
name_fn=motion_detection_4_name_fn,
supported_fn=guard_suite_supported_fn,
),
AxisBinarySensorDescription(
key="Fence guard state",
device_class=BinarySensorDeviceClass.MOTION,
event_topic=EventTopic.FENCE_GUARD,
name_fn=fence_guard_name_fn,
supported_fn=guard_suite_supported_fn,
),
AxisBinarySensorDescription(
key="Loitering guard state",
device_class=BinarySensorDeviceClass.MOTION,
event_topic=EventTopic.LOITERING_GUARD,
name_fn=loitering_guard_name_fn,
supported_fn=guard_suite_supported_fn,
),
AxisBinarySensorDescription(
key="Motion guard state",
device_class=BinarySensorDeviceClass.MOTION,
event_topic=EventTopic.MOTION_GUARD,
name_fn=motion_guard_name_fn,
supported_fn=guard_suite_supported_fn,
),
AxisBinarySensorDescription(
key="Object analytics state",
device_class=BinarySensorDeviceClass.MOTION,
event_topic=EventTopic.OBJECT_ANALYTICS,
name_fn=object_analytics_name_fn,
supported_fn=object_analytics_supported_fn,
),
)
@ -55,29 +192,40 @@ async def async_setup_entry(
hub = AxisHub.get_hub(hass, config_entry)
@callback
def async_create_entity(event: Event) -> None:
"""Create Axis binary sensor entity."""
async_add_entities([AxisBinarySensor(event, hub)])
def register_platform(descriptions: Iterable[AxisBinarySensorDescription]) -> None:
"""Register entity platform to create entities on event initialized signal."""
hub.api.event.subscribe(
async_create_entity,
topic_filter=EVENT_TOPICS,
operation_filter=EventOperation.INITIALIZED,
)
@callback
def create_entity(
description: AxisBinarySensorDescription, event: Event
) -> None:
"""Create Axis entity."""
if description.supported_fn(hub, event):
async_add_entities([AxisBinarySensor(hub, description, event)])
for description in descriptions:
hub.api.event.subscribe(
partial(create_entity, description),
topic_filter=description.event_topic,
operation_filter=EventOperation.INITIALIZED,
)
register_platform(ENTITY_DESCRIPTIONS)
class AxisBinarySensor(AxisEventEntity, BinarySensorEntity):
"""Representation of a binary Axis event."""
def __init__(self, event: Event, hub: AxisHub) -> None:
def __init__(
self, hub: AxisHub, description: AxisBinarySensorDescription, event: Event
) -> None:
"""Initialize the Axis binary sensor."""
super().__init__(event, hub)
self.cancel_scheduled_update: Callable[[], None] | None = None
self._attr_device_class = DEVICE_CLASS.get(event.group)
self.entity_description = description
self._attr_name = description.name_fn(hub, event) or self._attr_name
self._attr_is_on = event.is_tripped
self._set_name(event)
self._attr_device_class = description.device_class # temporary
self.cancel_scheduled_update: Callable[[], None] | None = None
@callback
def async_event_callback(self, event: Event) -> None:
@ -103,45 +251,3 @@ class AxisBinarySensor(AxisEventEntity, BinarySensorEntity):
timedelta(seconds=self.hub.config.trigger_time),
scheduled_update,
)
@callback
def _set_name(self, event: Event) -> None:
"""Set binary sensor name."""
if (
event.group == EventGroup.INPUT
and event.id in self.hub.api.vapix.ports
and self.hub.api.vapix.ports[event.id].name
):
self._attr_name = self.hub.api.vapix.ports[event.id].name
elif event.group == EventGroup.MOTION:
event_data: FenceGuardHandler | LoiteringGuardHandler | MotionGuardHandler | Vmd4Handler | None = None
if event.topic_base == EventTopic.FENCE_GUARD:
event_data = self.hub.api.vapix.fence_guard
elif event.topic_base == EventTopic.LOITERING_GUARD:
event_data = self.hub.api.vapix.loitering_guard
elif event.topic_base == EventTopic.MOTION_GUARD:
event_data = self.hub.api.vapix.motion_guard
elif event.topic_base == EventTopic.MOTION_DETECTION_4:
event_data = self.hub.api.vapix.vmd4
if (
event_data
and event_data.initialized
and (profiles := event_data["0"].profiles)
):
for profile_id, profile in profiles.items():
camera_id = profile.camera
if event.id == f"Camera{camera_id}Profile{profile_id}":
self._attr_name = f"{self._event_type} {profile.name}"
return
if (
event.topic_base == EventTopic.OBJECT_ANALYTICS
and self.hub.api.vapix.object_analytics.initialized
and (scenarios := self.hub.api.vapix.object_analytics["0"].scenarios)
):
for scenario_id, scenario in scenarios.items():
device_id = scenario.devices[0]["id"]
if event.id == f"Device{device_id}Scenario{scenario_id}":
self._attr_name = f"{self._event_type} {scenario.name}"
break