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:
Shaun Berryman 2023-10-26 04:20:55 -07:00 committed by GitHub
parent 859ab0e7fa
commit 36c1e00a6b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 141 additions and 6 deletions

View File

@ -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`.

View File

@ -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

View File

@ -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)

View File

@ -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():

View File

@ -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.")

View File

@ -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(

View File

@ -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):