ONVIF Event Implementation (#35406)

Initial implementation of ONVIF event sensors
This commit is contained in:
Jason Hunter 2020-05-11 13:12:12 -04:00 committed by GitHub
parent 9eb1505aa1
commit 132bb4e890
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 762 additions and 20 deletions

View file

@ -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

View file

@ -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
]
)
)

View file

@ -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

View 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)
)

View file

@ -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."""

View 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]

View file

@ -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

View file

@ -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

View 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

View 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)
)

View file

@ -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

View file

@ -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