Skip to content
This repository has been archived by the owner on Apr 6, 2023. It is now read-only.

Commit

Permalink
Merge pull request #453 from kmhallen/smtp
Browse files Browse the repository at this point in the history
Add optional SMTP server for event based AI detection
  • Loading branch information
cpainchaud authored Dec 12, 2022
2 parents 6144604 + 4822057 commit c72e0da
Show file tree
Hide file tree
Showing 9 changed files with 128 additions and 8 deletions.
21 changes: 18 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,17 +110,32 @@ This integration creates a camera entity, providing a live-stream configurable f
| Protocol | Switch between the RTMP or RTSP streaming protocol. |
| Channel | When using a single camera, choose stream 0. When using a NVR, switch between the different camera streams. |

## Binary Sensor
## Binary Sensors

When the camera supports motion detection events, a binary sensor is created for real-time motion detection. The time to switch motion detection off can be configured via the options menu, located at the integrations page. Please notice: for using the motion detection, your Homa Assistant should be reachable (within you local network) over http (not https).
When the camera supports motion detection events, a binary sensor is created for real-time motion detection. The time to switch motion detection off can be configured via the options menu, located at the integrations page. Please notice: for using the motion detection, your Homa Assistant should be reachable (within your local network) over http (not https).

| Parameter | Description |
| :------------------- | :---------------------------------------------------------------------------------------------------------- |
| Motion sensor off delay | Control how many seconds it takes (after the last motion detection) for the binary sensor to switch off. |

When the camera supports AI objects detection, a binary sensor is created for each type of object (person, vehicle, pet)

## Switch
The cameras only support webhooks for motion start/stop, and not any of the AI detections (person/vehicle/pet).
This may change in future firmware, but AI detections must be polled for now.
Optionally configure camera to send an email via SMTP on AI detection, and receive it in this Home Assistant plugin.
This allows event based start of AI detection start, but not stop.
The AI detection will be cleared in the next poll update.
Camera should be configured to email on person/vehicle detection (not motion), use Home Assistant's IP address, disable SSL/TLS, and select an arbitrary SMTP port.
The SMTP port should be unique for each integration.
Text, Text with Picture, and Text with Video will all work for email content, but there may be unnecessary delay with the picture or video options.
Other email fields in the camera configuration don't matter.
Tested on individual cameras, but not NVRs.

| Parameter | Description |
| :------------------- | :---------------------------------------------------------------------------------------------------------- |
| SMTP port | Optional port to listen for email event for AI detections. Default is 0 (disable). |

## Switches

Depending on the camera, the following switches are created:

Expand Down
6 changes: 6 additions & 0 deletions custom_components/reolink_dev/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@
BASE,
CONF_CHANNEL,
CONF_USE_HTTPS,
CONF_SMTP_PORT,
CONF_MOTION_OFF_DELAY,
CONF_PLAYBACK_MONTHS,
CONF_PROTOCOL,
CONF_STREAM,
CONF_THUMBNAIL_PATH,
CONF_STREAM_FORMAT,
CONF_MOTION_STATES_UPDATE_FALLBACK_DELAY,
DEFAULT_SMTP_PORT,
COORDINATOR,
MOTION_UPDATE_COORDINATOR,
DOMAIN,
Expand Down Expand Up @@ -93,6 +95,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry.data[CONF_PASSWORD],
)
await push.subscribe(base.event_id)
await push.set_smtp_port(entry.options.get(CONF_SMTP_PORT, DEFAULT_SMTP_PORT))
hass.data[DOMAIN][base.push_manager] = push

async def async_update_data():
Expand Down Expand Up @@ -159,6 +162,7 @@ async def update_listener(hass: HomeAssistant, entry: ConfigEntry):
await base.set_protocol(entry.options[CONF_PROTOCOL])
await base.set_stream(entry.options[CONF_STREAM])
await base.set_stream_format(entry.options[CONF_STREAM_FORMAT])
await base.set_smtp_port(entry.options[CONF_SMTP_PORT])

motion_state_coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][MOTION_UPDATE_COORDINATOR]

Expand All @@ -182,6 +186,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
base = hass.data[DOMAIN][entry.entry_id][BASE]
push = hass.data[DOMAIN][base.push_manager]

await base.set_smtp_port(0) # Stop SMTP server

if not await push.count_members() > 1:
await push.unsubscribe()
hass.data[DOMAIN].pop(base.push_manager)
Expand Down
87 changes: 84 additions & 3 deletions custom_components/reolink_dev/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
import logging
import os
import re
import base64

from aiosmtpd.controller import Controller

import datetime as dt
from typing import Optional
Expand Down Expand Up @@ -251,6 +254,10 @@ async def set_timeout(self, timeout):
self._timeout = timeout
await self._api.set_timeout(timeout)

async def set_smtp_port(self, port):
push = self._hass.data[DOMAIN][self.push_manager]
await push.set_smtp_port(port)

async def update_states(self):
"""Call the API of the camera device to update the states."""
await self._api.get_states()
Expand Down Expand Up @@ -345,6 +352,79 @@ def __init__(
self._webhook_id = None
self._event_id = None

self.smtp_motion_warn = True
self.smtp_port = 0
self.smtp = None

# Create/start/stop SMTP server on parameter change
async def set_smtp_port(self, port):
if self.smtp_port is not port:
if self.smtp:
_LOGGER.info("Stopping SMTP server on port %i", self.smtp_port)
self.smtp.stop()
self.smtp = None
if self.smtp is None and port is not None and port > 0:
_LOGGER.info("Starting SMTP server on port %i", port)
self.smtp = Controller(self, hostname='', port=port)
self.smtp.start()
self.smtp_port = port

# SMTP EHLO callback
async def handle_EHLO(server, session, envelope, hostname, responses):
_LOGGER.debug("SMTP EHLO")
return "" # Force error in EHLO querry so client falls back to HELO

# SMTP data callback
async def handle_DATA(self, server, session, envelope):
_LOGGER.debug("SMTP data")
handled = False
matches = re.findall(r'base64[\r\n]+(.+?)[\r\n]+', envelope.content.decode('ascii'))
if matches:
for x in matches:
_LOGGER.debug("SMTP data base64: %s", x)
try:
text = base64.b64decode(x).decode('ascii')
_LOGGER.debug("SMTP data ascii: %s", text)
except:
continue
if re.match(".*tested the e-mail alert.*", text) is not None:
# Full text: "If you receive this e-mail you have successfully set up and tested the e-mail alert from your IPC"
_LOGGER.warning("SMTP test email received")
handled = True
name = re.findall(r'Alarm Camera Name:\s*(.+?)\s*[\r\n]+', text)
event = re.findall(r'Alarm Event:\s*(.+?)\s*[\r\n]+', text)
if name and event:
_LOGGER.debug("SMTP name: %s", name[0])
_LOGGER.debug("SMTP event: %s", event[0])
if (event[0] == "Motion Detection"):
_LOGGER.info("SMTP motion detected")
handled = True
if self.smtp_motion_warn:
self.smtp_motion_warn = False
_LOGGER.warning("SMTP non-AI motion event is inferrior to webhooks,"
" and probably should be disabled."
" The time limit between events may mask AI detection events."
" This warning will only print once.")
self._hass.bus.async_fire(self._event_id, {"motion": True})
elif (event[0] == "Person Detected"):
_LOGGER.info("SMTP person detected")
handled = True
self._hass.bus.async_fire(self._event_id, {"motion": True, "smtp": "person"})
elif (event[0] == "Vehicle Detected"):
_LOGGER.info("SMTP vehicle detected")
handled = True
self._hass.bus.async_fire(self._event_id, {"motion": True, "smtp": "vehicle"})
elif (event[0] == "Pet Detected"):
_LOGGER.info("SMTP pet detected")
handled = True
self._hass.bus.async_fire(self._event_id, {"motion": True, "smtp": "pet"})

if not handled:
_LOGGER.warning("SMTP received unhandled message: %s", envelope.content.decode('ascii'))
return "541 ERROR"
else:
return "250 OK"

@property
def sman(self):
"""Return the session manager object."""
Expand Down Expand Up @@ -478,20 +558,21 @@ async def handle_webhook(hass, webhook_id, request):
_LOGGER.debug("Webhook called")

if not request.body_exists:
_LOGGER.debug("Webhook triggered without payload")
_LOGGER.warning("Webhook triggered without payload")

data = await request.text()
if not data:
_LOGGER.debug("Webhook triggered with unknown payload")
_LOGGER.warning("Webhook triggered with unknown payload")
return

_LOGGER_DATA.debug("Webhook received payload: %s", data)

matches = re.findall(r'Name="IsMotion" Value="(.+?)"', data)
if matches:
is_motion = matches[0] == "true"
_LOGGER_DATA.debug("Webhook received motion: %s", matches[0])
else:
_LOGGER.debug("Webhook triggered with unknown payload")
_LOGGER.warning("Webhook triggered with unknown payload")
return

event_id = await get_event_by_webhook(hass, webhook_id)
Expand Down
5 changes: 5 additions & 0 deletions custom_components/reolink_dev/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,11 @@ async def handle_event(self, event):
except KeyError:
pass

if event.data.get("smtp") is self._object_type:
self._event_state = True
if self.enabled:
self.async_schedule_update_ha_state()

if event.data.get("ai_refreshed") is not True:
return

Expand Down
8 changes: 8 additions & 0 deletions custom_components/reolink_dev/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@
BASE,
CONF_CHANNEL,
CONF_USE_HTTPS,
CONF_SMTP_PORT,
CONF_MOTION_OFF_DELAY,
CONF_PLAYBACK_MONTHS,
CONF_PROTOCOL,
CONF_STREAM,
CONF_STREAM_FORMAT,
CONF_THUMBNAIL_PATH,
CONF_MOTION_STATES_UPDATE_FALLBACK_DELAY,
DEFAULT_SMTP_PORT,
DEFAULT_MOTION_OFF_DELAY,
DEFAULT_USE_HTTPS,
DEFAULT_PLAYBACK_MONTHS,
Expand Down Expand Up @@ -186,6 +188,12 @@ async def async_step_init(self, user_input=None): # pylint: disable=unused-argu
), ): vol.In(
["h264", "h265"]
),
vol.Required(
CONF_SMTP_PORT,
default=self.config_entry.options.get(
CONF_SMTP_PORT, DEFAULT_SMTP_PORT
),
): cv.positive_int,
vol.Required(
CONF_MOTION_OFF_DELAY,
default=self.config_entry.options.get(
Expand Down
2 changes: 2 additions & 0 deletions custom_components/reolink_dev/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@
CONF_STREAM_FORMAT = "stream_format"
CONF_PROTOCOL = "protocol"
CONF_CHANNEL = "channel"
CONF_SMTP_PORT = "smtp_port"
CONF_MOTION_OFF_DELAY = "motion_off_delay"
CONF_PLAYBACK_MONTHS = "playback_months"
CONF_THUMBNAIL_PATH = "playback_thumbnail_path"
CONF_MOTION_STATES_UPDATE_FALLBACK_DELAY = "motion_states_update_fallback_delay"

DEFAULT_USE_HTTPS = True
DEFAULT_CHANNEL = 1
DEFAULT_SMTP_PORT = 0
DEFAULT_MOTION_OFF_DELAY = 60
DEFAULT_PROTOCOL = "rtmp"
DEFAULT_STREAM = "main"
Expand Down
3 changes: 2 additions & 1 deletion custom_components/reolink_dev/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"version": "0.58",
"iot_class": "local_polling",
"requirements": [
"reolink==0.0.62"
"reolink==0.0.62",
"aiosmtpd>=1.4.2"
],
"dependencies": [
"ffmpeg",
Expand Down
3 changes: 2 additions & 1 deletion custom_components/reolink_dev/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,12 @@
"protocol": "Protocol",
"stream": "Stream",
"timeout": "Timeout",
"smtp_port": "SMTP port for AI detection events (0 to disable)",
"motion_states_update_fallback_delay": "Motion states update fallback delay (seconds, 0 or less to disable)",
"motion_off_delay": "Motion sensor off delay (seconds)",
"playback_months": "Playback range (months)",
"playback_thumbnail_path": "Custom thumbnail path",
"stream_format": "Stream format"
"stream_format": "Stream format"
}
}
}
Expand Down
1 change: 1 addition & 0 deletions custom_components/reolink_dev/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"protocol": "Protocol",
"stream": "Stream",
"timeout": "Timeout (seconds)",
"smtp_port": "SMTP port for AI detection events (0 to disable)",
"motion_states_update_fallback_delay": "Motion states update fallback delay (seconds, 0 or less to disable): Reolink's event subscription is not always reliable, this will help to avoid event misses",
"motion_off_delay": "Motion sensor off delay (seconds)",
"playback_months": "Playback range (months)",
Expand Down

0 comments on commit c72e0da

Please sign in to comment.