ONVIF Event Implementation (#35406)
Initial implementation of ONVIF event sensors
This commit is contained in:
parent
9eb1505aa1
commit
132bb4e890
12 changed files with 762 additions and 20 deletions
|
@ -529,8 +529,12 @@ omit =
|
|||
homeassistant/components/onkyo/media_player.py
|
||||
homeassistant/components/onvif/__init__.py
|
||||
homeassistant/components/onvif/base.py
|
||||
homeassistant/components/onvif/binary_sensor.py
|
||||
homeassistant/components/onvif/camera.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/openevse/sensor.py
|
||||
homeassistant/components/openexchangerates/sensor.py
|
||||
|
|
|
@ -11,6 +11,7 @@ from homeassistant.const import (
|
|||
CONF_PASSWORD,
|
||||
CONF_PORT,
|
||||
CONF_USERNAME,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
@ -30,8 +31,6 @@ from .device import ONVIFDevice
|
|||
|
||||
CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
PLATFORMS = ["camera"]
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: dict):
|
||||
"""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
|
||||
|
||||
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.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):
|
||||
"""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(
|
||||
await asyncio.gather(
|
||||
*[
|
||||
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):
|
||||
"""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."""
|
||||
self.device = device
|
||||
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.exceptions import Fault
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_NAME,
|
||||
|
@ -18,6 +19,7 @@ from homeassistant.const import (
|
|||
CONF_PORT,
|
||||
CONF_USERNAME,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
|
@ -31,24 +33,26 @@ from .const import (
|
|||
TILT_FACTOR,
|
||||
ZOOM_FACTOR,
|
||||
)
|
||||
from .event import EventManager
|
||||
from .models import PTZ, Capabilities, DeviceInfo, Profile, Resolution, Video
|
||||
|
||||
|
||||
class ONVIFDevice:
|
||||
"""Manages an ONVIF device."""
|
||||
|
||||
def __init__(self, hass, config_entry=None):
|
||||
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry = None):
|
||||
"""Initialize the device."""
|
||||
self.hass = hass
|
||||
self.config_entry = config_entry
|
||||
self.available = True
|
||||
self.hass: HomeAssistant = hass
|
||||
self.config_entry: ConfigEntry = config_entry
|
||||
self.available: bool = True
|
||||
|
||||
self.device = None
|
||||
self.device: ONVIFCamera = None
|
||||
self.events: EventManager = None
|
||||
|
||||
self.info = DeviceInfo()
|
||||
self.capabilities = Capabilities()
|
||||
self.profiles = []
|
||||
self.max_resolution = 0
|
||||
self.info: DeviceInfo = DeviceInfo()
|
||||
self.capabilities: Capabilities = Capabilities()
|
||||
self.profiles: List[Profile] = []
|
||||
self.max_resolution: int = 0
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
|
@ -96,6 +100,11 @@ class ONVIFDevice:
|
|||
if self.capabilities.ptz:
|
||||
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
|
||||
self.max_resolution = max(
|
||||
profile.video.resolution.width
|
||||
|
@ -199,14 +208,18 @@ class ONVIFDevice:
|
|||
async def async_get_capabilities(self):
|
||||
"""Obtain information about the available services on the device."""
|
||||
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
|
||||
try:
|
||||
self.device.get_definition("ptz")
|
||||
ptz = True
|
||||
except ONVIFError:
|
||||
pass
|
||||
return Capabilities(capabilities.SnapshotUri, ptz)
|
||||
return Capabilities(
|
||||
media_capabilities.SnapshotUri, event_capabilities.WSPullPointSupport, ptz
|
||||
)
|
||||
|
||||
async def async_get_profiles(self) -> List[Profile]:
|
||||
"""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",
|
||||
"name": "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"],
|
||||
"codeowners": ["@hunterjm"],
|
||||
"config_flow": true
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
"""ONVIF models."""
|
||||
from dataclasses import dataclass
|
||||
from typing import List
|
||||
from typing import Any, List
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -55,4 +55,18 @@ class Capabilities:
|
|||
"""Represents Service capabilities."""
|
||||
|
||||
snapshot: bool = False
|
||||
events: 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
|
||||
|
||||
# homeassistant.components.onvif
|
||||
onvif-zeep-async==0.2.0
|
||||
onvif-zeep-async==0.3.0
|
||||
|
||||
# homeassistant.components.opengarage
|
||||
open-garage==0.1.2
|
||||
|
|
|
@ -414,7 +414,7 @@ numpy==1.18.4
|
|||
oauth2client==4.0.0
|
||||
|
||||
# homeassistant.components.onvif
|
||||
onvif-zeep-async==0.2.0
|
||||
onvif-zeep-async==0.3.0
|
||||
|
||||
# homeassistant.components.openerz
|
||||
openerz-api==0.1.0
|
||||
|
|
Loading…
Add table
Reference in a new issue