diff --git a/frigate/events/external.py b/frigate/events/external.py
index b02aaeba5..9c99ef50c 100644
--- a/frigate/events/external.py
+++ b/frigate/events/external.py
@@ -52,7 +52,7 @@ class ExternalEventProcessor:
(
EventTypeEnum.api,
"new",
- camera_config,
+ camera,
{
"id": event_id,
"label": label,
diff --git a/frigate/events/maintainer.py b/frigate/events/maintainer.py
index 19bb44ef4..eadf888c9 100644
--- a/frigate/events/maintainer.py
+++ b/frigate/events/maintainer.py
@@ -109,6 +109,16 @@ class EventProcessor(threading.Thread):
self.handle_object_detection(event_type, camera, event_data)
elif source_type == EventTypeEnum.api:
+ self.timeline_queue.put(
+ (
+ camera,
+ source_type,
+ event_type,
+ {},
+ event_data,
+ )
+ )
+
self.handle_external_detection(event_type, event_data)
# set an end_time on events without an end_time before exiting
diff --git a/frigate/timeline.py b/frigate/timeline.py
index 984dd3ae2..927fd6845 100644
--- a/frigate/timeline.py
+++ b/frigate/timeline.py
@@ -48,6 +48,8 @@ class TimelineProcessor(threading.Thread):
self.handle_object_detection(
camera, event_type, prev_event_data, event_data
)
+ elif input_type == EventTypeEnum.api:
+ self.handle_api_entry(camera, event_type, event_data)
def insert_or_save(
self,
@@ -140,3 +142,40 @@ class TimelineProcessor(threading.Thread):
if save:
self.insert_or_save(timeline_entry, prev_event_data, event_data)
+
+ def handle_api_entry(
+ self,
+ camera: str,
+ event_type: str,
+ event_data: dict[any, any],
+ ) -> bool:
+ if event_type != "new":
+ return False
+
+ if event_data.get("type", "api") == "audio":
+ timeline_entry = {
+ Timeline.class_type: "heard",
+ Timeline.timestamp: event_data["start_time"],
+ Timeline.camera: camera,
+ Timeline.source: "audio",
+ Timeline.source_id: event_data["id"],
+ Timeline.data: {
+ "label": event_data["label"],
+ "sub_label": event_data.get("sub_label"),
+ },
+ }
+ else:
+ timeline_entry = {
+ Timeline.class_type: "external",
+ Timeline.timestamp: event_data["start_time"],
+ Timeline.camera: camera,
+ Timeline.source: "api",
+ Timeline.source_id: event_data["id"],
+ Timeline.data: {
+ "label": event_data["label"],
+ "sub_label": event_data.get("sub_label"),
+ },
+ }
+
+ Timeline.insert(timeline_entry).execute()
+ return True
diff --git a/web/src/pages/History.tsx b/web/src/pages/History.tsx
index bc174a8ba..ec40a4db5 100644
--- a/web/src/pages/History.tsx
+++ b/web/src/pages/History.tsx
@@ -271,7 +271,7 @@ function History() {
);
})}
- {lastRow && }
+ {lastRow && !isDone && }
);
}
diff --git a/web/src/types/history.ts b/web/src/types/history.ts
index a5e7fa8a7..a6681dac9 100644
--- a/web/src/types/history.ts
+++ b/web/src/types/history.ts
@@ -27,7 +27,16 @@ type Timeline = {
data: {
[key: string]: any;
};
- class_type: string;
+ class_type:
+ | "visible"
+ | "gone"
+ | "sub_label"
+ | "entered_zone"
+ | "attribute"
+ | "active"
+ | "stationary"
+ | "heard"
+ | "external";
source_id: string;
source: string;
};
diff --git a/web/src/utils/timelineUtil.tsx b/web/src/utils/timelineUtil.tsx
index f85be6ad8..5f9333291 100644
--- a/web/src/utils/timelineUtil.tsx
+++ b/web/src/utils/timelineUtil.tsx
@@ -1,4 +1,11 @@
-import { LuCircle, LuPlay, LuPlayCircle, LuTruck } from "react-icons/lu";
+import {
+ LuCircle,
+ LuCircleDot,
+ LuEar,
+ LuPlay,
+ LuPlayCircle,
+ LuTruck,
+} from "react-icons/lu";
import { IoMdExit } from "react-icons/io";
import {
MdFaceUnlock,
@@ -33,7 +40,13 @@ export function getTimelineIcon(timelineItem: Timeline) {
return ;
case "car":
return ;
+ default:
+ return ;
}
+ case "heard":
+ return ;
+ case "external":
+ return ;
}
}
@@ -76,5 +89,9 @@ export function getTimelineItemDescription(timelineItem: Timeline) {
return `${timelineItem.data.label} recognized as ${timelineItem.data.sub_label}`;
case "gone":
return `${label} left`;
+ case "heard":
+ return `${label} heard`;
+ case "external":
+ return `${label} detected`;
}
}