Skip to content

Commit

Permalink
Merge pull request #18 from rroller/events
Browse files Browse the repository at this point in the history
Support binary sensors for all configured events
  • Loading branch information
rroller authored Jun 19, 2021
2 parents 10f7c01 + 631760e commit 7f3bbb0
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 91 deletions.
67 changes: 39 additions & 28 deletions custom_components/dahua/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,14 +106,21 @@ def __init__(self, hass: HomeAssistant, dahua_client: DahuaClient, events: list)
self.connected = None
self.channels = {"1": "1"}
self.events: list = events
self.motion_timestamp_seconds = 0
self.cross_line_detection_timestamp_seconds = 0
self.motion_listener: CALLBACK_TYPE
self.cross_line_detection_listener: CALLBACK_TYPE
self._supports_coaxial_control = False
self._supports_disarming_linkage = False

# This thread is what connects to the cameras event stream and fires on_receive when there's an event
self.dahua_event = DahuaEventThread(hass, dahua_client, self.on_receive, events)

# A dictionary of event name (CrossLineDetection, VideoMotion, etc) to a listener for that event
self._dahua_event_listeners: dict[str, CALLBACK_TYPE] = {}

# A dictionary of event name (CrossLineDetection, VideoMotion, etc) to the time the event fire or was cleared.
# If cleared the time will be 0. The time unit is seconds epoch
self._dahua_event_timestamp: dict[str, int] = {}

super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL_SECONDS)

async def async_start_event_listener(self):
Expand Down Expand Up @@ -224,39 +231,36 @@ def on_receive(self, data_bytes: bytes):
except Exception: # pylint: disable=broad-except
pass

# When there's a motion start event we'll set a flag to the current timestmap in seconds.
# We'll reset it when the motion stops. We'll use this elsewhere to know how long to trigger a motion sensor
# TODO: Generalize events so we don't create a block for each one
if event["Code"] == "VideoMotion":
action = event["action"]
if action == "Start":
self.motion_timestamp_seconds = int(time.time())
if self.motion_listener:
self.motion_listener()
elif action == "Stop":
self.motion_timestamp_seconds = 0
if self.motion_listener:
self.motion_listener()
if event["Code"] == "CrossLineDetection":
# When there's an event start we'll update the a map x to the current timestamp in seconds for the event.
# We'll reset it to 0 when the event stops.
# We'll use these timestamps in binary_sensor to know how long to trigger the sensor

# This is the event code, example: VideoMotion, CrossLineDetection, etc
event_name = event["Code"]

listener = self._dahua_event_listeners[event_name]
if listener is not None:
action = event["action"]
if action == "Start":
self.cross_line_detection_timestamp_seconds = int(time.time())
if self.cross_line_detection_listener:
self.cross_line_detection_listener()
self._dahua_event_timestamp[event_name] = int(time.time())
listener()
elif action == "Stop":
self.cross_line_detection_timestamp_seconds = 0
if self.cross_line_detection_listener:
self.cross_line_detection_listener()
self._dahua_event_timestamp[event_name] = 0
listener()

self.hass.bus.fire("dahua_event_received", event)

def add_motion_listener(self, listener: CALLBACK_TYPE):
""" Adds the motion listener. This callback will be called on motion events """
self.motion_listener = listener
def get_event_timestamp(self, event_name: str) -> int:
"""
Returns the event timestamp. If the event is firing then it will be the time of the firing. Otherwise returns 0.
event_name: the event name, example: CrossLineDetection
"""
return self._dahua_event_timestamp.get(event_name, 0)

def add_cross_line_detection_listener(self, listener: CALLBACK_TYPE):
""" Adds the CrossLineDetection listener. This callback will be called on CrossLineDetection events """
self.cross_line_detection_listener = listener
def add_dahua_event_listener(self, event_name: str, listener: CALLBACK_TYPE):
""" Adds an event listener for the given event (CrossLineDetection, etc).
This callback will be called when the event fire """
self._dahua_event_listeners[event_name] = listener

def supports_siren(self) -> bool:
"""
Expand Down Expand Up @@ -320,6 +324,13 @@ def get_serial_number(self) -> str:
""" returns the device serial number. This is unique per device """
return self.data.get("serialNumber")

def get_event_list(self) -> list:
"""
Returns the list of events selected when configuring the camera in Home Assistant. For example:
[VideoMotion, VideoLoss, CrossLineDetection]
"""
return self.events

def is_infrared_light_on(self) -> bool:
""" returns true if the infrared light is on """
return self.data.get("table.Lighting[0][0].Mode", "") == "Manual"
Expand Down
122 changes: 61 additions & 61 deletions custom_components/dahua/binary_sensor.py
Original file line number Diff line number Diff line change
@@ -1,110 +1,110 @@
"""Binary sensor platform for dahua."""
import re

from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.core import HomeAssistant
from custom_components.dahua import DahuaDataUpdateCoordinator

from .const import (
MOTION_SENSOR_DEVICE_CLASS,
DOMAIN,
DOMAIN, SAFETY_DEVICE_CLASS, CONNECTIVITY_DEVICE_CLASS,
)
from .entity import DahuaBaseEntity

# Override event names. Otherwise we'll generate the name from the event name for example SmartMotionHuman will
# become "Smart Motion Human"
NAME_OVERRIDES: dict[str, str] = {
"VideoMotion": "Motion Alarm",
"CrossLineDetection": "Cross Line Alarm",
}

# Override the device class for events
DEVICE_CLASS_OVERRIDES: dict[str, str] = {
"VideoMotion": MOTION_SENSOR_DEVICE_CLASS,
"CrossLineDetection": MOTION_SENSOR_DEVICE_CLASS,
"AlarmLocal": SAFETY_DEVICE_CLASS,
"VideoLoss": SAFETY_DEVICE_CLASS,
"VideoBlind": SAFETY_DEVICE_CLASS,
"StorageNotExist": CONNECTIVITY_DEVICE_CLASS,
"StorageFailure": CONNECTIVITY_DEVICE_CLASS,
"StorageLowSpace": SAFETY_DEVICE_CLASS,
"FireWarning": SAFETY_DEVICE_CLASS,
}


async def async_setup_entry(hass: HomeAssistant, entry, async_add_devices):
"""Setup binary_sensor platform."""
coordinator = hass.data[DOMAIN][entry.entry_id]

async_add_devices([
DahuaMotionSensor(coordinator, entry),
DahuaCrossLineDetectionSensor(coordinator, entry)
])


class DahuaMotionSensor(DahuaBaseEntity, BinarySensorEntity):
"""dahua binary_sensor class to record motion events"""

def __init__(self, coordinator: DahuaDataUpdateCoordinator, config_entry):
DahuaBaseEntity.__init__(self, coordinator, config_entry)
BinarySensorEntity.__init__(self)

self._name = coordinator.get_device_name()
self._coordinator = coordinator
self._unique_id = coordinator.get_serial_number() + "_motion_alarm"
sensors: list[DahuaEventSensor] = []
for event_name in coordinator.get_event_list():
sensors.append(DahuaEventSensor(coordinator, entry, event_name))

@property
def name(self):
"""Return the name of the binary_sensor."""
return f"{self._name} Motion Alarm"
if sensors:
async_add_devices(sensors)

@property
def device_class(self):
"""Return the class of this binary_sensor."""
return MOTION_SENSOR_DEVICE_CLASS

@property
def is_on(self):
"""
Return true if motion is activated.
This is the magic part of this sensor along with the async_added_to_hass method below.
The async_added_to_hass method adds a listener to the coordinator so when motion is started or stopped
it calls the async_write_ha_state function. async_write_ha_state just gets the current value from this is_on method.
"""
return self._coordinator.motion_timestamp_seconds > 0

async def async_added_to_hass(self):
"""Connect to dispatcher listening for entity data notifications."""
self._coordinator.add_motion_listener(self.async_write_ha_state)

@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


class DahuaCrossLineDetectionSensor(DahuaBaseEntity, BinarySensorEntity):
class DahuaEventSensor(DahuaBaseEntity, BinarySensorEntity):
"""
dahua binary_sensor class to record 'cross line detection' events. This is also known as 'tripwire'. This is
configured in the camera UI by going to Setting -> Event -> IVS -> and adding a tripwire rule.
dahua binary_sensor class to record events. Many of these events are configured in the camera UI by going to:
Setting -> Event -> IVS -> and adding a tripwire rule, etc. See the DahuaEventThread in thread.py on how we connect
to the cammera to listen to events.
"""

def __init__(self, coordinator: DahuaDataUpdateCoordinator, config_entry):
def __init__(self, coordinator: DahuaDataUpdateCoordinator, config_entry, event_name: str):
DahuaBaseEntity.__init__(self, coordinator, config_entry)
BinarySensorEntity.__init__(self)

self._name = coordinator.get_device_name()
# event_name is the event name, example: VideoMotion, CrossLineDetection, SmartMotionHuman, etc
self._event_name = event_name

self._coordinator = coordinator
self._unique_id = coordinator.get_serial_number() + "_cross_line_alarm"
self._device_name = coordinator.get_device_name()
self._device_class = DEVICE_CLASS_OVERRIDES.get(event_name, MOTION_SENSOR_DEVICE_CLASS)

# name is the friendly name, example: Cross Line Alarm. If the name is not found in the override it will be
# generated from the event_name. For example SmartMotionHuman willbecome "Smart Motion Human"
# https://stackoverflow.com/questions/25674532/pythonic-way-to-add-space-before-capital-letter-if-and-only-if-previous-letter-i/25674575
default_name = re.sub(r"(?<![A-Z])(?<!^)([A-Z])", r" \1", event_name)
self._name = NAME_OVERRIDES.get(event_name, default_name)

# Build the unique ID. This will convert the name to lower underscores. For example, "Smart Motion Vehicle" will
# become "smart_motion_vehicle" and will be added as a suffix to the device serial number
self._unique_id = coordinator.get_serial_number() + "_" + self._name.lower().replace(" ", "_")
if event_name == "VideoMotion":
# We need this for backwards compatibility as the VideoMotion was created with a unique ID of just the
# serial number and we don't want to break people who are upgrading
self._unique_id = coordinator.get_serial_number()

@ property
@property
def unique_id(self):
"""Return the entity unique ID."""
return self._unique_id

@property
def name(self):
"""Return the name of the binary_sensor."""
return f"{self._name} Cross Line Alarm"
"""Return the name of the binary_sensor. Example: Cam14 Motion Alarm"""
return f"{self._device_name} {self._name}"

@property
def device_class(self):
"""Return the class of this binary_sensor."""
return MOTION_SENSOR_DEVICE_CLASS
"""Return the class of this binary_sensor, Example: motion"""
return self._device_class

@property
def is_on(self):
"""
Return true if a cross line detection event activated.
Return true if the event is activated.
This is the magic part of this sensor along with the async_added_to_hass method below.
The async_added_to_hass method adds a listener to the coordinator so when the event is started or stopped
it calls the async_write_ha_state function. async_write_ha_state just gets the current value from this is_on method.
it calls the async_write_ha_state function. async_write_ha_state gets the current value from this is_on method.
"""
return self._coordinator.cross_line_detection_timestamp_seconds > 0
return self._coordinator.get_event_timestamp(self._event_name) > 0

async def async_added_to_hass(self):
"""Connect to dispatcher listening for entity data notifications."""
self._coordinator.add_cross_line_detection_listener(self.async_write_ha_state)
self._coordinator.add_dahua_event_listener(self._event_name, self.async_write_ha_state)

@property
def should_poll(self) -> bool:
Expand Down
4 changes: 2 additions & 2 deletions custom_components/dahua/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

STREAMS = [STREAM_MAIN, STREAM_SUB, STREAM_BOTH]

DEFUALT_EVENTS = ["VideoMotion", "CrossLineDetection", "AlarmLocal", "VideoLoss", "VideoBlind"]
DEFAULT_EVENTS = ["VideoMotion", "CrossLineDetection", "AlarmLocal", "VideoLoss", "VideoBlind"]

ALL_EVENTS = ["VideoMotion",
"VideoLoss",
Expand Down Expand Up @@ -124,7 +124,7 @@ async def _show_config_form(self, user_input): # pylint: disable=unused-argumen
vol.Required(CONF_PORT, default="80"): str,
vol.Required(CONF_RTSP_PORT, default="554"): str,
vol.Required(CONF_STREAMS, default=STREAMS[0]): vol.In(STREAMS),
vol.Optional(CONF_EVENTS, default=DEFUALT_EVENTS): cv.multi_select(ALL_EVENTS),
vol.Optional(CONF_EVENTS, default=DEFAULT_EVENTS): cv.multi_select(ALL_EVENTS),
}
),
errors=self._errors,
Expand Down
2 changes: 2 additions & 0 deletions custom_components/dahua/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

# Device classes - https://www.home-assistant.io/integrations/binary_sensor/#device-class
MOTION_SENSOR_DEVICE_CLASS = "motion"
SAFETY_DEVICE_CLASS = "safety"
CONNECTIVITY_DEVICE_CLASS = "connectivity"

# Platforms
BINARY_SENSOR = "binary_sensor"
Expand Down

0 comments on commit 7f3bbb0

Please sign in to comment.