mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +01:00 
			
		
		
		
	MQTT: Birdseye enabled/disabled and mode change support (#8291)
* support enabled and mode change for birdseye via mqtt * resolve feedback from PR review https://github.com/blakeblackshear/frigate/pull/8291#discussion_r1370083613 * change birdseye mode topic to set * type in the docs * these commented out lines should have never been in here
This commit is contained in:
		
							parent
							
								
									859ab0e7fa
								
							
						
					
					
						commit
						36c1e00a6b
					
				@ -220,3 +220,29 @@ Topic to turn the PTZ autotracker for a camera on and off. Expected values are `
 | 
				
			|||||||
### `frigate/<camera_name>/ptz_autotracker/state`
 | 
					### `frigate/<camera_name>/ptz_autotracker/state`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Topic with current state of the PTZ autotracker for a camera. Published values are `ON` and `OFF`.
 | 
					Topic with current state of the PTZ autotracker for a camera. Published values are `ON` and `OFF`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `frigate/<camera_name>/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/<camera_name>/birdseye/state`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Topic with current state of Birdseye for a camera. Published values are `ON` and `OFF`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `frigate/<camera_name>/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/<camera_name>/birdseye_mode/state`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Topic with current state of the Birdseye mode for a camera. Published values are `CONTINUOUS`, `MOTION`, `OBJECTS`.
 | 
				
			||||||
 | 
				
			|||||||
@ -21,7 +21,7 @@ from frigate.comms.dispatcher import Communicator, Dispatcher
 | 
				
			|||||||
from frigate.comms.inter_process import InterProcessCommunicator
 | 
					from frigate.comms.inter_process import InterProcessCommunicator
 | 
				
			||||||
from frigate.comms.mqtt import MqttClient
 | 
					from frigate.comms.mqtt import MqttClient
 | 
				
			||||||
from frigate.comms.ws import WebSocketClient
 | 
					from frigate.comms.ws import WebSocketClient
 | 
				
			||||||
from frigate.config import FrigateConfig
 | 
					from frigate.config import BirdseyeModeEnum, FrigateConfig
 | 
				
			||||||
from frigate.const import (
 | 
					from frigate.const import (
 | 
				
			||||||
    CACHE_DIR,
 | 
					    CACHE_DIR,
 | 
				
			||||||
    CLIPS_DIR,
 | 
					    CLIPS_DIR,
 | 
				
			||||||
@ -169,6 +169,20 @@ class FrigateApp:
 | 
				
			|||||||
                "process": None,
 | 
					                "process": None,
 | 
				
			||||||
                "audio_rms": mp.Value("d", 0.0),  # type: ignore[typeddict-item]
 | 
					                "audio_rms": mp.Value("d", 0.0),  # type: ignore[typeddict-item]
 | 
				
			||||||
                "audio_dBFS": 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] = {
 | 
					            self.ptz_metrics[camera_name] = {
 | 
				
			||||||
                "ptz_autotracker_enabled": mp.Value(  # type: ignore[typeddict-item]
 | 
					                "ptz_autotracker_enabled": mp.Value(  # type: ignore[typeddict-item]
 | 
				
			||||||
@ -455,6 +469,7 @@ class FrigateApp:
 | 
				
			|||||||
            args=(
 | 
					            args=(
 | 
				
			||||||
                self.config,
 | 
					                self.config,
 | 
				
			||||||
                self.video_output_queue,
 | 
					                self.video_output_queue,
 | 
				
			||||||
 | 
					                self.camera_metrics,
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        output_processor.daemon = True
 | 
					        output_processor.daemon = True
 | 
				
			||||||
 | 
				
			|||||||
@ -4,7 +4,7 @@ import logging
 | 
				
			|||||||
from abc import ABC, abstractmethod
 | 
					from abc import ABC, abstractmethod
 | 
				
			||||||
from typing import Any, Callable
 | 
					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.const import INSERT_MANY_RECORDINGS, REQUEST_REGION_GRID
 | 
				
			||||||
from frigate.models import Recordings
 | 
					from frigate.models import Recordings
 | 
				
			||||||
from frigate.ptz.onvif import OnvifCommandEnum, OnvifController
 | 
					from frigate.ptz.onvif import OnvifCommandEnum, OnvifController
 | 
				
			||||||
@ -63,6 +63,8 @@ class Dispatcher:
 | 
				
			|||||||
            "motion_threshold": self._on_motion_threshold_command,
 | 
					            "motion_threshold": self._on_motion_threshold_command,
 | 
				
			||||||
            "recordings": self._on_recordings_command,
 | 
					            "recordings": self._on_recordings_command,
 | 
				
			||||||
            "snapshots": self._on_snapshots_command,
 | 
					            "snapshots": self._on_snapshots_command,
 | 
				
			||||||
 | 
					            "birdseye": self._on_birdseye_command,
 | 
				
			||||||
 | 
					            "birdseye_mode": self._on_birdseye_mode_command,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for comm in self.comms:
 | 
					        for comm in self.comms:
 | 
				
			||||||
@ -296,3 +298,43 @@ class Dispatcher:
 | 
				
			|||||||
            logger.info(f"Setting ptz command to {command} for {camera_name}")
 | 
					            logger.info(f"Setting ptz command to {command} for {camera_name}")
 | 
				
			||||||
        except KeyError as k:
 | 
					        except KeyError as k:
 | 
				
			||||||
            logger.error(f"Invalid PTZ command {payload}: {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)
 | 
				
			||||||
 | 
				
			|||||||
@ -89,6 +89,18 @@ class MqttClient(Communicator):  # type: ignore[misc]
 | 
				
			|||||||
                "OFF",
 | 
					                "OFF",
 | 
				
			||||||
                retain=False,
 | 
					                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)
 | 
					        self.publish("available", "online", retain=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -160,6 +172,8 @@ class MqttClient(Communicator):  # type: ignore[misc]
 | 
				
			|||||||
            "ptz_autotracker",
 | 
					            "ptz_autotracker",
 | 
				
			||||||
            "motion_threshold",
 | 
					            "motion_threshold",
 | 
				
			||||||
            "motion_contour_area",
 | 
					            "motion_contour_area",
 | 
				
			||||||
 | 
					            "birdseye",
 | 
				
			||||||
 | 
					            "birdseye_mode",
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for name in self.config.cameras.keys():
 | 
					        for name in self.config.cameras.keys():
 | 
				
			||||||
 | 
				
			|||||||
@ -501,6 +501,14 @@ class BirdseyeModeEnum(str, Enum):
 | 
				
			|||||||
    motion = "motion"
 | 
					    motion = "motion"
 | 
				
			||||||
    continuous = "continuous"
 | 
					    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):
 | 
					class BirdseyeConfig(FrigateBaseModel):
 | 
				
			||||||
    enabled: bool = Field(default=True, title="Enable birdseye view.")
 | 
					    enabled: bool = Field(default=True, title="Enable birdseye view.")
 | 
				
			||||||
 | 
				
			|||||||
@ -24,6 +24,7 @@ from ws4py.websocket import WebSocket
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from frigate.config import BirdseyeModeEnum, FrigateConfig
 | 
					from frigate.config import BirdseyeModeEnum, FrigateConfig
 | 
				
			||||||
from frigate.const import BASE_DIR, BIRDSEYE_PIPE
 | 
					from frigate.const import BASE_DIR, BIRDSEYE_PIPE
 | 
				
			||||||
 | 
					from frigate.types import CameraMetricsTypes
 | 
				
			||||||
from frigate.util.image import (
 | 
					from frigate.util.image import (
 | 
				
			||||||
    SharedMemoryFrameManager,
 | 
					    SharedMemoryFrameManager,
 | 
				
			||||||
    copy_yuv_to_position,
 | 
					    copy_yuv_to_position,
 | 
				
			||||||
@ -238,6 +239,7 @@ class BirdsEyeFrameManager:
 | 
				
			|||||||
        config: FrigateConfig,
 | 
					        config: FrigateConfig,
 | 
				
			||||||
        frame_manager: SharedMemoryFrameManager,
 | 
					        frame_manager: SharedMemoryFrameManager,
 | 
				
			||||||
        stop_event: mp.Event,
 | 
					        stop_event: mp.Event,
 | 
				
			||||||
 | 
					        camera_metrics: dict[str, CameraMetricsTypes],
 | 
				
			||||||
    ):
 | 
					    ):
 | 
				
			||||||
        self.config = config
 | 
					        self.config = config
 | 
				
			||||||
        self.mode = config.birdseye.mode
 | 
					        self.mode = config.birdseye.mode
 | 
				
			||||||
@ -248,6 +250,7 @@ class BirdsEyeFrameManager:
 | 
				
			|||||||
        self.frame = np.ndarray(self.yuv_shape, dtype=np.uint8)
 | 
					        self.frame = np.ndarray(self.yuv_shape, dtype=np.uint8)
 | 
				
			||||||
        self.canvas = Canvas(width, height)
 | 
					        self.canvas = Canvas(width, height)
 | 
				
			||||||
        self.stop_event = stop_event
 | 
					        self.stop_event = stop_event
 | 
				
			||||||
 | 
					        self.camera_metrics = camera_metrics
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # initialize the frame as black and with the Frigate logo
 | 
					        # initialize the frame as black and with the Frigate logo
 | 
				
			||||||
        self.blank_frame = np.zeros(self.yuv_shape, np.uint8)
 | 
					        self.blank_frame = np.zeros(self.yuv_shape, np.uint8)
 | 
				
			||||||
@ -579,9 +582,25 @@ class BirdsEyeFrameManager:
 | 
				
			|||||||
        if not camera_config.enabled:
 | 
					        if not camera_config.enabled:
 | 
				
			||||||
            return False
 | 
					            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
 | 
					        # update the last active frame for the camera
 | 
				
			||||||
        self.cameras[camera]["current_frame"] = frame_time
 | 
					        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
 | 
					            self.cameras[camera]["last_active_frame"] = frame_time
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        now = datetime.datetime.now().timestamp()
 | 
					        now = datetime.datetime.now().timestamp()
 | 
				
			||||||
@ -605,7 +624,11 @@ class BirdsEyeFrameManager:
 | 
				
			|||||||
        return False
 | 
					        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"
 | 
					    threading.current_thread().name = "output"
 | 
				
			||||||
    setproctitle("frigate.output")
 | 
					    setproctitle("frigate.output")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -661,7 +684,10 @@ def output_frames(config: FrigateConfig, video_output_queue):
 | 
				
			|||||||
            config.birdseye.restream,
 | 
					            config.birdseye.restream,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        broadcasters["birdseye"] = BroadcastThread(
 | 
					        broadcasters["birdseye"] = BroadcastThread(
 | 
				
			||||||
            "birdseye", converters["birdseye"], websocket_server, stop_event
 | 
					            "birdseye",
 | 
				
			||||||
 | 
					            converters["birdseye"],
 | 
				
			||||||
 | 
					            websocket_server,
 | 
				
			||||||
 | 
					            stop_event,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    websocket_thread.start()
 | 
					    websocket_thread.start()
 | 
				
			||||||
@ -669,7 +695,9 @@ def output_frames(config: FrigateConfig, video_output_queue):
 | 
				
			|||||||
    for t in broadcasters.values():
 | 
					    for t in broadcasters.values():
 | 
				
			||||||
        t.start()
 | 
					        t.start()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    birdseye_manager = BirdsEyeFrameManager(config, frame_manager, stop_event)
 | 
					    birdseye_manager = BirdsEyeFrameManager(
 | 
				
			||||||
 | 
					        config, frame_manager, stop_event, camera_metrics
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if config.birdseye.restream:
 | 
					    if config.birdseye.restream:
 | 
				
			||||||
        birdseye_buffer = frame_manager.create(
 | 
					        birdseye_buffer = frame_manager.create(
 | 
				
			||||||
 | 
				
			|||||||
@ -25,6 +25,8 @@ class CameraMetricsTypes(TypedDict):
 | 
				
			|||||||
    skipped_fps: Synchronized
 | 
					    skipped_fps: Synchronized
 | 
				
			||||||
    audio_rms: Synchronized
 | 
					    audio_rms: Synchronized
 | 
				
			||||||
    audio_dBFS: Synchronized
 | 
					    audio_dBFS: Synchronized
 | 
				
			||||||
 | 
					    birdseye_enabled: Synchronized
 | 
				
			||||||
 | 
					    birdseye_mode: Synchronized
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PTZMetricsTypes(TypedDict):
 | 
					class PTZMetricsTypes(TypedDict):
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user