diff --git a/docs/docs/integrations/mqtt.md b/docs/docs/integrations/mqtt.md index cd6de8651..9b4ad7547 100644 --- a/docs/docs/integrations/mqtt.md +++ b/docs/docs/integrations/mqtt.md @@ -220,3 +220,29 @@ Topic to turn the PTZ autotracker for a camera on and off. Expected values are ` ### `frigate//ptz_autotracker/state` Topic with current state of the PTZ autotracker for a camera. Published values are `ON` and `OFF`. + +### `frigate//birdseye/set` + +Topic to turn Birdseye for a camera on and off. Expected values are `ON` and `OFF`. Birdseye mode +must be enabled in the configuration. + +### `frigate//birdseye/state` + +Topic with current state of Birdseye for a camera. Published values are `ON` and `OFF`. + +### `frigate//birdseye_mode/set` + +Topic to set Birdseye mode for a camera. Birdseye offers different modes to customize under which circumstances the camera is shown. + +_Note: Changing the value from `CONTINUOUS` -> `MOTION | OBJECTS` will take up to 30 seconds for +the camera to be removed from the view._ + +| Command | Description | +| ------------ | ----------------------------------------------------------------- | +| `CONTINUOUS` | Always included | +| `MOTION` | Show when detected motion within the last 30 seconds are included | +| `OBJECTS` | Shown if an active object tracked within the last 30 seconds | + +### `frigate//birdseye_mode/state` + +Topic with current state of the Birdseye mode for a camera. Published values are `CONTINUOUS`, `MOTION`, `OBJECTS`. diff --git a/frigate/app.py b/frigate/app.py index c35206a31..9ee110751 100644 --- a/frigate/app.py +++ b/frigate/app.py @@ -21,7 +21,7 @@ from frigate.comms.dispatcher import Communicator, Dispatcher from frigate.comms.inter_process import InterProcessCommunicator from frigate.comms.mqtt import MqttClient from frigate.comms.ws import WebSocketClient -from frigate.config import FrigateConfig +from frigate.config import BirdseyeModeEnum, FrigateConfig from frigate.const import ( CACHE_DIR, CLIPS_DIR, @@ -169,6 +169,20 @@ class FrigateApp: "process": None, "audio_rms": mp.Value("d", 0.0), # type: ignore[typeddict-item] "audio_dBFS": mp.Value("d", 0.0), # type: ignore[typeddict-item] + "birdseye_enabled": mp.Value( # type: ignore[typeddict-item] + # issue https://github.com/python/typeshed/issues/8799 + # from mypy 0.981 onwards + "i", + self.config.cameras[camera_name].birdseye.enabled, + ), + "birdseye_mode": mp.Value( # type: ignore[typeddict-item] + # issue https://github.com/python/typeshed/issues/8799 + # from mypy 0.981 onwards + "i", + BirdseyeModeEnum.get_index( + self.config.cameras[camera_name].birdseye.mode.value + ), + ), } self.ptz_metrics[camera_name] = { "ptz_autotracker_enabled": mp.Value( # type: ignore[typeddict-item] @@ -455,6 +469,7 @@ class FrigateApp: args=( self.config, self.video_output_queue, + self.camera_metrics, ), ) output_processor.daemon = True diff --git a/frigate/comms/dispatcher.py b/frigate/comms/dispatcher.py index 15ce9d34d..bcef9a2fb 100644 --- a/frigate/comms/dispatcher.py +++ b/frigate/comms/dispatcher.py @@ -4,7 +4,7 @@ import logging from abc import ABC, abstractmethod from typing import Any, Callable -from frigate.config import FrigateConfig +from frigate.config import BirdseyeModeEnum, FrigateConfig from frigate.const import INSERT_MANY_RECORDINGS, REQUEST_REGION_GRID from frigate.models import Recordings from frigate.ptz.onvif import OnvifCommandEnum, OnvifController @@ -63,6 +63,8 @@ class Dispatcher: "motion_threshold": self._on_motion_threshold_command, "recordings": self._on_recordings_command, "snapshots": self._on_snapshots_command, + "birdseye": self._on_birdseye_command, + "birdseye_mode": self._on_birdseye_mode_command, } for comm in self.comms: @@ -296,3 +298,43 @@ class Dispatcher: logger.info(f"Setting ptz command to {command} for {camera_name}") except KeyError as k: logger.error(f"Invalid PTZ command {payload}: {k}") + + def _on_birdseye_command(self, camera_name: str, payload: str) -> None: + """Callback for birdseye topic.""" + birdseye_settings = self.config.cameras[camera_name].birdseye + + if payload == "ON": + if not self.camera_metrics[camera_name]["birdseye_enabled"].value: + logger.info(f"Turning on birdseye for {camera_name}") + self.camera_metrics[camera_name]["birdseye_enabled"].value = True + birdseye_settings.enabled = True + + elif payload == "OFF": + if self.camera_metrics[camera_name]["birdseye_enabled"].value: + logger.info(f"Turning off birdseye for {camera_name}") + self.camera_metrics[camera_name]["birdseye_enabled"].value = False + birdseye_settings.enabled = False + + self.publish(f"{camera_name}/birdseye/state", payload, retain=True) + + def _on_birdseye_mode_command(self, camera_name: str, payload: str) -> None: + """Callback for birdseye mode topic.""" + + if payload not in ["CONTINUOUS", "MOTION", "OBJECTS"]: + logger.info(f"Invalid birdseye_mode command: {payload}") + return + + birdseye_config = self.config.cameras[camera_name].birdseye + if not birdseye_config.enabled: + logger.info(f"Birdseye mode not enabled for {camera_name}") + return + + new_birdseye_mode = BirdseyeModeEnum(payload.lower()) + logger.info(f"Setting birdseye mode for {camera_name} to {new_birdseye_mode}") + + # update the metric (need the mode converted to an int) + self.camera_metrics[camera_name][ + "birdseye_mode" + ].value = BirdseyeModeEnum.get_index(new_birdseye_mode) + + self.publish(f"{camera_name}/birdseye_mode/state", payload, retain=True) diff --git a/frigate/comms/mqtt.py b/frigate/comms/mqtt.py index 76c4f28af..b1b52ad90 100644 --- a/frigate/comms/mqtt.py +++ b/frigate/comms/mqtt.py @@ -89,6 +89,18 @@ class MqttClient(Communicator): # type: ignore[misc] "OFF", retain=False, ) + self.publish( + f"{camera_name}/birdseye/state", + "ON" if camera.birdseye.enabled else "OFF", + retain=True, + ) + self.publish( + f"{camera_name}/birdseye_mode/state", + camera.birdseye.mode.value.upper() + if camera.birdseye.enabled + else "OFF", + retain=True, + ) self.publish("available", "online", retain=True) @@ -160,6 +172,8 @@ class MqttClient(Communicator): # type: ignore[misc] "ptz_autotracker", "motion_threshold", "motion_contour_area", + "birdseye", + "birdseye_mode", ] for name in self.config.cameras.keys(): diff --git a/frigate/config.py b/frigate/config.py index 3ccc76a35..2c9734ec6 100644 --- a/frigate/config.py +++ b/frigate/config.py @@ -501,6 +501,14 @@ class BirdseyeModeEnum(str, Enum): motion = "motion" continuous = "continuous" + @classmethod + def get_index(cls, type): + return list(cls).index(type) + + @classmethod + def get(cls, index): + return list(cls)[index] + class BirdseyeConfig(FrigateBaseModel): enabled: bool = Field(default=True, title="Enable birdseye view.") diff --git a/frigate/output.py b/frigate/output.py index 4c72247d5..df0bd9f76 100644 --- a/frigate/output.py +++ b/frigate/output.py @@ -24,6 +24,7 @@ from ws4py.websocket import WebSocket from frigate.config import BirdseyeModeEnum, FrigateConfig from frigate.const import BASE_DIR, BIRDSEYE_PIPE +from frigate.types import CameraMetricsTypes from frigate.util.image import ( SharedMemoryFrameManager, copy_yuv_to_position, @@ -238,6 +239,7 @@ class BirdsEyeFrameManager: config: FrigateConfig, frame_manager: SharedMemoryFrameManager, stop_event: mp.Event, + camera_metrics: dict[str, CameraMetricsTypes], ): self.config = config self.mode = config.birdseye.mode @@ -248,6 +250,7 @@ class BirdsEyeFrameManager: self.frame = np.ndarray(self.yuv_shape, dtype=np.uint8) self.canvas = Canvas(width, height) self.stop_event = stop_event + self.camera_metrics = camera_metrics # initialize the frame as black and with the Frigate logo self.blank_frame = np.zeros(self.yuv_shape, np.uint8) @@ -579,9 +582,25 @@ class BirdsEyeFrameManager: if not camera_config.enabled: return False + # get our metrics (sync'd across processes) + # which allows us to control it via mqtt (or any other dispatcher) + camera_metrics = self.camera_metrics[camera] + + # disabling birdseye is a little tricky + if not camera_metrics["birdseye_enabled"].value: + # if we've rendered a frame (we have a value for last_active_frame) + # then we need to set it to zero + if self.cameras[camera]["last_active_frame"] > 0: + self.cameras[camera]["last_active_frame"] = 0 + + return False + + # get the birdseye mode state from camera metrics + birdseye_mode = BirdseyeModeEnum.get(camera_metrics["birdseye_mode"].value) + # update the last active frame for the camera self.cameras[camera]["current_frame"] = frame_time - if self.camera_active(camera_config.mode, object_count, motion_count): + if self.camera_active(birdseye_mode, object_count, motion_count): self.cameras[camera]["last_active_frame"] = frame_time now = datetime.datetime.now().timestamp() @@ -605,7 +624,11 @@ class BirdsEyeFrameManager: return False -def output_frames(config: FrigateConfig, video_output_queue): +def output_frames( + config: FrigateConfig, + video_output_queue, + camera_metrics: dict[str, CameraMetricsTypes], +): threading.current_thread().name = "output" setproctitle("frigate.output") @@ -661,7 +684,10 @@ def output_frames(config: FrigateConfig, video_output_queue): config.birdseye.restream, ) broadcasters["birdseye"] = BroadcastThread( - "birdseye", converters["birdseye"], websocket_server, stop_event + "birdseye", + converters["birdseye"], + websocket_server, + stop_event, ) websocket_thread.start() @@ -669,7 +695,9 @@ def output_frames(config: FrigateConfig, video_output_queue): for t in broadcasters.values(): t.start() - birdseye_manager = BirdsEyeFrameManager(config, frame_manager, stop_event) + birdseye_manager = BirdsEyeFrameManager( + config, frame_manager, stop_event, camera_metrics + ) if config.birdseye.restream: birdseye_buffer = frame_manager.create( diff --git a/frigate/types.py b/frigate/types.py index ab8f07e9c..26d2e960b 100644 --- a/frigate/types.py +++ b/frigate/types.py @@ -25,6 +25,8 @@ class CameraMetricsTypes(TypedDict): skipped_fps: Synchronized audio_rms: Synchronized audio_dBFS: Synchronized + birdseye_enabled: Synchronized + birdseye_mode: Synchronized class PTZMetricsTypes(TypedDict):