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( diff --git a/frigate/comms/dispatcher.py b/frigate/comms/dispatcher.py index 0c2ba5a89..6e45ac175 100644 --- a/frigate/comms/dispatcher.py +++ b/frigate/comms/dispatcher.py @@ -607,23 +607,27 @@ 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 "preset" in payload.lower(): + preset: str = ( + payload.decode("utf-8") if isinstance(payload, bytes) else 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) 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.""" 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 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)}`; } diff --git a/web/src/components/timeline/DetailStream.tsx b/web/src/components/timeline/DetailStream.tsx index ec2bc3b27..dc87e1f0f 100644 --- a/web/src/components/timeline/DetailStream.tsx +++ b/web/src/components/timeline/DetailStream.tsx @@ -316,7 +316,7 @@ function ReviewGroup({ date_style: "medium", }); - const shouldFetchEvents = review?.data?.detections?.length > 0; + const shouldFetchEvents = open && review?.data?.detections?.length > 0; const { data: fetchedEvents, isValidating } = useSWR( shouldFetchEvents 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) { 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); }}