mirror of
https://github.com/home-assistant/core.git
synced 2025-07-14 00:37:13 +00:00
ONVIF Event Implementation (#35406)
Initial implementation of ONVIF event sensors
This commit is contained in:
parent
9eb1505aa1
commit
132bb4e890
@ -529,8 +529,12 @@ omit =
|
|||||||
homeassistant/components/onkyo/media_player.py
|
homeassistant/components/onkyo/media_player.py
|
||||||
homeassistant/components/onvif/__init__.py
|
homeassistant/components/onvif/__init__.py
|
||||||
homeassistant/components/onvif/base.py
|
homeassistant/components/onvif/base.py
|
||||||
|
homeassistant/components/onvif/binary_sensor.py
|
||||||
homeassistant/components/onvif/camera.py
|
homeassistant/components/onvif/camera.py
|
||||||
homeassistant/components/onvif/device.py
|
homeassistant/components/onvif/device.py
|
||||||
|
homeassistant/components/onvif/event.py
|
||||||
|
homeassistant/components/onvif/parsers.py
|
||||||
|
homeassistant/components/onvif/sensor.py
|
||||||
homeassistant/components/opencv/*
|
homeassistant/components/opencv/*
|
||||||
homeassistant/components/openevse/sensor.py
|
homeassistant/components/openevse/sensor.py
|
||||||
homeassistant/components/openexchangerates/sensor.py
|
homeassistant/components/openexchangerates/sensor.py
|
||||||
|
@ -11,6 +11,7 @@ from homeassistant.const import (
|
|||||||
CONF_PASSWORD,
|
CONF_PASSWORD,
|
||||||
CONF_PORT,
|
CONF_PORT,
|
||||||
CONF_USERNAME,
|
CONF_USERNAME,
|
||||||
|
EVENT_HOMEASSISTANT_STOP,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
@ -30,8 +31,6 @@ from .device import ONVIFDevice
|
|||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA)
|
CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
PLATFORMS = ["camera"]
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, config: dict):
|
async def async_setup(hass: HomeAssistant, config: dict):
|
||||||
"""Set up the ONVIF component."""
|
"""Set up the ONVIF component."""
|
||||||
@ -79,7 +78,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
|||||||
|
|
||||||
hass.data[DOMAIN][entry.unique_id] = device
|
hass.data[DOMAIN][entry.unique_id] = device
|
||||||
|
|
||||||
for component in PLATFORMS:
|
platforms = ["camera"]
|
||||||
|
|
||||||
|
if device.capabilities.events and await device.events.async_start():
|
||||||
|
platforms += ["binary_sensor", "sensor"]
|
||||||
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, device.events.async_stop)
|
||||||
|
|
||||||
|
for component in platforms:
|
||||||
hass.async_create_task(
|
hass.async_create_task(
|
||||||
hass.config_entries.async_forward_entry_setup(entry, component)
|
hass.config_entries.async_forward_entry_setup(entry, component)
|
||||||
)
|
)
|
||||||
@ -89,11 +94,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
|||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
|
|
||||||
|
device = hass.data[DOMAIN][entry.unique_id]
|
||||||
|
platforms = ["camera"]
|
||||||
|
|
||||||
|
if device.capabilities.events and device.events.started:
|
||||||
|
platforms += ["binary_sensor", "sensor"]
|
||||||
|
await device.events.async_stop()
|
||||||
|
|
||||||
return all(
|
return all(
|
||||||
await asyncio.gather(
|
await asyncio.gather(
|
||||||
*[
|
*[
|
||||||
hass.config_entries.async_forward_entry_unload(entry, component)
|
hass.config_entries.async_forward_entry_unload(entry, component)
|
||||||
for component in PLATFORMS
|
for component in platforms
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -9,7 +9,7 @@ from .models import Profile
|
|||||||
class ONVIFBaseEntity(Entity):
|
class ONVIFBaseEntity(Entity):
|
||||||
"""Base class common to all ONVIF entities."""
|
"""Base class common to all ONVIF entities."""
|
||||||
|
|
||||||
def __init__(self, device: ONVIFDevice, profile: Profile) -> None:
|
def __init__(self, device: ONVIFDevice, profile: Profile = None) -> None:
|
||||||
"""Initialize the ONVIF entity."""
|
"""Initialize the ONVIF entity."""
|
||||||
self.device = device
|
self.device = device
|
||||||
self.profile = profile
|
self.profile = profile
|
||||||
|
84
homeassistant/components/onvif/binary_sensor.py
Normal file
84
homeassistant/components/onvif/binary_sensor.py
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
"""Support for ONVIF binary sensors."""
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from homeassistant.components.binary_sensor import BinarySensorEntity
|
||||||
|
from homeassistant.core import callback
|
||||||
|
|
||||||
|
from .base import ONVIFBaseEntity
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
|
"""Set up a ONVIF binary sensor."""
|
||||||
|
device = hass.data[DOMAIN][config_entry.unique_id]
|
||||||
|
|
||||||
|
entities = {
|
||||||
|
event.uid: ONVIFBinarySensor(event.uid, device)
|
||||||
|
for event in device.events.get_platform("binary_sensor")
|
||||||
|
}
|
||||||
|
|
||||||
|
async_add_entities(entities.values())
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_check_entities():
|
||||||
|
"""Check if we have added an entity for the event."""
|
||||||
|
new_entities = []
|
||||||
|
for event in device.events.get_platform("binary_sensor"):
|
||||||
|
if event.uid not in entities:
|
||||||
|
entities[event.uid] = ONVIFBinarySensor(event.uid, device)
|
||||||
|
new_entities.append(entities[event.uid])
|
||||||
|
async_add_entities(new_entities)
|
||||||
|
|
||||||
|
device.events.async_add_listener(async_check_entities)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class ONVIFBinarySensor(ONVIFBaseEntity, BinarySensorEntity):
|
||||||
|
"""Representation of a binary ONVIF event."""
|
||||||
|
|
||||||
|
def __init__(self, uid, device):
|
||||||
|
"""Initialize the ONVIF binary sensor."""
|
||||||
|
ONVIFBaseEntity.__init__(self, device)
|
||||||
|
BinarySensorEntity.__init__(self)
|
||||||
|
|
||||||
|
self.uid = uid
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self) -> bool:
|
||||||
|
"""Return true if event is active."""
|
||||||
|
return self.device.events.get_uid(self.uid).value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
"""Return the name of the event."""
|
||||||
|
return self.device.events.get_uid(self.uid).name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_class(self) -> Optional[str]:
|
||||||
|
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||||
|
return self.device.events.get_uid(self.uid).device_class
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self) -> str:
|
||||||
|
"""Return a unique ID."""
|
||||||
|
return self.uid
|
||||||
|
|
||||||
|
@property
|
||||||
|
def entity_registry_enabled_default(self) -> bool:
|
||||||
|
"""Return if the entity should be enabled when first added to the entity registry."""
|
||||||
|
return self.device.events.get_uid(self.uid).entity_enabled
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_poll(self) -> bool:
|
||||||
|
"""Return True if entity has to be polled for state.
|
||||||
|
|
||||||
|
False if entity pushes its state to HA.
|
||||||
|
"""
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def async_added_to_hass(self):
|
||||||
|
"""Connect to dispatcher listening for entity data notifications."""
|
||||||
|
self.async_on_remove(
|
||||||
|
self.device.events.async_add_listener(self.async_write_ha_state)
|
||||||
|
)
|
@ -11,6 +11,7 @@ from onvif.exceptions import ONVIFError
|
|||||||
from zeep.asyncio import AsyncTransport
|
from zeep.asyncio import AsyncTransport
|
||||||
from zeep.exceptions import Fault
|
from zeep.exceptions import Fault
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_HOST,
|
CONF_HOST,
|
||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
@ -18,6 +19,7 @@ from homeassistant.const import (
|
|||||||
CONF_PORT,
|
CONF_PORT,
|
||||||
CONF_USERNAME,
|
CONF_USERNAME,
|
||||||
)
|
)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
@ -31,24 +33,26 @@ from .const import (
|
|||||||
TILT_FACTOR,
|
TILT_FACTOR,
|
||||||
ZOOM_FACTOR,
|
ZOOM_FACTOR,
|
||||||
)
|
)
|
||||||
|
from .event import EventManager
|
||||||
from .models import PTZ, Capabilities, DeviceInfo, Profile, Resolution, Video
|
from .models import PTZ, Capabilities, DeviceInfo, Profile, Resolution, Video
|
||||||
|
|
||||||
|
|
||||||
class ONVIFDevice:
|
class ONVIFDevice:
|
||||||
"""Manages an ONVIF device."""
|
"""Manages an ONVIF device."""
|
||||||
|
|
||||||
def __init__(self, hass, config_entry=None):
|
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry = None):
|
||||||
"""Initialize the device."""
|
"""Initialize the device."""
|
||||||
self.hass = hass
|
self.hass: HomeAssistant = hass
|
||||||
self.config_entry = config_entry
|
self.config_entry: ConfigEntry = config_entry
|
||||||
self.available = True
|
self.available: bool = True
|
||||||
|
|
||||||
self.device = None
|
self.device: ONVIFCamera = None
|
||||||
|
self.events: EventManager = None
|
||||||
|
|
||||||
self.info = DeviceInfo()
|
self.info: DeviceInfo = DeviceInfo()
|
||||||
self.capabilities = Capabilities()
|
self.capabilities: Capabilities = Capabilities()
|
||||||
self.profiles = []
|
self.profiles: List[Profile] = []
|
||||||
self.max_resolution = 0
|
self.max_resolution: int = 0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
@ -96,6 +100,11 @@ class ONVIFDevice:
|
|||||||
if self.capabilities.ptz:
|
if self.capabilities.ptz:
|
||||||
self.device.create_ptz_service()
|
self.device.create_ptz_service()
|
||||||
|
|
||||||
|
if self.capabilities.events:
|
||||||
|
self.events = EventManager(
|
||||||
|
self.hass, self.device, self.config_entry.unique_id
|
||||||
|
)
|
||||||
|
|
||||||
# Determine max resolution from profiles
|
# Determine max resolution from profiles
|
||||||
self.max_resolution = max(
|
self.max_resolution = max(
|
||||||
profile.video.resolution.width
|
profile.video.resolution.width
|
||||||
@ -199,14 +208,18 @@ class ONVIFDevice:
|
|||||||
async def async_get_capabilities(self):
|
async def async_get_capabilities(self):
|
||||||
"""Obtain information about the available services on the device."""
|
"""Obtain information about the available services on the device."""
|
||||||
media_service = self.device.create_media_service()
|
media_service = self.device.create_media_service()
|
||||||
capabilities = await media_service.GetServiceCapabilities()
|
media_capabilities = await media_service.GetServiceCapabilities()
|
||||||
|
event_service = self.device.create_events_service()
|
||||||
|
event_capabilities = await event_service.GetServiceCapabilities()
|
||||||
ptz = False
|
ptz = False
|
||||||
try:
|
try:
|
||||||
self.device.get_definition("ptz")
|
self.device.get_definition("ptz")
|
||||||
ptz = True
|
ptz = True
|
||||||
except ONVIFError:
|
except ONVIFError:
|
||||||
pass
|
pass
|
||||||
return Capabilities(capabilities.SnapshotUri, ptz)
|
return Capabilities(
|
||||||
|
media_capabilities.SnapshotUri, event_capabilities.WSPullPointSupport, ptz
|
||||||
|
)
|
||||||
|
|
||||||
async def async_get_profiles(self) -> List[Profile]:
|
async def async_get_profiles(self) -> List[Profile]:
|
||||||
"""Obtain media profiles for this device."""
|
"""Obtain media profiles for this device."""
|
||||||
|
169
homeassistant/components/onvif/event.py
Normal file
169
homeassistant/components/onvif/event.py
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
"""ONVIF event abstraction."""
|
||||||
|
import datetime as dt
|
||||||
|
from typing import Callable, Dict, List, Optional, Set
|
||||||
|
|
||||||
|
from aiohttp.client_exceptions import ServerDisconnectedError
|
||||||
|
from onvif import ONVIFCamera, ONVIFService
|
||||||
|
from zeep.exceptions import Fault
|
||||||
|
|
||||||
|
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||||
|
from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||||
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
|
from .const import LOGGER
|
||||||
|
from .models import Event
|
||||||
|
from .parsers import PARSERS
|
||||||
|
|
||||||
|
UNHANDLED_TOPICS = set()
|
||||||
|
|
||||||
|
|
||||||
|
class EventManager:
|
||||||
|
"""ONVIF Event Manager."""
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant, device: ONVIFCamera, unique_id: str):
|
||||||
|
"""Initialize event manager."""
|
||||||
|
self.hass: HomeAssistant = hass
|
||||||
|
self.device: ONVIFCamera = device
|
||||||
|
self.unique_id: str = unique_id
|
||||||
|
self.started: bool = False
|
||||||
|
|
||||||
|
self._subscription: ONVIFService = None
|
||||||
|
self._events: Dict[str, Event] = {}
|
||||||
|
self._listeners: List[CALLBACK_TYPE] = []
|
||||||
|
self._unsub_refresh: Optional[CALLBACK_TYPE] = None
|
||||||
|
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def platforms(self) -> Set[str]:
|
||||||
|
"""Return platforms to setup."""
|
||||||
|
return {event.platform for event in self._events.values()}
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_add_listener(self, update_callback: CALLBACK_TYPE) -> Callable[[], None]:
|
||||||
|
"""Listen for data updates."""
|
||||||
|
# This is the first listener, set up polling.
|
||||||
|
if not self._listeners:
|
||||||
|
self._unsub_refresh = async_track_point_in_utc_time(
|
||||||
|
self.hass,
|
||||||
|
self.async_pull_messages,
|
||||||
|
dt_util.utcnow() + dt.timedelta(seconds=1),
|
||||||
|
)
|
||||||
|
|
||||||
|
self._listeners.append(update_callback)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def remove_listener() -> None:
|
||||||
|
"""Remove update listener."""
|
||||||
|
self.async_remove_listener(update_callback)
|
||||||
|
|
||||||
|
return remove_listener
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_remove_listener(self, update_callback: CALLBACK_TYPE) -> None:
|
||||||
|
"""Remove data update."""
|
||||||
|
self._listeners.remove(update_callback)
|
||||||
|
|
||||||
|
if not self._listeners and self._unsub_refresh:
|
||||||
|
self._unsub_refresh()
|
||||||
|
self._unsub_refresh = None
|
||||||
|
|
||||||
|
async def async_start(self) -> bool:
|
||||||
|
"""Start polling events."""
|
||||||
|
if await self.device.create_pullpoint_subscription():
|
||||||
|
# Initialize events
|
||||||
|
pullpoint = self.device.create_pullpoint_service()
|
||||||
|
await pullpoint.SetSynchronizationPoint()
|
||||||
|
req = pullpoint.create_type("PullMessages")
|
||||||
|
req.MessageLimit = 100
|
||||||
|
req.Timeout = dt.timedelta(seconds=5)
|
||||||
|
response = await pullpoint.PullMessages(req)
|
||||||
|
|
||||||
|
# Parse event initialization
|
||||||
|
await self.async_parse_messages(response.NotificationMessage)
|
||||||
|
|
||||||
|
# Create subscription manager
|
||||||
|
self._subscription = self.device.create_subscription_service(
|
||||||
|
"PullPointSubscription"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.started = True
|
||||||
|
|
||||||
|
return self.started
|
||||||
|
|
||||||
|
async def async_stop(self, event=None) -> None:
|
||||||
|
"""Unsubscribe from events."""
|
||||||
|
if not self._subscription:
|
||||||
|
return
|
||||||
|
|
||||||
|
await self._subscription.Unsubscribe()
|
||||||
|
self._subscription = None
|
||||||
|
|
||||||
|
async def async_renew(self) -> None:
|
||||||
|
"""Renew subscription."""
|
||||||
|
if not self._subscription:
|
||||||
|
return
|
||||||
|
|
||||||
|
await self._subscription.Renew(dt_util.utcnow() + dt.timedelta(minutes=10))
|
||||||
|
|
||||||
|
async def async_pull_messages(self, _now: dt = None) -> None:
|
||||||
|
"""Pull messages from device."""
|
||||||
|
try:
|
||||||
|
pullpoint = self.device.get_service("pullpoint")
|
||||||
|
req = pullpoint.create_type("PullMessages")
|
||||||
|
req.MessageLimit = 100
|
||||||
|
req.Timeout = dt.timedelta(seconds=60)
|
||||||
|
response = await pullpoint.PullMessages(req)
|
||||||
|
|
||||||
|
# Renew subscription if less than 60 seconds left
|
||||||
|
if (response.TerminationTime - dt_util.utcnow()).total_seconds() < 60:
|
||||||
|
await self.async_renew()
|
||||||
|
|
||||||
|
# Parse response
|
||||||
|
await self.async_parse_messages(response.NotificationMessage)
|
||||||
|
|
||||||
|
except ServerDisconnectedError:
|
||||||
|
pass
|
||||||
|
except Fault:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Update entities
|
||||||
|
for update_callback in self._listeners:
|
||||||
|
update_callback()
|
||||||
|
|
||||||
|
# Reschedule another pull
|
||||||
|
if self._listeners:
|
||||||
|
self._unsub_refresh = async_track_point_in_utc_time(
|
||||||
|
self.hass,
|
||||||
|
self.async_pull_messages,
|
||||||
|
dt_util.utcnow() + dt.timedelta(seconds=1),
|
||||||
|
)
|
||||||
|
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
async def async_parse_messages(self, messages) -> None:
|
||||||
|
"""Parse notification message."""
|
||||||
|
for msg in messages:
|
||||||
|
# LOGGER.debug("ONVIF Event Message %s: %s", self.device.host, pformat(msg))
|
||||||
|
topic = msg.Topic._value_1
|
||||||
|
parser = PARSERS.get(topic)
|
||||||
|
if not parser:
|
||||||
|
if topic not in UNHANDLED_TOPICS:
|
||||||
|
LOGGER.info("No registered handler for event: %s", msg)
|
||||||
|
UNHANDLED_TOPICS.add(topic)
|
||||||
|
continue
|
||||||
|
|
||||||
|
event = await parser(self.unique_id, msg)
|
||||||
|
|
||||||
|
if not event:
|
||||||
|
LOGGER.warning("Unable to parse event: %s", msg)
|
||||||
|
return
|
||||||
|
|
||||||
|
self._events[event.uid] = event
|
||||||
|
|
||||||
|
def get_uid(self, uid) -> Event:
|
||||||
|
"""Retrieve event for given id."""
|
||||||
|
return self._events[uid]
|
||||||
|
|
||||||
|
def get_platform(self, platform) -> List[Event]:
|
||||||
|
"""Retrieve events for given platform."""
|
||||||
|
return [event for event in self._events.values() if event.platform == platform]
|
@ -2,7 +2,7 @@
|
|||||||
"domain": "onvif",
|
"domain": "onvif",
|
||||||
"name": "ONVIF",
|
"name": "ONVIF",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/onvif",
|
"documentation": "https://www.home-assistant.io/integrations/onvif",
|
||||||
"requirements": ["onvif-zeep-async==0.2.0", "WSDiscovery==2.0.0"],
|
"requirements": ["onvif-zeep-async==0.3.0", "WSDiscovery==2.0.0"],
|
||||||
"dependencies": ["ffmpeg"],
|
"dependencies": ["ffmpeg"],
|
||||||
"codeowners": ["@hunterjm"],
|
"codeowners": ["@hunterjm"],
|
||||||
"config_flow": true
|
"config_flow": true
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
"""ONVIF models."""
|
"""ONVIF models."""
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import List
|
from typing import Any, List
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -55,4 +55,18 @@ class Capabilities:
|
|||||||
"""Represents Service capabilities."""
|
"""Represents Service capabilities."""
|
||||||
|
|
||||||
snapshot: bool = False
|
snapshot: bool = False
|
||||||
|
events: bool = False
|
||||||
ptz: bool = False
|
ptz: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Event:
|
||||||
|
"""Represents a ONVIF event."""
|
||||||
|
|
||||||
|
uid: str
|
||||||
|
name: str
|
||||||
|
platform: str
|
||||||
|
device_class: str = None
|
||||||
|
unit_of_measurement: str = None
|
||||||
|
value: Any = None
|
||||||
|
entity_enabled: bool = True
|
||||||
|
358
homeassistant/components/onvif/parsers.py
Normal file
358
homeassistant/components/onvif/parsers.py
Normal file
@ -0,0 +1,358 @@
|
|||||||
|
"""ONVIF event parsers."""
|
||||||
|
from homeassistant.util import dt as dt_util
|
||||||
|
from homeassistant.util.decorator import Registry
|
||||||
|
|
||||||
|
from .models import Event
|
||||||
|
|
||||||
|
PARSERS = Registry()
|
||||||
|
|
||||||
|
|
||||||
|
@PARSERS.register("tns1:VideoSource/MotionAlarm")
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
async def async_parse_motion_alarm(uid: str, msg) -> Event:
|
||||||
|
"""Handle parsing event message.
|
||||||
|
|
||||||
|
Topic: tns1:VideoSource/MotionAlarm
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
source = msg.Message._value_1.Source.SimpleItem[0].Value
|
||||||
|
return Event(
|
||||||
|
f"{uid}_{msg.Topic._value_1}_{source}",
|
||||||
|
f"{source} Motion Alarm",
|
||||||
|
"binary_sensor",
|
||||||
|
"motion",
|
||||||
|
None,
|
||||||
|
msg.Message._value_1.Data.SimpleItem[0].Value == "true",
|
||||||
|
)
|
||||||
|
except (AttributeError, KeyError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@PARSERS.register("tns1:VideoSource/ImageTooBlurry/AnalyticsService")
|
||||||
|
@PARSERS.register("tns1:VideoSource/ImageTooBlurry/ImagingService")
|
||||||
|
@PARSERS.register("tns1:VideoSource/ImageTooBlurry/RecordingService")
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
async def async_parse_image_too_blurry(uid: str, msg) -> Event:
|
||||||
|
"""Handle parsing event message.
|
||||||
|
|
||||||
|
Topic: tns1:VideoSource/ImageTooBlurry/*
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
source = msg.Message._value_1.Source.SimpleItem[0].Value
|
||||||
|
return Event(
|
||||||
|
f"{uid}_{msg.Topic._value_1}_{source}",
|
||||||
|
f"{source} Image Too Blurry",
|
||||||
|
"binary_sensor",
|
||||||
|
"problem",
|
||||||
|
None,
|
||||||
|
msg.Message._value_1.Data.SimpleItem[0].Value == "true",
|
||||||
|
)
|
||||||
|
except (AttributeError, KeyError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@PARSERS.register("tns1:VideoSource/ImageTooDark/AnalyticsService")
|
||||||
|
@PARSERS.register("tns1:VideoSource/ImageTooDark/ImagingService")
|
||||||
|
@PARSERS.register("tns1:VideoSource/ImageTooDark/RecordingService")
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
async def async_parse_image_too_dark(uid: str, msg) -> Event:
|
||||||
|
"""Handle parsing event message.
|
||||||
|
|
||||||
|
Topic: tns1:VideoSource/ImageTooDark/*
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
source = msg.Message._value_1.Source.SimpleItem[0].Value
|
||||||
|
return Event(
|
||||||
|
f"{uid}_{msg.Topic._value_1}_{source}",
|
||||||
|
f"{source} Image Too Dark",
|
||||||
|
"binary_sensor",
|
||||||
|
"problem",
|
||||||
|
None,
|
||||||
|
msg.Message._value_1.Data.SimpleItem[0].Value == "true",
|
||||||
|
)
|
||||||
|
except (AttributeError, KeyError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@PARSERS.register("tns1:VideoSource/ImageTooBright/AnalyticsService")
|
||||||
|
@PARSERS.register("tns1:VideoSource/ImageTooBright/ImagingService")
|
||||||
|
@PARSERS.register("tns1:VideoSource/ImageTooBright/RecordingService")
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
async def async_parse_image_too_bright(uid: str, msg) -> Event:
|
||||||
|
"""Handle parsing event message.
|
||||||
|
|
||||||
|
Topic: tns1:VideoSource/ImageTooBright/*
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
source = msg.Message._value_1.Source.SimpleItem[0].Value
|
||||||
|
return Event(
|
||||||
|
f"{uid}_{msg.Topic._value_1}_{source}",
|
||||||
|
f"{source} Image Too Bright",
|
||||||
|
"binary_sensor",
|
||||||
|
"problem",
|
||||||
|
None,
|
||||||
|
msg.Message._value_1.Data.SimpleItem[0].Value == "true",
|
||||||
|
)
|
||||||
|
except (AttributeError, KeyError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@PARSERS.register("tns1:VideoSource/GlobalSceneChange/AnalyticsService")
|
||||||
|
@PARSERS.register("tns1:VideoSource/GlobalSceneChange/ImagingService")
|
||||||
|
@PARSERS.register("tns1:VideoSource/GlobalSceneChange/RecordingService")
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
async def async_parse_scene_change(uid: str, msg) -> Event:
|
||||||
|
"""Handle parsing event message.
|
||||||
|
|
||||||
|
Topic: tns1:VideoSource/GlobalSceneChange/*
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
source = msg.Message._value_1.Source.SimpleItem[0].Value
|
||||||
|
return Event(
|
||||||
|
f"{uid}_{msg.Topic._value_1}_{source}",
|
||||||
|
f"{source} Global Scene Change",
|
||||||
|
"binary_sensor",
|
||||||
|
"problem",
|
||||||
|
None,
|
||||||
|
msg.Message._value_1.Data.SimpleItem[0].Value == "true",
|
||||||
|
)
|
||||||
|
except (AttributeError, KeyError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@PARSERS.register("tns1:AudioAnalytics/Audio/DetectedSound")
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
async def async_parse_detected_sound(uid: str, msg) -> Event:
|
||||||
|
"""Handle parsing event message.
|
||||||
|
|
||||||
|
Topic: tns1:AudioAnalytics/Audio/DetectedSound
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
audio_source = ""
|
||||||
|
audio_analytics = ""
|
||||||
|
rule = ""
|
||||||
|
for source in msg.Message._value_1.Source.SimpleItem:
|
||||||
|
if source.Name == "AudioSourceConfigurationToken":
|
||||||
|
audio_source = source.Value
|
||||||
|
if source.Name == "AudioAnalyticsConfigurationToken":
|
||||||
|
audio_analytics = source.Value
|
||||||
|
if source.Name == "Rule":
|
||||||
|
rule = source.Value
|
||||||
|
|
||||||
|
return Event(
|
||||||
|
f"{uid}_{msg.Topic._value_1}_{audio_source}_{audio_analytics}_{rule}",
|
||||||
|
f"{rule} Detected Sound",
|
||||||
|
"binary_sensor",
|
||||||
|
"sound",
|
||||||
|
None,
|
||||||
|
msg.Message._value_1.Data.SimpleItem[0].Value == "true",
|
||||||
|
)
|
||||||
|
except (AttributeError, KeyError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@PARSERS.register("tns1:RuleEngine/FieldDetector/ObjectsInside")
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
async def async_parse_field_detector(uid: str, msg) -> Event:
|
||||||
|
"""Handle parsing event message.
|
||||||
|
|
||||||
|
Topic: tns1:RuleEngine/FieldDetector/ObjectsInside
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
video_source = ""
|
||||||
|
video_analytics = ""
|
||||||
|
rule = ""
|
||||||
|
for source in msg.Message._value_1.Source.SimpleItem:
|
||||||
|
if source.Name == "VideoSourceConfigurationToken":
|
||||||
|
video_source = source.Value
|
||||||
|
if source.Name == "VideoAnalyticsConfigurationToken":
|
||||||
|
video_analytics = source.Value
|
||||||
|
if source.Name == "Rule":
|
||||||
|
rule = source.Value
|
||||||
|
|
||||||
|
evt = Event(
|
||||||
|
f"{uid}_{msg.Topic._value_1}_{video_source}_{video_analytics}_{rule}",
|
||||||
|
f"{rule} Field Detection",
|
||||||
|
"binary_sensor",
|
||||||
|
"motion",
|
||||||
|
None,
|
||||||
|
msg.Message._value_1.Data.SimpleItem[0].Value == "true",
|
||||||
|
)
|
||||||
|
return evt
|
||||||
|
except (AttributeError, KeyError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@PARSERS.register("tns1:RuleEngine/CellMotionDetector/Motion")
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
async def async_parse_cell_motion_detector(uid: str, msg) -> Event:
|
||||||
|
"""Handle parsing event message.
|
||||||
|
|
||||||
|
Topic: tns1:RuleEngine/CellMotionDetector/Motion
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
video_source = ""
|
||||||
|
video_analytics = ""
|
||||||
|
rule = ""
|
||||||
|
for source in msg.Message._value_1.Source.SimpleItem:
|
||||||
|
if source.Name == "VideoSourceConfigurationToken":
|
||||||
|
video_source = source.Value
|
||||||
|
if source.Name == "VideoAnalyticsConfigurationToken":
|
||||||
|
video_analytics = source.Value
|
||||||
|
if source.Name == "Rule":
|
||||||
|
rule = source.Value
|
||||||
|
|
||||||
|
return Event(
|
||||||
|
f"{uid}_{msg.Topic._value_1}_{video_source}_{video_analytics}_{rule}",
|
||||||
|
f"{rule} Cell Motion Detection",
|
||||||
|
"binary_sensor",
|
||||||
|
"motion",
|
||||||
|
None,
|
||||||
|
msg.Message._value_1.Data.SimpleItem[0].Value == "true",
|
||||||
|
)
|
||||||
|
except (AttributeError, KeyError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@PARSERS.register("tns1:RuleEngine/TamperDetector/Tamper")
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
async def async_parse_tamper_detector(uid: str, msg) -> Event:
|
||||||
|
"""Handle parsing event message.
|
||||||
|
|
||||||
|
Topic: tns1:RuleEngine/TamperDetector/Tamper
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
video_source = ""
|
||||||
|
video_analytics = ""
|
||||||
|
rule = ""
|
||||||
|
for source in msg.Message._value_1.Source.SimpleItem:
|
||||||
|
if source.Name == "VideoSourceConfigurationToken":
|
||||||
|
video_source = source.Value
|
||||||
|
if source.Name == "VideoAnalyticsConfigurationToken":
|
||||||
|
video_analytics = source.Value
|
||||||
|
if source.Name == "Rule":
|
||||||
|
rule = source.Value
|
||||||
|
|
||||||
|
return Event(
|
||||||
|
f"{uid}_{msg.Topic._value_1}_{video_source}_{video_analytics}_{rule}",
|
||||||
|
f"{rule} Tamper Detection",
|
||||||
|
"binary_sensor",
|
||||||
|
"problem",
|
||||||
|
None,
|
||||||
|
msg.Message._value_1.Data.SimpleItem[0].Value == "true",
|
||||||
|
)
|
||||||
|
except (AttributeError, KeyError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@PARSERS.register("tns1:Device/HardwareFailure/StorageFailure")
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
async def async_parse_storage_failure(uid: str, msg) -> Event:
|
||||||
|
"""Handle parsing event message.
|
||||||
|
|
||||||
|
Topic: tns1:Device/HardwareFailure/StorageFailure
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
source = msg.Message._value_1.Source.SimpleItem[0].Value
|
||||||
|
return Event(
|
||||||
|
f"{uid}_{msg.Topic._value_1}_{source}",
|
||||||
|
"Storage Failure",
|
||||||
|
"binary_sensor",
|
||||||
|
"problem",
|
||||||
|
None,
|
||||||
|
msg.Message._value_1.Data.SimpleItem[0].Value == "true",
|
||||||
|
)
|
||||||
|
except (AttributeError, KeyError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@PARSERS.register("tns1:Monitoring/ProcessorUsage")
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
async def async_parse_processor_usage(uid: str, msg) -> Event:
|
||||||
|
"""Handle parsing event message.
|
||||||
|
|
||||||
|
Topic: tns1:Monitoring/ProcessorUsage
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
usage = float(msg.Message._value_1.Data.SimpleItem[0].Value)
|
||||||
|
if usage <= 1:
|
||||||
|
usage *= 100
|
||||||
|
|
||||||
|
return Event(
|
||||||
|
f"{uid}_{msg.Topic._value_1}",
|
||||||
|
"Processor Usage",
|
||||||
|
"sensor",
|
||||||
|
None,
|
||||||
|
"percent",
|
||||||
|
int(usage),
|
||||||
|
)
|
||||||
|
except (AttributeError, KeyError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@PARSERS.register("tns1:Monitoring/OperatingTime/LastReboot")
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
async def async_parse_last_reboot(uid: str, msg) -> Event:
|
||||||
|
"""Handle parsing event message.
|
||||||
|
|
||||||
|
Topic: tns1:Monitoring/OperatingTime/LastReboot
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return Event(
|
||||||
|
f"{uid}_{msg.Topic._value_1}",
|
||||||
|
"Last Reboot",
|
||||||
|
"sensor",
|
||||||
|
"timestamp",
|
||||||
|
None,
|
||||||
|
dt_util.as_local(
|
||||||
|
dt_util.parse_datetime(msg.Message._value_1.Data.SimpleItem[0].Value)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
except (AttributeError, KeyError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@PARSERS.register("tns1:Monitoring/OperatingTime/LastReset")
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
async def async_parse_last_reset(uid: str, msg) -> Event:
|
||||||
|
"""Handle parsing event message.
|
||||||
|
|
||||||
|
Topic: tns1:Monitoring/OperatingTime/LastReset
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return Event(
|
||||||
|
f"{uid}_{msg.Topic._value_1}",
|
||||||
|
"Last Reset",
|
||||||
|
"sensor",
|
||||||
|
"timestamp",
|
||||||
|
None,
|
||||||
|
dt_util.as_local(
|
||||||
|
dt_util.parse_datetime(msg.Message._value_1.Data.SimpleItem[0].Value)
|
||||||
|
),
|
||||||
|
entity_enabled=False,
|
||||||
|
)
|
||||||
|
except (AttributeError, KeyError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@PARSERS.register("tns1:Monitoring/OperatingTime/LastClockSynchronization")
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
async def async_parse_last_clock_sync(uid: str, msg) -> Event:
|
||||||
|
"""Handle parsing event message.
|
||||||
|
|
||||||
|
Topic: tns1:Monitoring/OperatingTime/LastClockSynchronization
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return Event(
|
||||||
|
f"{uid}_{msg.Topic._value_1}",
|
||||||
|
"Last Clock Synchronization",
|
||||||
|
"sensor",
|
||||||
|
"timestamp",
|
||||||
|
None,
|
||||||
|
dt_util.as_local(
|
||||||
|
dt_util.parse_datetime(msg.Message._value_1.Data.SimpleItem[0].Value)
|
||||||
|
),
|
||||||
|
entity_enabled=False,
|
||||||
|
)
|
||||||
|
except (AttributeError, KeyError):
|
||||||
|
return None
|
87
homeassistant/components/onvif/sensor.py
Normal file
87
homeassistant/components/onvif/sensor.py
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
"""Support for ONVIF binary sensors."""
|
||||||
|
from typing import Optional, Union
|
||||||
|
|
||||||
|
from homeassistant.core import callback
|
||||||
|
|
||||||
|
from .base import ONVIFBaseEntity
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
|
"""Set up a ONVIF binary sensor."""
|
||||||
|
device = hass.data[DOMAIN][config_entry.unique_id]
|
||||||
|
|
||||||
|
entities = {
|
||||||
|
event.uid: ONVIFSensor(event.uid, device)
|
||||||
|
for event in device.events.get_platform("sensor")
|
||||||
|
}
|
||||||
|
|
||||||
|
async_add_entities(entities.values())
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_check_entities():
|
||||||
|
"""Check if we have added an entity for the event."""
|
||||||
|
new_entities = []
|
||||||
|
for event in device.events.get_platform("sensor"):
|
||||||
|
if event.uid not in entities:
|
||||||
|
entities[event.uid] = ONVIFSensor(event.uid, device)
|
||||||
|
new_entities.append(entities[event.uid])
|
||||||
|
async_add_entities(new_entities)
|
||||||
|
|
||||||
|
device.events.async_add_listener(async_check_entities)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class ONVIFSensor(ONVIFBaseEntity):
|
||||||
|
"""Representation of a ONVIF sensor event."""
|
||||||
|
|
||||||
|
def __init__(self, uid, device):
|
||||||
|
"""Initialize the ONVIF binary sensor."""
|
||||||
|
self.uid = uid
|
||||||
|
|
||||||
|
super().__init__(device)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self) -> Union[None, str, int, float]:
|
||||||
|
"""Return the state of the entity."""
|
||||||
|
return self.device.events.get_uid(self.uid).value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the event."""
|
||||||
|
return self.device.events.get_uid(self.uid).name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_class(self) -> Optional[str]:
|
||||||
|
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||||
|
return self.device.events.get_uid(self.uid).device_class
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unit_of_measurement(self) -> Optional[str]:
|
||||||
|
"""Return the unit of measurement of this entity, if any."""
|
||||||
|
return self.device.events.get_uid(self.uid).unit_of_measurement
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self) -> str:
|
||||||
|
"""Return a unique ID."""
|
||||||
|
return self.uid
|
||||||
|
|
||||||
|
@property
|
||||||
|
def entity_registry_enabled_default(self) -> bool:
|
||||||
|
"""Return if the entity should be enabled when first added to the entity registry."""
|
||||||
|
return self.device.events.get_uid(self.uid).entity_enabled
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_poll(self) -> bool:
|
||||||
|
"""Return True if entity has to be polled for state.
|
||||||
|
|
||||||
|
False if entity pushes its state to HA.
|
||||||
|
"""
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def async_added_to_hass(self):
|
||||||
|
"""Connect to dispatcher listening for entity data notifications."""
|
||||||
|
self.async_on_remove(
|
||||||
|
self.device.events.async_add_listener(self.async_write_ha_state)
|
||||||
|
)
|
@ -994,7 +994,7 @@ oemthermostat==1.1
|
|||||||
onkyo-eiscp==1.2.7
|
onkyo-eiscp==1.2.7
|
||||||
|
|
||||||
# homeassistant.components.onvif
|
# homeassistant.components.onvif
|
||||||
onvif-zeep-async==0.2.0
|
onvif-zeep-async==0.3.0
|
||||||
|
|
||||||
# homeassistant.components.opengarage
|
# homeassistant.components.opengarage
|
||||||
open-garage==0.1.2
|
open-garage==0.1.2
|
||||||
|
@ -414,7 +414,7 @@ numpy==1.18.4
|
|||||||
oauth2client==4.0.0
|
oauth2client==4.0.0
|
||||||
|
|
||||||
# homeassistant.components.onvif
|
# homeassistant.components.onvif
|
||||||
onvif-zeep-async==0.2.0
|
onvif-zeep-async==0.3.0
|
||||||
|
|
||||||
# homeassistant.components.openerz
|
# homeassistant.components.openerz
|
||||||
openerz-api==0.1.0
|
openerz-api==0.1.0
|
||||||
|
Loading…
x
Reference in New Issue
Block a user