mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-08-10 21:01:10 +03:00
Merge 5c38f6ef96 into d02a1156b7
This commit is contained in:
@@ -25,7 +25,6 @@ peewee_migrate == 1.14.*
|
|||||||
psutil == 7.1.*
|
psutil == 7.1.*
|
||||||
pydantic == 2.10.*
|
pydantic == 2.10.*
|
||||||
git+https://github.com/fbcotter/py3nvml#egg=py3nvml
|
git+https://github.com/fbcotter/py3nvml#egg=py3nvml
|
||||||
pytz == 2025.*
|
|
||||||
pyzmq == 26.2.*
|
pyzmq == 26.2.*
|
||||||
ruamel.yaml == 0.18.*
|
ruamel.yaml == 0.18.*
|
||||||
tzlocal == 5.2
|
tzlocal == 5.2
|
||||||
|
|||||||
Vendored
+7
-7
@@ -2013,7 +2013,7 @@ paths:
|
|||||||
required: false
|
required: false
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
default: utc
|
default: UTC
|
||||||
title: Timezone
|
title: Timezone
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
@@ -3055,7 +3055,7 @@ paths:
|
|||||||
anyOf:
|
anyOf:
|
||||||
- type: string
|
- type: string
|
||||||
- type: 'null'
|
- type: 'null'
|
||||||
default: utc
|
default: UTC
|
||||||
title: Timezone
|
title: Timezone
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
@@ -4153,7 +4153,7 @@ paths:
|
|||||||
anyOf:
|
anyOf:
|
||||||
- type: string
|
- type: string
|
||||||
- type: 'null'
|
- type: 'null'
|
||||||
default: utc
|
default: UTC
|
||||||
title: Timezone
|
title: Timezone
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
@@ -4405,7 +4405,7 @@ paths:
|
|||||||
anyOf:
|
anyOf:
|
||||||
- type: string
|
- type: string
|
||||||
- type: 'null'
|
- type: 'null'
|
||||||
default: utc
|
default: UTC
|
||||||
title: Timezone
|
title: Timezone
|
||||||
- name: min_score
|
- name: min_score
|
||||||
in: query
|
in: query
|
||||||
@@ -4485,7 +4485,7 @@ paths:
|
|||||||
anyOf:
|
anyOf:
|
||||||
- type: string
|
- type: string
|
||||||
- type: 'null'
|
- type: 'null'
|
||||||
default: utc
|
default: UTC
|
||||||
title: Timezone
|
title: Timezone
|
||||||
- name: has_clip
|
- name: has_clip
|
||||||
in: query
|
in: query
|
||||||
@@ -6780,7 +6780,7 @@ paths:
|
|||||||
required: false
|
required: false
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
default: utc
|
default: UTC
|
||||||
title: Timezone
|
title: Timezone
|
||||||
- name: cameras
|
- name: cameras
|
||||||
in: query
|
in: query
|
||||||
@@ -6830,7 +6830,7 @@ paths:
|
|||||||
required: false
|
required: false
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
default: utc
|
default: UTC
|
||||||
title: Timezone
|
title: Timezone
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
|
|||||||
@@ -7,4 +7,4 @@ class AppTimelineHourlyQueryParameters(BaseModel):
|
|||||||
after: float | None = None
|
after: float | None = None
|
||||||
before: float | None = None
|
before: float | None = None
|
||||||
limit: int | None = 200
|
limit: int | None = 200
|
||||||
timezone: str | None = "utc"
|
timezone: str | None = "UTC"
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class EventsQueryParams(BaseModel):
|
|||||||
max_length: float | None = None
|
max_length: float | None = None
|
||||||
event_id: str | None = None
|
event_id: str | None = None
|
||||||
sort: str | None = None
|
sort: str | None = None
|
||||||
timezone: str | None = "utc"
|
timezone: str | None = "UTC"
|
||||||
|
|
||||||
|
|
||||||
class EventsSearchQueryParams(BaseModel):
|
class EventsSearchQueryParams(BaseModel):
|
||||||
@@ -66,7 +66,7 @@ class EventsSearchQueryParams(BaseModel):
|
|||||||
has_clip: bool | None = None
|
has_clip: bool | None = None
|
||||||
has_snapshot: bool | None = None
|
has_snapshot: bool | None = None
|
||||||
is_submitted: bool | None = None
|
is_submitted: bool | None = None
|
||||||
timezone: str | None = "utc"
|
timezone: str | None = "UTC"
|
||||||
min_score: float | None = None
|
min_score: float | None = None
|
||||||
max_score: float | None = None
|
max_score: float | None = None
|
||||||
min_speed: float | None = None
|
min_speed: float | None = None
|
||||||
@@ -76,6 +76,6 @@ class EventsSearchQueryParams(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class EventsSummaryQueryParams(BaseModel):
|
class EventsSummaryQueryParams(BaseModel):
|
||||||
timezone: str | None = "utc"
|
timezone: str | None = "UTC"
|
||||||
has_clip: int | None = None
|
has_clip: int | None = None
|
||||||
has_snapshot: int | None = None
|
has_snapshot: int | None = None
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from pydantic.json_schema import SkipJsonSchema
|
|||||||
|
|
||||||
|
|
||||||
class MediaRecordingsSummaryQueryParams(BaseModel):
|
class MediaRecordingsSummaryQueryParams(BaseModel):
|
||||||
timezone: str = "utc"
|
timezone: str = "UTC"
|
||||||
cameras: str | None = "all"
|
cameras: str | None = "all"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class ReviewSummaryQueryParams(BaseModel):
|
|||||||
cameras: str = "all"
|
cameras: str = "all"
|
||||||
labels: str = "all"
|
labels: str = "all"
|
||||||
zones: str = "all"
|
zones: str = "all"
|
||||||
timezone: str = "utc"
|
timezone: str = "UTC"
|
||||||
|
|
||||||
|
|
||||||
class ReviewActivityMotionQueryParams(BaseModel):
|
class ReviewActivityMotionQueryParams(BaseModel):
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
from fastapi import Depends, FastAPI, Request
|
from fastapi import Depends, FastAPI, Request
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
@@ -75,9 +76,20 @@ def create_fastapi_app(
|
|||||||
profile_manager: ProfileManager | None = None,
|
profile_manager: ProfileManager | None = None,
|
||||||
enforce_default_admin: bool = True,
|
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")
|
logger.info("Starting FastAPI app")
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
debug=False,
|
debug=False,
|
||||||
|
lifespan=lifespan,
|
||||||
swagger_ui_parameters={"apisSorter": "alpha", "operationsSorter": "alpha"},
|
swagger_ui_parameters={"apisSorter": "alpha", "operationsSorter": "alpha"},
|
||||||
dependencies=[Depends(require_admin_by_default())]
|
dependencies=[Depends(require_admin_by_default())]
|
||||||
if enforce_default_admin
|
if enforce_default_admin
|
||||||
@@ -113,15 +125,6 @@ def create_fastapi_app(
|
|||||||
database.close()
|
database.close()
|
||||||
return response
|
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)
|
# Rate limiter (used for login endpoint)
|
||||||
if frigate_config.auth.failed_login_rate_limit is None:
|
if frigate_config.auth.failed_login_rate_limit is None:
|
||||||
limiter.enabled = False
|
limiter.enabled = False
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ from datetime import UTC, datetime, timedelta
|
|||||||
from pathlib import Path as FilePath
|
from pathlib import Path as FilePath
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pytz
|
|
||||||
from fastapi import APIRouter, Depends, Path, Query, Request, Response
|
from fastapi import APIRouter, Depends, Path, Query, Request, Response
|
||||||
from fastapi.responses import FileResponse, JSONResponse, StreamingResponse
|
from fastapi.responses import FileResponse, JSONResponse, StreamingResponse
|
||||||
from pathvalidate import sanitize_filename
|
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.image import get_image_from_recording, get_image_quality_params
|
||||||
from frigate.util.media import get_keyframe_before
|
from frigate.util.media import get_keyframe_before
|
||||||
|
from frigate.util.time import get_normalized_tz_name
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -713,7 +714,9 @@ async def vod_hour(
|
|||||||
parts = year_month.split("-")
|
parts = year_month.split("-")
|
||||||
start_date = (
|
start_date = (
|
||||||
datetime(int(parts[0]), int(parts[1]), day, hour, tzinfo=UTC)
|
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)
|
end_date = start_date + timedelta(hours=1) - timedelta(milliseconds=1)
|
||||||
start_ts = start_date.timestamp()
|
start_ts = start_date.timestamp()
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import threading
|
import threading
|
||||||
from datetime import UTC, datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
import pytz
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from fastapi.responses import JSONResponse
|
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.api.defs.tags import Tags
|
||||||
from frigate.const import BASE_DIR, CACHE_DIR, PREVIEW_FRAME_TYPE
|
from frigate.const import BASE_DIR, CACHE_DIR, PREVIEW_FRAME_TYPE
|
||||||
from frigate.models import Previews
|
from frigate.models import Previews
|
||||||
|
from frigate.util.time import get_normalized_tz_name
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -126,7 +127,9 @@ def preview_hour(
|
|||||||
parts = year_month.split("-")
|
parts = year_month.split("-")
|
||||||
start_date = (
|
start_date = (
|
||||||
datetime(int(parts[0]), int(parts[1]), int(day), int(hour), tzinfo=UTC)
|
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)
|
end_date = start_date + timedelta(hours=1) - timedelta(milliseconds=1)
|
||||||
start_ts = start_date.timestamp()
|
start_ts = start_date.timestamp()
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ def all_recordings_summary(
|
|||||||
@router.get(
|
@router.get(
|
||||||
"/{camera_name}/recordings/summary", dependencies=[Depends(require_camera_access)]
|
"/{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"""
|
"""Returns hourly summary for recordings of given camera"""
|
||||||
|
|
||||||
time_range_query = (
|
time_range_query = (
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import threading
|
|||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
||||||
|
|
||||||
import pytz # type: ignore[import-untyped]
|
|
||||||
from peewee import DoesNotExist
|
from peewee import DoesNotExist
|
||||||
|
|
||||||
from frigate.config import FfmpegConfig, FrigateConfig
|
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.models import Export, Previews, Recordings, ReviewSegment
|
||||||
from frigate.util.ffmpeg import run_ffmpeg_with_progress
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -371,8 +371,8 @@ class RecordingExporter(threading.Thread):
|
|||||||
tz_name = self.config.ui.timezone
|
tz_name = self.config.ui.timezone
|
||||||
if tz_name:
|
if tz_name:
|
||||||
try:
|
try:
|
||||||
tz = pytz.timezone(tz_name)
|
tz = ZoneInfo(get_normalized_tz_name(tz_name))
|
||||||
except pytz.UnknownTimeZoneError:
|
except (ValueError, ZoneInfoNotFoundError):
|
||||||
tz = None
|
tz = None
|
||||||
if tz is not None:
|
if tz is not None:
|
||||||
return datetime.datetime.fromtimestamp(timestamp, tz=tz).strftime(
|
return datetime.datetime.fromtimestamp(timestamp, tz=tz).strftime(
|
||||||
@@ -533,8 +533,8 @@ class RecordingExporter(threading.Thread):
|
|||||||
tz: datetime.tzinfo | None = None
|
tz: datetime.tzinfo | None = None
|
||||||
if tz_name:
|
if tz_name:
|
||||||
try:
|
try:
|
||||||
tz = pytz.timezone(tz_name)
|
tz = ZoneInfo(get_normalized_tz_name(tz_name))
|
||||||
except pytz.UnknownTimeZoneError:
|
except (ValueError, ZoneInfoNotFoundError):
|
||||||
tz = None
|
tz = None
|
||||||
if tz is None:
|
if tz is None:
|
||||||
tz = datetime.UTC
|
tz = datetime.UTC
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
"""Unit tests for recordings/media API endpoints."""
|
"""Unit tests for recordings/media API endpoints."""
|
||||||
|
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
import pytz
|
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
|
|
||||||
from frigate.api.auth import get_allowed_cameras_for_filter, get_current_user
|
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
|
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)
|
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, 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, 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, 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:
|
with AuthTestClient(self.app) as client:
|
||||||
# Insert recordings for each day
|
# 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
|
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)
|
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)
|
# 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)
|
# 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 = datetime(2024, 11, 3, 12, 0, 0, tzinfo=tz).timestamp()
|
||||||
nov_3_noon = tz.localize(
|
|
||||||
datetime(2024, 11, 3, 12, 0, 0), is_dst=False
|
|
||||||
).timestamp()
|
|
||||||
|
|
||||||
# November 4, 2024 at 12:00 PM EST (after DST)
|
# 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:
|
with AuthTestClient(self.app) as client:
|
||||||
# Insert recordings for each day
|
# Insert recordings for each day
|
||||||
@@ -197,13 +194,13 @@ class TestHttpMedia(BaseTestHttp):
|
|||||||
"""
|
"""
|
||||||
Test recordings summary with multiple cameras across DST boundary.
|
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, 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, 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:
|
with AuthTestClient(self.app) as client:
|
||||||
# Override allowed cameras for this test to include both
|
# 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.
|
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)
|
# March 10, 2024 at 1:00 AM EST (1 hour before DST transition)
|
||||||
# At 2:00 AM, clocks jump to 3:00 AM
|
# 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)
|
# 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
|
# 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:
|
with AuthTestClient(self.app) as client:
|
||||||
Recordings.insert(
|
Recordings.insert(
|
||||||
@@ -334,7 +331,7 @@ class TestHttpMedia(BaseTestHttp):
|
|||||||
|
|
||||||
# Test with UTC timezone
|
# Test with UTC timezone
|
||||||
response = client.get(
|
response = client.get(
|
||||||
"/recordings/summary", params={"timezone": "utc", "cameras": "all"}
|
"/recordings/summary", params={"timezone": "UTC", "cameras": "all"}
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
@@ -365,8 +362,8 @@ class TestHttpMedia(BaseTestHttp):
|
|||||||
"""
|
"""
|
||||||
Test recordings summary filtered to a single camera.
|
Test recordings summary filtered to a single camera.
|
||||||
"""
|
"""
|
||||||
tz = pytz.timezone("America/New_York")
|
tz = ZoneInfo("America/New_York")
|
||||||
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()
|
||||||
|
|
||||||
with AuthTestClient(self.app) as client:
|
with AuthTestClient(self.app) as client:
|
||||||
# Insert recordings for both cameras
|
# Insert recordings for both cameras
|
||||||
|
|||||||
@@ -250,7 +250,7 @@ class TestHttpReview(BaseTestHttp):
|
|||||||
"cameras": "front_door",
|
"cameras": "front_door",
|
||||||
"labels": "all",
|
"labels": "all",
|
||||||
"zones": "all",
|
"zones": "all",
|
||||||
"timezone": "utc",
|
"timezone": "UTC",
|
||||||
}
|
}
|
||||||
response = client.get("/review/summary", params=params)
|
response = client.get("/review/summary", params=params)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import unittest
|
|||||||
from unittest.mock import Mock, patch
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from pydantic import parse_obj_as
|
from pydantic import TypeAdapter
|
||||||
|
|
||||||
import frigate.detectors as detectors
|
import frigate.detectors as detectors
|
||||||
import frigate.object_detection.base
|
import frigate.object_detection.base
|
||||||
@@ -19,8 +19,8 @@ class TestLocalObjectDetector(unittest.TestCase):
|
|||||||
"frigate.detectors.api_types",
|
"frigate.detectors.api_types",
|
||||||
{det_type: Mock() for det_type in DetectorTypeEnum},
|
{det_type: Mock() for det_type in DetectorTypeEnum},
|
||||||
):
|
):
|
||||||
test_cfg = parse_obj_as(
|
test_cfg = TypeAdapter(DetectorConfig).validate_python(
|
||||||
DetectorConfig, ({"type": det_type, "model": {}})
|
{"type": det_type, "model": {}}
|
||||||
)
|
)
|
||||||
test_cfg.model.path = "/test/modelpath"
|
test_cfg.model.path = "/test/modelpath"
|
||||||
test_obj = frigate.object_detection.base.LocalObjectDetector(
|
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_DATA = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
|
||||||
TEST_DETECT_RESULT = np.ndarray([1, 2, 4, 8, 16, 32])
|
TEST_DETECT_RESULT = np.ndarray([1, 2, 4, 8, 16, 32])
|
||||||
test_obj_detect = frigate.object_detection.base.LocalObjectDetector(
|
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
|
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_DATA = np.zeros((1, 32, 32, 3), np.uint8)
|
||||||
TEST_DETECT_RESULT = np.ndarray([1, 2, 4, 8, 16, 32])
|
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_cfg.model.input_tensor = InputTensorEnum.nchw
|
||||||
|
|
||||||
test_obj_detect = frigate.object_detection.base.LocalObjectDetector(
|
test_obj_detect = frigate.object_detection.base.LocalObjectDetector(
|
||||||
@@ -116,7 +120,9 @@ class TestLocalObjectDetector(unittest.TestCase):
|
|||||||
"label-5",
|
"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_cfg.model = ModelConfig()
|
||||||
test_obj_detect = frigate.object_detection.base.LocalObjectDetector(
|
test_obj_detect = frigate.object_detection.base.LocalObjectDetector(
|
||||||
detector_config=test_cfg,
|
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 datetime
|
||||||
import logging
|
import logging
|
||||||
from zoneinfo import ZoneInfoNotFoundError
|
from typing import Final
|
||||||
|
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError, available_timezones
|
||||||
|
|
||||||
import pytz
|
|
||||||
from tzlocal import get_localzone
|
from tzlocal import get_localzone
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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]:
|
def get_tz_modifiers(tz_name: str) -> tuple[str, str, float]:
|
||||||
seconds_offset = (
|
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)
|
hours_offset = int(seconds_offset / 60 / 60)
|
||||||
minutes_offset = int(seconds_offset / 60 - hours_offset * 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."""
|
"""Returns the datetime of the following day at 2am."""
|
||||||
try:
|
try:
|
||||||
tomorrow = datetime.datetime.now(get_localzone()) + datetime.timedelta(days=1)
|
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)
|
tomorrow = datetime.datetime.now(datetime.UTC) + datetime.timedelta(days=1)
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Using utc for maintenance due to missing or incorrect timezone set"
|
"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(
|
def get_dst_transitions(
|
||||||
tz_name: str, start_time: float, end_time: float
|
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.
|
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
|
continuous periods with the same UTC offset
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
tz = pytz.timezone(tz_name)
|
tz = ZoneInfo(get_normalized_tz_name(tz_name))
|
||||||
except pytz.UnknownTimeZoneError:
|
except (ValueError, ZoneInfoNotFoundError):
|
||||||
# If timezone is invalid, return single period with no offset
|
# If timezone is invalid, return single period with no offset
|
||||||
return [(start_time, end_time, 0)]
|
return [(start_time, end_time, 0)]
|
||||||
|
|
||||||
@@ -68,14 +82,14 @@ def get_dst_transitions(
|
|||||||
current = start_time
|
current = start_time
|
||||||
|
|
||||||
# Get initial offset
|
# 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)
|
local_dt = dt.astimezone(tz)
|
||||||
prev_offset = local_dt.utcoffset().total_seconds()
|
prev_offset = local_dt.utcoffset().total_seconds()
|
||||||
period_start = start_time
|
period_start = start_time
|
||||||
|
|
||||||
# Check each day for offset changes
|
# Check each day for offset changes
|
||||||
while current <= end_time:
|
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)
|
local_dt = dt.astimezone(tz)
|
||||||
current_offset = local_dt.utcoffset().total_seconds()
|
current_offset = local_dt.utcoffset().total_seconds()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user