2022-11-20 14:36:01 +01:00
|
|
|
import datetime
|
2021-06-14 14:31:13 +02:00
|
|
|
import json
|
2020-11-04 04:26:39 +01:00
|
|
|
import logging
|
2020-11-04 13:28:07 +01:00
|
|
|
import threading
|
2021-06-14 14:31:13 +02:00
|
|
|
from wsgiref.simple_server import make_server
|
2020-11-01 15:06:15 +01:00
|
|
|
|
2020-11-04 13:31:25 +01:00
|
|
|
import paho.mqtt.client as mqtt
|
2021-06-14 14:31:13 +02:00
|
|
|
from ws4py.server.wsgirefserver import (
|
|
|
|
WebSocketWSGIHandler,
|
|
|
|
WebSocketWSGIRequestHandler,
|
|
|
|
WSGIServer,
|
|
|
|
)
|
|
|
|
from ws4py.server.wsgiutils import WebSocketWSGIApplication
|
|
|
|
from ws4py.websocket import WebSocket
|
2020-11-04 13:31:25 +01:00
|
|
|
|
2020-12-23 15:54:08 +01:00
|
|
|
from frigate.config import FrigateConfig
|
2022-11-20 14:36:01 +01:00
|
|
|
from frigate.types import CameraMetricsTypes
|
2021-06-21 17:00:50 +02:00
|
|
|
from frigate.util import restart_frigate
|
2020-11-03 15:15:58 +01:00
|
|
|
|
2020-11-04 04:26:39 +01:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
2021-02-17 14:23:32 +01:00
|
|
|
|
2022-11-20 14:36:01 +01:00
|
|
|
class FrigateMqttClient:
|
|
|
|
"""Frigate wrapper for mqtt client."""
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
self, config: FrigateConfig, camera_metrics: dict[str, CameraMetricsTypes]
|
|
|
|
) -> None:
|
|
|
|
self.config = config
|
|
|
|
self.mqtt_config = config.mqtt
|
|
|
|
self.camera_metrics = camera_metrics
|
|
|
|
self.connected: bool = False
|
|
|
|
self._start()
|
|
|
|
|
|
|
|
def _set_initial_topics(self) -> None:
|
|
|
|
"""Set initial state topics."""
|
|
|
|
for camera_name, camera in self.config.cameras.items():
|
|
|
|
self.publish(
|
|
|
|
f"{self.mqtt_config.topic_prefix}/{camera_name}/recordings/state",
|
|
|
|
"ON" if camera.record.enabled else "OFF",
|
|
|
|
retain=True,
|
|
|
|
)
|
|
|
|
self.publish(
|
|
|
|
f"{self.mqtt_config.topic_prefix}/{camera_name}/snapshots/state",
|
|
|
|
"ON" if camera.snapshots.enabled else "OFF",
|
|
|
|
retain=True,
|
|
|
|
)
|
|
|
|
self.publish(
|
|
|
|
f"{self.mqtt_config.topic_prefix}/{camera_name}/detect/state",
|
|
|
|
"ON" if camera.detect.enabled else "OFF",
|
|
|
|
retain=True,
|
|
|
|
)
|
|
|
|
self.publish(
|
|
|
|
f"{self.mqtt_config.topic_prefix}/{camera_name}/motion/state",
|
|
|
|
"ON",
|
|
|
|
retain=True,
|
|
|
|
)
|
|
|
|
self.publish(
|
|
|
|
f"{self.mqtt_config.topic_prefix}/{camera_name}/improve_contrast/state",
|
|
|
|
"ON" if camera.motion.improve_contrast else "OFF",
|
|
|
|
retain=True,
|
|
|
|
)
|
|
|
|
self.publish(
|
|
|
|
f"{self.mqtt_config.topic_prefix}/{camera_name}/motion_threshold/state",
|
|
|
|
camera.motion.threshold,
|
|
|
|
retain=True,
|
|
|
|
)
|
|
|
|
self.publish(
|
|
|
|
f"{self.mqtt_config.topic_prefix}/{camera_name}/motion_contour_area/state",
|
|
|
|
camera.motion.contour_area,
|
|
|
|
retain=True,
|
|
|
|
)
|
|
|
|
self.publish(
|
|
|
|
f"{self.mqtt_config.topic_prefix}/{camera_name}/motion",
|
|
|
|
"OFF",
|
|
|
|
retain=False,
|
|
|
|
)
|
|
|
|
|
|
|
|
self.publish(
|
|
|
|
self.mqtt_config.topic_prefix + "/available", "online", retain=True
|
|
|
|
)
|
2020-12-23 15:54:08 +01:00
|
|
|
|
2022-11-20 14:36:01 +01:00
|
|
|
def on_recordings_command(
|
|
|
|
self, client: mqtt.Client, userdata, message: mqtt.MQTTMessage
|
|
|
|
) -> None:
|
|
|
|
"""Callback for recordings topic."""
|
2020-12-23 15:54:08 +01:00
|
|
|
payload = message.payload.decode()
|
2021-07-11 21:49:10 +02:00
|
|
|
logger.debug(f"on_recordings_toggle: {message.topic} {payload}")
|
2020-12-23 15:54:08 +01:00
|
|
|
|
2021-02-17 14:23:32 +01:00
|
|
|
camera_name = message.topic.split("/")[-3]
|
2020-12-23 15:54:08 +01:00
|
|
|
|
2022-11-20 14:36:01 +01:00
|
|
|
record_settings = self.config.cameras[camera_name].record
|
2020-12-24 05:16:36 +01:00
|
|
|
|
2021-02-17 14:23:32 +01:00
|
|
|
if payload == "ON":
|
2021-07-11 21:49:10 +02:00
|
|
|
if not record_settings.enabled:
|
|
|
|
logger.info(f"Turning on recordings for {camera_name} via mqtt")
|
|
|
|
record_settings.enabled = True
|
2021-02-17 14:23:32 +01:00
|
|
|
elif payload == "OFF":
|
2021-07-11 21:49:10 +02:00
|
|
|
if record_settings.enabled:
|
|
|
|
logger.info(f"Turning off recordings for {camera_name} via mqtt")
|
|
|
|
record_settings.enabled = False
|
2020-12-24 05:16:36 +01:00
|
|
|
else:
|
|
|
|
logger.warning(f"Received unsupported value at {message.topic}: {payload}")
|
|
|
|
|
2021-01-19 14:41:17 +01:00
|
|
|
state_topic = f"{message.topic[:-4]}/state"
|
2022-11-20 14:36:01 +01:00
|
|
|
self.publish(state_topic, payload, retain=True)
|
2020-12-24 05:16:36 +01:00
|
|
|
|
2022-11-20 14:36:01 +01:00
|
|
|
def on_snapshots_command(
|
|
|
|
self, client: mqtt.Client, userdata, message: mqtt.MQTTMessage
|
|
|
|
) -> None:
|
|
|
|
"""Callback for snapshots topic."""
|
2020-12-24 05:16:36 +01:00
|
|
|
payload = message.payload.decode()
|
|
|
|
logger.debug(f"on_snapshots_toggle: {message.topic} {payload}")
|
|
|
|
|
2021-02-17 14:23:32 +01:00
|
|
|
camera_name = message.topic.split("/")[-3]
|
2020-12-24 05:16:36 +01:00
|
|
|
|
2022-11-20 14:36:01 +01:00
|
|
|
snapshots_settings = self.config.cameras[camera_name].snapshots
|
2020-12-24 05:16:36 +01:00
|
|
|
|
2021-02-17 14:23:32 +01:00
|
|
|
if payload == "ON":
|
2020-12-24 05:16:36 +01:00
|
|
|
if not snapshots_settings.enabled:
|
|
|
|
logger.info(f"Turning on snapshots for {camera_name} via mqtt")
|
Use dataclasses for config handling
Use config data classes to eliminate some of the boilerplate associated
with setting up the configuration. In particular, using dataclasses
removes a lot of the boilerplate around assigning properties to the
object and allows these to be easily immutable by freezing them. In the
case of simple, non-nested dataclasses, this also provides more
convenient `asdict` helpers.
To set this up, where previously the objects would be parsed from the
config via the `__init__` method, create a `build` classmethod that does
this and calls the dataclass initializer.
Some of the objects are mutated at runtime, in particular some of the
zones are mutated to set the color (this might be able to be refactored
out) and some of the camera functionality can be enabled/disabled. Some
of the configs with `enabled` properties don't seem to have mqtt hooks
to be able to toggle this, in particular, the clips, snapshots, and
detect can be toggled but rtmp and record configs do not, but all of
these configs are still not frozen in case there is some other
functionality I am missing.
There are a couple other minor fixes here, one that was introduced
by me recently where `max_seconds` was not defined, the other to
properly `get()` the message payload when handling publishing mqtt
messages sent via websocket.
2021-05-23 00:28:15 +02:00
|
|
|
snapshots_settings.enabled = True
|
2021-02-17 14:23:32 +01:00
|
|
|
elif payload == "OFF":
|
2020-12-24 05:16:36 +01:00
|
|
|
if snapshots_settings.enabled:
|
|
|
|
logger.info(f"Turning off snapshots for {camera_name} via mqtt")
|
Use dataclasses for config handling
Use config data classes to eliminate some of the boilerplate associated
with setting up the configuration. In particular, using dataclasses
removes a lot of the boilerplate around assigning properties to the
object and allows these to be easily immutable by freezing them. In the
case of simple, non-nested dataclasses, this also provides more
convenient `asdict` helpers.
To set this up, where previously the objects would be parsed from the
config via the `__init__` method, create a `build` classmethod that does
this and calls the dataclass initializer.
Some of the objects are mutated at runtime, in particular some of the
zones are mutated to set the color (this might be able to be refactored
out) and some of the camera functionality can be enabled/disabled. Some
of the configs with `enabled` properties don't seem to have mqtt hooks
to be able to toggle this, in particular, the clips, snapshots, and
detect can be toggled but rtmp and record configs do not, but all of
these configs are still not frozen in case there is some other
functionality I am missing.
There are a couple other minor fixes here, one that was introduced
by me recently where `max_seconds` was not defined, the other to
properly `get()` the message payload when handling publishing mqtt
messages sent via websocket.
2021-05-23 00:28:15 +02:00
|
|
|
snapshots_settings.enabled = False
|
2020-12-23 15:54:08 +01:00
|
|
|
else:
|
|
|
|
logger.warning(f"Received unsupported value at {message.topic}: {payload}")
|
|
|
|
|
2021-01-19 14:41:17 +01:00
|
|
|
state_topic = f"{message.topic[:-4]}/state"
|
2022-11-20 14:36:01 +01:00
|
|
|
self.publish(state_topic, payload, retain=True)
|
2021-02-17 14:23:32 +01:00
|
|
|
|
2022-11-20 14:36:01 +01:00
|
|
|
def on_detect_command(
|
|
|
|
self, client: mqtt.Client, userdata, message: mqtt.MQTTMessage
|
|
|
|
) -> None:
|
|
|
|
"""Callback for detect topic."""
|
2021-01-16 03:52:59 +01:00
|
|
|
payload = message.payload.decode()
|
|
|
|
logger.debug(f"on_detect_toggle: {message.topic} {payload}")
|
|
|
|
|
2021-02-17 14:23:32 +01:00
|
|
|
camera_name = message.topic.split("/")[-3]
|
2021-01-16 03:52:59 +01:00
|
|
|
|
2022-11-20 14:36:01 +01:00
|
|
|
detect_settings = self.config.cameras[camera_name].detect
|
2021-01-16 04:53:40 +01:00
|
|
|
|
2021-02-17 14:23:32 +01:00
|
|
|
if payload == "ON":
|
2022-11-20 14:36:01 +01:00
|
|
|
if not self.camera_metrics[camera_name]["detection_enabled"].value:
|
2021-01-16 03:52:59 +01:00
|
|
|
logger.info(f"Turning on detection for {camera_name} via mqtt")
|
2022-11-20 14:36:01 +01:00
|
|
|
self.camera_metrics[camera_name]["detection_enabled"].value = True
|
Use dataclasses for config handling
Use config data classes to eliminate some of the boilerplate associated
with setting up the configuration. In particular, using dataclasses
removes a lot of the boilerplate around assigning properties to the
object and allows these to be easily immutable by freezing them. In the
case of simple, non-nested dataclasses, this also provides more
convenient `asdict` helpers.
To set this up, where previously the objects would be parsed from the
config via the `__init__` method, create a `build` classmethod that does
this and calls the dataclass initializer.
Some of the objects are mutated at runtime, in particular some of the
zones are mutated to set the color (this might be able to be refactored
out) and some of the camera functionality can be enabled/disabled. Some
of the configs with `enabled` properties don't seem to have mqtt hooks
to be able to toggle this, in particular, the clips, snapshots, and
detect can be toggled but rtmp and record configs do not, but all of
these configs are still not frozen in case there is some other
functionality I am missing.
There are a couple other minor fixes here, one that was introduced
by me recently where `max_seconds` was not defined, the other to
properly `get()` the message payload when handling publishing mqtt
messages sent via websocket.
2021-05-23 00:28:15 +02:00
|
|
|
detect_settings.enabled = True
|
2022-04-26 14:29:28 +02:00
|
|
|
|
2022-11-20 14:36:01 +01:00
|
|
|
if not self.camera_metrics[camera_name]["motion_enabled"].value:
|
2022-04-26 14:29:28 +02:00
|
|
|
logger.info(
|
|
|
|
f"Turning on motion for {camera_name} due to detection being enabled."
|
|
|
|
)
|
2022-11-20 14:36:01 +01:00
|
|
|
self.camera_metrics[camera_name]["motion_enabled"].value = True
|
2022-10-01 16:01:43 +02:00
|
|
|
state_topic = f"{message.topic[:-11]}/motion/state"
|
2022-11-20 14:36:01 +01:00
|
|
|
self.publish(state_topic, payload, retain=True)
|
2021-02-17 14:23:32 +01:00
|
|
|
elif payload == "OFF":
|
2022-11-20 14:36:01 +01:00
|
|
|
if self.camera_metrics[camera_name]["detection_enabled"].value:
|
2021-01-16 03:52:59 +01:00
|
|
|
logger.info(f"Turning off detection for {camera_name} via mqtt")
|
2022-11-20 14:36:01 +01:00
|
|
|
self.camera_metrics[camera_name]["detection_enabled"].value = False
|
Use dataclasses for config handling
Use config data classes to eliminate some of the boilerplate associated
with setting up the configuration. In particular, using dataclasses
removes a lot of the boilerplate around assigning properties to the
object and allows these to be easily immutable by freezing them. In the
case of simple, non-nested dataclasses, this also provides more
convenient `asdict` helpers.
To set this up, where previously the objects would be parsed from the
config via the `__init__` method, create a `build` classmethod that does
this and calls the dataclass initializer.
Some of the objects are mutated at runtime, in particular some of the
zones are mutated to set the color (this might be able to be refactored
out) and some of the camera functionality can be enabled/disabled. Some
of the configs with `enabled` properties don't seem to have mqtt hooks
to be able to toggle this, in particular, the clips, snapshots, and
detect can be toggled but rtmp and record configs do not, but all of
these configs are still not frozen in case there is some other
functionality I am missing.
There are a couple other minor fixes here, one that was introduced
by me recently where `max_seconds` was not defined, the other to
properly `get()` the message payload when handling publishing mqtt
messages sent via websocket.
2021-05-23 00:28:15 +02:00
|
|
|
detect_settings.enabled = False
|
2021-01-16 03:52:59 +01:00
|
|
|
else:
|
|
|
|
logger.warning(f"Received unsupported value at {message.topic}: {payload}")
|
|
|
|
|
2021-01-19 14:41:17 +01:00
|
|
|
state_topic = f"{message.topic[:-4]}/state"
|
2022-11-20 14:36:01 +01:00
|
|
|
self.publish(state_topic, payload, retain=True)
|
2020-12-23 15:54:08 +01:00
|
|
|
|
2022-11-20 14:36:01 +01:00
|
|
|
def on_motion_command(
|
|
|
|
self, client: mqtt.Client, userdata, message: mqtt.MQTTMessage
|
|
|
|
) -> None:
|
|
|
|
"""Callback for motion topic."""
|
2022-04-26 14:29:28 +02:00
|
|
|
payload = message.payload.decode()
|
|
|
|
logger.debug(f"on_motion_toggle: {message.topic} {payload}")
|
|
|
|
|
|
|
|
camera_name = message.topic.split("/")[-3]
|
|
|
|
|
|
|
|
if payload == "ON":
|
2022-11-20 14:36:01 +01:00
|
|
|
if not self.camera_metrics[camera_name]["motion_enabled"].value:
|
2022-04-26 14:29:28 +02:00
|
|
|
logger.info(f"Turning on motion for {camera_name} via mqtt")
|
2022-11-20 14:36:01 +01:00
|
|
|
self.camera_metrics[camera_name]["motion_enabled"].value = True
|
2022-04-26 14:29:28 +02:00
|
|
|
elif payload == "OFF":
|
2022-11-20 14:36:01 +01:00
|
|
|
if self.camera_metrics[camera_name]["detection_enabled"].value:
|
2022-04-26 14:29:28 +02:00
|
|
|
logger.error(
|
|
|
|
f"Turning off motion is not allowed when detection is enabled."
|
|
|
|
)
|
|
|
|
return
|
|
|
|
|
2022-11-20 14:36:01 +01:00
|
|
|
if self.camera_metrics[camera_name]["motion_enabled"].value:
|
2022-04-26 14:29:28 +02:00
|
|
|
logger.info(f"Turning off motion for {camera_name} via mqtt")
|
2022-11-20 14:36:01 +01:00
|
|
|
self.camera_metrics[camera_name]["motion_enabled"].value = False
|
2022-04-26 14:29:28 +02:00
|
|
|
else:
|
|
|
|
logger.warning(f"Received unsupported value at {message.topic}: {payload}")
|
|
|
|
|
|
|
|
state_topic = f"{message.topic[:-4]}/state"
|
2022-11-20 14:36:01 +01:00
|
|
|
self.publish(state_topic, payload, retain=True)
|
2022-04-26 14:29:28 +02:00
|
|
|
|
2022-11-20 14:36:01 +01:00
|
|
|
def on_improve_contrast_command(
|
|
|
|
self, client: mqtt.Client, userdata, message: mqtt.MQTTMessage
|
|
|
|
) -> None:
|
|
|
|
"""Callback for improve_contrast topic."""
|
2022-04-16 15:52:02 +02:00
|
|
|
payload = message.payload.decode()
|
|
|
|
logger.debug(f"on_improve_contrast_toggle: {message.topic} {payload}")
|
|
|
|
|
|
|
|
camera_name = message.topic.split("/")[-3]
|
|
|
|
|
2022-11-20 14:36:01 +01:00
|
|
|
motion_settings = self.config.cameras[camera_name].motion
|
2022-04-16 15:52:02 +02:00
|
|
|
|
|
|
|
if payload == "ON":
|
2022-11-20 14:36:01 +01:00
|
|
|
if not self.camera_metrics[camera_name]["improve_contrast_enabled"].value:
|
2022-04-16 15:52:02 +02:00
|
|
|
logger.info(f"Turning on improve contrast for {camera_name} via mqtt")
|
2022-11-20 14:36:01 +01:00
|
|
|
self.camera_metrics[camera_name][
|
|
|
|
"improve_contrast_enabled"
|
|
|
|
].value = True
|
2022-04-16 15:52:02 +02:00
|
|
|
motion_settings.improve_contrast = True
|
|
|
|
elif payload == "OFF":
|
2022-11-20 14:36:01 +01:00
|
|
|
if self.camera_metrics[camera_name]["improve_contrast_enabled"].value:
|
2022-04-16 15:52:02 +02:00
|
|
|
logger.info(f"Turning off improve contrast for {camera_name} via mqtt")
|
2022-11-20 14:36:01 +01:00
|
|
|
self.camera_metrics[camera_name][
|
|
|
|
"improve_contrast_enabled"
|
|
|
|
].value = False
|
2022-04-16 15:52:02 +02:00
|
|
|
motion_settings.improve_contrast = False
|
|
|
|
else:
|
|
|
|
logger.warning(f"Received unsupported value at {message.topic}: {payload}")
|
|
|
|
|
|
|
|
state_topic = f"{message.topic[:-4]}/state"
|
2022-11-20 14:36:01 +01:00
|
|
|
self.publish(state_topic, payload, retain=True)
|
2022-04-16 15:52:02 +02:00
|
|
|
|
2022-11-20 14:36:01 +01:00
|
|
|
def on_motion_threshold_command(
|
|
|
|
self, client: mqtt.Client, userdata, message: mqtt.MQTTMessage
|
|
|
|
) -> None:
|
|
|
|
"""Callback for motion threshold topic."""
|
2022-04-27 16:52:45 +02:00
|
|
|
try:
|
|
|
|
payload = int(message.payload.decode())
|
|
|
|
except ValueError:
|
|
|
|
logger.warning(
|
|
|
|
f"Received unsupported value at {message.topic}: {message.payload.decode()}"
|
|
|
|
)
|
|
|
|
return
|
|
|
|
|
|
|
|
logger.debug(f"on_motion_threshold_toggle: {message.topic} {payload}")
|
|
|
|
|
|
|
|
camera_name = message.topic.split("/")[-3]
|
|
|
|
|
2022-11-20 14:36:01 +01:00
|
|
|
motion_settings = self.config.cameras[camera_name].motion
|
2022-04-27 16:52:45 +02:00
|
|
|
|
|
|
|
logger.info(f"Setting motion threshold for {camera_name} via mqtt: {payload}")
|
2022-11-20 14:36:01 +01:00
|
|
|
self.camera_metrics[camera_name]["motion_threshold"].value = payload
|
2022-04-27 16:52:45 +02:00
|
|
|
motion_settings.threshold = payload
|
|
|
|
|
|
|
|
state_topic = f"{message.topic[:-4]}/state"
|
2022-11-20 14:36:01 +01:00
|
|
|
self.publish(state_topic, payload, retain=True)
|
2022-04-27 16:52:45 +02:00
|
|
|
|
2022-11-20 14:36:01 +01:00
|
|
|
def on_motion_contour_area_command(
|
|
|
|
self, client: mqtt.Client, userdata, message: mqtt.MQTTMessage
|
|
|
|
) -> None:
|
|
|
|
"""Callback for motion contour topic."""
|
2022-04-27 16:52:45 +02:00
|
|
|
try:
|
|
|
|
payload = int(message.payload.decode())
|
|
|
|
except ValueError:
|
|
|
|
logger.warning(
|
|
|
|
f"Received unsupported value at {message.topic}: {message.payload.decode()}"
|
|
|
|
)
|
|
|
|
return
|
|
|
|
|
|
|
|
logger.debug(f"on_motion_contour_area_toggle: {message.topic} {payload}")
|
|
|
|
|
|
|
|
camera_name = message.topic.split("/")[-3]
|
|
|
|
|
2022-11-20 14:36:01 +01:00
|
|
|
motion_settings = self.config.cameras[camera_name].motion
|
2022-04-27 16:52:45 +02:00
|
|
|
|
|
|
|
logger.info(
|
|
|
|
f"Setting motion contour area for {camera_name} via mqtt: {payload}"
|
|
|
|
)
|
2022-11-20 14:36:01 +01:00
|
|
|
self.camera_metrics[camera_name]["motion_contour_area"].value = payload
|
2022-04-27 16:52:45 +02:00
|
|
|
motion_settings.contour_area = payload
|
|
|
|
|
|
|
|
state_topic = f"{message.topic[:-4]}/state"
|
2022-11-20 14:36:01 +01:00
|
|
|
self.publish(state_topic, payload, retain=True)
|
2022-04-27 16:52:45 +02:00
|
|
|
|
2022-11-20 14:36:01 +01:00
|
|
|
def on_restart_command(
|
|
|
|
client: mqtt.Client, userdata, message: mqtt.MQTTMessage
|
|
|
|
) -> None:
|
|
|
|
"""Callback to restart frigate."""
|
2021-07-06 14:22:17 +02:00
|
|
|
restart_frigate()
|
2021-06-21 17:00:50 +02:00
|
|
|
|
2022-11-20 14:36:01 +01:00
|
|
|
def _on_connect(self, client: mqtt.Client, userdata, flags, rc) -> None:
|
|
|
|
"""Mqtt connection callback."""
|
2020-11-04 13:28:07 +01:00
|
|
|
threading.current_thread().name = "mqtt"
|
2020-11-01 15:06:15 +01:00
|
|
|
if rc != 0:
|
|
|
|
if rc == 3:
|
2022-04-16 15:42:44 +02:00
|
|
|
logger.error(
|
|
|
|
"Unable to connect to MQTT server: MQTT Server unavailable"
|
|
|
|
)
|
2020-11-01 15:06:15 +01:00
|
|
|
elif rc == 4:
|
2022-04-16 15:42:44 +02:00
|
|
|
logger.error(
|
|
|
|
"Unable to connect to MQTT server: MQTT Bad username or password"
|
|
|
|
)
|
2020-11-01 15:06:15 +01:00
|
|
|
elif rc == 5:
|
2021-08-24 14:59:31 +02:00
|
|
|
logger.error("Unable to connect to MQTT server: MQTT Not authorized")
|
2020-11-01 15:06:15 +01:00
|
|
|
else:
|
2021-02-17 14:23:32 +01:00
|
|
|
logger.error(
|
2021-08-24 14:59:31 +02:00
|
|
|
"Unable to connect to MQTT server: Connection refused. Error code: "
|
2021-02-17 14:23:32 +01:00
|
|
|
+ str(rc)
|
|
|
|
)
|
|
|
|
|
2022-11-20 14:36:01 +01:00
|
|
|
self.connected = True
|
2021-12-31 18:59:43 +01:00
|
|
|
logger.debug("MQTT connected")
|
2022-11-20 14:36:01 +01:00
|
|
|
client.subscribe(f"{self.mqtt_config.topic_prefix}/#")
|
|
|
|
self._set_initial_topics()
|
|
|
|
|
|
|
|
def _on_disconnect(self, client: mqtt.Client, userdata, flags, rc) -> None:
|
|
|
|
"""Mqtt disconnection callback."""
|
|
|
|
self.connected = False
|
|
|
|
logger.error("MQTT disconnected")
|
|
|
|
|
|
|
|
def _start(self) -> None:
|
|
|
|
"""Start mqtt client."""
|
|
|
|
self.client = mqtt.Client(client_id=self.mqtt_config.client_id)
|
|
|
|
self.client.on_connect = self._on_connect
|
|
|
|
self.client.will_set(
|
|
|
|
self.mqtt_config.topic_prefix + "/available",
|
|
|
|
payload="offline",
|
|
|
|
qos=1,
|
|
|
|
retain=True,
|
2022-04-27 16:52:45 +02:00
|
|
|
)
|
2020-12-23 15:54:08 +01:00
|
|
|
|
2022-11-20 14:36:01 +01:00
|
|
|
# register callbacks
|
|
|
|
for name in self.config.cameras.keys():
|
|
|
|
self.client.message_callback_add(
|
|
|
|
f"{self.mqtt_config.topic_prefix}/{name}/recordings/set",
|
|
|
|
self.on_recordings_command,
|
2021-06-14 14:31:13 +02:00
|
|
|
)
|
2022-11-20 14:36:01 +01:00
|
|
|
self.client.message_callback_add(
|
|
|
|
f"{self.mqtt_config.topic_prefix}/{name}/snapshots/set",
|
|
|
|
self.on_snapshots_command,
|
|
|
|
)
|
|
|
|
self.client.message_callback_add(
|
|
|
|
f"{self.mqtt_config.topic_prefix}/{name}/detect/set",
|
|
|
|
self.on_detect_command,
|
|
|
|
)
|
|
|
|
self.client.message_callback_add(
|
|
|
|
f"{self.mqtt_config.topic_prefix}/{name}/motion/set",
|
|
|
|
self.on_motion_command,
|
|
|
|
)
|
|
|
|
self.client.message_callback_add(
|
|
|
|
f"{self.mqtt_config.topic_prefix}/{name}/improve_contrast/set",
|
|
|
|
self.on_improve_contrast_command,
|
|
|
|
)
|
|
|
|
self.client.message_callback_add(
|
|
|
|
f"{self.mqtt_config.topic_prefix}/{name}/motion_threshold/set",
|
|
|
|
self.on_motion_threshold_command,
|
|
|
|
)
|
|
|
|
self.client.message_callback_add(
|
|
|
|
f"{self.mqtt_config.topic_prefix}/{name}/motion_contour_area/set",
|
|
|
|
self.on_motion_contour_area_command,
|
|
|
|
)
|
|
|
|
|
|
|
|
self.client.message_callback_add(
|
|
|
|
f"{self.mqtt_config.topic_prefix}/restart", self.on_restart_command
|
2022-07-15 15:05:35 +02:00
|
|
|
)
|
2020-12-23 15:54:08 +01:00
|
|
|
|
2022-11-20 14:36:01 +01:00
|
|
|
if not self.mqtt_config.tls_ca_certs is None:
|
|
|
|
if (
|
|
|
|
not self.mqtt_config.tls_client_cert is None
|
|
|
|
and not self.mqtt_config.tls_client_key is None
|
|
|
|
):
|
|
|
|
self.client.tls_set(
|
|
|
|
self.mqtt_config.tls_ca_certs,
|
|
|
|
self.mqtt_config.tls_client_cert,
|
|
|
|
self.mqtt_config.tls_client_key,
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
self.client.tls_set(self.mqtt_config.tls_ca_certs)
|
|
|
|
if not self.mqtt_config.tls_insecure is None:
|
|
|
|
self.client.tls_insecure_set(self.mqtt_config.tls_insecure)
|
|
|
|
if not self.mqtt_config.user is None:
|
|
|
|
self.client.username_pw_set(
|
|
|
|
self.mqtt_config.user, password=self.mqtt_config.password
|
|
|
|
)
|
|
|
|
try:
|
|
|
|
# https://stackoverflow.com/a/55390477
|
|
|
|
# with connect_async, retries are handled automatically
|
|
|
|
self.client.connect_async(self.mqtt_config.host, self.mqtt_config.port, 60)
|
|
|
|
self.client.loop_start()
|
|
|
|
except Exception as e:
|
|
|
|
logger.error(f"Unable to connect to MQTT server: {e}")
|
|
|
|
return
|
|
|
|
|
|
|
|
def publish(self, topic: str, payload, retain: bool = False) -> None:
|
|
|
|
"""Wrapper for publishing when client is in valid state."""
|
|
|
|
if not self.connected:
|
|
|
|
logger.error(f"Unable to publish to {topic}: client is not connected")
|
|
|
|
return
|
|
|
|
|
|
|
|
self.client.publish(topic, payload, retain=retain)
|
|
|
|
|
|
|
|
def add_topic_callback(self, topic: str, callback) -> None:
|
|
|
|
self.client.message_callback_add(topic, callback)
|
2021-06-14 14:31:13 +02:00
|
|
|
|
|
|
|
|
|
|
|
class MqttSocketRelay:
|
2022-11-20 14:36:01 +01:00
|
|
|
def __init__(self, mqtt_client: FrigateMqttClient, topic_prefix: str):
|
2021-06-14 14:31:13 +02:00
|
|
|
self.mqtt_client = mqtt_client
|
|
|
|
self.topic_prefix = topic_prefix
|
|
|
|
|
|
|
|
def start(self):
|
|
|
|
class MqttWebSocket(WebSocket):
|
|
|
|
topic_prefix = self.topic_prefix
|
|
|
|
mqtt_client = self.mqtt_client
|
|
|
|
|
|
|
|
def received_message(self, message):
|
|
|
|
try:
|
|
|
|
json_message = json.loads(message.data.decode("utf-8"))
|
|
|
|
json_message = {
|
|
|
|
"topic": f"{self.topic_prefix}/{json_message['topic']}",
|
2021-07-06 14:22:17 +02:00
|
|
|
"payload": json_message.get("payload"),
|
2021-06-14 14:31:13 +02:00
|
|
|
"retain": json_message.get("retain", False),
|
|
|
|
}
|
|
|
|
except Exception as e:
|
|
|
|
logger.warning("Unable to parse websocket message as valid json.")
|
|
|
|
return
|
|
|
|
|
|
|
|
logger.debug(
|
|
|
|
f"Publishing mqtt message from websockets at {json_message['topic']}."
|
|
|
|
)
|
|
|
|
self.mqtt_client.publish(
|
|
|
|
json_message["topic"],
|
|
|
|
json_message["payload"],
|
|
|
|
retain=json_message["retain"],
|
|
|
|
)
|
|
|
|
|
|
|
|
# start a websocket server on 5002
|
|
|
|
WebSocketWSGIHandler.http_version = "1.1"
|
|
|
|
self.websocket_server = make_server(
|
|
|
|
"127.0.0.1",
|
|
|
|
5002,
|
|
|
|
server_class=WSGIServer,
|
|
|
|
handler_class=WebSocketWSGIRequestHandler,
|
|
|
|
app=WebSocketWSGIApplication(handler_cls=MqttWebSocket),
|
|
|
|
)
|
|
|
|
self.websocket_server.initialize_websockets_manager()
|
|
|
|
self.websocket_thread = threading.Thread(
|
|
|
|
target=self.websocket_server.serve_forever
|
|
|
|
)
|
|
|
|
|
|
|
|
def send(client, userdata, message):
|
|
|
|
"""Sends mqtt messages to clients."""
|
|
|
|
try:
|
|
|
|
logger.debug(f"Received mqtt message on {message.topic}.")
|
|
|
|
ws_message = json.dumps(
|
|
|
|
{
|
|
|
|
"topic": message.topic.replace(f"{self.topic_prefix}/", ""),
|
|
|
|
"payload": message.payload.decode(),
|
|
|
|
}
|
|
|
|
)
|
|
|
|
except Exception as e:
|
|
|
|
# if the payload can't be decoded don't relay to clients
|
|
|
|
logger.debug(
|
|
|
|
f"MQTT payload for {message.topic} wasn't text. Skipping..."
|
|
|
|
)
|
|
|
|
return
|
|
|
|
|
|
|
|
self.websocket_server.manager.broadcast(ws_message)
|
|
|
|
|
2022-11-20 14:36:01 +01:00
|
|
|
self.mqtt_client.add_topic_callback(f"{self.topic_prefix}/#", send)
|
2021-06-14 14:31:13 +02:00
|
|
|
|
|
|
|
self.websocket_thread.start()
|
|
|
|
|
|
|
|
def stop(self):
|
|
|
|
self.websocket_server.manager.close_all()
|
|
|
|
self.websocket_server.manager.stop()
|
|
|
|
self.websocket_server.manager.join()
|
|
|
|
self.websocket_server.shutdown()
|
|
|
|
self.websocket_thread.join()
|