diff --git a/nixos/modules/services/video/frigate.nix b/nixos/modules/services/video/frigate.nix index 43941e64ac0d..6ee20f7cf582 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"; @@ -797,9 +792,53 @@ in # Sockets/IPC RuntimeDirectory = "frigate"; + RemoveIPC = true; # Reduce visible process scope to cgroup ProtectProc = "invisible"; + + # Allow wide /proc inspection, e.g. for cpuinfo + ProcSubset = "all"; + + # Protect various system locations/interfaces + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectSystem = "strict"; + + # No JIT compilation + MemoryDenyWriteExecute = true; + + # No ABI personality changes + LockPersonality = true; + + # Only IP/Unix sockets + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_UNIX" + ]; + + # Deny namespace creation + RestrictNamespaces = true; + + # No privilege escalation + NoNewPrivileges = true; + RestrictSUIDSGID = true; + + # No realtime schedulign + RestrictRealtime = true; + + # Restrict allowed syscalls + SystemCallFilter = [ + "@system-service" + "~@privileged" + ]; + SystemCallArchitectures = "native"; + SystemCallErrorNumber = "EPERM"; }; }; diff --git a/nixos/tests/frigate.nix b/nixos/tests/frigate.nix index 10712387f421..ca6fc5a5c621 100644 --- a/nixos/tests/frigate.nix +++ b/nixos/tests/frigate.nix @@ -77,5 +77,7 @@ # wait for a recording to appear machine.wait_for_file("/var/cache/frigate/test@*.mp4") + + machine.log(machine.execute("systemd-analyze security frigate.service | grep -v ✓")[1]) ''; } 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/models/edgetpu/ssdlite_mobiledet_coco_qat_postprocess/package.nix b/pkgs/by-name/fr/frigate/models/edgetpu/ssdlite_mobiledet_coco_qat_postprocess/package.nix new file mode 100644 index 000000000000..925323b6fbcb --- /dev/null +++ b/pkgs/by-name/fr/frigate/models/edgetpu/ssdlite_mobiledet_coco_qat_postprocess/package.nix @@ -0,0 +1,14 @@ +{ + fetchurl, +}: + +let + model = fetchurl { + url = "https://github.com/google-coral/test_data/raw/release-frogfish/ssdlite_mobiledet_coco_qat_postprocess_edgetpu.tflite"; + hash = "sha256-Siviu7YU5XbVbcuRT6UnUr8PE0EVEnENNV2X+qGzVkE="; + }; +in + +model.overrideAttrs (_: { + passthru.model = model; +}) diff --git a/pkgs/by-name/fr/frigate/models/openvino/ssdlite_mobilenet_v2_coco/package.nix b/pkgs/by-name/fr/frigate/models/openvino/ssdlite_mobilenet_v2_coco/package.nix new file mode 100644 index 000000000000..c961d1291e8a --- /dev/null +++ b/pkgs/by-name/fr/frigate/models/openvino/ssdlite_mobilenet_v2_coco/package.nix @@ -0,0 +1,64 @@ +{ + fetchurl, + frigate, + lib, + stdenv, + ... +}: + +let + inherit (frigate) python3Packages; +in + +stdenv.mkDerivation (finalAttrs: { + pname = "ssdlite_mobilenet_v2_coco"; + version = "2018_05_09"; + + src = fetchurl { + url = "http://download.tensorflow.org/models/object_detection/ssdlite_mobilenet_v2_coco_2018_05_09.tar.gz"; + hash = "sha256-VCRFzOg02/u33xmRQl1HXoWi1+xoxgpPJiuxiqwQyLI="; + }; + + nativeBuildInputs = [ + python3Packages.openvino + ]; + + postPatch = '' + cp --no-preserve=mode ${frigate.src}/docker/main/build_ov_model.py . + substituteInPlace build_ov_model.py \ + --replace-fail "/models/${finalAttrs.pname}_${finalAttrs.version}" "./" \ + --replace-fail "/models/" "dist/" + ''; + + buildPhase = '' + runHook preBuild + mkdir dist + python3 build_ov_model.py + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + mkdir -p $out + cp dist/*.{bin,xml} $out/ + runHook postInstall + ''; + + passthru = { + model = "${finalAttrs.finalPackage}/ssdlite_mobilnet_v2.xml"; + labelmap = fetchurl { + url = "https://github.com/openvinotoolkit/open_model_zoo/raw/master/data/dataset_classes/coco_91cl_bkgr.txt"; + hash = "sha256-cQBhlxYm2yS6s7rm/06c5uZmUFZZkBI/P4bxLThbN9E="; + # https://github.com/blakeblackshear/frigate/commit/8ac3114f9a014356272b8a44b752e82df342f245 + postFetch = '' + sed -i "s/truck/car/g" $out + ''; + }; + }; + + meta = { + description = "Object detection model trained on the COCO dataset."; + license = lib.licenses.asl20; + homepage = "https://www.kaggle.com/models/tensorflow/ssdlite-mobilenet-v2/"; + }; +}) diff --git a/pkgs/by-name/fr/frigate/models/tflite/ssdlite_mobiledet_coco_qat_postprocess/package.nix b/pkgs/by-name/fr/frigate/models/tflite/ssdlite_mobiledet_coco_qat_postprocess/package.nix new file mode 100644 index 000000000000..a9399e969a32 --- /dev/null +++ b/pkgs/by-name/fr/frigate/models/tflite/ssdlite_mobiledet_coco_qat_postprocess/package.nix @@ -0,0 +1,14 @@ +{ + fetchurl, +}: + +let + model = fetchurl { + url = "https://github.com/google-coral/test_data/raw/release-frogfish/ssdlite_mobiledet_coco_qat_postprocess.tflite"; + hash = "sha256-kLszpjTgQZFMwYGapd+ZgY5sOWxNLblSwP16nP/Eck8="; + }; +in + +model.overrideAttrs (_: { + passthru.model = model; +}) diff --git a/pkgs/by-name/fr/frigate/models/tflite/yamnet_classification_v1/package.nix b/pkgs/by-name/fr/frigate/models/tflite/yamnet_classification_v1/package.nix new file mode 100644 index 000000000000..255d8f4d1b2e --- /dev/null +++ b/pkgs/by-name/fr/frigate/models/tflite/yamnet_classification_v1/package.nix @@ -0,0 +1,32 @@ +{ + fetchzip, + lib, + stdenv, +}: + +stdenv.mkDerivation (finalAttrs: { + pname = "yamnet_classification_v1"; + version = "0-unstable"; + + src = fetchzip { + url = "https://www.kaggle.com/api/v1/models/google/yamnet/tfLite/classification-tflite/1/download"; + extension = "tar.gz"; + hash = "sha256-FRL0lpbjgAV7HlaZIR1Hf/hr1bcfSMSeYdeGzMYpDf0="; + }; + + dontBuild = true; + + installPhase = '' + runHook preInstall + mkdir $out + mv 1.tflite $out/yamnet_classification_v1.tflite + runHook postInstall + ''; + + passthru.model = "${finalAttrs.finalPackage}/${finalAttrs.pname}.tflite"; + + meta = { + homepage = "https://www.kaggle.com/models/google/yamnet/tfLite/classification-tflite"; + license = lib.licenses.asl20; + }; +}) diff --git a/pkgs/by-name/fr/frigate/package.nix b/pkgs/by-name/fr/frigate/package.nix index f395c2a74eb1..bdf4fb1931d4 100644 --- a/pkgs/by-name/fr/frigate/package.nix +++ b/pkgs/by-name/fr/frigate/package.nix @@ -1,47 +1,21 @@ { - lib, - stdenv, - replaceVars, addDriverRunpath, callPackage, - python313Packages, fetchFromGitHub, rustPlatform, - fetchurl, ffmpeg-headless, - sqlite-vec, frigate, + lib, nixosTests, - go2rtc, + python313Packages, + replaceVars, + sqlite-vec, + stdenv, }: 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: @@ -86,40 +60,19 @@ let ); inherit (python3Packages) python; - - # Tensorflow audio model - # https://github.com/blakeblackshear/frigate/blob/v0.15.0/docker/main/Dockerfile#L125 - tflite_audio_model = fetchurl { - url = "https://www.kaggle.com/api/v1/models/google/yamnet/tfLite/classification-tflite/1/download"; - hash = "sha256-G5cbITJ2AnOl+49dxQToZ4OyeFO7MTXVVa4G8eHjZfM="; - }; - - # Tensorflow Lite models - # https://github.com/blakeblackshear/frigate/blob/v0.15.0/docker/main/Dockerfile#L115-L117 - tflite_cpu_model = fetchurl { - url = "https://github.com/google-coral/test_data/raw/release-frogfish/ssdlite_mobiledet_coco_qat_postprocess.tflite"; - hash = "sha256-kLszpjTgQZFMwYGapd+ZgY5sOWxNLblSwP16nP/Eck8="; - }; - tflite_edgetpu_model = fetchurl { - url = "https://github.com/google-coral/test_data/raw/release-frogfish/ssdlite_mobiledet_coco_qat_postprocess_edgetpu.tflite"; - hash = "sha256-Siviu7YU5XbVbcuRT6UnUr8PE0EVEnENNV2X+qGzVkE="; - }; - - # TODO: OpenVino model - # https://github.com/blakeblackshear/frigate/blob/v0.15.0/docker/main/Dockerfile#L64-L77 - # https://github.com/blakeblackshear/frigate/blob/v0.15.0/docker/main/Dockerfile#L120-L123 - # Convert https://www.kaggle.com/models/tensorflow/ssdlite-mobilenet-v2 with https://github.com/blakeblackshear/frigate/blob/v0.15.0/docker/main/build_ov_model.py into OpenVino IR format - coco_91cl_bkgr = fetchurl { - url = "https://github.com/openvinotoolkit/open_model_zoo/raw/master/data/dataset_classes/coco_91cl_bkgr.txt"; - 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 +83,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 \ @@ -174,90 +130,94 @@ python3Packages.buildPythonApplication rec { substituteInPlace frigate/db/sqlitevecq.py \ --replace-fail "/usr/local/lib/vec0" "${lib.getLib sqlite-vec}/lib/vec0${stdenv.hostPlatform.extensions.sharedLibrary}" - # provide default paths for models and maps that are shipped with frigate + # hardcoded default models shipped with Frigate substituteInPlace frigate/config/config.py \ - --replace-fail "/cpu_model.tflite" "${tflite_cpu_model}" \ - --replace-fail "/edgetpu_model.tflite" "${tflite_edgetpu_model}" + --replace-fail "/cpu_model.tflite" "${frigate.models.tflite.ssdlite_mobiledet_coco_qat_postprocess}" \ + --replace-fail "/edgetpu_model.tflite" "${frigate.models.edgetpu.ssdlite_mobiledet_coco_qat_postprocess}" \ + --replace-fail "/openvino-model/ssdlite_mobilenet_v2.xml" "${frigate.models.openvino.ssdlite_mobilenet_v2_coco.model}" \ + --replace-fail "/openvino-model/coco_91cl_bkgr.txt" "${frigate.models.openvino.ssdlite_mobilenet_v2_coco.labelmap}" substituteInPlace frigate/detectors/detector_config.py \ --replace-fail "/labelmap.txt" "${placeholder "out"}/share/frigate/labelmap.txt" substituteInPlace frigate/events/audio.py \ - --replace-fail "/cpu_audio_model.tflite" "${placeholder "out"}/share/frigate/cpu_audio_model.tflite" \ + --replace-fail "/cpu_audio_model.tflite" "${frigate.models.tflite.yamnet_classification_v1.model}" \ --replace-fail "/audio-labelmap.txt" "${placeholder "out"}/share/frigate/audio-labelmap.txt" ''; 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 @@ -268,11 +228,7 @@ python3Packages.buildPythonApplication rec { mkdir -p $out/share/frigate cp -R {migrations,labelmap.txt,audio-labelmap.txt} $out/share/frigate/ - tar --extract --gzip --file ${tflite_audio_model} - cp --no-preserve=mode ./1.tflite $out/share/frigate/cpu_audio_model.tflite - - cp --no-preserve=mode ${coco_91cl_bkgr} $out/share/frigate/coco_91cl_bkgr.txt - sed -i 's/truck/car/g' $out/share/frigate/coco_91cl_bkgr.txt + ln -s ${frigate.models.tflite.yamnet_classification_v1.model} $out/share/frigate/cpu_audio_model.tflite runHook postInstall ''; @@ -283,9 +239,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 +256,47 @@ python3Packages.buildPythonApplication rec { ]; passthru = { - web = frigate-web; - inherit python; - pythonPath = (python3Packages.makePythonPath dependencies) + ":${frigate}/${python.sitePackages}"; + inherit python python3Packages; + pythonPath = + (python3Packages.makePythonPath finalAttrs.finalPackage.dependencies) + + ":${frigate}/${python.sitePackages}"; + web = callPackage ./web.nix { + inherit (finalAttrs) version src; + }; + models = { + # Google Coral Accelerators as Tensorflow Delegate + # https://docs.frigate.video/configuration/object_detectors/#edge-tpu-detector + edgetpu = lib.recurseIntoAttrs ( + lib.packagesFromDirectoryRecursive { + inherit callPackage; + directory = ./models/edgetpu; + } + ); + # Intel OpenVINO for CPU/GPU/NPU + # https://docs.frigate.video/configuration/object_detectors/#openvino-detector + openvino = lib.recurseIntoAttrs ( + lib.packagesFromDirectoryRecursive { + inherit callPackage; + directory = ./models/openvino; + } + ); + # Tensorflow Lite CPU models + # https://docs.frigate.video/configuration/object_detectors/#cpu-detector-not-recommended + # https://docs.frigate.video/configuration/audio_detectors/ + tflite = lib.recurseIntoAttrs ( + lib.packagesFromDirectoryRecursive { + inherit callPackage; + directory = ./models/tflite; + } + ); + }; 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 +307,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 diff --git a/pkgs/by-name/op/openvino/package.nix b/pkgs/by-name/op/openvino/package.nix index 8946009c1340..65acf6a9453e 100644 --- a/pkgs/by-name/op/openvino/package.nix +++ b/pkgs/by-name/op/openvino/package.nix @@ -94,6 +94,17 @@ stdenv.mkDerivation (finalAttrs: { ./cmake-install-paths.patch ]; + # Fix arm computelib ar/ranlib toolchain paths for LTO awareness + postPatch = '' + substituteInPlace src/plugins/intel_cpu/thirdparty/ComputeLibrary/SConstruct \ + --replace-fail \ + "env['AR'] = toolchain_prefix + \"ar\"" \ + "env['AR'] = \"${lib.getExe' stdenv.cc.cc "gcc-ar"}\"" \ + --replace-fail \ + "env['RANLIB'] = toolchain_prefix + \"ranlib\"" \ + "env['RANLIB'] = \"${lib.getExe' stdenv.cc.cc "gcc-ranlib"}\"" + ''; + dontUseSconsCheck = true; dontUseSconsBuild = true; dontUseSconsInstall = true; @@ -127,7 +138,7 @@ stdenv.mkDerivation (finalAttrs: { (cmakeBool "ENABLE_SAMPLES" false) # features - (cmakeBool "ENABLE_INTEL_CPU" stdenv.hostPlatform.isx86_64) + (cmakeBool "ENABLE_INTEL_CPU" true) (cmakeBool "ENABLE_INTEL_GPU" true) (cmakeBool "ENABLE_INTEL_NPU" stdenv.hostPlatform.isx86_64) (cmakeBool "ENABLE_JS" false) diff --git a/pkgs/development/python-modules/filterpy/default.nix b/pkgs/development/python-modules/filterpy/default.nix index 110d3b136b0d..c5417046b1f6 100644 --- a/pkgs/development/python-modules/filterpy/default.nix +++ b/pkgs/development/python-modules/filterpy/default.nix @@ -24,7 +24,12 @@ buildPythonPackage { hash = "sha256-KuuVu0tqrmQuNKYmDmdy+TU6BnnhDxh4G8n9BGzjGag="; }; - patches = [ ./numpy-2.4-compat.patch ]; + patches = [ + ./numpy-2.4-compat.patch + + # https://github.com/rlabbe/filterpy/pull/331 + ./scipy-1.12-deprecation.patch + ]; build-system = [ setuptools ]; diff --git a/pkgs/development/python-modules/filterpy/scipy-1.12-deprecation.patch b/pkgs/development/python-modules/filterpy/scipy-1.12-deprecation.patch new file mode 100644 index 000000000000..ef26dbefafa2 --- /dev/null +++ b/pkgs/development/python-modules/filterpy/scipy-1.12-deprecation.patch @@ -0,0 +1,14 @@ +diff --git a/filterpy/discrete_bayes/discrete_bayes.py b/filterpy/discrete_bayes/discrete_bayes.py +index 084ba8c..c465835 100644 +--- a/filterpy/discrete_bayes/discrete_bayes.py ++++ b/filterpy/discrete_bayes/discrete_bayes.py +@@ -19,8 +19,7 @@ from __future__ import (absolute_import, division, print_function, + unicode_literals) + + import numpy as np +-from scipy.ndimage.filters import convolve +-from scipy.ndimage.interpolation import shift ++from scipy.ndimage import convolve, shift + + + def normalize(pdf):