mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-08-31 07:27:57 +00:00
Categorize manual events as alerts when their label is an alert label (#23981)
* Categorize manual events as alerts when their label is an alert label * tweak docs
This commit is contained in:
parent
6816050a46
commit
fd98977506
@ -121,6 +121,31 @@ cameras:
|
||||
</TabItem>
|
||||
</ConfigTabs>
|
||||
|
||||
## Categorizing manual events
|
||||
|
||||
Events created with the [create manual event API](../integrations/api/create-event-events-camera-name-label-create-post.api.mdx) are categorized with the same label lists, using the label from the request path:
|
||||
|
||||
1. If alerts are enabled and the label is listed in `review -> alerts -> labels`, the review item is an alert.
|
||||
2. Otherwise, if detections are enabled and the label is listed in `review -> detections -> labels`, the review item is a detection.
|
||||
3. If the label is in neither list, the review item is an alert, or no review item is created if alerts are disabled.
|
||||
|
||||
This means manual events are alerts unless you explicitly list their label as a detection label. For example, to have PIR sensors create detections instead of alerts, post to `/api/events/front_door/pir_sensor/create` with the following config:
|
||||
|
||||
```yaml {5-7}
|
||||
cameras:
|
||||
front_door:
|
||||
review:
|
||||
detections:
|
||||
labels:
|
||||
- pir_sensor
|
||||
```
|
||||
|
||||
:::note
|
||||
|
||||
Required zones do not apply to manual events, since they are created through the API rather than by the object tracker. Setting `review -> alerts -> labels` to an empty list also does not stop manual events from becoming alerts, as a label in neither list still falls back to an alert.
|
||||
|
||||
:::
|
||||
|
||||
## Restricting review items to specific zones
|
||||
|
||||
By default a review item will be created if any `review -> alerts -> labels` and `review -> detections -> labels` are detected anywhere in the camera frame. You will likely want to configure review items to only be created when the object enters an area of interest, [see the zone docs for more information](./zones.md#restricting-alerts-and-detections-to-specific-zones)
|
||||
|
||||
1
docs/static/frigate-api.yaml
vendored
1
docs/static/frigate-api.yaml
vendored
@ -5093,6 +5093,7 @@ paths:
|
||||
NOTES:
|
||||
- Creating a manual event does not trigger an update to /events MQTT topic.
|
||||
- If a duration is set to null, the event will need to be ended manually by calling /events/{event_id}/end.
|
||||
- The review item is an alert unless the label is listed in the camera's review -> detections -> labels config.
|
||||
operationId: create_event_events__camera_name___label__create_post
|
||||
parameters:
|
||||
- name: camera_name
|
||||
|
||||
@ -1748,6 +1748,7 @@ async def delete_events(request: Request, body: EventsDeleteBody):
|
||||
NOTES:
|
||||
- Creating a manual event does not trigger an update to /events MQTT topic.
|
||||
- If a duration is set to null, the event will need to be ended manually by calling /events/{event_id}/end.
|
||||
- The review item is an alert unless the label is listed in the camera's review -> detections -> labels config.
|
||||
""",
|
||||
)
|
||||
def create_event(
|
||||
|
||||
@ -392,6 +392,32 @@ class ReviewSegmentMaintainer(threading.Thread):
|
||||
return self._publish_segment_end(segment, prev_data)
|
||||
return None
|
||||
|
||||
def get_manual_event_severity(self, camera: str, label: str) -> SeverityEnum | None:
|
||||
"""Determine the review severity for a manual event label.
|
||||
|
||||
Alert labels take precedence over detection labels, matching how
|
||||
tracked objects are categorized. Labels in neither list default to
|
||||
alerts so manual events keep their historical severity.
|
||||
"""
|
||||
review_config = self.config.cameras[camera].review
|
||||
# label contains 'label: sub_label', only the label is categorized
|
||||
label = label.split(": ")[0]
|
||||
|
||||
if review_config.alerts.enabled and label in review_config.alerts.labels:
|
||||
return SeverityEnum.alert
|
||||
|
||||
if (
|
||||
review_config.detections.enabled
|
||||
and review_config.detections.labels is not None
|
||||
and label in review_config.detections.labels
|
||||
):
|
||||
return SeverityEnum.detection
|
||||
|
||||
if review_config.alerts.enabled:
|
||||
return SeverityEnum.alert
|
||||
|
||||
return None
|
||||
|
||||
def update_existing_segment(
|
||||
self,
|
||||
segment: PendingReviewSegment,
|
||||
@ -734,24 +760,19 @@ class ReviewSegmentMaintainer(threading.Thread):
|
||||
manual_info["label"]
|
||||
)
|
||||
if topic == DetectionTypeEnum.api:
|
||||
# manual_info["label"] contains 'label: sub_label'
|
||||
# so split out the label without modifying manual_info
|
||||
det_labels = self.config.cameras[
|
||||
camera
|
||||
].review.detections.labels
|
||||
if (
|
||||
self.config.cameras[camera].review.detections.enabled
|
||||
and det_labels is not None
|
||||
and manual_info["label"].split(": ")[0] in det_labels
|
||||
):
|
||||
current_segment.last_detection_time = manual_info[
|
||||
"end_time"
|
||||
]
|
||||
elif self.config.cameras[camera].review.alerts.enabled:
|
||||
severity = self.get_manual_event_severity(
|
||||
camera, manual_info["label"]
|
||||
)
|
||||
|
||||
if severity == SeverityEnum.alert:
|
||||
current_segment.severity = SeverityEnum.alert
|
||||
current_segment.last_alert_time = manual_info[
|
||||
"end_time"
|
||||
]
|
||||
elif severity == SeverityEnum.detection:
|
||||
current_segment.last_detection_time = manual_info[
|
||||
"end_time"
|
||||
]
|
||||
elif (
|
||||
topic == DetectionTypeEnum.lpr
|
||||
and self.config.cameras[camera].review.detections.enabled
|
||||
@ -765,21 +786,12 @@ class ReviewSegmentMaintainer(threading.Thread):
|
||||
current_segment.detections[manual_info["event_id"]] = (
|
||||
manual_info["label"]
|
||||
)
|
||||
if (
|
||||
topic == DetectionTypeEnum.api
|
||||
and self.config.cameras[camera].review.alerts.enabled
|
||||
):
|
||||
# manual_info["label"] contains 'label: sub_label'
|
||||
# so split out the label without modifying manual_info
|
||||
det_labels = self.config.cameras[
|
||||
camera
|
||||
].review.detections.labels
|
||||
if topic == DetectionTypeEnum.api:
|
||||
if (
|
||||
not self.config.cameras[
|
||||
camera
|
||||
].review.detections.enabled
|
||||
or det_labels is None
|
||||
or manual_info["label"].split(": ")[0] not in det_labels
|
||||
self.get_manual_event_severity(
|
||||
camera, manual_info["label"]
|
||||
)
|
||||
== SeverityEnum.alert
|
||||
):
|
||||
current_segment.severity = SeverityEnum.alert
|
||||
elif (
|
||||
@ -853,18 +865,9 @@ class ReviewSegmentMaintainer(threading.Thread):
|
||||
detections,
|
||||
)
|
||||
elif topic == DetectionTypeEnum.api:
|
||||
severity = None
|
||||
# manual_info["label"] contains 'label: sub_label'
|
||||
# so split out the label without modifying manual_info
|
||||
det_labels = self.config.cameras[camera].review.detections.labels
|
||||
if (
|
||||
self.config.cameras[camera].review.detections.enabled
|
||||
and det_labels is not None
|
||||
and manual_info["label"].split(": ")[0] in det_labels
|
||||
):
|
||||
severity = SeverityEnum.detection
|
||||
elif self.config.cameras[camera].review.alerts.enabled:
|
||||
severity = SeverityEnum.alert
|
||||
severity = self.get_manual_event_severity(
|
||||
camera, manual_info["label"]
|
||||
)
|
||||
|
||||
if severity:
|
||||
api_segment = PendingReviewSegment(
|
||||
|
||||
154
frigate/test/test_review_manual_event_severity.py
Normal file
154
frigate/test/test_review_manual_event_severity.py
Normal file
@ -0,0 +1,154 @@
|
||||
"""Tests for manual event severity categorization.
|
||||
|
||||
Regression coverage for manual events created via the events API being
|
||||
categorized as detections when their label appears in both the alerts and
|
||||
detections label lists. Alert labels must win, matching how tracked objects
|
||||
are categorized, and labels in neither list must default to alerts so the
|
||||
historical behavior of the API is preserved.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
|
||||
from frigate.config import FrigateConfig
|
||||
from frigate.review.maintainer import ReviewSegmentMaintainer
|
||||
from frigate.review.types import SeverityEnum
|
||||
|
||||
BASE_CONFIG = """
|
||||
mqtt:
|
||||
enabled: False
|
||||
cameras:
|
||||
front_door:
|
||||
ffmpeg:
|
||||
inputs:
|
||||
- path: rtsp://10.0.0.1:554/video
|
||||
roles:
|
||||
- detect
|
||||
detect:
|
||||
width: 1920
|
||||
height: 1080
|
||||
fps: 5
|
||||
%s
|
||||
"""
|
||||
|
||||
|
||||
class TestManualEventSeverity(unittest.TestCase):
|
||||
def _make_maintainer(self, review_config: str = "") -> ReviewSegmentMaintainer:
|
||||
"""Build a maintainer without invoking __init__ (avoids needing ZMQ
|
||||
sockets, shared memory, and clip dirs). Only the config is read when
|
||||
categorizing a manual event label."""
|
||||
maintainer = ReviewSegmentMaintainer.__new__(ReviewSegmentMaintainer)
|
||||
maintainer.config = FrigateConfig.parse_yaml(BASE_CONFIG % review_config)
|
||||
return maintainer
|
||||
|
||||
def test_defaults_to_alert(self) -> None:
|
||||
maintainer = self._make_maintainer()
|
||||
|
||||
self.assertEqual(
|
||||
maintainer.get_manual_event_severity("front_door", "person"),
|
||||
SeverityEnum.alert,
|
||||
)
|
||||
|
||||
def test_unlisted_label_defaults_to_alert(self) -> None:
|
||||
maintainer = self._make_maintainer(
|
||||
"""
|
||||
review:
|
||||
detections:
|
||||
labels:
|
||||
- dog
|
||||
"""
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
maintainer.get_manual_event_severity("front_door", "pir_sensor"),
|
||||
SeverityEnum.alert,
|
||||
)
|
||||
|
||||
def test_detection_label_is_detection(self) -> None:
|
||||
maintainer = self._make_maintainer(
|
||||
"""
|
||||
review:
|
||||
alerts:
|
||||
labels:
|
||||
- person
|
||||
detections:
|
||||
labels:
|
||||
- pir_sensor
|
||||
"""
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
maintainer.get_manual_event_severity("front_door", "pir_sensor"),
|
||||
SeverityEnum.detection,
|
||||
)
|
||||
|
||||
def test_alert_label_wins_over_detection_label(self) -> None:
|
||||
maintainer = self._make_maintainer(
|
||||
"""
|
||||
review:
|
||||
alerts:
|
||||
labels:
|
||||
- person
|
||||
detections:
|
||||
labels:
|
||||
- person
|
||||
- dog
|
||||
"""
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
maintainer.get_manual_event_severity("front_door", "person"),
|
||||
SeverityEnum.alert,
|
||||
)
|
||||
|
||||
def test_sub_label_is_stripped_before_categorizing(self) -> None:
|
||||
maintainer = self._make_maintainer(
|
||||
"""
|
||||
review:
|
||||
alerts:
|
||||
labels:
|
||||
- person
|
||||
detections:
|
||||
labels:
|
||||
- person
|
||||
"""
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
maintainer.get_manual_event_severity("front_door", "person: Bob"),
|
||||
SeverityEnum.alert,
|
||||
)
|
||||
|
||||
def test_alert_label_is_detection_when_alerts_disabled(self) -> None:
|
||||
maintainer = self._make_maintainer(
|
||||
"""
|
||||
review:
|
||||
alerts:
|
||||
enabled: False
|
||||
labels:
|
||||
- person
|
||||
detections:
|
||||
labels:
|
||||
- person
|
||||
"""
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
maintainer.get_manual_event_severity("front_door", "person"),
|
||||
SeverityEnum.detection,
|
||||
)
|
||||
|
||||
def test_no_severity_when_alerts_disabled_and_label_not_a_detection(self) -> None:
|
||||
maintainer = self._make_maintainer(
|
||||
"""
|
||||
review:
|
||||
alerts:
|
||||
enabled: False
|
||||
detections:
|
||||
labels:
|
||||
- dog
|
||||
"""
|
||||
)
|
||||
|
||||
self.assertIsNone(
|
||||
maintainer.get_manual_event_severity("front_door", "pir_sensor")
|
||||
)
|
||||
Loading…
x
Reference in New Issue
Block a user