mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 19:07:46 +01:00
Add support for stationary and active timeline entires (#7344)
* Add support for stationary and active timeline entires * Be sure to update stored event data copy with stationary
This commit is contained in:
parent
f57d21039e
commit
b7ff6735f6
@ -37,6 +37,14 @@ def should_update_db(prev_event: Event, current_event: Event) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def should_update_state(prev_event: Event, current_event: Event) -> bool:
|
||||||
|
"""If current event should update state, but not necessarily update the db."""
|
||||||
|
if prev_event["stationary"] != current_event["stationary"]:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class EventProcessor(threading.Thread):
|
class EventProcessor(threading.Thread):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@ -107,8 +115,11 @@ class EventProcessor(threading.Thread):
|
|||||||
event_data: Event,
|
event_data: Event,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""handle tracked object event updates."""
|
"""handle tracked object event updates."""
|
||||||
|
updated_db = False
|
||||||
|
|
||||||
# if this is the first message, just store it and continue, its not time to insert it in the db
|
# if this is the first message, just store it and continue, its not time to insert it in the db
|
||||||
if should_update_db(self.events_in_process[event_data["id"]], event_data):
|
if should_update_db(self.events_in_process[event_data["id"]], event_data):
|
||||||
|
updated_db = True
|
||||||
camera_config = self.config.cameras[camera]
|
camera_config = self.config.cameras[camera]
|
||||||
event_config: EventsConfig = camera_config.record.events
|
event_config: EventsConfig = camera_config.record.events
|
||||||
width = camera_config.detect.width
|
width = camera_config.detect.width
|
||||||
@ -210,6 +221,10 @@ class EventProcessor(threading.Thread):
|
|||||||
.execute()
|
.execute()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# check if the stored event_data should be updated
|
||||||
|
if updated_db or should_update_state(
|
||||||
|
self.events_in_process[event_data["id"]], event_data
|
||||||
|
):
|
||||||
# update the stored copy for comparison on future update messages
|
# update the stored copy for comparison on future update messages
|
||||||
self.events_in_process[event_data["id"]] = event_data
|
self.events_in_process[event_data["id"]] = event_data
|
||||||
|
|
||||||
|
@ -79,14 +79,20 @@ class TimelineProcessor(threading.Thread):
|
|||||||
if event_type == "start":
|
if event_type == "start":
|
||||||
timeline_entry[Timeline.class_type] = "visible"
|
timeline_entry[Timeline.class_type] = "visible"
|
||||||
Timeline.insert(timeline_entry).execute()
|
Timeline.insert(timeline_entry).execute()
|
||||||
elif (
|
elif event_type == "update":
|
||||||
event_type == "update"
|
# zones have been updated
|
||||||
and prev_event_data["current_zones"] != event_data["current_zones"]
|
if (
|
||||||
|
prev_event_data["current_zones"] != event_data["current_zones"]
|
||||||
and len(event_data["current_zones"]) > 0
|
and len(event_data["current_zones"]) > 0
|
||||||
):
|
):
|
||||||
timeline_entry[Timeline.class_type] = "entered_zone"
|
timeline_entry[Timeline.class_type] = "entered_zone"
|
||||||
timeline_entry[Timeline.data]["zones"] = event_data["current_zones"]
|
timeline_entry[Timeline.data]["zones"] = event_data["current_zones"]
|
||||||
Timeline.insert(timeline_entry).execute()
|
Timeline.insert(timeline_entry).execute()
|
||||||
|
elif prev_event_data["stationary"] != event_data["stationary"]:
|
||||||
|
timeline_entry[Timeline.class_type] = (
|
||||||
|
"stationary" if event_data["stationary"] else "active"
|
||||||
|
)
|
||||||
|
Timeline.insert(timeline_entry).execute()
|
||||||
elif event_type == "end":
|
elif event_type == "end":
|
||||||
timeline_entry[Timeline.class_type] = "gone"
|
timeline_entry[Timeline.class_type] = "gone"
|
||||||
Timeline.insert(timeline_entry).execute()
|
Timeline.insert(timeline_entry).execute()
|
||||||
|
@ -3,8 +3,10 @@ import useSWR from 'swr';
|
|||||||
import ActivityIndicator from './ActivityIndicator';
|
import ActivityIndicator from './ActivityIndicator';
|
||||||
import { formatUnixTimestampToDateTime } from '../utils/dateUtil';
|
import { formatUnixTimestampToDateTime } from '../utils/dateUtil';
|
||||||
import About from '../icons/About';
|
import About from '../icons/About';
|
||||||
|
import ActiveObjectIcon from '../icons/ActiveObject';
|
||||||
import PlayIcon from '../icons/Play';
|
import PlayIcon from '../icons/Play';
|
||||||
import ExitIcon from '../icons/Exit';
|
import ExitIcon from '../icons/Exit';
|
||||||
|
import StationaryObjectIcon from '../icons/StationaryObject';
|
||||||
import { Zone } from '../icons/Zone';
|
import { Zone } from '../icons/Zone';
|
||||||
import { useMemo, useState } from 'preact/hooks';
|
import { useMemo, useState } from 'preact/hooks';
|
||||||
import Button from './Button';
|
import Button from './Button';
|
||||||
@ -77,8 +79,7 @@ export default function TimelineSummary({ event, onFrameSelected }) {
|
|||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="h-14 flex justify-center">
|
<div className="h-14 flex justify-center">
|
||||||
<div className="sm:w-1 md:w-1/4 flex flex-row flex-nowrap justify-between overflow-auto">
|
<div className="sm:w-1 md:w-1/4 flex flex-row flex-nowrap justify-between overflow-auto">
|
||||||
{eventTimeline.map((item, index) =>
|
{eventTimeline.map((item, index) => (
|
||||||
item.class_type == 'visible' || item.class_type == 'gone' ? (
|
|
||||||
<Button
|
<Button
|
||||||
key={index}
|
key={index}
|
||||||
className="rounded-full"
|
className="rounded-full"
|
||||||
@ -87,21 +88,9 @@ export default function TimelineSummary({ event, onFrameSelected }) {
|
|||||||
aria-label={window.innerWidth > 640 ? getTimelineItemDescription(config, item, event) : ''}
|
aria-label={window.innerWidth > 640 ? getTimelineItemDescription(config, item, event) : ''}
|
||||||
onClick={() => onSelectMoment(index)}
|
onClick={() => onSelectMoment(index)}
|
||||||
>
|
>
|
||||||
{item.class_type == 'visible' ? <PlayIcon className="w-8" /> : <ExitIcon className="w-8" />}
|
{getTimelineIcon(item.class_type)}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
))}
|
||||||
<Button
|
|
||||||
key={index}
|
|
||||||
className="rounded-full"
|
|
||||||
type="iconOnly"
|
|
||||||
color={index == timeIndex ? 'blue' : 'gray'}
|
|
||||||
aria-label={window.innerWidth > 640 ? getTimelineItemDescription(config, item, event) : ''}
|
|
||||||
onClick={() => onSelectMoment(index)}
|
|
||||||
>
|
|
||||||
<Zone className="w-8" />
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{timeIndex >= 0 ? (
|
{timeIndex >= 0 ? (
|
||||||
@ -124,14 +113,30 @@ export default function TimelineSummary({ event, onFrameSelected }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getTimelineIcon(classType) {
|
||||||
|
switch (classType) {
|
||||||
|
case 'visible':
|
||||||
|
return <PlayIcon className="w-8" />;
|
||||||
|
case 'gone':
|
||||||
|
return <ExitIcon className="w-8" />;
|
||||||
|
case 'active':
|
||||||
|
return <ActiveObjectIcon className="w-8" />;
|
||||||
|
case 'stationary':
|
||||||
|
return <StationaryObjectIcon className="w-8" />;
|
||||||
|
case 'entered_zone':
|
||||||
|
return <Zone className="w-8" />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getTimelineItemDescription(config, timelineItem, event) {
|
function getTimelineItemDescription(config, timelineItem, event) {
|
||||||
if (timelineItem.class_type == 'visible') {
|
switch (timelineItem.class_type) {
|
||||||
|
case 'visible':
|
||||||
return `${event.label} detected at ${formatUnixTimestampToDateTime(timelineItem.timestamp, {
|
return `${event.label} detected at ${formatUnixTimestampToDateTime(timelineItem.timestamp, {
|
||||||
date_style: 'short',
|
date_style: 'short',
|
||||||
time_style: 'medium',
|
time_style: 'medium',
|
||||||
time_format: config.ui.time_format,
|
time_format: config.ui.time_format,
|
||||||
})}`;
|
})}`;
|
||||||
} else if (timelineItem.class_type == 'entered_zone') {
|
case 'entered_zone':
|
||||||
return `${event.label.replaceAll('_', ' ')} entered ${timelineItem.data.zones
|
return `${event.label.replaceAll('_', ' ')} entered ${timelineItem.data.zones
|
||||||
.join(' and ')
|
.join(' and ')
|
||||||
.replaceAll('_', ' ')} at ${formatUnixTimestampToDateTime(timelineItem.timestamp, {
|
.replaceAll('_', ' ')} at ${formatUnixTimestampToDateTime(timelineItem.timestamp, {
|
||||||
@ -139,11 +144,23 @@ function getTimelineItemDescription(config, timelineItem, event) {
|
|||||||
time_style: 'medium',
|
time_style: 'medium',
|
||||||
time_format: config.ui.time_format,
|
time_format: config.ui.time_format,
|
||||||
})}`;
|
})}`;
|
||||||
}
|
case 'active':
|
||||||
|
return `${event.label} became active at ${formatUnixTimestampToDateTime(timelineItem.timestamp, {
|
||||||
|
date_style: 'short',
|
||||||
|
time_style: 'medium',
|
||||||
|
time_format: config.ui.time_format,
|
||||||
|
})}`;
|
||||||
|
case 'stationary':
|
||||||
|
return `${event.label} became stationary 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, {
|
return `${event.label} left at ${formatUnixTimestampToDateTime(timelineItem.timestamp, {
|
||||||
date_style: 'short',
|
date_style: 'short',
|
||||||
time_style: 'medium',
|
time_style: 'medium',
|
||||||
time_format: config.ui.time_format,
|
time_format: config.ui.time_format,
|
||||||
})}`;
|
})}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
15
web/src/icons/ActiveObject.jsx
Normal file
15
web/src/icons/ActiveObject.jsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { h } from 'preact';
|
||||||
|
import { memo } from 'preact/compat';
|
||||||
|
|
||||||
|
export function ActiveObject({ className = '' }) {
|
||||||
|
return (
|
||||||
|
<svg className={`fill-current ${className}`} viewBox="0 -960 960 960">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M360-80q-58 0-109-22t-89-60q-38-38-60-89T80-360q0-81 42-148t110-102q20-39 49.5-68.5T350-728q33-68 101-110t149-42q58 0 109 22t89 60q38 38 60 89t22 109q0 85-42 150T728-350q-20 39-49.5 68.5T610-232q-35 68-102 110T360-80Zm0-80q33 0 63.5-10t56.5-30q-58 0-109-22t-89-60q-38-38-60-89t-22-109q-20 26-30 56.5T160-360q0 42 16 78t43 63q27 27 63 43t78 16Zm120-120q33 0 64.5-10t57.5-30q-59 0-110-22.5T403-403q-38-38-60.5-89T320-602q-20 26-30 57.5T280-480q0 42 15.5 78t43.5 63q27 28 63 43.5t78 15.5Zm120-120q18 0 34.5-3t33.5-9q22-60 6.5-115.5T621-621q-38-38-93.5-53.5T412-668q-6 17-9 33.5t-3 34.5q0 42 15.5 78t43.5 63q27 28 63 43.5t78 15.5Zm160-78q20-26 30-57.5t10-64.5q0-42-15.5-78T741-741q-27-28-63-43.5T600-800q-35 0-65.5 10T478-760q59 0 110 22.5t89 60.5q38 38 60.5 89T760-478Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(ActiveObject);
|
15
web/src/icons/StationaryObject.jsx
Normal file
15
web/src/icons/StationaryObject.jsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { h } from 'preact';
|
||||||
|
import { memo } from 'preact/compat';
|
||||||
|
|
||||||
|
export function StationaryObject({ className = '' }) {
|
||||||
|
return (
|
||||||
|
<svg className={`fill-current ${className}`} viewBox="0 -960 960 960">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(StationaryObject);
|
Loading…
Reference in New Issue
Block a user