frigate: 0.17.2 -> 0.18.0 (#541441)

This commit is contained in:
Martin Weinelt
2026-09-21 20:35:03 +00:00
committed by GitHub
16 changed files with 1182 additions and 344 deletions

View File

@@ -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";
};
};

View File

@@ -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])
'';
}

View File

@@ -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__)

View File

@@ -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.

View File

@@ -0,0 +1,32 @@
From 91711507df5ad880bfcba1e11619441f7d0f9a58 Mon Sep 17 00:00:00 2001
From: Martin Weinelt <hexa@darmstadt.ccc.de>
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()

View File

@@ -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;
})

View File

@@ -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/";
};
})

View File

@@ -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;
})

View File

@@ -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;
};
})

View File

@@ -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 ];
};
}
})

View File

@@ -0,0 +1,770 @@
From e4bba6f9c03af6fe20c8bd090bb1bc94cbd9ef1f Mon Sep 17 00:00:00 2001
From: Martin Weinelt <hexa@darmstadt.ccc.de>
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 <hexa@darmstadt.ccc.de>
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 <hexa@darmstadt.ccc.de>
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 <hexa@darmstadt.ccc.de>
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 <hexa@darmstadt.ccc.de>
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")

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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 ];

View File

@@ -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):