Skip to content

Commit

Permalink
Merge pull request #16 from rroller/crossline
Browse files Browse the repository at this point in the history
Add cross line detection support
  • Loading branch information
rroller authored Jun 18, 2021
2 parents 7dc0e3e + e5da89c commit 10f7c01
Show file tree
Hide file tree
Showing 4 changed files with 81 additions and 7 deletions.
21 changes: 19 additions & 2 deletions custom_components/dahua/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ class DahuaDataUpdateCoordinator(DataUpdateCoordinator):
def __init__(self, hass: HomeAssistant, dahua_client: DahuaClient, events: list) -> None:
"""Initialize."""
self.client: DahuaClient = dahua_client
self.dahua_event: DahuaEventThread = None
self.dahua_event: DahuaEventThread
self.platforms = []
self.initialized = False
self.model = ""
Expand All @@ -107,16 +107,18 @@ def __init__(self, hass: HomeAssistant, dahua_client: DahuaClient, events: list)
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
self.dahua_event = DahuaEventThread(hass, dahua_client, self.on_receive, events)

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

async def async_start_event_listener(self):
""" setup """
if self.events is not None:
self.dahua_event = DahuaEventThread(self.hass, self.machine_name, self.client, self.on_receive, self.events)
self.dahua_event.start()

async def async_stop(self, event: Any):
Expand Down Expand Up @@ -224,6 +226,7 @@ def on_receive(self, data_bytes: bytes):

# 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":
Expand All @@ -234,13 +237,27 @@ def on_receive(self, data_bytes: bytes):
self.motion_timestamp_seconds = 0
if self.motion_listener:
self.motion_listener()
if event["Code"] == "CrossLineDetection":
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()
elif action == "Stop":
self.cross_line_detection_timestamp_seconds = 0
if self.cross_line_detection_listener:
self.cross_line_detection_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 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 supports_siren(self) -> bool:
"""
Returns true if this camera has a siren. For example, the IPC-HDW3849HP-AS-PV does
Expand Down
57 changes: 56 additions & 1 deletion custom_components/dahua/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@
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)])

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


class DahuaMotionSensor(DahuaBaseEntity, BinarySensorEntity):
Expand All @@ -25,6 +29,7 @@ def __init__(self, coordinator: DahuaDataUpdateCoordinator, config_entry):

self._name = coordinator.get_device_name()
self._coordinator = coordinator
self._unique_id = coordinator.get_serial_number() + "_motion_alarm"

@property
def name(self):
Expand Down Expand Up @@ -55,3 +60,53 @@ async def async_added_to_hass(self):
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):
"""
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.
"""

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() + "_cross_line_alarm"

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

@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 a cross line detection event 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.
"""
return self._coordinator.cross_line_detection_timestamp_seconds > 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)

@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
1 change: 1 addition & 0 deletions custom_components/dahua/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,7 @@ async def stream_events(self, on_receive, events: list):
message to the client,the heartbeat message are "Heartbeat".
Note: Heartbeat message must be sent before heartbeat timeout
"""
# Use codes=[All] for all codes
codes = ",".join(events)
url = "http://{0}/cgi-bin/eventManager.cgi?action=attach&codes=[{1}]&heartbeat=2".format(self._address_with_port, codes)
if self._username is not None and self._password is not None:
Expand Down
9 changes: 5 additions & 4 deletions custom_components/dahua/thread.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,9 @@
class DahuaEventThread(threading.Thread):
"""Connects to device and subscribes to events. Mainly to capture motion detection events. """

def __init__(self, hass: HomeAssistant, name: str, client: DahuaClient, on_receive, events: list):
def __init__(self, hass: HomeAssistant, client: DahuaClient, on_receive, events: list):
"""Construct a thread listening for events."""
threading.Thread.__init__(self)
self.name = name
self.hass = hass
self.stopped = threading.Event()
self.on_receive = on_receive
Expand All @@ -28,6 +27,7 @@ def __init__(self, hass: HomeAssistant, name: str, client: DahuaClient, on_recei
def run(self):
"""Fetch events"""
self.started = True
_LOGGER.debug("Starting DahuaEventThread")

while 1:
# submit the coroutine to the event loop thread
Expand All @@ -39,20 +39,21 @@ def run(self):
# wait for the coroutine to finish
future.result()
except asyncio.TimeoutError as ex:
_LOGGER.warning("TimeoutError connecting to %s", self.name)
_LOGGER.warning("TimeoutError connecting to camera")
future.cancel()
except Exception as ex: # pylint: disable=broad-except
_LOGGER.debug("%s", ex)

if not self.started:
_LOGGER.debug("Exiting DahuaEventThread")
return

end_time = int(time.time())
if (end_time - start_time) < 10:
# We are failing fast when trying to connect to the camera. Let's retry slowly
time.sleep(60)

_LOGGER.debug("reconnecting to camera's %s event stream...", self.name)
_LOGGER.debug("reconnecting to camera's event stream...")

def stop(self):
""" Signals to the thread loop that we should stop """
Expand Down

0 comments on commit 10f7c01

Please sign in to comment.