mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-19 05:02:49 +03:00
Compare commits
8
Commits
aa18de4509
...
696664b25a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
696664b25a | ||
|
|
20c2be4368 | ||
|
|
8b72c7aa1f | ||
|
|
5c38f6ef96 | ||
|
|
38cabaa6aa | ||
|
|
f7528937e0 | ||
|
|
f4e0781ea5 | ||
|
|
a9e6f3b2e3 |
@@ -10,7 +10,8 @@ body:
|
||||
|
||||
Before submitting, read the [beta documentation][docs].
|
||||
|
||||
[docs]: https://deploy-preview-19787--frigate-docs.netlify.app/
|
||||
[docs]: https://docs-dev.frigate.video/
|
||||
[discussions]: https://github.com/blakeblackshear/frigate/discussions
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
@@ -22,8 +23,8 @@ body:
|
||||
id: version
|
||||
attributes:
|
||||
label: Beta Version
|
||||
description: Visible on the System page in the Web UI. Please include the full version including the build identifier (eg. 0.17.0-beta1)
|
||||
placeholder: "0.17.0-beta1"
|
||||
description: Visible on the System Metrics page in the Web UI. Please include the full version including the build identifier (eg. 0.18.0-beta1, 0.18.0-8b72c7a, etc.)
|
||||
placeholder: "0.18.0-beta1"
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
@@ -71,11 +72,12 @@ body:
|
||||
attributes:
|
||||
label: Install method
|
||||
options:
|
||||
- Home Assistant Add-on
|
||||
- Home Assistant App
|
||||
- Docker Compose
|
||||
- Docker CLI
|
||||
- Proxmox via Docker
|
||||
- Proxmox via TTeck Script
|
||||
- Proxmox via installation script
|
||||
- Proxomox via VM
|
||||
- Windows WSL2
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -87,11 +87,12 @@ body:
|
||||
attributes:
|
||||
label: Install method
|
||||
options:
|
||||
- Home Assistant Add-on
|
||||
- Home Assistant App
|
||||
- Docker Compose
|
||||
- Docker CLI
|
||||
- Proxmox via Docker
|
||||
- Proxmox via TTeck Script
|
||||
- Proxmox via installation script
|
||||
- Proxomox via VM
|
||||
- Windows WSL2
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -73,11 +73,12 @@ body:
|
||||
attributes:
|
||||
label: Install method
|
||||
options:
|
||||
- Home Assistant Add-on
|
||||
- Home Assistant App
|
||||
- Docker Compose
|
||||
- Docker CLI
|
||||
- Proxmox via Docker
|
||||
- Proxmox via TTeck Script
|
||||
- Proxmox via installation script
|
||||
- Proxomox via VM
|
||||
- Windows WSL2
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -53,11 +53,12 @@ body:
|
||||
attributes:
|
||||
label: Install method
|
||||
options:
|
||||
- Home Assistant Add-on
|
||||
- Home Assistant App
|
||||
- Docker Compose
|
||||
- Docker CLI
|
||||
- Proxmox via Docker
|
||||
- Proxmox via TTeck Script
|
||||
- Proxmox via installation script
|
||||
- Proxomox via VM
|
||||
- Windows WSL2
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -73,11 +73,12 @@ body:
|
||||
attributes:
|
||||
label: Install method
|
||||
options:
|
||||
- Home Assistant Add-on
|
||||
- Home Assistant App
|
||||
- Docker Compose
|
||||
- Docker CLI
|
||||
- Proxmox via Docker
|
||||
- Proxmox via TTeck Script
|
||||
- Proxmox via installation script
|
||||
- Proxmox via VM
|
||||
- Windows WSL2
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -69,11 +69,12 @@ body:
|
||||
attributes:
|
||||
label: Install method
|
||||
options:
|
||||
- Home Assistant Add-on
|
||||
- Home Assistant App
|
||||
- Docker Compose
|
||||
- Docker CLI
|
||||
- Proxmox via Docker
|
||||
- Proxmox via TTeck Script
|
||||
- Proxmox via installation script
|
||||
- Proxomox via VM
|
||||
- Windows WSL2
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -6,7 +6,7 @@ body:
|
||||
value: |
|
||||
Use this form to submit a reproducible bug in Frigate or Frigate's UI.
|
||||
|
||||
**⚠️ If you are running a beta version (0.17.0-beta or similar), please use the [Beta Support template](https://github.com/blakeblackshear/frigate/discussions/new?category=beta-support) instead.**
|
||||
**⚠️ If you are running a beta version (0.18.0-beta or similar), please use the [Beta Support template](https://github.com/blakeblackshear/frigate/discussions/new?category=beta-support) instead.**
|
||||
|
||||
Before submitting your bug report, please ask the AI with the "Ask AI" button on the [official documentation site][ai] about your issue, [search the discussions][discussions], look at recent open and closed [pull requests][prs], read the [official Frigate documentation][docs], and read the [Frigate FAQ][faq] pinned at the Discussion page to see if your bug has already been fixed by the developers or reported by the community.
|
||||
|
||||
@@ -116,9 +116,13 @@ body:
|
||||
attributes:
|
||||
label: Install method
|
||||
options:
|
||||
- Home Assistant Add-on
|
||||
- Home Assistant App
|
||||
- Docker Compose
|
||||
- Docker CLI
|
||||
- Proxmox via Docker
|
||||
- Proxmox via installation script
|
||||
- Proxomox via VM
|
||||
- Windows WSL2
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
@@ -63,7 +63,7 @@ RUN --mount=type=bind,source=docker/main/build_intel_media_driver.sh,target=/dep
|
||||
FROM scratch AS go2rtc
|
||||
ARG TARGETARCH
|
||||
WORKDIR /rootfs/usr/local/go2rtc/bin
|
||||
ADD --link --chmod=755 "https://github.com/AlexxIT/go2rtc/releases/download/v1.9.13/go2rtc_linux_${TARGETARCH}" go2rtc
|
||||
ADD --link --chmod=755 "https://github.com/AlexxIT/go2rtc/releases/download/v1.9.14/go2rtc_linux_${TARGETARCH}" go2rtc
|
||||
|
||||
FROM wget AS tempio
|
||||
ARG TARGETARCH
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -820,7 +820,7 @@ classification:
|
||||
interval: None
|
||||
|
||||
# Optional: Restream configuration
|
||||
# Uses https://github.com/AlexxIT/go2rtc (v1.9.13)
|
||||
# Uses https://github.com/AlexxIT/go2rtc (v1.9.14)
|
||||
# NOTE: The default go2rtc API port (1984) must be used,
|
||||
# changing this port for the integrated go2rtc instance is not supported.
|
||||
go2rtc:
|
||||
|
||||
@@ -354,7 +354,7 @@ To do this:
|
||||
|
||||
### Custom go2rtc version
|
||||
|
||||
Frigate currently includes go2rtc v1.9.13, there may be certain cases where you want to run a different version of go2rtc.
|
||||
Frigate currently includes go2rtc v1.9.14, there may be certain cases where you want to run a different version of go2rtc.
|
||||
|
||||
To do this:
|
||||
|
||||
|
||||
@@ -264,7 +264,7 @@ go2rtc:
|
||||
- rtspx://192.168.1.1:7441/abcdefghijk
|
||||
```
|
||||
|
||||
[See the go2rtc docs for more information](https://github.com/AlexxIT/go2rtc/tree/v1.9.13#source-rtsp)
|
||||
[See the go2rtc docs for more information](https://github.com/AlexxIT/go2rtc/tree/v1.9.14#source-rtsp)
|
||||
|
||||
In the Unifi 2.0 update Unifi Protect Cameras had a change in audio sample rate which causes issues for ffmpeg. The input rate needs to be set for record if used directly with unifi protect.
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ This guide is mainly useful if you are **upgrading from an older version and hav
|
||||
|
||||
If you added your cameras with the wizard, go2rtc is already configured. You can skip straight to [troubleshooting](/troubleshooting/go2rtc). The steps below are for upgrading users with existing cameras that aren't using go2rtc yet, or for anyone who prefers to configure a stream by hand.
|
||||
|
||||
Configure go2rtc to connect to your camera by adding the stream you want to use for live view. Avoid changing any other parts of your config at this step. Note that go2rtc supports [many different stream types](https://github.com/AlexxIT/go2rtc/tree/v1.9.13#module-streams), not just rtsp.
|
||||
Configure go2rtc to connect to your camera by adding the stream you want to use for live view. Avoid changing any other parts of your config at this step. Note that go2rtc supports [many different stream types](https://github.com/AlexxIT/go2rtc/tree/v1.9.14#module-streams), not just rtsp.
|
||||
|
||||
:::tip
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import NavPath from "@site/src/components/NavPath";
|
||||
|
||||
Frigate can restream your video feed as an RTSP feed for other applications such as Home Assistant to utilize it at `rtsp://<frigate_host>:8554/<camera_name>`. Port 8554 must be open. [This allows you to use a video feed for detection in Frigate and Home Assistant live view at the same time without having to make two separate connections to the camera](#reduce-connections-to-camera). The video feed is copied from the original video feed directly to avoid re-encoding. This feed does not include any annotation by Frigate.
|
||||
|
||||
Frigate uses [go2rtc](https://github.com/AlexxIT/go2rtc/tree/v1.9.13) to provide its restream and MSE/WebRTC capabilities. The go2rtc config is hosted at the `go2rtc` in the config, see [go2rtc docs](https://github.com/AlexxIT/go2rtc/tree/v1.9.13#configuration) for more advanced configurations and features.
|
||||
Frigate uses [go2rtc](https://github.com/AlexxIT/go2rtc/tree/v1.9.14) to provide its restream and MSE/WebRTC capabilities. The go2rtc config is hosted at the `go2rtc` in the config, see [go2rtc docs](https://github.com/AlexxIT/go2rtc/tree/v1.9.14#configuration) for more advanced configurations and features.
|
||||
|
||||
:::note
|
||||
|
||||
@@ -236,7 +236,7 @@ Enabling arbitrary exec sources allows execution of arbitrary commands through g
|
||||
|
||||
## Advanced Restream Configurations
|
||||
|
||||
The [exec](https://github.com/AlexxIT/go2rtc/tree/v1.9.13#source-exec) source in go2rtc can be used for custom ffmpeg commands and other applications. An example is below:
|
||||
The [exec](https://github.com/AlexxIT/go2rtc/tree/v1.9.14#source-exec) source in go2rtc can be used for custom ffmpeg commands and other applications. An example is below:
|
||||
|
||||
:::warning
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ The following models are downloaded automatically the first time their associate
|
||||
| [License plate recognition](/configuration/license_plate_recognition) | PaddleOCR (detection, classification, recognition) + YOLOv9 plate detector | GitHub |
|
||||
| [Bird classification](/configuration/bird_classification) | MobileNetV2 bird model + label map | GitHub |
|
||||
| [Custom classification](/configuration/custom_classification/state_classification) (training) | MobileNetV2 ImageNet base weights (via Keras) | Google storage |
|
||||
| [Audio transcription](/configuration/advanced/system) | Whisper or Sherpa-ONNX streaming model | HuggingFace / OpenAI |
|
||||
| [Audio transcription](/configuration/advanced/system) | Whisper or Sherpa-ONNX streaming model | HuggingFace / OpenAI |
|
||||
|
||||
### Hardware-Specific Detector Models
|
||||
|
||||
@@ -91,13 +91,13 @@ See [Frigate+](/integrations/plus) for details.
|
||||
|
||||
When a Generative AI provider is configured, Frigate sends images and prompts to the configured provider for event descriptions, chat, and camera monitoring. Available providers:
|
||||
|
||||
| Provider | Internet Required |
|
||||
| ------------- | ---------------------------------------------------------------- |
|
||||
| OpenAI | Yes, connects to OpenAI API (or custom base URL) |
|
||||
| Google Gemini | Yes, connects to Google Generative AI API |
|
||||
| Azure OpenAI | Yes, connects to your Azure endpoint |
|
||||
| Ollama | Depends: typically local (`localhost:11434`), but can be remote |
|
||||
| llama.cpp | No, runs entirely locally |
|
||||
| Provider | Internet Required |
|
||||
| ------------- | --------------------------------------------------------------- |
|
||||
| OpenAI | Yes, connects to OpenAI API (or custom base URL) |
|
||||
| Google Gemini | Yes, connects to Google Generative AI API |
|
||||
| Azure OpenAI | Yes, connects to your Azure endpoint |
|
||||
| Ollama | Depends: typically local (`localhost:11434`), but can be remote |
|
||||
| llama.cpp | No, runs entirely locally |
|
||||
|
||||
Disable Generative AI by removing the `genai` configuration from your cameras. See [Generative AI](/configuration/genai/genai_config) for details.
|
||||
|
||||
@@ -131,7 +131,7 @@ For [WebRTC live streaming](/configuration/live), Frigate uses STUN for NAT trav
|
||||
|
||||
## Home Assistant Supervisor
|
||||
|
||||
When running as a Home Assistant add-on, the go2rtc startup script queries the local Supervisor API (`http://supervisor/`) to discover the host IP address and WebRTC port. This is a local network call to the Home Assistant host, not an internet connection.
|
||||
When running as a Home Assistant App, the go2rtc startup script queries the local Supervisor API (`http://supervisor/`) to discover the host IP address and WebRTC port. This is a local network call to the Home Assistant host, not an internet connection.
|
||||
|
||||
## What Does NOT Require Internet
|
||||
|
||||
|
||||
@@ -220,7 +220,7 @@ go2rtc:
|
||||
|
||||
`/dev/dri/renderD128` is the usual render node; on a system with more than one GPU you may need `renderD129` (or higher), and the device must be passed into the container (e.g. `devices: - /dev/dri:/dev/dri` in Docker Compose).
|
||||
|
||||
If you use a **different hardware acceleration backend**, you will likely need to specify its device in the same way, using the option that matches that backend instead of `-vaapi_device`. See the [go2rtc FFmpeg source documentation](https://github.com/AlexxIT/go2rtc/tree/v1.9.13#source-ffmpeg) and the upstream report ([go2rtc issue #1984](https://github.com/AlexxIT/go2rtc/issues/1984)) for background and other examples.
|
||||
If you use a **different hardware acceleration backend**, you will likely need to specify its device in the same way, using the option that matches that backend instead of `-vaapi_device`. See the [go2rtc FFmpeg source documentation](https://github.com/AlexxIT/go2rtc/tree/v1.9.14#source-ffmpeg) and the upstream report ([go2rtc issue #1984](https://github.com/AlexxIT/go2rtc/issues/1984)) for background and other examples.
|
||||
|
||||
:::tip
|
||||
|
||||
@@ -230,6 +230,6 @@ If you don't transcode in go2rtc with hardware acceleration, this does not affec
|
||||
|
||||
## Home Assistant and port access
|
||||
|
||||
When running Frigate as a Home Assistant add-on, the go2rtc API (port `1984`), the RTSP restream (port `8554`), and WebRTC (port `8555`) are **disabled and hidden by default**. To use them (for example to reach the go2rtc web interface for troubleshooting, or to open a go2rtc stream externally in an app like VLC), go to <NavPath path="Settings > Add-ons > Frigate > Configuration > Network" />, click **Show disabled ports**, enable the port you need, and save. Use the host's IP address rather than an mDNS name like `homeassistant.local`.
|
||||
When running Frigate as a Home Assistant App, the go2rtc API (port `1984`), the RTSP restream (port `8554`), and WebRTC (port `8555`) are **disabled and hidden by default**. To use them (for example to reach the go2rtc web interface for troubleshooting, or to open a go2rtc stream externally in an app like VLC), go to <NavPath path="Settings > Apps > Frigate > Configuration > Network" />, click **Show disabled ports**, enable the port you need, and save. Use the host's IP address rather than an mDNS name like `homeassistant.local`.
|
||||
|
||||
If live view works in the Frigate UI but not in Home Assistant, the most common cause is the go2rtc stream name not matching the camera name: name the primary go2rtc stream exactly like the camera, or add a `live -> streams` mapping, so the integration can resolve the restream.
|
||||
|
||||
+1
-1
@@ -132,7 +132,7 @@ const sidebars: SidebarsConfig = {
|
||||
{
|
||||
type: "link",
|
||||
label: "Go2RTC Configuration Reference",
|
||||
href: "https://github.com/AlexxIT/go2rtc/tree/v1.9.13#configuration",
|
||||
href: "https://github.com/AlexxIT/go2rtc/tree/v1.9.14#configuration",
|
||||
} as PropSidebarItemLink,
|
||||
],
|
||||
},
|
||||
|
||||
Vendored
+7
-7
@@ -2013,7 +2013,7 @@ paths:
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
default: utc
|
||||
default: UTC
|
||||
title: Timezone
|
||||
responses:
|
||||
'200':
|
||||
@@ -3055,7 +3055,7 @@ paths:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: 'null'
|
||||
default: utc
|
||||
default: UTC
|
||||
title: Timezone
|
||||
responses:
|
||||
'200':
|
||||
@@ -4153,7 +4153,7 @@ paths:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: 'null'
|
||||
default: utc
|
||||
default: UTC
|
||||
title: Timezone
|
||||
responses:
|
||||
'200':
|
||||
@@ -4405,7 +4405,7 @@ paths:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: 'null'
|
||||
default: utc
|
||||
default: UTC
|
||||
title: Timezone
|
||||
- name: min_score
|
||||
in: query
|
||||
@@ -4485,7 +4485,7 @@ paths:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: 'null'
|
||||
default: utc
|
||||
default: UTC
|
||||
title: Timezone
|
||||
- name: has_clip
|
||||
in: query
|
||||
@@ -6780,7 +6780,7 @@ paths:
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
default: utc
|
||||
default: UTC
|
||||
title: Timezone
|
||||
- name: cameras
|
||||
in: query
|
||||
@@ -6830,7 +6830,7 @@ paths:
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
default: utc
|
||||
default: UTC
|
||||
title: Timezone
|
||||
responses:
|
||||
'200':
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,7 +3,7 @@ from pydantic.json_schema import SkipJsonSchema
|
||||
|
||||
|
||||
class MediaRecordingsSummaryQueryParams(BaseModel):
|
||||
timezone: str = "utc"
|
||||
timezone: str = "UTC"
|
||||
cameras: str | None = "all"
|
||||
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
@@ -75,9 +76,20 @@ def create_fastapi_app(
|
||||
profile_manager: ProfileManager | None = None,
|
||||
enforce_default_admin: bool = True,
|
||||
):
|
||||
@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
|
||||
@@ -113,15 +125,6 @@ def create_fastapi_app(
|
||||
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
|
||||
|
||||
@@ -11,10 +11,10 @@ from datetime import UTC, datetime, timedelta
|
||||
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
|
||||
@@ -53,6 +53,7 @@ from frigate.util.file import (
|
||||
)
|
||||
from frigate.util.image import get_image_from_recording, get_image_quality_params
|
||||
from frigate.util.media import get_keyframe_before
|
||||
from frigate.util.time import get_normalized_tz_name
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -713,7 +714,9 @@ 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(get_normalized_tz_name(tz_name.replace(",", "/")))
|
||||
).utcoffset()
|
||||
)
|
||||
end_date = start_date + timedelta(hours=1) - timedelta(milliseconds=1)
|
||||
start_ts = start_date.timestamp()
|
||||
|
||||
@@ -5,8 +5,8 @@ import logging
|
||||
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
|
||||
|
||||
@@ -22,6 +22,7 @@ from frigate.api.defs.response.preview_response import (
|
||||
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(pytz.timezone(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()
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -12,8 +12,8 @@ import threading
|
||||
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
|
||||
@@ -31,7 +31,7 @@ from frigate.ffmpeg_presets import (
|
||||
)
|
||||
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,8 +371,8 @@ class RecordingExporter(threading.Thread):
|
||||
tz_name = self.config.ui.timezone
|
||||
if tz_name:
|
||||
try:
|
||||
tz = pytz.timezone(tz_name)
|
||||
except pytz.UnknownTimeZoneError:
|
||||
tz = ZoneInfo(get_normalized_tz_name(tz_name))
|
||||
except (ValueError, ZoneInfoNotFoundError):
|
||||
tz = None
|
||||
if tz is not None:
|
||||
return datetime.datetime.fromtimestamp(timestamp, tz=tz).strftime(
|
||||
@@ -533,8 +533,8 @@ class RecordingExporter(threading.Thread):
|
||||
tz: datetime.tzinfo | None = None
|
||||
if tz_name:
|
||||
try:
|
||||
tz = pytz.timezone(tz_name)
|
||||
except pytz.UnknownTimeZoneError:
|
||||
tz = ZoneInfo(get_normalized_tz_name(tz_name))
|
||||
except (ValueError, ZoneInfoNotFoundError):
|
||||
tz = None
|
||||
if tz is None:
|
||||
tz = datetime.UTC
|
||||
|
||||
@@ -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 @@ class TestHttpMedia(BaseTestHttp):
|
||||
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 @@ class TestHttpMedia(BaseTestHttp):
|
||||
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 @@ class TestHttpMedia(BaseTestHttp):
|
||||
"""
|
||||
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 @@ class TestHttpMedia(BaseTestHttp):
|
||||
"""
|
||||
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 @@ class TestHttpMedia(BaseTestHttp):
|
||||
|
||||
# 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 @@ class TestHttpMedia(BaseTestHttp):
|
||||
"""
|
||||
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
|
||||
|
||||
@@ -250,7 +250,7 @@ class TestHttpReview(BaseTestHttp):
|
||||
"cameras": "front_door",
|
||||
"labels": "all",
|
||||
"zones": "all",
|
||||
"timezone": "utc",
|
||||
"timezone": "UTC",
|
||||
}
|
||||
response = client.get("/review/summary", params=params)
|
||||
assert response.status_code == 200
|
||||
|
||||
@@ -2,7 +2,7 @@ import unittest
|
||||
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 @@ class TestLocalObjectDetector(unittest.TestCase):
|
||||
"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 @@ class TestLocalObjectDetector(unittest.TestCase):
|
||||
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 @@ class TestLocalObjectDetector(unittest.TestCase):
|
||||
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 @@ class TestLocalObjectDetector(unittest.TestCase):
|
||||
"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,
|
||||
|
||||
@@ -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")
|
||||
+23
-9
@@ -2,17 +2,31 @@
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
from zoneinfo import ZoneInfoNotFoundError
|
||||
from typing import Final
|
||||
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError, available_timezones
|
||||
|
||||
import pytz
|
||||
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:
|
||||
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(pytz.timezone(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)
|
||||
@@ -25,7 +39,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 +59,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 +73,8 @@ def get_dst_transitions(
|
||||
continuous periods with the same UTC offset
|
||||
"""
|
||||
try:
|
||||
tz = pytz.timezone(tz_name)
|
||||
except pytz.UnknownTimeZoneError:
|
||||
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)]
|
||||
|
||||
@@ -68,14 +82,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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user