diff --git a/frigate/events/maintainer.py b/frigate/events/maintainer.py
index 40057958c..db8341656 100644
--- a/frigate/events/maintainer.py
+++ b/frigate/events/maintainer.py
@@ -42,6 +42,9 @@ def should_update_state(prev_event: Event, current_event: Event) -> bool:
if prev_event["stationary"] != current_event["stationary"]:
return True
+ if prev_event["attributes"] != current_event["attributes"]:
+ return True
+
return False
diff --git a/frigate/timeline.py b/frigate/timeline.py
index 73c0a61b4..93451acdb 100644
--- a/frigate/timeline.py
+++ b/frigate/timeline.py
@@ -74,6 +74,7 @@ class TimelineProcessor(threading.Thread):
camera_config.detect.height,
event_data["region"],
),
+ "attribute": "",
},
}
if event_type == "start":
@@ -93,6 +94,12 @@ class TimelineProcessor(threading.Thread):
"stationary" if event_data["stationary"] else "active"
)
Timeline.insert(timeline_entry).execute()
+ elif prev_event_data["attributes"] == {} and event_data["attributes"] != {}:
+ timeline_entry[Timeline.class_type] = "attribute"
+ timeline_entry[Timeline.data]["attribute"] = list(
+ event_data["attributes"].keys()
+ )[0]
+ Timeline.insert(timeline_entry).execute()
elif event_type == "end":
timeline_entry[Timeline.class_type] = "gone"
Timeline.insert(timeline_entry).execute()
diff --git a/web/src/components/TimelineSummary.jsx b/web/src/components/TimelineSummary.jsx
index 92d79012e..715eb8e84 100644
--- a/web/src/components/TimelineSummary.jsx
+++ b/web/src/components/TimelineSummary.jsx
@@ -7,7 +7,10 @@ import ActiveObjectIcon from '../icons/ActiveObject';
import PlayIcon from '../icons/Play';
import ExitIcon from '../icons/Exit';
import StationaryObjectIcon from '../icons/StationaryObject';
-import { Zone } from '../icons/Zone';
+import FaceIcon from '../icons/Face';
+import LicensePlateIcon from '../icons/LicensePlate';
+import DeliveryTruckIcon from '../icons/DeliveryTruck';
+import ZoneIcon from '../icons/Zone';
import { useMemo, useState } from 'preact/hooks';
import Button from './Button';
@@ -88,7 +91,7 @@ export default function TimelineSummary({ event, onFrameSelected }) {
aria-label={window.innerWidth > 640 ? getTimelineItemDescription(config, item, event) : ''}
onClick={() => onSelectMoment(index)}
>
- {getTimelineIcon(item.class_type)}
+ {getTimelineIcon(item)}
))}
@@ -113,8 +116,8 @@ export default function TimelineSummary({ event, onFrameSelected }) {
);
}
-function getTimelineIcon(classType) {
- switch (classType) {
+function getTimelineIcon(timelineItem) {
+ switch (timelineItem.class_type) {
case 'visible':
return ;
case 'gone':
@@ -124,7 +127,16 @@ function getTimelineIcon(classType) {
case 'stationary':
return ;
case 'entered_zone':
- return ;
+ return ;
+ case 'attribute':
+ switch (timelineItem.data.attribute) {
+ case 'face':
+ return ;
+ case 'license_plate':
+ return ;
+ default:
+ return ;
+ }
}
}
@@ -156,6 +168,15 @@ function getTimelineItemDescription(config, timelineItem, event) {
time_style: 'medium',
time_format: config.ui.time_format,
})}`;
+ case 'attribute':
+ return `${timelineItem.data.attribute.replaceAll("_", " ")} detected for ${event.label} at ${formatUnixTimestampToDateTime(
+ timelineItem.timestamp,
+ {
+ date_style: 'short',
+ time_style: 'medium',
+ time_format: config.ui.time_format,
+ }
+ )}`;
case 'gone':
return `${event.label} left at ${formatUnixTimestampToDateTime(timelineItem.timestamp, {
date_style: 'short',
diff --git a/web/src/icons/DeliveryTruck.jsx b/web/src/icons/DeliveryTruck.jsx
new file mode 100644
index 000000000..01a48e5f2
--- /dev/null
+++ b/web/src/icons/DeliveryTruck.jsx
@@ -0,0 +1,15 @@
+import { h } from 'preact';
+import { memo } from 'preact/compat';
+
+export function StationaryObject({ className = '' }) {
+ return (
+
+ );
+}
+
+export default memo(StationaryObject);
diff --git a/web/src/icons/Face.jsx b/web/src/icons/Face.jsx
new file mode 100644
index 000000000..f68218711
--- /dev/null
+++ b/web/src/icons/Face.jsx
@@ -0,0 +1,22 @@
+import { h } from 'preact';
+import { memo } from 'preact/compat';
+
+export function Zone({ className = 'h-6 w-6', stroke = 'currentColor', fill = 'none', onClick = () => {} }) {
+ return (
+
+ );
+}
+
+export default memo(Zone);
diff --git a/web/src/icons/LicensePlate.jsx b/web/src/icons/LicensePlate.jsx
new file mode 100644
index 000000000..e2f4aae8f
--- /dev/null
+++ b/web/src/icons/LicensePlate.jsx
@@ -0,0 +1,15 @@
+import { h } from 'preact';
+import { memo } from 'preact/compat';
+
+export function StationaryObject({ className = '' }) {
+ return (
+
+ );
+}
+
+export default memo(StationaryObject);