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