diff --git a/nixos/modules/services/video/frigate.nix b/nixos/modules/services/video/frigate.nix index 43941e64ac0d..c358f0cd63b2 100644 --- a/nixos/modules/services/video/frigate.nix +++ b/nixos/modules/services/video/frigate.nix @@ -405,7 +405,9 @@ in extraConfig = nginxAuthRequest + '' types { video/mp4 mp4; - image/jpeg jpg; + image/jpeg jpg jpeg; + image/png png; + image/webp webp; } expires 7d; @@ -493,19 +495,6 @@ in } ''; }; - # frontend uses this to fetch the version - "/api/go2rtc/api" = { - proxyPass = "http://frigate-go2rtc/api"; - recommendedProxySettings = true; - extraConfig = - nginxAuthRequest - + nginxProxySettings - + '' - limit_except GET { - deny all; - } - ''; - }; # integrationn uses this to add webrtc candidate "/api/go2rtc/webrtc" = { proxyPass = "http://frigate-go2rtc/api/webrtc"; @@ -541,6 +530,7 @@ in expires off; proxy_cache frigate_api_cache; + proxy_cache_key "$scheme$proxy_host$request_uri|$role|$groups|$user"; proxy_cache_lock on; proxy_cache_use_stale updating; proxy_cache_valid 200 5s; @@ -563,6 +553,13 @@ in ${nginxProxySettings} } + location /api/logout { + auth_request off; + rewrite ^/api(/.*)$ $1 break; + proxy_pass http://frigate-api; + ${nginxProxySettings} + } + location /api/auth/first_time_login { auth_request off; limit_except GET { @@ -747,7 +744,6 @@ in ] ++ optionals (!stdenv.hostPlatform.isAarch64) [ # not available on aarch64-linux - intel-gpu-tools rocmPackages.rocminfo ]; serviceConfig = { @@ -775,11 +771,10 @@ in Group = "frigate"; SupplementaryGroups = [ "render" ] ++ optionals withCoral [ "coral" ]; - AmbientCapabilities = optionals (elem cfg.vaapiDriver [ - "i965" - "iHD" - ]) [ "CAP_PERFMON" ]; # for intel_gpu_top + # No capabilities + CapabilityBoundingSet = [ "" ]; + # Allow delegating access UMask = "0027"; StateDirectory = "frigate"; diff --git a/pkgs/by-name/fr/frigate/ai-edge-litert.patch b/pkgs/by-name/fr/frigate/ai-edge-litert.patch deleted file mode 100644 index 3a8c3f8a566f..000000000000 --- a/pkgs/by-name/fr/frigate/ai-edge-litert.patch +++ /dev/null @@ -1,100 +0,0 @@ -diff --git a/frigate/data_processing/real_time/bird.py b/frigate/data_processing/real_time/bird.py -index 7851c0997..520440005 100644 ---- a/frigate/data_processing/real_time/bird.py -+++ b/frigate/data_processing/real_time/bird.py -@@ -22,7 +22,7 @@ from .api import RealTimeProcessorApi - try: - from tflite_runtime.interpreter import Interpreter - except ModuleNotFoundError: -- from tensorflow.lite.python.interpreter import Interpreter -+ from ai_edge_litert.interpreter import Interpreter - - logger = logging.getLogger(__name__) - -diff --git a/frigate/data_processing/real_time/custom_classification.py b/frigate/data_processing/real_time/custom_classification.py -index 229383d9f..2c74a6575 100644 ---- a/frigate/data_processing/real_time/custom_classification.py -+++ b/frigate/data_processing/real_time/custom_classification.py -@@ -32,7 +32,7 @@ from .api import RealTimeProcessorApi - try: - from tflite_runtime.interpreter import Interpreter - except ModuleNotFoundError: -- from tensorflow.lite.python.interpreter import Interpreter -+ from ai_edge_litert.interpreter import Interpreter - - logger = logging.getLogger(__name__) - -@@ -76,7 +76,7 @@ class CustomStateClassificationProcessor(RealTimeProcessorApi): - try: - from tflite_runtime.interpreter import Interpreter - except ModuleNotFoundError: -- from tensorflow.lite.python.interpreter import Interpreter -+ from ai_edge_litert.interpreter import Interpreter - - model_path = os.path.join(self.model_dir, "model.tflite") - labelmap_path = os.path.join(self.model_dir, "labelmap.txt") -diff --git a/frigate/detectors/detector_utils.py b/frigate/detectors/detector_utils.py -index d732de871..d8930b2ae 100644 ---- a/frigate/detectors/detector_utils.py -+++ b/frigate/detectors/detector_utils.py -@@ -6,7 +6,7 @@ import numpy as np - try: - from tflite_runtime.interpreter import Interpreter, load_delegate - except ModuleNotFoundError: -- from tensorflow.lite.python.interpreter import Interpreter, load_delegate -+ from ai_edge_litert.interpreter import Interpreter, load_delegate - - - logger = logging.getLogger(__name__) -diff --git a/frigate/detectors/plugins/cpu_tfl.py b/frigate/detectors/plugins/cpu_tfl.py -index 00351f519..6d336bb6b 100644 ---- a/frigate/detectors/plugins/cpu_tfl.py -+++ b/frigate/detectors/plugins/cpu_tfl.py -@@ -12,7 +12,7 @@ from ..detector_utils import tflite_detect_raw, tflite_init - try: - from tflite_runtime.interpreter import Interpreter - except ModuleNotFoundError: -- from tensorflow.lite.python.interpreter import Interpreter -+ from ai_edge_litert.interpreter import Interpreter - - - logger = logging.getLogger(__name__) -diff --git a/frigate/detectors/plugins/edgetpu_tfl.py b/frigate/detectors/plugins/edgetpu_tfl.py -index 2b94fde39..36c769b4b 100644 ---- a/frigate/detectors/plugins/edgetpu_tfl.py -+++ b/frigate/detectors/plugins/edgetpu_tfl.py -@@ -13,7 +13,7 @@ from frigate.detectors.detector_config import BaseDetectorConfig, ModelTypeEnum - try: - from tflite_runtime.interpreter import Interpreter, load_delegate - except ModuleNotFoundError: -- from tensorflow.lite.python.interpreter import Interpreter, load_delegate -+ from ai_edge_litert.interpreter import Interpreter, load_delegate - - logger = logging.getLogger(__name__) - -diff --git a/frigate/embeddings/onnx/face_embedding.py b/frigate/embeddings/onnx/face_embedding.py -index 04d756897..75dfedc94 100644 ---- a/frigate/embeddings/onnx/face_embedding.py -+++ b/frigate/embeddings/onnx/face_embedding.py -@@ -17,7 +17,7 @@ from .base_embedding import BaseEmbedding - try: - from tflite_runtime.interpreter import Interpreter - except ModuleNotFoundError: -- from tensorflow.lite.python.interpreter import Interpreter -+ from ai_edge_litert.interpreter import Interpreter - - logger = logging.getLogger(__name__) - -diff --git a/frigate/events/audio.py b/frigate/events/audio.py -index e88f2ae71..ad87d19c1 100644 ---- a/frigate/events/audio.py -+++ b/frigate/events/audio.py -@@ -43,7 +43,7 @@ from frigate.video import start_or_restart_ffmpeg, stop_ffmpeg - try: - from tflite_runtime.interpreter import Interpreter - except ModuleNotFoundError: -- from tensorflow.lite.python.interpreter import Interpreter -+ from ai_edge_litert.interpreter import Interpreter - - - logger = logging.getLogger(__name__) diff --git a/pkgs/by-name/fr/frigate/ffmpeg.patch b/pkgs/by-name/fr/frigate/ffmpeg.patch index 2b9d08cf2d06..d2b4a5eea85a 100644 --- a/pkgs/by-name/fr/frigate/ffmpeg.patch +++ b/pkgs/by-name/fr/frigate/ffmpeg.patch @@ -1,47 +1,23 @@ -diff --git a/frigate/config/camera/ffmpeg.py b/frigate/config/camera/ffmpeg.py -index 2c1e4cdca..d1a1f7065 100644 ---- a/frigate/config/camera/ffmpeg.py -+++ b/frigate/config/camera/ffmpeg.py -@@ -1,4 +1,5 @@ - from enum import Enum -+from os.path import join - from typing import Union +diff --git a/frigate/util/config.py b/frigate/util/config.py +index 3e093a978..c9a5d7462 100644 +--- a/frigate/util/config.py ++++ b/frigate/util/config.py +@@ -32,17 +32,7 @@ def resolve_ffmpeg_path(path: str, binary: str = "ffmpeg") -> str: + bundled version so existing configs keep working across an upgrade or a + revert. Custom install paths (anything absolute) are used as-is. + """ +- if path == "default" or ( +- not path.startswith("/") and path not in INCLUDED_FFMPEG_VERSIONS +- ): +- version = DEFAULT_FFMPEG_VERSION +- elif path in INCLUDED_FFMPEG_VERSIONS: +- version = path +- else: +- return f"{path}/bin/{binary}" +- +- return f"/usr/lib/ffmpeg/{version}/bin/{binary}" +- ++ return os.path.join(path, "bin", binary) - from pydantic import Field, field_validator -@@ -71,21 +72,11 @@ class FfmpegConfig(FrigateBaseModel): - - @property - def ffmpeg_path(self) -> str: -- if self.path == "default": -- return f"/usr/lib/ffmpeg/{DEFAULT_FFMPEG_VERSION}/bin/ffmpeg" -- elif self.path in INCLUDED_FFMPEG_VERSIONS: -- return f"/usr/lib/ffmpeg/{self.path}/bin/ffmpeg" -- else: -- return f"{self.path}/bin/ffmpeg" -+ return join(self.path, "bin/ffmpeg") - - @property - def ffprobe_path(self) -> str: -- if self.path == "default": -- return f"/usr/lib/ffmpeg/{DEFAULT_FFMPEG_VERSION}/bin/ffprobe" -- elif self.path in INCLUDED_FFMPEG_VERSIONS: -- return f"/usr/lib/ffmpeg/{self.path}/bin/ffprobe" -- else: -- return f"{self.path}/bin/ffprobe" -+ return join(self.path, "bin/ffprobe") - - - class CameraRoleEnum(str, Enum): -diff --git a/frigate/record/export.py b/frigate/record/export.py -index d4b49bb4b..bbcccff61 100644 ---- a/frigate/record/export.py -+++ b/frigate/record/export.py -@@ -127,7 +127,7 @@ class RecordingExporter(threading.Thread): - minutes = int(diff / 60) - seconds = int(diff % 60) - ffmpeg_cmd = [ -- "/usr/lib/ffmpeg/7.0/bin/ffmpeg", # hardcode path for exports thumbnail due to missing libwebp support -+ self.config.ffmpeg.ffmpeg_path, # hardcode path for exports thumbnail due to missing libwebp support - "-hide_banner", - "-loglevel", - "warning", + def redact_credential(obj: dict[str, Any], key: str) -> None: + """Replace obj[key] with the redaction sentinel if a value is saved, else drop. diff --git a/pkgs/by-name/fr/frigate/fix-tests-media-auth-paths.patch b/pkgs/by-name/fr/frigate/fix-tests-media-auth-paths.patch new file mode 100644 index 000000000000..ac0f75afda85 --- /dev/null +++ b/pkgs/by-name/fr/frigate/fix-tests-media-auth-paths.patch @@ -0,0 +1,32 @@ +From 91711507df5ad880bfcba1e11619441f7d0f9a58 Mon Sep 17 00:00:00 2001 +From: Martin Weinelt +Date: Mon, 13 Jul 2026 14:11:18 +0200 +Subject: [PATCH] Reuse constants in media auth tests + +--- + frigate/test/test_media_auth.py | 5 +++-- + 1 file changed, 3 insertions(+), 2 deletions(-) + +diff --git a/frigate/test/test_media_auth.py b/frigate/test/test_media_auth.py +index d025fea614..0b106047b2 100644 +--- a/frigate/test/test_media_auth.py ++++ b/frigate/test/test_media_auth.py +@@ -20,6 +20,7 @@ + resolve_media_uri, + ) + from frigate.config import FrigateConfig ++from frigate.const import EXPORT_DIR + from frigate.models import Event, Export, Recordings, ReviewSegment + from frigate.test.const import TEST_DB, TEST_DB_CLEANUPS + +@@ -231,8 +232,8 @@ def _insert_export(self, export_id, camera, filename): + camera=camera, + name=f"export-{export_id}", + date=int(datetime.datetime.now().timestamp()), +- video_path=f"/media/frigate/exports/{filename}", +- thumb_path=f"/media/frigate/exports/{filename}.jpg", ++ video_path=os.path.join(EXPORT_DIR, filename), ++ thumb_path=os.path.join(EXPORT_DIR, f"{filename}.jpg"), + in_progress=False, + ).execute() + diff --git a/pkgs/by-name/fr/frigate/package.nix b/pkgs/by-name/fr/frigate/package.nix index f395c2a74eb1..e60770f91fbc 100644 --- a/pkgs/by-name/fr/frigate/package.nix +++ b/pkgs/by-name/fr/frigate/package.nix @@ -12,36 +12,11 @@ sqlite-vec, frigate, nixosTests, - go2rtc, }: let - version = "0.17.2"; - - src = fetchFromGitHub { - name = "frigate-${version}-source"; - owner = "blakeblackshear"; - repo = "frigate"; - tag = "v${version}"; - hash = "sha256-8ujG5rVGqIJxM+IiQKvudrA0xqfz+3Uisl/zXwARPpY="; - }; - - frigate-web = callPackage ./web.nix { - inherit version src; - }; - python3Packages = python313Packages.overrideScope ( self: super: { - joserfc = super.joserfc.overridePythonAttrs (oldAttrs: { - version = "1.1.0"; - src = fetchFromGitHub { - owner = "authlib"; - repo = "joserfc"; - tag = version; - hash = "sha256-95xtUzzIxxvDtpHX/5uCHnTQTB8Fc08DZGUOR/SdKLs="; - }; - }); - # transformers 4.* is not compatible with the latest tokenizers tokenizers = super.tokenizers.overridePythonAttrs ( oldAttrs: @@ -114,12 +89,18 @@ let hash = "sha256-5Cj2vEiWR8Z9d2xBmVoLZuNRv4UOuxHSGZQWTJorXUQ="; }; in -python3Packages.buildPythonApplication rec { +python3Packages.buildPythonApplication (finalAttrs: { pname = "frigate"; - inherit version; + version = "0.18.0"; pyproject = false; - inherit src; + src = fetchFromGitHub { + name = "frigate-${finalAttrs.version}-source"; + owner = "blakeblackshear"; + repo = "frigate"; + tag = "v${finalAttrs.version}"; + hash = "sha256-2FJG7tEZCuCQ6VJga9BMiuLTeswmlG8oN1MO6t0eroM="; + }; patches = [ # Always lookup ffmpeg from config setting @@ -130,24 +111,27 @@ python3Packages.buildPythonApplication rec { inherit (addDriverRunpath) driverLink; }) - # Use ai-edge-litert as tensorflow interpreter - # https://github.com/blakeblackshear/frigate/pull/21876 - ./ai-edge-litert.patch - # Disable failing optimization in onnxruntime # https://github.com/microsoft/onnxruntime/issues/26717 + # https://github.com/microsoft/onnxruntime/pull/29196 ./onnxruntime-compat.patch - # Fix excessive trailing whitespaces in process commandlines - # https://github.com/blakeblackshear/frigate/pull/22089 - ./proc-cmdline-strip.patch + # Reuse EXPORT_DIR in tests + ./fix-tests-media-auth-paths.patch # Fix more granular dtype resolution in Pandas 3.0 ./pandas3-compat.patch + + # Various fixes for newer library packages, migrates to + # - FastAPI lifespan, + # - native tz-aware objects, + # - Pydantic TypeAdapter + # https://github.com/blakeblackshear/frigate/pull/23641 + ./pr23641.patch ]; postPatch = '' - echo 'VERSION = "${version}"' > frigate/version.py + echo 'VERSION = "${finalAttrs.version}"' > frigate/version.py substituteInPlace \ frigate/app.py \ @@ -189,75 +173,77 @@ python3Packages.buildPythonApplication rec { dontBuild = true; - dependencies = with python3Packages; [ - # docker/main/requirements-wheel.txt - # TODO: degirum (no source repo, binary wheels only) - ai-edge-litert - aiofiles - aiohttp - appdirs - argcomplete - click - contextlib2 - cryptography - distlib - fastapi - faster-whisper - filelock - google-genai - importlib-metadata - importlib-resources - joserfc - keras # via tensorflow.keras - librosa - markupsafe - memray - netaddr - netifaces - norfair - numpy - ollama - onnxruntime - onvif-zeep-async - openai - opencv4 - openvino - paho-mqtt - pandas - pathvalidate - peewee - peewee-migrate - prometheus-client - psutil - py3nvml - pyclipper - pydantic - python-multipart - pytz - py-vapid - pywebpush - pyzmq - rapidfuzz - requests - ruamel-yaml - scipy - setproctitle - shapely - sherpa-onnx - slowapi - soundfile - starlette - starlette-context - tensorflow - titlecase - transformers - tzlocal - unidecode - uvicorn - verboselogs - virtualenv - ws4py - ]; + dependencies = + with python3Packages; + [ + # docker/main/requirements-wheel.txt + ai-edge-litert + aiofiles + aiohttp + appdirs + argcomplete + click + contextlib2 + cryptography + distlib + fastapi + faster-whisper + filelock + google-genai + httpx + importlib-metadata + importlib-resources + joserfc + keras # via tensorflow.keras + librosa + markupsafe + memray + netaddr + netifaces + norfair + numpy + ollama + onnxruntime + onvif-zeep-async + openai + opencv4 + openvino + paho-mqtt + pandas + pathvalidate + peewee + peewee-migrate + prometheus-client + psutil + py3nvml + pyclipper + pydantic + python-multipart + py-vapid + pywebpush + pyzmq + rapidfuzz + requests + ruamel-yaml + scipy + setproctitle + shapely + sherpa-onnx + slowapi + soundfile + starlette + starlette-context + tensorflow + titlecase + transformers + tzlocal + unidecode + uvicorn + verboselogs + virtualenv + ws4py + ] + ++ httpx.optional-dependencies.http2; installPhase = '' runHook preInstall @@ -283,9 +269,9 @@ python3Packages.buildPythonApplication rec { ]; preCheck = '' - # Unavailable in the build sandbox + # FHS paths are unreachable in the build sandbox substituteInPlace frigate/const.py \ - --replace-fail "/var/lib/frigate" "$TMPDIR/" \ + --replace-fail "/var/lib/frigate" "$TMPDIR" \ --replace-fail "/var/cache/frigate" "$TMPDIR" ''; @@ -300,16 +286,20 @@ python3Packages.buildPythonApplication rec { ]; passthru = { - web = frigate-web; inherit python; - pythonPath = (python3Packages.makePythonPath dependencies) + ":${frigate}/${python.sitePackages}"; + pythonPath = + (python3Packages.makePythonPath finalAttrs.finalPackage.dependencies) + + ":${frigate}/${python.sitePackages}"; + web = callPackage ./web.nix { + inherit (finalAttrs) version src; + }; tests = { inherit (nixosTests) frigate; }; }; meta = { - changelog = "https://github.com/blakeblackshear/frigate/releases/tag/${src.tag}"; + changelog = "https://github.com/blakeblackshear/frigate/releases/tag/${finalAttrs.src.tag}"; description = "NVR with realtime local object detection for IP cameras"; longDescription = '' A complete and local NVR designed for Home Assistant with AI @@ -320,4 +310,4 @@ python3Packages.buildPythonApplication rec { license = lib.licenses.mit; maintainers = with lib.maintainers; [ hexa ]; }; -} +}) diff --git a/pkgs/by-name/fr/frigate/pr23641.patch b/pkgs/by-name/fr/frigate/pr23641.patch new file mode 100644 index 000000000000..281c651fcdde --- /dev/null +++ b/pkgs/by-name/fr/frigate/pr23641.patch @@ -0,0 +1,770 @@ +From e4bba6f9c03af6fe20c8bd090bb1bc94cbd9ef1f Mon Sep 17 00:00:00 2001 +From: Martin Weinelt +Date: Mon, 6 Jul 2026 13:14:54 +0200 +Subject: [PATCH 1/5] Migrate to fastapi lifespan for startup events + +The `on_event` style decorator is deprecated + +https://fastapi.tiangolo.com/advanced/events/ +--- + frigate/api/fastapi_app.py | 21 ++++++++++++--------- + 1 file changed, 12 insertions(+), 9 deletions(-) + +diff --git a/frigate/api/fastapi_app.py b/frigate/api/fastapi_app.py +index 387f0473fc..86058bfdaa 100644 +--- a/frigate/api/fastapi_app.py ++++ b/frigate/api/fastapi_app.py +@@ -1,6 +1,7 @@ + import asyncio + import logging + import re ++from contextlib import asynccontextmanager + + from fastapi import Depends, FastAPI, Request + from fastapi.responses import JSONResponse +@@ -77,9 +78,20 @@ def create_fastapi_app( + enforce_default_admin: bool = True, + config_holder: ConfigHolder | None = None, + ): ++ @asynccontextmanager ++ async def lifespan(app: FastAPI): ++ logger.info("FastAPI started") ++ asyncio.create_task( ++ debug_replay_auto_stop_watchdog( ++ replay_manager, frigate_config, config_publisher ++ ) ++ ) ++ yield ++ + logger.info("Starting FastAPI app") + app = FastAPI( + debug=False, ++ lifespan=lifespan, + swagger_ui_parameters={"apisSorter": "alpha", "operationsSorter": "alpha"}, + dependencies=[Depends(require_admin_by_default())] + if enforce_default_admin +@@ -115,15 +127,6 @@ async def frigate_middleware(request: Request, call_next): + database.close() + return response + +- @app.on_event("startup") +- async def startup(): +- logger.info("FastAPI started") +- asyncio.create_task( +- debug_replay_auto_stop_watchdog( +- replay_manager, frigate_config, config_publisher +- ) +- ) +- + # Rate limiter (used for login endpoint) + if frigate_config.auth.failed_login_rate_limit is None: + limiter.enabled = False + +From ef8fcf2328bd59b6d30c7ca523e39909a8b8c01e Mon Sep 17 00:00:00 2001 +From: Martin Weinelt +Date: Mon, 6 Jul 2026 13:16:58 +0200 +Subject: [PATCH 2/5] Migrate to pydantic TypeAdapter + +https://pydantic.dev/docs/validation/2.12/get-started/migration/#introduction-of-typeadapter +--- + frigate/test/test_object_detector.py | 18 ++++++++++++------ + 1 file changed, 12 insertions(+), 6 deletions(-) + +diff --git a/frigate/test/test_object_detector.py b/frigate/test/test_object_detector.py +index dc15b23516..33748c2807 100644 +--- a/frigate/test/test_object_detector.py ++++ b/frigate/test/test_object_detector.py +@@ -2,7 +2,7 @@ + from unittest.mock import Mock, patch + + import numpy as np +-from pydantic import parse_obj_as ++from pydantic import TypeAdapter + + import frigate.detectors as detectors + import frigate.object_detection.base +@@ -19,8 +19,8 @@ def test_localdetectorprocess_should_only_create_specified_detector_type(self): + "frigate.detectors.api_types", + {det_type: Mock() for det_type in DetectorTypeEnum}, + ): +- test_cfg = parse_obj_as( +- DetectorConfig, ({"type": det_type, "model": {}}) ++ test_cfg = TypeAdapter(DetectorConfig).validate_python( ++ {"type": det_type, "model": {}} + ) + test_cfg.model.path = "/test/modelpath" + test_obj = frigate.object_detection.base.LocalObjectDetector( +@@ -44,7 +44,9 @@ def test_detect_raw_given_tensor_input_should_return_api_detect_raw_result(self) + TEST_DATA = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + TEST_DETECT_RESULT = np.ndarray([1, 2, 4, 8, 16, 32]) + test_obj_detect = frigate.object_detection.base.LocalObjectDetector( +- detector_config=parse_obj_as(DetectorConfig, {"type": "cpu", "model": {}}) ++ detector_config=TypeAdapter(DetectorConfig).validate_python( ++ {"type": "cpu", "model": {}} ++ ) + ) + + mock_det_api = mock_cputfl.return_value +@@ -67,7 +69,9 @@ def test_detect_raw_given_tensor_input_should_call_api_detect_raw_with_transpose + TEST_DATA = np.zeros((1, 32, 32, 3), np.uint8) + TEST_DETECT_RESULT = np.ndarray([1, 2, 4, 8, 16, 32]) + +- test_cfg = parse_obj_as(DetectorConfig, {"type": "cpu", "model": {}}) ++ test_cfg = TypeAdapter(DetectorConfig).validate_python( ++ {"type": "cpu", "model": {}} ++ ) + test_cfg.model.input_tensor = InputTensorEnum.nchw + + test_obj_detect = frigate.object_detection.base.LocalObjectDetector( +@@ -116,7 +120,9 @@ def test_detect_given_tensor_input_should_return_lfiltered_detections( + "label-5", + ] + +- test_cfg = parse_obj_as(DetectorConfig, {"type": "cpu", "model": {}}) ++ test_cfg = TypeAdapter(DetectorConfig).validate_python( ++ {"type": "cpu", "model": {}} ++ ) + test_cfg.model = ModelConfig() + test_obj_detect = frigate.object_detection.base.LocalObjectDetector( + detector_config=test_cfg, + +From e9f1435eeb337bf7e269af1b09a0b1b87de719d5 Mon Sep 17 00:00:00 2001 +From: Martin Weinelt +Date: Mon, 6 Jul 2026 13:17:16 +0200 +Subject: [PATCH 3/5] Replace pytz with native tz-aware objects and zoneinfo + library + +Supported from Python 3.9, see https://peps.python.org/pep-0615/. Also +https://blog.ganssle.io/articles/2019/11/utcnow.html. +--- + docker/main/requirements-wheels.txt | 1 - + docs/static/frigate-api.yaml | 14 +++---- + .../api/defs/query/app_query_parameters.py | 2 +- + .../api/defs/query/events_query_parameters.py | 6 +-- + .../defs/query/recordings_query_parameters.py | 2 +- + .../api/defs/query/review_query_parameters.py | 2 +- + frigate/api/media.py | 4 +- + frigate/api/preview.py | 4 +- + frigate/api/record.py | 2 +- + frigate/record/export.py | 10 ++--- + frigate/test/http_api/test_http_media.py | 39 +++++++++---------- + frigate/test/http_api/test_http_review.py | 2 +- + frigate/util/time.py | 17 ++++---- + 13 files changed, 50 insertions(+), 55 deletions(-) + +diff --git a/docker/main/requirements-wheels.txt b/docker/main/requirements-wheels.txt +index 5ed9664fc2..164ab3b853 100644 +--- a/docker/main/requirements-wheels.txt ++++ b/docker/main/requirements-wheels.txt +@@ -25,7 +25,6 @@ peewee_migrate == 1.14.* + psutil == 7.1.* + pydantic == 2.10.* + git+https://github.com/fbcotter/py3nvml#egg=py3nvml +-pytz == 2025.* + pyzmq == 26.2.* + ruamel.yaml == 0.18.* + tzlocal == 5.2 +diff --git a/docs/static/frigate-api.yaml b/docs/static/frigate-api.yaml +index fe467e0c62..3ce79e4ae6 100644 +--- a/docs/static/frigate-api.yaml ++++ b/docs/static/frigate-api.yaml +@@ -2087,7 +2087,7 @@ paths: + required: false + schema: + type: string +- default: utc ++ default: UTC + title: Timezone + responses: + '200': +@@ -3129,7 +3129,7 @@ paths: + anyOf: + - type: string + - type: 'null' +- default: utc ++ default: UTC + title: Timezone + responses: + '200': +@@ -4227,7 +4227,7 @@ paths: + anyOf: + - type: string + - type: 'null' +- default: utc ++ default: UTC + title: Timezone + responses: + '200': +@@ -4479,7 +4479,7 @@ paths: + anyOf: + - type: string + - type: 'null' +- default: utc ++ default: UTC + title: Timezone + - name: min_score + in: query +@@ -4559,7 +4559,7 @@ paths: + anyOf: + - type: string + - type: 'null' +- default: utc ++ default: UTC + title: Timezone + - name: has_clip + in: query +@@ -6854,7 +6854,7 @@ paths: + required: false + schema: + type: string +- default: utc ++ default: UTC + title: Timezone + - name: cameras + in: query +@@ -6904,7 +6904,7 @@ paths: + required: false + schema: + type: string +- default: utc ++ default: UTC + title: Timezone + responses: + '200': +diff --git a/frigate/api/defs/query/app_query_parameters.py b/frigate/api/defs/query/app_query_parameters.py +index 626d39679b..26974d59b9 100644 +--- a/frigate/api/defs/query/app_query_parameters.py ++++ b/frigate/api/defs/query/app_query_parameters.py +@@ -7,4 +7,4 @@ class AppTimelineHourlyQueryParameters(BaseModel): + after: float | None = None + before: float | None = None + limit: int | None = 200 +- timezone: str | None = "utc" ++ timezone: str | None = "UTC" +diff --git a/frigate/api/defs/query/events_query_parameters.py b/frigate/api/defs/query/events_query_parameters.py +index 06d0dfc3af..30d0fcac50 100644 +--- a/frigate/api/defs/query/events_query_parameters.py ++++ b/frigate/api/defs/query/events_query_parameters.py +@@ -39,7 +39,7 @@ class EventsQueryParams(BaseModel): + max_length: float | None = None + event_id: str | None = None + sort: str | None = None +- timezone: str | None = "utc" ++ timezone: str | None = "UTC" + + + class EventsSearchQueryParams(BaseModel): +@@ -66,7 +66,7 @@ class EventsSearchQueryParams(BaseModel): + has_clip: bool | None = None + has_snapshot: bool | None = None + is_submitted: bool | None = None +- timezone: str | None = "utc" ++ timezone: str | None = "UTC" + min_score: float | None = None + max_score: float | None = None + min_speed: float | None = None +@@ -76,6 +76,6 @@ class EventsSearchQueryParams(BaseModel): + + + class EventsSummaryQueryParams(BaseModel): +- timezone: str | None = "utc" ++ timezone: str | None = "UTC" + has_clip: int | None = None + has_snapshot: int | None = None +diff --git a/frigate/api/defs/query/recordings_query_parameters.py b/frigate/api/defs/query/recordings_query_parameters.py +index 770da96bb8..cd7b4221c2 100644 +--- a/frigate/api/defs/query/recordings_query_parameters.py ++++ b/frigate/api/defs/query/recordings_query_parameters.py +@@ -3,7 +3,7 @@ + + + class MediaRecordingsSummaryQueryParams(BaseModel): +- timezone: str = "utc" ++ timezone: str = "UTC" + cameras: str | None = "all" + + +diff --git a/frigate/api/defs/query/review_query_parameters.py b/frigate/api/defs/query/review_query_parameters.py +index b16146a9bc..16f759f42a 100644 +--- a/frigate/api/defs/query/review_query_parameters.py ++++ b/frigate/api/defs/query/review_query_parameters.py +@@ -19,7 +19,7 @@ class ReviewSummaryQueryParams(BaseModel): + cameras: str = "all" + labels: str = "all" + zones: str = "all" +- timezone: str = "utc" ++ timezone: str = "UTC" + + + class ReviewActivityMotionQueryParams(BaseModel): +diff --git a/frigate/api/media.py b/frigate/api/media.py +index c6fa90c068..a5f5e1bab8 100644 +--- a/frigate/api/media.py ++++ b/frigate/api/media.py +@@ -11,10 +11,10 @@ + from pathlib import Path as FilePath + from typing import Any + from urllib.parse import unquote ++from zoneinfo import ZoneInfo + + import cv2 + import numpy as np +-import pytz + from fastapi import APIRouter, Depends, Path, Query, Request, Response + from fastapi.responses import FileResponse, JSONResponse, StreamingResponse + from pathvalidate import sanitize_filename +@@ -714,7 +714,7 @@ async def vod_hour( + parts = year_month.split("-") + start_date = ( + datetime(int(parts[0]), int(parts[1]), day, hour, tzinfo=UTC) +- - datetime.now(pytz.timezone(tz_name.replace(",", "/"))).utcoffset() ++ - datetime.now(ZoneInfo(tz_name.replace(",", "/"))).utcoffset() + ) + end_date = start_date + timedelta(hours=1) - timedelta(milliseconds=1) + start_ts = start_date.timestamp() +diff --git a/frigate/api/preview.py b/frigate/api/preview.py +index 1047591434..af84117fd6 100644 +--- a/frigate/api/preview.py ++++ b/frigate/api/preview.py +@@ -5,8 +5,8 @@ + import os + import threading + from datetime import UTC, datetime, timedelta ++from zoneinfo import ZoneInfo + +-import pytz + from fastapi import APIRouter, Depends, HTTPException + from fastapi.responses import JSONResponse + +@@ -126,7 +126,7 @@ def preview_hour( + parts = year_month.split("-") + start_date = ( + datetime(int(parts[0]), int(parts[1]), int(day), int(hour), tzinfo=UTC) +- - datetime.now(pytz.timezone(tz_name.replace(",", "/"))).utcoffset() ++ - datetime.now(ZoneInfo(tz_name.replace(",", "/"))).utcoffset() + ) + end_date = start_date + timedelta(hours=1) - timedelta(milliseconds=1) + start_ts = start_date.timestamp() +diff --git a/frigate/api/record.py b/frigate/api/record.py +index 5db257f482..3352ec039b 100644 +--- a/frigate/api/record.py ++++ b/frigate/api/record.py +@@ -120,7 +120,7 @@ def all_recordings_summary( + @router.get( + "/{camera_name}/recordings/summary", dependencies=[Depends(require_camera_access)] + ) +-async def recordings_summary(camera_name: str, timezone: str = "utc"): ++async def recordings_summary(camera_name: str, timezone: str = "UTC"): + """Returns hourly summary for recordings of given camera""" + + time_range_query = ( +diff --git a/frigate/record/export.py b/frigate/record/export.py +index 5d307077c9..0564dee04b 100644 +--- a/frigate/record/export.py ++++ b/frigate/record/export.py +@@ -12,8 +12,8 @@ + from collections.abc import Callable + from enum import Enum + from pathlib import Path ++from zoneinfo import ZoneInfo, ZoneInfoNotFoundError + +-import pytz # type: ignore[import-untyped] + from peewee import DoesNotExist + + from frigate.config import FfmpegConfig, FrigateConfig +@@ -371,8 +371,8 @@ def get_datetime_from_timestamp(self, timestamp: int) -> str: + tz_name = self.config.ui.timezone + if tz_name: + try: +- tz = pytz.timezone(tz_name) +- except pytz.UnknownTimeZoneError: ++ tz = ZoneInfo(tz_name) ++ except (ValueError, ZoneInfoNotFoundError): + tz = None + if tz is not None: + return datetime.datetime.fromtimestamp(timestamp, tz=tz).strftime( +@@ -533,8 +533,8 @@ def _build_recording_segment_chapter_metadata_file( + tz: datetime.tzinfo | None = None + if tz_name: + try: +- tz = pytz.timezone(tz_name) +- except pytz.UnknownTimeZoneError: ++ tz = ZoneInfo(tz_name) ++ except (ValueError, ZoneInfoNotFoundError): + tz = None + if tz is None: + tz = datetime.UTC +diff --git a/frigate/test/http_api/test_http_media.py b/frigate/test/http_api/test_http_media.py +index b2d83cbc8a..8a3835c324 100644 +--- a/frigate/test/http_api/test_http_media.py ++++ b/frigate/test/http_api/test_http_media.py +@@ -1,8 +1,8 @@ + """Unit tests for recordings/media API endpoints.""" + + from datetime import UTC, datetime ++from zoneinfo import ZoneInfo + +-import pytz + from fastapi import Request + + from frigate.api.auth import get_allowed_cameras_for_filter, get_current_user +@@ -51,16 +51,16 @@ def test_recordings_summary_across_dst_spring_forward(self): + In 2024, DST in America/New_York transitions on March 10, 2024 at 2:00 AM + Clocks spring forward from 2:00 AM to 3:00 AM (EST to EDT) + """ +- tz = pytz.timezone("America/New_York") ++ tz = ZoneInfo("America/New_York") + + # March 9, 2024 at 12:00 PM EST (before DST) +- march_9_noon = tz.localize(datetime(2024, 3, 9, 12, 0, 0)).timestamp() ++ march_9_noon = datetime(2024, 3, 9, 12, 0, 0, tzinfo=tz).timestamp() + + # March 10, 2024 at 12:00 PM EDT (after DST transition) +- march_10_noon = tz.localize(datetime(2024, 3, 10, 12, 0, 0)).timestamp() ++ march_10_noon = datetime(2024, 3, 10, 12, 0, 0, tzinfo=tz).timestamp() + + # March 11, 2024 at 12:00 PM EDT (after DST) +- march_11_noon = tz.localize(datetime(2024, 3, 11, 12, 0, 0)).timestamp() ++ march_11_noon = datetime(2024, 3, 11, 12, 0, 0, tzinfo=tz).timestamp() + + with AuthTestClient(self.app) as client: + # Insert recordings for each day +@@ -124,19 +124,16 @@ def test_recordings_summary_across_dst_fall_back(self): + In 2024, DST in America/New_York transitions on November 3, 2024 at 2:00 AM + Clocks fall back from 2:00 AM to 1:00 AM (EDT to EST) + """ +- tz = pytz.timezone("America/New_York") ++ tz = ZoneInfo("America/New_York") + + # November 2, 2024 at 12:00 PM EDT (before DST transition) +- nov_2_noon = tz.localize(datetime(2024, 11, 2, 12, 0, 0)).timestamp() ++ nov_2_noon = datetime(2024, 11, 2, 12, 0, 0, tzinfo=tz).timestamp() + + # November 3, 2024 at 12:00 PM EST (after DST transition) +- # Need to specify is_dst=False to get the time after fall back +- nov_3_noon = tz.localize( +- datetime(2024, 11, 3, 12, 0, 0), is_dst=False +- ).timestamp() ++ nov_3_noon = datetime(2024, 11, 3, 12, 0, 0, tzinfo=tz).timestamp() + + # November 4, 2024 at 12:00 PM EST (after DST) +- nov_4_noon = tz.localize(datetime(2024, 11, 4, 12, 0, 0)).timestamp() ++ nov_4_noon = datetime(2024, 11, 4, 12, 0, 0, tzinfo=tz).timestamp() + + with AuthTestClient(self.app) as client: + # Insert recordings for each day +@@ -197,13 +194,13 @@ def test_recordings_summary_multiple_cameras_across_dst(self): + """ + Test recordings summary with multiple cameras across DST boundary. + """ +- tz = pytz.timezone("America/New_York") ++ tz = ZoneInfo("America/New_York") + + # March 9, 2024 at 10:00 AM EST (before DST) +- march_9_morning = tz.localize(datetime(2024, 3, 9, 10, 0, 0)).timestamp() ++ march_9_morning = datetime(2024, 3, 9, 10, 0, 0, tzinfo=tz).timestamp() + + # March 10, 2024 at 3:00 PM EDT (after DST transition) +- march_10_afternoon = tz.localize(datetime(2024, 3, 10, 15, 0, 0)).timestamp() ++ march_10_afternoon = datetime(2024, 3, 10, 15, 0, 0, tzinfo=tz).timestamp() + + with AuthTestClient(self.app) as client: + # Override allowed cameras for this test to include both +@@ -266,15 +263,15 @@ def test_recordings_summary_at_dst_transition_time(self): + """ + Test recordings that span the exact DST transition time. + """ +- tz = pytz.timezone("America/New_York") ++ tz = ZoneInfo("America/New_York") + + # March 10, 2024 at 1:00 AM EST (1 hour before DST transition) + # At 2:00 AM, clocks jump to 3:00 AM +- before_transition = tz.localize(datetime(2024, 3, 10, 1, 0, 0)).timestamp() ++ before_transition = datetime(2024, 3, 10, 1, 0, 0, tzinfo=tz).timestamp() + + # Recording that spans the transition (1:00 AM to 3:30 AM EDT) + # This is 1.5 hours of actual time but spans the "missing" hour +- after_transition = tz.localize(datetime(2024, 3, 10, 3, 30, 0)).timestamp() ++ after_transition = datetime(2024, 3, 10, 3, 30, 0, tzinfo=tz).timestamp() + + with AuthTestClient(self.app) as client: + Recordings.insert( +@@ -334,7 +331,7 @@ def test_recordings_summary_utc_timezone(self): + + # Test with UTC timezone + response = client.get( +- "/recordings/summary", params={"timezone": "utc", "cameras": "all"} ++ "/recordings/summary", params={"timezone": "UTC", "cameras": "all"} + ) + + assert response.status_code == 200 +@@ -365,8 +362,8 @@ def test_recordings_summary_single_camera_filter(self): + """ + Test recordings summary filtered to a single camera. + """ +- tz = pytz.timezone("America/New_York") +- march_10_noon = tz.localize(datetime(2024, 3, 10, 12, 0, 0)).timestamp() ++ tz = ZoneInfo("America/New_York") ++ march_10_noon = datetime(2024, 3, 10, 12, 0, 0, tzinfo=tz).timestamp() + + with AuthTestClient(self.app) as client: + # Insert recordings for both cameras +diff --git a/frigate/test/http_api/test_http_review.py b/frigate/test/http_api/test_http_review.py +index d13a7bd273..62d4f1608d 100644 +--- a/frigate/test/http_api/test_http_review.py ++++ b/frigate/test/http_api/test_http_review.py +@@ -250,7 +250,7 @@ def test_get_review_summary_all_filters(self): + "cameras": "front_door", + "labels": "all", + "zones": "all", +- "timezone": "utc", ++ "timezone": "UTC", + } + response = client.get("/review/summary", params=params) + assert response.status_code == 200 +diff --git a/frigate/util/time.py b/frigate/util/time.py +index 861bec17f6..777ae9cc11 100644 +--- a/frigate/util/time.py ++++ b/frigate/util/time.py +@@ -2,9 +2,8 @@ + + import datetime + import logging +-from zoneinfo import ZoneInfoNotFoundError ++from zoneinfo import ZoneInfo, ZoneInfoNotFoundError + +-import pytz + from tzlocal import get_localzone + + logger = logging.getLogger(__name__) +@@ -12,7 +11,7 @@ + + def get_tz_modifiers(tz_name: str) -> tuple[str, str, float]: + seconds_offset = ( +- datetime.datetime.now(pytz.timezone(tz_name)).utcoffset().total_seconds() ++ datetime.datetime.now(ZoneInfo(tz_name)).utcoffset().total_seconds() + ) + hours_offset = int(seconds_offset / 60 / 60) + minutes_offset = int(seconds_offset / 60 - hours_offset * 60) +@@ -25,7 +24,7 @@ def get_tomorrow_at_time(hour: int) -> datetime.datetime: + """Returns the datetime of the following day at 2am.""" + try: + tomorrow = datetime.datetime.now(get_localzone()) + datetime.timedelta(days=1) +- except ZoneInfoNotFoundError: ++ except (ValueError, ZoneInfoNotFoundError): + tomorrow = datetime.datetime.now(datetime.UTC) + datetime.timedelta(days=1) + logger.warning( + "Using utc for maintenance due to missing or incorrect timezone set" +@@ -45,7 +44,7 @@ def is_current_hour(timestamp: int) -> bool: + + def get_dst_transitions( + tz_name: str, start_time: float, end_time: float +-) -> list[tuple[float, float]]: ++) -> list[tuple[float, float, int]]: + """ + Find DST transition points and return time periods with consistent offsets. + +@@ -59,8 +58,8 @@ def get_dst_transitions( + continuous periods with the same UTC offset + """ + try: +- tz = pytz.timezone(tz_name) +- except pytz.UnknownTimeZoneError: ++ tz = ZoneInfo(tz_name) ++ except (ValueError, ZoneInfoNotFoundError): + # If timezone is invalid, return single period with no offset + return [(start_time, end_time, 0)] + +@@ -68,14 +67,14 @@ def get_dst_transitions( + current = start_time + + # Get initial offset +- dt = datetime.datetime.utcfromtimestamp(current).replace(tzinfo=pytz.UTC) ++ dt = datetime.datetime.fromtimestamp(current, tz=datetime.UTC) + local_dt = dt.astimezone(tz) + prev_offset = local_dt.utcoffset().total_seconds() + period_start = start_time + + # Check each day for offset changes + while current <= end_time: +- dt = datetime.datetime.utcfromtimestamp(current).replace(tzinfo=pytz.UTC) ++ dt = datetime.datetime.fromtimestamp(current, tz=datetime.UTC) + local_dt = dt.astimezone(tz) + current_offset = local_dt.utcoffset().total_seconds() + + +From af86312f3dd57c068c3f6b3ae68d8fa6f77c6e30 Mon Sep 17 00:00:00 2001 +From: Martin Weinelt +Date: Mon, 6 Jul 2026 16:21:01 +0200 +Subject: [PATCH 4/5] Provide lookup for normalized timezone names + +This provides a helper to look up the correct timezone name independent +of the casing of the given timezone name. +--- + frigate/api/media.py | 5 ++++- + frigate/api/preview.py | 5 ++++- + frigate/record/export.py | 6 +++--- + frigate/util/time.py | 22 +++++++++++++++++++--- + 4 files changed, 30 insertions(+), 8 deletions(-) + +diff --git a/frigate/api/media.py b/frigate/api/media.py +index a5f5e1bab8..14ae263053 100644 +--- a/frigate/api/media.py ++++ b/frigate/api/media.py +@@ -54,6 +54,7 @@ + from frigate.util.image import get_image_from_recording, get_image_quality_params + from frigate.util.media import get_keyframe_before + from frigate.util.object import create_empty_regions_grid ++from frigate.util.time import get_normalized_tz_name + + logger = logging.getLogger(__name__) + +@@ -714,7 +715,9 @@ async def vod_hour( + parts = year_month.split("-") + start_date = ( + datetime(int(parts[0]), int(parts[1]), day, hour, tzinfo=UTC) +- - datetime.now(ZoneInfo(tz_name.replace(",", "/"))).utcoffset() ++ - datetime.now( ++ ZoneInfo(get_normalized_tz_name(tz_name.replace(",", "/"))) ++ ).utcoffset() + ) + end_date = start_date + timedelta(hours=1) - timedelta(milliseconds=1) + start_ts = start_date.timestamp() +diff --git a/frigate/api/preview.py b/frigate/api/preview.py +index af84117fd6..81de23d003 100644 +--- a/frigate/api/preview.py ++++ b/frigate/api/preview.py +@@ -22,6 +22,7 @@ + from frigate.api.defs.tags import Tags + from frigate.const import BASE_DIR, CACHE_DIR, PREVIEW_FRAME_TYPE + from frigate.models import Previews ++from frigate.util.time import get_normalized_tz_name + + logger = logging.getLogger(__name__) + +@@ -126,7 +127,9 @@ def preview_hour( + parts = year_month.split("-") + start_date = ( + datetime(int(parts[0]), int(parts[1]), int(day), int(hour), tzinfo=UTC) +- - datetime.now(ZoneInfo(tz_name.replace(",", "/"))).utcoffset() ++ - datetime.now( ++ ZoneInfo(get_normalized_tz_name(tz_name.replace(",", "/"))) ++ ).utcoffset() + ) + end_date = start_date + timedelta(hours=1) - timedelta(milliseconds=1) + start_ts = start_date.timestamp() +diff --git a/frigate/record/export.py b/frigate/record/export.py +index 0564dee04b..ff33063bc3 100644 +--- a/frigate/record/export.py ++++ b/frigate/record/export.py +@@ -31,7 +31,7 @@ + ) + from frigate.models import Export, Previews, Recordings, ReviewSegment + from frigate.util.ffmpeg import run_ffmpeg_with_progress +-from frigate.util.time import is_current_hour ++from frigate.util.time import get_normalized_tz_name, is_current_hour + + logger = logging.getLogger(__name__) + +@@ -371,7 +371,7 @@ def get_datetime_from_timestamp(self, timestamp: int) -> str: + tz_name = self.config.ui.timezone + if tz_name: + try: +- tz = ZoneInfo(tz_name) ++ tz = ZoneInfo(get_normalized_tz_name(tz_name)) + except (ValueError, ZoneInfoNotFoundError): + tz = None + if tz is not None: +@@ -533,7 +533,7 @@ def _build_recording_segment_chapter_metadata_file( + tz: datetime.tzinfo | None = None + if tz_name: + try: +- tz = ZoneInfo(tz_name) ++ tz = ZoneInfo(get_normalized_tz_name(tz_name)) + except (ValueError, ZoneInfoNotFoundError): + tz = None + if tz is None: +diff --git a/frigate/util/time.py b/frigate/util/time.py +index 777ae9cc11..a4395c5d5d 100644 +--- a/frigate/util/time.py ++++ b/frigate/util/time.py +@@ -2,16 +2,32 @@ + + import datetime + import logging +-from zoneinfo import ZoneInfo, ZoneInfoNotFoundError ++from typing import Final ++from zoneinfo import ZoneInfo, ZoneInfoNotFoundError, available_timezones + + from tzlocal import get_localzone + + logger = logging.getLogger(__name__) + ++TIMEZONE_CASEFOLD_INDEX: Final = { ++ timezone.casefold(): timezone for timezone in available_timezones() ++} ++ ++ ++def get_normalized_tz_name(tz_name: str) -> str: ++ """Return the canonical name for a case-insensitive IANA timezone.""" ++ tz = TIMEZONE_CASEFOLD_INDEX.get(tz_name.casefold()) ++ if tz: ++ return tz ++ ++ raise ValueError(f"Unknown timezone: {tz_name}") ++ + + def get_tz_modifiers(tz_name: str) -> tuple[str, str, float]: + seconds_offset = ( +- datetime.datetime.now(ZoneInfo(tz_name)).utcoffset().total_seconds() ++ datetime.datetime.now(ZoneInfo(get_normalized_tz_name(tz_name))) ++ .utcoffset() ++ .total_seconds() + ) + hours_offset = int(seconds_offset / 60 / 60) + minutes_offset = int(seconds_offset / 60 - hours_offset * 60) +@@ -58,7 +74,7 @@ def get_dst_transitions( + continuous periods with the same UTC offset + """ + try: +- tz = ZoneInfo(tz_name) ++ tz = ZoneInfo(get_normalized_tz_name(tz_name)) + except (ValueError, ZoneInfoNotFoundError): + # If timezone is invalid, return single period with no offset + return [(start_time, end_time, 0)] + +From 490f1b93f8cc2aa450e74e882aa31516d6c1078c Mon Sep 17 00:00:00 2001 +From: Martin Weinelt +Date: Mon, 6 Jul 2026 18:12:37 +0200 +Subject: [PATCH 5/5] Test timezone naming normalization and error handling + +--- + frigate/test/test_tz.py | 19 +++++++++++++++++++ + 1 file changed, 19 insertions(+) + create mode 100644 frigate/test/test_tz.py + +diff --git a/frigate/test/test_tz.py b/frigate/test/test_tz.py +new file mode 100644 +index 0000000000..5be8010dda +--- /dev/null ++++ b/frigate/test/test_tz.py +@@ -0,0 +1,19 @@ ++import unittest ++ ++from frigate.util.time import get_normalized_tz_name ++ ++ ++class TestGetNormalizedTzName(unittest.TestCase): ++ def test_valid(self): ++ cases = [ ++ ("utc", "UTC"), ++ ("america/new_york", "America/New_York"), ++ ] ++ ++ for tz_name, expected in cases: ++ with self.subTest(tz_name=tz_name): ++ self.assertEqual(get_normalized_tz_name(tz_name), expected) ++ ++ def test_invalid(self): ++ with self.assertRaisesRegex(ValueError, r"Unknown timezone: foo/bar"): ++ get_normalized_tz_name("foo/bar") diff --git a/pkgs/by-name/fr/frigate/proc-cmdline-strip.patch b/pkgs/by-name/fr/frigate/proc-cmdline-strip.patch deleted file mode 100644 index 2dd131ebf419..000000000000 --- a/pkgs/by-name/fr/frigate/proc-cmdline-strip.patch +++ /dev/null @@ -1,22 +0,0 @@ -diff --git a/frigate/util/services.py b/frigate/util/services.py -index 64d83833d..912ce67bd 100644 ---- a/frigate/util/services.py -+++ b/frigate/util/services.py -@@ -121,7 +121,7 @@ def get_cpu_stats() -> dict[str, dict]: - pid = str(process.info["pid"]) - try: - cpu_percent = process.info["cpu_percent"] -- cmdline = process.info["cmdline"] -+ cmdline = " ".join(process.info["cmdline"]).rstrip() - - with open(f"/proc/{pid}/stat", "r") as f: - stats = f.readline().split() -@@ -155,7 +155,7 @@ def get_cpu_stats() -> dict[str, dict]: - "cpu": str(cpu_percent), - "cpu_average": str(round(cpu_average_usage, 2)), - "mem": f"{mem_pct}", -- "cmdline": clean_camera_user_pass(" ".join(cmdline)), -+ "cmdline": clean_camera_user_pass(cmdline), - } - except Exception: - continue diff --git a/pkgs/by-name/fr/frigate/web.nix b/pkgs/by-name/fr/frigate/web.nix index 7e284818b975..7ea2aecce45d 100644 --- a/pkgs/by-name/fr/frigate/web.nix +++ b/pkgs/by-name/fr/frigate/web.nix @@ -31,7 +31,7 @@ buildNpmPackage { --replace-fail "/tmp/cache" "/var/cache/frigate" ''; - npmDepsHash = "sha256-iIqP3pspkDbaXZkZ5MIT/GVGiKBJCkFXQ7Av5h1rWKk="; + npmDepsHash = "sha256-k21UBr4agbJDZD0PYVjylRcyuRCFeBE2YBqZzE8v+jU="; installPhase = '' cp -rv dist/ $out