hass-core/homeassistant/components/onvif/parsers.py
J. Nick Koston c8c1466c06
Fix duplicate ONVIF sensors ()
Some cameras do not configure the video source correctly
when using webhooks but work fine with PullPoint which
results in duplicate sensors
2023-05-05 14:32:55 -04:00

715 lines
22 KiB
Python

"""ONVIF event parsers."""
from __future__ import annotations
from collections.abc import Callable, Coroutine
import datetime
from typing import Any
from homeassistant.const import EntityCategory
from homeassistant.util import dt as dt_util
from homeassistant.util.decorator import Registry
from .models import Event
PARSERS: Registry[
str, Callable[[str, Any], Coroutine[Any, Any, Event | None]]
] = Registry()
VIDEO_SOURCE_MAPPING = {
"vsconf": "VideoSourceToken",
}
def _normalize_video_source(source: str) -> str:
"""Normalize video source.
Some cameras do not set the VideoSourceToken correctly so we get duplicate
sensors, so we need to normalize it to the correct value.
"""
return VIDEO_SOURCE_MAPPING.get(source, source)
def local_datetime_or_none(value: str) -> datetime.datetime | None:
"""Convert strings to datetimes, if invalid, return None."""
# To handle cameras that return times like '0000-00-00T00:00:00Z' (e.g. hikvision)
try:
ret = dt_util.parse_datetime(value)
except ValueError:
return None
if ret is not None:
return dt_util.as_local(ret)
return None
@PARSERS.register("tns1:VideoSource/MotionAlarm")
# pylint: disable=protected-access
async def async_parse_motion_alarm(uid: str, msg) -> Event | None:
"""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}",
"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 | None:
"""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}",
"Image Too Blurry",
"binary_sensor",
"problem",
None,
msg.Message._value_1.Data.SimpleItem[0].Value == "true",
EntityCategory.DIAGNOSTIC,
)
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 | None:
"""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}",
"Image Too Dark",
"binary_sensor",
"problem",
None,
msg.Message._value_1.Data.SimpleItem[0].Value == "true",
EntityCategory.DIAGNOSTIC,
)
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 | None:
"""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}",
"Image Too Bright",
"binary_sensor",
"problem",
None,
msg.Message._value_1.Data.SimpleItem[0].Value == "true",
EntityCategory.DIAGNOSTIC,
)
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 | None:
"""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}",
"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 | None:
"""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}",
"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 | None:
"""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 = _normalize_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}",
"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 | None:
"""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 = _normalize_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}",
"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/MotionRegionDetector/Motion")
# pylint: disable=protected-access
async def async_parse_motion_region_detector(uid: str, msg) -> Event | None:
"""Handle parsing event message.
Topic: tns1:RuleEngine/MotionRegionDetector/Motion
"""
try:
video_source = ""
video_analytics = ""
rule = ""
for source in msg.Message._value_1.Source.SimpleItem:
if source.Name == "VideoSourceConfigurationToken":
video_source = _normalize_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}",
"Motion Region Detection",
"binary_sensor",
"motion",
None,
msg.Message._value_1.Data.SimpleItem[0].Value in ["1", "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 | None:
"""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 = _normalize_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}",
"Tamper Detection",
"binary_sensor",
"problem",
None,
msg.Message._value_1.Data.SimpleItem[0].Value == "true",
EntityCategory.DIAGNOSTIC,
)
except (AttributeError, KeyError):
return None
@PARSERS.register("tns1:RuleEngine/MyRuleDetector/DogCatDetect")
# pylint: disable=protected-access
async def async_parse_dog_cat_detector(uid: str, msg) -> Event | None:
"""Handle parsing event message.
Topic: tns1:RuleEngine/MyRuleDetector/DogCatDetect
"""
try:
video_source = ""
for source in msg.Message._value_1.Source.SimpleItem:
if source.Name == "Source":
video_source = _normalize_video_source(source.Value)
return Event(
f"{uid}_{msg.Topic._value_1}_{video_source}",
"Pet Detection",
"binary_sensor",
"motion",
None,
msg.Message._value_1.Data.SimpleItem[0].Value == "true",
)
except (AttributeError, KeyError):
return None
@PARSERS.register("tns1:RuleEngine/MyRuleDetector/VehicleDetect")
# pylint: disable=protected-access
async def async_parse_vehicle_detector(uid: str, msg) -> Event | None:
"""Handle parsing event message.
Topic: tns1:RuleEngine/MyRuleDetector/VehicleDetect
"""
try:
video_source = ""
for source in msg.Message._value_1.Source.SimpleItem:
if source.Name == "Source":
video_source = _normalize_video_source(source.Value)
return Event(
f"{uid}_{msg.Topic._value_1}_{video_source}",
"Vehicle Detection",
"binary_sensor",
"motion",
None,
msg.Message._value_1.Data.SimpleItem[0].Value == "true",
)
except (AttributeError, KeyError):
return None
@PARSERS.register("tns1:RuleEngine/MyRuleDetector/PeopleDetect")
# pylint: disable=protected-access
async def async_parse_person_detector(uid: str, msg) -> Event | None:
"""Handle parsing event message.
Topic: tns1:RuleEngine/MyRuleDetector/PeopleDetect
"""
try:
video_source = ""
for source in msg.Message._value_1.Source.SimpleItem:
if source.Name == "Source":
video_source = _normalize_video_source(source.Value)
return Event(
f"{uid}_{msg.Topic._value_1}_{video_source}",
"Person Detection",
"binary_sensor",
"motion",
None,
msg.Message._value_1.Data.SimpleItem[0].Value == "true",
)
except (AttributeError, KeyError):
return None
@PARSERS.register("tns1:RuleEngine/MyRuleDetector/FaceDetect")
# pylint: disable=protected-access
async def async_parse_face_detector(uid: str, msg) -> Event | None:
"""Handle parsing event message.
Topic: tns1:RuleEngine/MyRuleDetector/FaceDetect
"""
try:
video_source = ""
for source in msg.Message._value_1.Source.SimpleItem:
if source.Name == "Source":
video_source = _normalize_video_source(source.Value)
return Event(
f"{uid}_{msg.Topic._value_1}_{video_source}",
"Face Detection",
"binary_sensor",
"motion",
None,
msg.Message._value_1.Data.SimpleItem[0].Value == "true",
)
except (AttributeError, KeyError):
return None
@PARSERS.register("tns1:RuleEngine/MyRuleDetector/Visitor")
# pylint: disable=protected-access
async def async_parse_visitor_detector(uid: str, msg) -> Event | None:
"""Handle parsing event message.
Topic: tns1:RuleEngine/MyRuleDetector/Visitor
"""
try:
video_source = ""
for source in msg.Message._value_1.Source.SimpleItem:
if source.Name == "Source":
video_source = _normalize_video_source(source.Value)
return Event(
f"{uid}_{msg.Topic._value_1}_{video_source}",
"Visitor Detection",
"binary_sensor",
"occupancy",
None,
msg.Message._value_1.Data.SimpleItem[0].Value == "true",
)
except (AttributeError, KeyError):
return None
@PARSERS.register("tns1:Device/Trigger/DigitalInput")
# pylint: disable=protected-access
async def async_parse_digital_input(uid: str, msg) -> Event | None:
"""Handle parsing event message.
Topic: tns1:Device/Trigger/DigitalInput
"""
try:
source = msg.Message._value_1.Source.SimpleItem[0].Value
return Event(
f"{uid}_{msg.Topic._value_1}_{source}",
"Digital Input",
"binary_sensor",
None,
None,
msg.Message._value_1.Data.SimpleItem[0].Value == "true",
)
except (AttributeError, KeyError):
return None
@PARSERS.register("tns1:Device/Trigger/Relay")
# pylint: disable=protected-access
async def async_parse_relay(uid: str, msg) -> Event | None:
"""Handle parsing event message.
Topic: tns1:Device/Trigger/Relay
"""
try:
source = msg.Message._value_1.Source.SimpleItem[0].Value
return Event(
f"{uid}_{msg.Topic._value_1}_{source}",
"Relay Triggered",
"binary_sensor",
None,
None,
msg.Message._value_1.Data.SimpleItem[0].Value == "active",
)
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 | None:
"""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",
EntityCategory.DIAGNOSTIC,
)
except (AttributeError, KeyError):
return None
@PARSERS.register("tns1:Monitoring/ProcessorUsage")
# pylint: disable=protected-access
async def async_parse_processor_usage(uid: str, msg) -> Event | None:
"""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),
EntityCategory.DIAGNOSTIC,
)
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 | None:
"""Handle parsing event message.
Topic: tns1:Monitoring/OperatingTime/LastReboot
"""
try:
date_time = local_datetime_or_none(
msg.Message._value_1.Data.SimpleItem[0].Value
)
return Event(
f"{uid}_{msg.Topic._value_1}",
"Last Reboot",
"sensor",
"timestamp",
None,
date_time,
EntityCategory.DIAGNOSTIC,
)
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 | None:
"""Handle parsing event message.
Topic: tns1:Monitoring/OperatingTime/LastReset
"""
try:
date_time = local_datetime_or_none(
msg.Message._value_1.Data.SimpleItem[0].Value
)
return Event(
f"{uid}_{msg.Topic._value_1}",
"Last Reset",
"sensor",
"timestamp",
None,
date_time,
EntityCategory.DIAGNOSTIC,
entity_enabled=False,
)
except (AttributeError, KeyError):
return None
@PARSERS.register("tns1:Monitoring/Backup/Last")
# pylint: disable=protected-access
async def async_parse_backup_last(uid: str, msg) -> Event | None:
"""Handle parsing event message.
Topic: tns1:Monitoring/Backup/Last
"""
try:
date_time = local_datetime_or_none(
msg.Message._value_1.Data.SimpleItem[0].Value
)
return Event(
f"{uid}_{msg.Topic._value_1}",
"Last Backup",
"sensor",
"timestamp",
None,
date_time,
EntityCategory.DIAGNOSTIC,
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 | None:
"""Handle parsing event message.
Topic: tns1:Monitoring/OperatingTime/LastClockSynchronization
"""
try:
date_time = local_datetime_or_none(
msg.Message._value_1.Data.SimpleItem[0].Value
)
return Event(
f"{uid}_{msg.Topic._value_1}",
"Last Clock Synchronization",
"sensor",
"timestamp",
None,
date_time,
EntityCategory.DIAGNOSTIC,
entity_enabled=False,
)
except (AttributeError, KeyError):
return None
@PARSERS.register("tns1:RecordingConfig/JobState")
# pylint: disable=protected-access
async def async_parse_jobstate(uid: str, msg) -> Event | None:
"""Handle parsing event message.
Topic: tns1:RecordingConfig/JobState
"""
try:
source = msg.Message._value_1.Source.SimpleItem[0].Value
return Event(
f"{uid}_{msg.Topic._value_1}_{source}",
"Recording Job State",
"binary_sensor",
None,
None,
msg.Message._value_1.Data.SimpleItem[0].Value == "Active",
EntityCategory.DIAGNOSTIC,
)
except (AttributeError, KeyError):
return None
@PARSERS.register("tns1:RuleEngine/LineDetector/Crossed")
# pylint: disable=protected-access
async def async_parse_linedetector_crossed(uid: str, msg) -> Event | None:
"""Handle parsing event message.
Topic: tns1:RuleEngine/LineDetector/Crossed
"""
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}",
"Line Detector Crossed",
"sensor",
None,
None,
msg.Message._value_1.Data.SimpleItem[0].Value,
EntityCategory.DIAGNOSTIC,
)
except (AttributeError, KeyError):
return None
@PARSERS.register("tns1:RuleEngine/CountAggregation/Counter")
# pylint: disable=protected-access
async def async_parse_count_aggregation_counter(uid: str, msg) -> Event | None:
"""Handle parsing event message.
Topic: tns1:RuleEngine/CountAggregation/Counter
"""
try:
video_source = ""
video_analytics = ""
rule = ""
for source in msg.Message._value_1.Source.SimpleItem:
if source.Name == "VideoSourceConfigurationToken":
video_source = _normalize_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}",
"Count Aggregation Counter",
"sensor",
None,
None,
msg.Message._value_1.Data.SimpleItem[0].Value,
EntityCategory.DIAGNOSTIC,
)
except (AttributeError, KeyError):
return None