From 63ea076aa2dbfa7af801ad39d438244a5e68df2f Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Mon, 8 Dec 2025 06:44:03 -0700 Subject: [PATCH 1/9] Fix saving zone friendly name when it wasn't set --- web/src/components/settings/ZoneEditPane.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/settings/ZoneEditPane.tsx b/web/src/components/settings/ZoneEditPane.tsx index f68546aa2..ebcb5d2ed 100644 --- a/web/src/components/settings/ZoneEditPane.tsx +++ b/web/src/components/settings/ZoneEditPane.tsx @@ -441,7 +441,7 @@ export default function ZoneEditPane({ } let friendlyNameQuery = ""; - if (friendly_name) { + if (friendly_name && friendly_name !== zoneName) { friendlyNameQuery = `&cameras.${polygon?.camera}.zones.${zoneName}.friendly_name=${encodeURIComponent(friendly_name)}`; } From 17ac273b90ffadbf8600d932da442ec6ca369137 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Mon, 8 Dec 2025 07:36:33 -0700 Subject: [PATCH 2/9] Fix UTF-8 handling for Onvif --- frigate/comms/dispatcher.py | 15 ++++++++++----- frigate/ptz/onvif.py | 30 +++++++++++++++++++++++++----- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/frigate/comms/dispatcher.py b/frigate/comms/dispatcher.py index 0c2ba5a89..af7581023 100644 --- a/frigate/comms/dispatcher.py +++ b/frigate/comms/dispatcher.py @@ -610,14 +610,19 @@ class Dispatcher: def _on_ptz_command(self, camera_name: str, payload: str) -> None: """Callback for ptz topic.""" try: - if "preset" in payload.lower(): + if isinstance(payload, bytes): + payload = payload.decode("utf-8") + + preset = payload.lower() + + if "preset" in preset: command = OnvifCommandEnum.preset - param = payload.lower()[payload.index("_") + 1 :] - elif "move_relative" in payload.lower(): + param = preset[preset.index("_") + 1 :] + elif "move_relative" in preset: command = OnvifCommandEnum.move_relative - param = payload.lower()[payload.index("_") + 1 :] + param = preset[preset.index("_") + 1 :] else: - command = OnvifCommandEnum[payload.lower()] + command = OnvifCommandEnum[preset] param = "" self.onvif.handle_command(camera_name, command, param) diff --git a/frigate/ptz/onvif.py b/frigate/ptz/onvif.py index e7539b1d6..488dbd278 100644 --- a/frigate/ptz/onvif.py +++ b/frigate/ptz/onvif.py @@ -95,12 +95,21 @@ class OnvifController: cam = self.camera_configs[cam_name] try: + user = cam.onvif.user + password = cam.onvif.password + + if user is not None and isinstance(user, bytes): + user = user.decode("utf-8") + + if password is not None and isinstance(password, bytes): + password = password.decode("utf-8") + self.cams[cam_name] = { "onvif": ONVIFCamera( cam.onvif.host, cam.onvif.port, - cam.onvif.user, - cam.onvif.password, + user, + password, wsdl_dir=str(Path(find_spec("onvif").origin).parent / "wsdl"), adjust_time=cam.onvif.ignore_time_mismatch, encrypt=not cam.onvif.tls_insecure, @@ -325,9 +334,15 @@ class OnvifController: presets = [] for preset in presets: - self.cams[camera_name]["presets"][ - (getattr(preset, "Name") or f"preset {preset['token']}").lower() - ] = preset["token"] + # Ensure preset name is a Unicode string and handle UTF-8 characters correctly + preset_name = getattr(preset, "Name") or f"preset {preset['token']}" + + if isinstance(preset_name, bytes): + preset_name = preset_name.decode("utf-8") + + # Convert to lowercase while preserving UTF-8 characters + preset_name_lower = preset_name.lower() + self.cams[camera_name]["presets"][preset_name_lower] = preset["token"] # get list of supported features supported_features = [] @@ -563,6 +578,11 @@ class OnvifController: self.cams[camera_name]["active"] = False async def _move_to_preset(self, camera_name: str, preset: str) -> None: + if isinstance(preset, bytes): + preset = preset.decode("utf-8") + + preset = preset.lower() + if preset not in self.cams[camera_name]["presets"]: logger.error(f"{preset} is not a valid preset for {camera_name}") return From 406382124a4b54eee9cbc86e6a30a2f03d853d92 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Mon, 8 Dec 2025 09:02:17 -0700 Subject: [PATCH 3/9] Don't remove none directory for classes --- frigate/api/classification.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frigate/api/classification.py b/frigate/api/classification.py index 6a738409e..deafaf956 100644 --- a/frigate/api/classification.py +++ b/frigate/api/classification.py @@ -710,7 +710,7 @@ def delete_classification_dataset_images( if os.path.isfile(file_path): os.unlink(file_path) - if os.path.exists(folder) and not os.listdir(folder): + if os.path.exists(folder) and not os.listdir(folder) and category.lower() != "none": os.rmdir(folder) return JSONResponse( From 6a33fbc28adf877e835cd97f4ac662449e33d4db Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Mon, 8 Dec 2025 09:08:06 -0700 Subject: [PATCH 4/9] Lookup all event IDs for review item immediately --- web/src/components/timeline/DetailStream.tsx | 50 +++++++++++++++++--- 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/web/src/components/timeline/DetailStream.tsx b/web/src/components/timeline/DetailStream.tsx index ec2bc3b27..f05f0a988 100644 --- a/web/src/components/timeline/DetailStream.tsx +++ b/web/src/components/timeline/DetailStream.tsx @@ -67,6 +67,37 @@ export default function DetailStream({ onSeek(timestamp, isPlaying); }; + // Collect all unique event IDs from all review items + const allEventIds = useMemo(() => { + if (!reviewItems || reviewItems.length === 0) return []; + const idsSet = new Set(); + reviewItems.forEach((review) => { + if (review?.data?.detections?.length > 0) { + review.data.detections.forEach((id) => idsSet.add(id)); + } + }); + return Array.from(idsSet); + }, [reviewItems]); + + // Fetch all events in a single API call + const { data: allFetchedEvents, isValidating: isValidatingEvents } = useSWR< + Event[] + >( + allEventIds.length > 0 + ? ["event_ids", { ids: allEventIds.join(",") }] + : null, + ); + + // Create a Map for quick event lookup by ID + const eventsById = useMemo(() => { + if (!allFetchedEvents) return new Map(); + const map = new Map(); + allFetchedEvents.forEach((event) => { + map.set(event.id, event); + }); + return map; + }, [allFetchedEvents]); + // Ensure we initialize the active review when reviewItems first arrive. // This helps when the component mounts while the video is already // playing — it guarantees the matching review is highlighted right @@ -216,6 +247,8 @@ export default function DetailStream({ onActivate={() => setActiveReviewId(id)} onOpenUpload={(e) => setUpload(e)} alwaysExpandActive={alwaysExpandActive} + eventsById={eventsById} + isValidatingEvents={isValidatingEvents} /> ); }) @@ -279,6 +312,8 @@ type ReviewGroupProps = { effectiveTime?: number; annotationOffset: number; alwaysExpandActive?: boolean; + eventsById: Map; + isValidatingEvents: boolean; }; function ReviewGroup({ @@ -292,6 +327,8 @@ function ReviewGroup({ effectiveTime, annotationOffset, alwaysExpandActive = false, + eventsById, + isValidatingEvents, }: ReviewGroupProps) { const { t } = useTranslation("views/events"); const [open, setOpen] = useState(false); @@ -318,11 +355,12 @@ function ReviewGroup({ const shouldFetchEvents = review?.data?.detections?.length > 0; - const { data: fetchedEvents, isValidating } = useSWR( - shouldFetchEvents - ? ["event_ids", { ids: review.data.detections.join(",") }] - : null, - ); + const fetchedEvents = useMemo(() => { + if (!shouldFetchEvents || !review?.data?.detections) return undefined; + return review.data.detections + .map((eventId) => eventsById.get(eventId)) + .filter((event): event is Event => event !== undefined); + }, [shouldFetchEvents, review?.data?.detections, eventsById]); const rawIconLabels: string[] = [ ...(fetchedEvents @@ -445,7 +483,7 @@ function ReviewGroup({ {open && (
- {shouldFetchEvents && isValidating && !fetchedEvents ? ( + {shouldFetchEvents && isValidatingEvents && !fetchedEvents ? ( ) : ( (fetchedEvents || []).map((event, index) => { From 1c9c111a5ed54c9b8d8d80a3255e43ffbd9b7433 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Mon, 8 Dec 2025 09:17:17 -0700 Subject: [PATCH 5/9] Cleanup typing --- frigate/comms/dispatcher.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/frigate/comms/dispatcher.py b/frigate/comms/dispatcher.py index af7581023..6e45ac175 100644 --- a/frigate/comms/dispatcher.py +++ b/frigate/comms/dispatcher.py @@ -607,13 +607,12 @@ class Dispatcher: ) self.publish(f"{camera_name}/snapshots/state", payload, retain=True) - def _on_ptz_command(self, camera_name: str, payload: str) -> None: + def _on_ptz_command(self, camera_name: str, payload: str | bytes) -> None: """Callback for ptz topic.""" try: - if isinstance(payload, bytes): - payload = payload.decode("utf-8") - - preset = payload.lower() + preset: str = ( + payload.decode("utf-8") if isinstance(payload, bytes) else payload + ).lower() if "preset" in preset: command = OnvifCommandEnum.preset @@ -628,7 +627,7 @@ class Dispatcher: self.onvif.handle_command(camera_name, command, param) logger.info(f"Setting ptz command to {command} for {camera_name}") except KeyError as k: - logger.error(f"Invalid PTZ command {payload}: {k}") + logger.error(f"Invalid PTZ command {preset}: {k}") def _on_birdseye_command(self, camera_name: str, payload: str) -> None: """Callback for birdseye topic.""" From 5bff7214a3f8001e41c83909a797f092c69df673 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Mon, 8 Dec 2025 10:20:49 -0700 Subject: [PATCH 6/9] Only fetch events when review group is open --- web/src/components/timeline/DetailStream.tsx | 54 +++----------------- 1 file changed, 8 insertions(+), 46 deletions(-) diff --git a/web/src/components/timeline/DetailStream.tsx b/web/src/components/timeline/DetailStream.tsx index f05f0a988..25c2f6eab 100644 --- a/web/src/components/timeline/DetailStream.tsx +++ b/web/src/components/timeline/DetailStream.tsx @@ -25,7 +25,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; import { Link } from "react-router-dom"; import { Switch } from "@/components/ui/switch"; import { useUserPersistence } from "@/hooks/use-user-persistence"; -import { isDesktop } from "react-device-detect"; +import { isDesktop, isOpera } from "react-device-detect"; import { resolveZoneName } from "@/hooks/use-zone-friendly-name"; import { PiSlidersHorizontalBold } from "react-icons/pi"; import { MdAutoAwesome } from "react-icons/md"; @@ -67,37 +67,6 @@ export default function DetailStream({ onSeek(timestamp, isPlaying); }; - // Collect all unique event IDs from all review items - const allEventIds = useMemo(() => { - if (!reviewItems || reviewItems.length === 0) return []; - const idsSet = new Set(); - reviewItems.forEach((review) => { - if (review?.data?.detections?.length > 0) { - review.data.detections.forEach((id) => idsSet.add(id)); - } - }); - return Array.from(idsSet); - }, [reviewItems]); - - // Fetch all events in a single API call - const { data: allFetchedEvents, isValidating: isValidatingEvents } = useSWR< - Event[] - >( - allEventIds.length > 0 - ? ["event_ids", { ids: allEventIds.join(",") }] - : null, - ); - - // Create a Map for quick event lookup by ID - const eventsById = useMemo(() => { - if (!allFetchedEvents) return new Map(); - const map = new Map(); - allFetchedEvents.forEach((event) => { - map.set(event.id, event); - }); - return map; - }, [allFetchedEvents]); - // Ensure we initialize the active review when reviewItems first arrive. // This helps when the component mounts while the video is already // playing — it guarantees the matching review is highlighted right @@ -247,8 +216,6 @@ export default function DetailStream({ onActivate={() => setActiveReviewId(id)} onOpenUpload={(e) => setUpload(e)} alwaysExpandActive={alwaysExpandActive} - eventsById={eventsById} - isValidatingEvents={isValidatingEvents} /> ); }) @@ -312,8 +279,6 @@ type ReviewGroupProps = { effectiveTime?: number; annotationOffset: number; alwaysExpandActive?: boolean; - eventsById: Map; - isValidatingEvents: boolean; }; function ReviewGroup({ @@ -327,8 +292,6 @@ function ReviewGroup({ effectiveTime, annotationOffset, alwaysExpandActive = false, - eventsById, - isValidatingEvents, }: ReviewGroupProps) { const { t } = useTranslation("views/events"); const [open, setOpen] = useState(false); @@ -353,14 +316,13 @@ function ReviewGroup({ date_style: "medium", }); - const shouldFetchEvents = review?.data?.detections?.length > 0; + const shouldFetchEvents = open && review?.data?.detections?.length > 0; - const fetchedEvents = useMemo(() => { - if (!shouldFetchEvents || !review?.data?.detections) return undefined; - return review.data.detections - .map((eventId) => eventsById.get(eventId)) - .filter((event): event is Event => event !== undefined); - }, [shouldFetchEvents, review?.data?.detections, eventsById]); + const { data: fetchedEvents, isValidating } = useSWR( + shouldFetchEvents + ? ["event_ids", { ids: review.data.detections.join(",") }] + : null, + ); const rawIconLabels: string[] = [ ...(fetchedEvents @@ -483,7 +445,7 @@ function ReviewGroup({ {open && (
- {shouldFetchEvents && isValidatingEvents && !fetchedEvents ? ( + {shouldFetchEvents && isValidating && !fetchedEvents ? ( ) : ( (fetchedEvents || []).map((event, index) => { From b5e8c360c0904e838746e8e8348cc21a18a2a2e6 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Mon, 8 Dec 2025 10:27:20 -0700 Subject: [PATCH 7/9] Cleanup --- web/src/components/timeline/DetailStream.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/timeline/DetailStream.tsx b/web/src/components/timeline/DetailStream.tsx index 25c2f6eab..dc87e1f0f 100644 --- a/web/src/components/timeline/DetailStream.tsx +++ b/web/src/components/timeline/DetailStream.tsx @@ -25,7 +25,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; import { Link } from "react-router-dom"; import { Switch } from "@/components/ui/switch"; import { useUserPersistence } from "@/hooks/use-user-persistence"; -import { isDesktop, isOpera } from "react-device-detect"; +import { isDesktop } from "react-device-detect"; import { resolveZoneName } from "@/hooks/use-zone-friendly-name"; import { PiSlidersHorizontalBold } from "react-icons/pi"; import { MdAutoAwesome } from "react-icons/md"; From 0b58d58f6646006cd9e9549b4f29474784310358 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Mon, 8 Dec 2025 14:43:49 -0600 Subject: [PATCH 8/9] disable debug paths switch for autotracking cameras --- web/src/views/settings/ObjectSettingsView.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/web/src/views/settings/ObjectSettingsView.tsx b/web/src/views/settings/ObjectSettingsView.tsx index 836cb3cc3..82977e80c 100644 --- a/web/src/views/settings/ObjectSettingsView.tsx +++ b/web/src/views/settings/ObjectSettingsView.tsx @@ -252,6 +252,10 @@ export default function ObjectSettingsView({ className="ml-1" id={param} checked={options && options[param]} + disabled={ + param === "paths" && + cameraConfig?.onvif?.autotracking?.enabled_in_config + } onCheckedChange={(isChecked) => { handleSetOption(param, isChecked); }} From ae90da261346f4c0c11c2385a58400789283256f Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Mon, 8 Dec 2025 19:10:41 -0600 Subject: [PATCH 9/9] fix clickable birdseye --- web/src/pages/Live.tsx | 19 +++++++++++++++---- web/src/views/live/LiveBirdseyeView.tsx | 6 ++++-- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/web/src/pages/Live.tsx b/web/src/pages/Live.tsx index a8777aec7..1b4bfb33a 100644 --- a/web/src/pages/Live.tsx +++ b/web/src/pages/Live.tsx @@ -134,10 +134,20 @@ function Live() { .sort((aConf, bConf) => aConf.ui.order - bConf.ui.order); }, [config, cameraGroup, allowedCameras]); - const selectedCamera = useMemo( - () => cameras.find((cam) => cam.name == selectedCameraName), - [cameras, selectedCameraName], - ); + const selectedCamera = useMemo(() => { + if (!config || !selectedCameraName || selectedCameraName === "birdseye") { + return undefined; + } + const camera = config.cameras[selectedCameraName]; + if ( + camera && + allowedCameras.includes(selectedCameraName) && + camera.enabled_in_config + ) { + return camera; + } + return undefined; + }, [config, selectedCameraName, allowedCameras]); return (
@@ -146,6 +156,7 @@ function Live() { supportsFullscreen={supportsFullScreen} fullscreen={fullscreen} toggleFullscreen={toggleFullscreen} + onSelectCamera={setSelectedCameraName} /> ) : selectedCamera ? ( void; + onSelectCamera?: (cameraName: string) => void; }; export default function LiveBirdseyeView({ supportsFullscreen, fullscreen, toggleFullscreen, + onSelectCamera, }: LiveBirdseyeViewProps) { const { t } = useTranslation(["views/live"]); const { data: config } = useSWR("config"); @@ -181,13 +183,13 @@ export default function LiveBirdseyeView({ canvasY >= parsedCoords.y && canvasY < parsedCoords.y + parsedCoords.height ) { - navigate(`/#${cameraName}`); + onSelectCamera?.(cameraName); break; } } } }, - [playerRef, config, birdseyeLayout, navigate], + [playerRef, config, birdseyeLayout, onSelectCamera], ); if (!config) {