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:
Nicolas Mowen 2023-07-31 20:43:48 -06:00 committed by GitHub
parent f57d21039e
commit b7ff6735f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 122 additions and 54 deletions

View File

@ -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

View File

@ -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()

View File

@ -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,
})}`; })}`;
} }
}

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

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