diff --git a/docs/docs/configuration/index.md b/docs/docs/configuration/index.md index 06734c9d6..5fc9c57b8 100644 --- a/docs/docs/configuration/index.md +++ b/docs/docs/configuration/index.md @@ -246,12 +246,14 @@ motion: # Enables dynamic contrast improvement. This should help improve night detections at the cost of making motion detection more sensitive # for daytime. improve_contrast: False + # Optional: Delay when updating camera motion through MQTT from ON -> OFF (default: shown below). + mqtt_off_delay: 30 # Optional: Record configuration # NOTE: Can be overridden at the camera level record: # Optional: Enable recording (default: shown below) - # WARNING: If recording is disabled in the config, turning it on via + # WARNING: If recording is disabled in the config, turning it on via # the UI or MQTT later will have no effect. # WARNING: Frigate does not currently support limiting recordings based # on available disk space automatically. If using recordings, diff --git a/docs/docs/integrations/mqtt.md b/docs/docs/integrations/mqtt.md index cdf4fb637..fca590b61 100644 --- a/docs/docs/integrations/mqtt.md +++ b/docs/docs/integrations/mqtt.md @@ -123,6 +123,12 @@ Topic with current state of snapshots for a camera. Published values are `ON` an Topic to turn motion detection for a camera on and off. Expected values are `ON` and `OFF`. NOTE: Turning off motion detection will fail if detection is not disabled. +### `frigate//motion` + +Whether camera_name is currently detecting motion. Expected values are `ON` and `OFF`. +NOTE: After motion is initially detected, `ON` will be set until no motion has +been detected for `mqtt_off_delay` seconds (30 by default). + ### `frigate//motion/state` Topic with current state of motion detection for a camera. Published values are `ON` and `OFF`. diff --git a/frigate/config.py b/frigate/config.py index 58b064214..863538504 100644 --- a/frigate/config.py +++ b/frigate/config.py @@ -134,6 +134,10 @@ class MotionConfig(FrigateBaseModel): mask: Union[str, List[str]] = Field( default="", title="Coordinates polygon for the motion mask." ) + mqtt_off_delay: int = Field( + default=30, + title="Delay for updating MQTT with no motion detected.", + ) class RuntimeMotionConfig(MotionConfig): diff --git a/frigate/object_processing.py b/frigate/object_processing.py index a7509ef49..4206021e2 100644 --- a/frigate/object_processing.py +++ b/frigate/object_processing.py @@ -1,29 +1,24 @@ import base64 -import copy import datetime -import hashlib -import itertools import json import logging import os import queue import threading -import time from collections import Counter, defaultdict -from statistics import mean, median +from statistics import median from typing import Callable import cv2 import numpy as np from frigate.config import CameraConfig, SnapshotsConfig, RecordConfig, FrigateConfig -from frigate.const import CACHE_DIR, CLIPS_DIR, RECORD_DIR +from frigate.const import CLIPS_DIR from frigate.util import ( SharedMemoryFrameManager, calculate_region, draw_box_with_label, draw_timestamp, - load_labels, ) logger = logging.getLogger(__name__) @@ -652,6 +647,7 @@ class TrackedObjectProcessor(threading.Thread): self.stop_event = stop_event self.camera_states: dict[str, CameraState] = {} self.frame_manager = SharedMemoryFrameManager() + self.last_motion_updates: dict[str, int] = {} def start(camera, obj: TrackedObject, current_frame_time): self.event_queue.put(("start", camera, obj.to_dict())) @@ -844,6 +840,33 @@ class TrackedObjectProcessor(threading.Thread): return True + def should_mqtt_motion(self, camera, motion_boxes): + # publish if motion is currently being detected + if motion_boxes: + # only send True if motion hasn't been detected recently + if self.last_motion_updates.get(camera, 0) == 0: + self.client.publish( + f"{self.topic_prefix}/{camera}/motion", + "ON", + retain=False, + ) + + # always updated latest motion + self.last_motion_updates[camera] = int(datetime.datetime.now().timestamp()) + elif not motion_boxes and self.last_motion_updates.get(camera, 0) != 0: + mqtt_delay = self.config.cameras[camera].motion.mqtt_off_delay + now = int(datetime.datetime.now().timestamp()) + + # If no motion, make sure the off_delay has passed + if now - self.last_motion_updates.get(camera, 0) >= mqtt_delay: + self.client.publish( + f"{self.topic_prefix}/{camera}/motion", + "OFF", + retain=False, + ) + # reset the last_motion so redundant `off` commands aren't sent + self.last_motion_updates[camera] = 0 + def get_best(self, camera, label): # TODO: need a lock here camera_state = self.camera_states[camera] @@ -879,6 +902,8 @@ class TrackedObjectProcessor(threading.Thread): frame_time, current_tracked_objects, motion_boxes, regions ) + self.should_mqtt_motion(camera, motion_boxes) + tracked_objects = [ o.to_dict() for o in camera_state.tracked_objects.values() ]