From f31b8bf60a55cef1db2601a0aa4b1438097ca01b Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Sat, 14 Dec 2024 11:20:40 -0600 Subject: [PATCH] backend --- frigate/comms/dispatcher.py | 48 +++++++++ frigate/comms/mqtt.py | 10 ++ frigate/config/camera/review.py | 12 +++ frigate/review/maintainer.py | 167 ++++++++++++++++++++++++-------- 4 files changed, 194 insertions(+), 43 deletions(-) diff --git a/frigate/comms/dispatcher.py b/frigate/comms/dispatcher.py index 2bddc97a5..23f2045e0 100644 --- a/frigate/comms/dispatcher.py +++ b/frigate/comms/dispatcher.py @@ -80,6 +80,8 @@ class Dispatcher: "snapshots": self._on_snapshots_command, "birdseye": self._on_birdseye_command, "birdseye_mode": self._on_birdseye_mode_command, + "alerts": self._on_alerts_command, + "detections": self._on_detections_command, } self._global_settings_handlers: dict[str, Callable] = { "notifications": self._on_notification_command, @@ -182,6 +184,8 @@ class Dispatcher: "autotracking": self.config.cameras[ camera ].onvif.autotracking.enabled, + "alerts": self.config.cameras[camera].review.alerts.enabled, + "detections": self.config.cameras[camera].review.detections.enabled, } self.publish("camera_activity", json.dumps(camera_status)) @@ -490,3 +494,47 @@ class Dispatcher: self.config_updater.publish(f"config/birdseye/{camera_name}", birdseye_settings) self.publish(f"{camera_name}/birdseye_mode/state", payload, retain=True) + + def _on_alerts_command(self, camera_name: str, payload: str) -> None: + """Callback for alerts topic.""" + review_settings = self.config.cameras[camera_name].review + + if payload == "ON": + if not self.config.cameras[camera_name].review.alerts.enabled_in_config: + logger.error( + "Alerts must be enabled in the config to be turned on via MQTT." + ) + return + + if not review_settings.alerts.enabled: + logger.info(f"Turning on alerts for {camera_name}") + review_settings.alerts.enabled = True + elif payload == "OFF": + if review_settings.alerts.enabled: + logger.info(f"Turning off alerts for {camera_name}") + review_settings.alerts.enabled = False + + self.config_updater.publish(f"config/review/{camera_name}", review_settings) + self.publish(f"{camera_name}/review/alerts/state", payload, retain=True) + + def _on_detections_command(self, camera_name: str, payload: str) -> None: + """Callback for detections topic.""" + review_settings = self.config.cameras[camera_name].review + + if payload == "ON": + if not self.config.cameras[camera_name].review.detections.enabled_in_config: + logger.error( + "Detections must be enabled in the config to be turned on via MQTT." + ) + return + + if not review_settings.detections.enabled: + logger.info(f"Turning on detections for {camera_name}") + review_settings.detections.enabled = True + elif payload == "OFF": + if review_settings.detections.enabled: + logger.info(f"Turning off detections for {camera_name}") + review_settings.detections.enabled = False + + self.config_updater.publish(f"config/review/{camera_name}", review_settings) + self.publish(f"{camera_name}/review/detections/state", payload, retain=True) diff --git a/frigate/comms/mqtt.py b/frigate/comms/mqtt.py index 5a85a710b..68578ad17 100644 --- a/frigate/comms/mqtt.py +++ b/frigate/comms/mqtt.py @@ -104,6 +104,16 @@ class MqttClient(Communicator): # type: ignore[misc] ), retain=True, ) + self.publish( + f"{camera_name}/review/alerts/state", + "ON" if camera.review.alerts.enabled_in_config else "OFF", + retain=True, + ) + self.publish( + f"{camera_name}/review/detections/state", + "ON" if camera.review.detections.enabled_in_config else "OFF", + retain=True, + ) if self.config.notifications.enabled_in_config: self.publish( diff --git a/frigate/config/camera/review.py b/frigate/config/camera/review.py index 549c37db4..d8d26edb9 100644 --- a/frigate/config/camera/review.py +++ b/frigate/config/camera/review.py @@ -13,6 +13,8 @@ DEFAULT_ALERT_OBJECTS = ["person", "car"] class AlertsConfig(FrigateBaseModel): """Configure alerts""" + enabled: bool = Field(default=True, title="Enable alerts.") + labels: list[str] = Field( default=DEFAULT_ALERT_OBJECTS, title="Labels to create alerts for." ) @@ -21,6 +23,10 @@ class AlertsConfig(FrigateBaseModel): title="List of required zones to be entered in order to save the event as an alert.", ) + enabled_in_config: Optional[bool] = Field( + default=None, title="Keep track of original state of alerts." + ) + @field_validator("required_zones", mode="before") @classmethod def validate_required_zones(cls, v): @@ -33,6 +39,8 @@ class AlertsConfig(FrigateBaseModel): class DetectionsConfig(FrigateBaseModel): """Configure detections""" + enabled: bool = Field(default=True, title="Enable detections.") + labels: Optional[list[str]] = Field( default=None, title="Labels to create detections for." ) @@ -41,6 +49,10 @@ class DetectionsConfig(FrigateBaseModel): title="List of required zones to be entered in order to save the event as a detection.", ) + enabled_in_config: Optional[bool] = Field( + default=None, title="Keep track of original state of detections." + ) + @field_validator("required_zones", mode="before") @classmethod def validate_required_zones(cls, v): diff --git a/frigate/review/maintainer.py b/frigate/review/maintainer.py index 8aa0f65e0..dbbb5ac23 100644 --- a/frigate/review/maintainer.py +++ b/frigate/review/maintainer.py @@ -148,7 +148,8 @@ class ReviewSegmentMaintainer(threading.Thread): # create communication for review segments self.requestor = InterProcessRequestor() - self.config_subscriber = ConfigSubscriber("config/record/") + self.record_config_subscriber = ConfigSubscriber("config/record/") + self.review_config_subscriber = ConfigSubscriber("config/review/") self.detection_subscriber = DetectionSubscriber(DetectionTypeEnum.all) # manual events @@ -226,6 +227,13 @@ class ReviewSegmentMaintainer(threading.Thread): ) self.active_review_segments[segment.camera] = None + def end_segment(self, camera: str) -> None: + """End the pending segment for a camera.""" + segment = self.active_review_segments.get(camera) + if segment: + prev_data = segment.get_data(False) + self._publish_segment_end(segment, prev_data) + def update_existing_segment( self, segment: PendingReviewSegment, @@ -273,6 +281,7 @@ class ReviewSegmentMaintainer(threading.Thread): & set(camera_config.review.alerts.required_zones) ) ) + and camera_config.review.alerts.enabled ): segment.severity = SeverityEnum.alert should_update = True @@ -369,13 +378,14 @@ class ReviewSegmentMaintainer(threading.Thread): & set(camera_config.review.alerts.required_zones) ) ) + and camera_config.review.alerts.enabled ): severity = SeverityEnum.alert # if object is detection label # and review is not already a detection or alert # and has entered required zones or required zones is not set - # mark this review as alert + # mark this review as detection if ( not severity and ( @@ -390,6 +400,7 @@ class ReviewSegmentMaintainer(threading.Thread): & set(camera_config.review.detections.required_zones) ) ) + and camera_config.review.detections.enabled ): severity = SeverityEnum.detection @@ -430,15 +441,55 @@ class ReviewSegmentMaintainer(threading.Thread): # check if there is an updated config while True: ( - updated_topic, + updated_record_topic, updated_record_config, - ) = self.config_subscriber.check_for_update() + ) = self.record_config_subscriber.check_for_update() - if not updated_topic: + ( + updated_review_topic, + updated_review_config, + ) = self.review_config_subscriber.check_for_update() + + if not updated_record_topic and not updated_review_topic: break - camera_name = updated_topic.rpartition("/")[-1] - self.config.cameras[camera_name].record = updated_record_config + if updated_record_topic: + camera_name = updated_record_topic.rpartition("/")[-1] + self.config.cameras[camera_name].record = updated_record_config + + if updated_review_topic: + camera_name = updated_review_topic.rpartition("/")[-1] + old_alerts_enabled = self.config.cameras[ + camera_name + ].review.alerts.enabled + old_detections_enabled = self.config.cameras[ + camera_name + ].review.detections.enabled + self.config.cameras[camera_name].review = updated_review_config + + # Check if alerts.enabled or detections.enabled has changed + if ( + old_alerts_enabled + != self.config.cameras[camera_name].review.alerts.enabled + or old_detections_enabled + != self.config.cameras[camera_name].review.detections.enabled + ): + segment = self.active_review_segments.get(camera_name) + if segment: + if ( + not self.config.cameras[ + camera_name + ].review.alerts.enabled + and segment.severity == SeverityEnum.alert + ): + self.end_segment(camera_name) + elif ( + not self.config.cameras[ + camera_name + ].review.detections.enabled + and segment.severity == SeverityEnum.detection + ): + self.end_segment(camera_name) (topic, data) = self.detection_subscriber.check_for_update(timeout=1) @@ -475,12 +526,22 @@ class ReviewSegmentMaintainer(threading.Thread): if not self.config.cameras[camera].record.enabled: if current_segment: - self.update_existing_segment( - current_segment, frame_name, frame_time, [] - ) - + self.end_segment(camera) continue + # Check if the current segment should be processed based on enabled settings + if current_segment: + if ( + current_segment.severity == SeverityEnum.alert + and not self.config.cameras[camera].review.alerts.enabled + ) or ( + current_segment.severity == SeverityEnum.detection + and not self.config.cameras[camera].review.detections.enabled + ): + self.end_segment(camera) + continue + + # If we reach here, the segment can be processed (if it exists) if current_segment is not None: if topic == DetectionTypeEnum.video: self.update_existing_segment( @@ -496,20 +557,24 @@ class ReviewSegmentMaintainer(threading.Thread): current_segment.last_update = frame_time for audio in audio_detections: - if audio in camera_config.review.alerts.labels: + if ( + audio in camera_config.review.alerts.labels + and camera_config.review.alerts.enabled + ): current_segment.audio.add(audio) current_segment.severity = SeverityEnum.alert elif ( camera_config.review.detections.labels is None or audio in camera_config.review.detections.labels - ): + ) and camera_config.review.detections.enabled: current_segment.audio.add(audio) elif topic == DetectionTypeEnum.api: if manual_info["state"] == ManualEventState.complete: current_segment.detections[manual_info["event_id"]] = ( manual_info["label"] ) - current_segment.severity = SeverityEnum.alert + if self.config.cameras[camera].review.alerts.enabled: + current_segment.severity = SeverityEnum.alert current_segment.last_update = manual_info["end_time"] elif manual_info["state"] == ManualEventState.start: self.indefinite_events[camera][manual_info["event_id"]] = ( @@ -518,7 +583,8 @@ class ReviewSegmentMaintainer(threading.Thread): current_segment.detections[manual_info["event_id"]] = ( manual_info["label"] ) - current_segment.severity = SeverityEnum.alert + if self.config.cameras[camera].review.alerts.enabled: + current_segment.severity = SeverityEnum.alert # temporarily make it so this event can not end current_segment.last_update = sys.maxsize @@ -534,12 +600,16 @@ class ReviewSegmentMaintainer(threading.Thread): ) else: if topic == DetectionTypeEnum.video: - self.check_if_new_segment( - camera, - frame_name, - frame_time, - current_tracked_objects, - ) + if ( + self.config.cameras[camera].review.alerts.enabled + or self.config.cameras[camera].review.detections.enabled + ): + self.check_if_new_segment( + camera, + frame_name, + frame_time, + current_tracked_objects, + ) elif topic == DetectionTypeEnum.audio and len(audio_detections) > 0: severity = None @@ -547,13 +617,16 @@ class ReviewSegmentMaintainer(threading.Thread): detections = set() for audio in audio_detections: - if audio in camera_config.review.alerts.labels: + if ( + audio in camera_config.review.alerts.labels + and camera_config.review.alerts.enabled + ): detections.add(audio) severity = SeverityEnum.alert elif ( camera_config.review.detections.labels is None or audio in camera_config.review.detections.labels - ): + ) and camera_config.review.detections.enabled: detections.add(audio) if not severity: @@ -570,28 +643,36 @@ class ReviewSegmentMaintainer(threading.Thread): detections, ) elif topic == DetectionTypeEnum.api: - self.active_review_segments[camera] = PendingReviewSegment( - camera, - frame_time, - SeverityEnum.alert, - {manual_info["event_id"]: manual_info["label"]}, - {}, - [], - set(), - ) - - if manual_info["state"] == ManualEventState.start: - self.indefinite_events[camera][manual_info["event_id"]] = ( - manual_info["label"] + if self.config.cameras[camera].review.alerts.enabled: + self.active_review_segments[camera] = PendingReviewSegment( + camera, + frame_time, + SeverityEnum.alert, + {manual_info["event_id"]: manual_info["label"]}, + {}, + [], + set(), ) - # temporarily make it so this event can not end - self.active_review_segments[camera].last_update = sys.maxsize - elif manual_info["state"] == ManualEventState.complete: - self.active_review_segments[camera].last_update = manual_info[ - "end_time" - ] - self.config_subscriber.stop() + if manual_info["state"] == ManualEventState.start: + self.indefinite_events[camera][manual_info["event_id"]] = ( + manual_info["label"] + ) + # temporarily make it so this event can not end + self.active_review_segments[ + camera + ].last_update = sys.maxsize + elif manual_info["state"] == ManualEventState.complete: + self.active_review_segments[ + camera + ].last_update = manual_info["end_time"] + else: + logger.warning( + f"Manual event API has been called for {camera}, but alerts are disabled. This manual event will not appear as an alert." + ) + + self.record_config_subscriber.stop() + self.review_config_subscriber.stop() self.requestor.stop() self.detection_subscriber.stop() logger.info("Exiting review maintainer...")