Migrate pydantic to V2 (#10142)

* Run pydantic migration tool

* Finish removing deprecated functions

* Formatting

* Fix movement weights type

* Fix movement weight test

* Fix config checks

* formatting

* fix typing

* formatting

* Fix

* Fix serialization issues

* Formatting

* fix model namespace warnings

* Update formatting

* Format go2rtc file

* Cleanup migrations

* Fix warnings

* Don't include null values in config json

* Formatting

* Fix test

---------

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
This commit is contained in:
Nicolas Mowen 2024-02-29 16:10:13 -07:00 committed by GitHub
parent a1424bad6c
commit cb30450060
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 209 additions and 149 deletions

View File

@ -12,7 +12,7 @@ pandas == 2.1.4
peewee == 3.17.* peewee == 3.17.*
peewee_migrate == 1.12.* peewee_migrate == 1.12.*
psutil == 5.9.* psutil == 5.9.*
pydantic == 1.10.* pydantic == 2.6.*
git+https://github.com/fbcotter/py3nvml#egg=py3nvml git+https://github.com/fbcotter/py3nvml#egg=py3nvml
PyYAML == 6.0.* PyYAML == 6.0.*
pytz == 2023.3.post1 pytz == 2023.3.post1

View File

@ -109,9 +109,9 @@ if int(os.environ["LIBAVFORMAT_VERSION_MAJOR"]) < 59:
"rtsp": "-fflags nobuffer -flags low_delay -stimeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_transport tcp -i {input}" "rtsp": "-fflags nobuffer -flags low_delay -stimeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_transport tcp -i {input}"
} }
elif go2rtc_config["ffmpeg"].get("rtsp") is None: elif go2rtc_config["ffmpeg"].get("rtsp") is None:
go2rtc_config["ffmpeg"][ go2rtc_config["ffmpeg"]["rtsp"] = (
"rtsp" "-fflags nobuffer -flags low_delay -stimeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_transport tcp -i {input}"
] = "-fflags nobuffer -flags low_delay -stimeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_transport tcp -i {input}" )
# add hardware acceleration presets for rockchip devices # add hardware acceleration presets for rockchip devices
# may be removed if frigate uses a go2rtc version that includes these presets # may be removed if frigate uses a go2rtc version that includes these presets

View File

@ -6,11 +6,19 @@ import logging
import os import os
from enum import Enum from enum import Enum
from pathlib import Path from pathlib import Path
from typing import Dict, List, Optional, Tuple, Union from typing import Any, Dict, List, Optional, Tuple, Union
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import numpy as np import numpy as np
from pydantic import BaseModel, Extra, Field, parse_obj_as, validator from pydantic import (
BaseModel,
ConfigDict,
Field,
TypeAdapter,
ValidationInfo,
field_serializer,
field_validator,
)
from pydantic.fields import PrivateAttr from pydantic.fields import PrivateAttr
from frigate.const import ( from frigate.const import (
@ -66,8 +74,7 @@ DEFAULT_TIME_LAPSE_FFMPEG_ARGS = "-vf setpts=0.04*PTS -r 30"
class FrigateBaseModel(BaseModel): class FrigateBaseModel(BaseModel):
class Config: model_config = ConfigDict(extra="forbid", protected_namespaces=())
extra = Extra.forbid
class LiveModeEnum(str, Enum): class LiveModeEnum(str, Enum):
@ -93,7 +100,7 @@ class UIConfig(FrigateBaseModel):
live_mode: LiveModeEnum = Field( live_mode: LiveModeEnum = Field(
default=LiveModeEnum.mse, title="Default Live Mode." default=LiveModeEnum.mse, title="Default Live Mode."
) )
timezone: Optional[str] = Field(title="Override UI timezone.") timezone: Optional[str] = Field(default=None, title="Override UI timezone.")
use_experimental: bool = Field(default=False, title="Experimental UI") use_experimental: bool = Field(default=False, title="Experimental UI")
time_format: TimeFormatEnum = Field( time_format: TimeFormatEnum = Field(
default=TimeFormatEnum.browser, title="Override UI time format." default=TimeFormatEnum.browser, title="Override UI time format."
@ -135,16 +142,17 @@ class MqttConfig(FrigateBaseModel):
topic_prefix: str = Field(default="frigate", title="MQTT Topic Prefix") topic_prefix: str = Field(default="frigate", title="MQTT Topic Prefix")
client_id: str = Field(default="frigate", title="MQTT Client ID") client_id: str = Field(default="frigate", title="MQTT Client ID")
stats_interval: int = Field(default=60, title="MQTT Camera Stats Interval") stats_interval: int = Field(default=60, title="MQTT Camera Stats Interval")
user: Optional[str] = Field(title="MQTT Username") user: Optional[str] = Field(None, title="MQTT Username")
password: Optional[str] = Field(title="MQTT Password") password: Optional[str] = Field(None, title="MQTT Password", validate_default=True)
tls_ca_certs: Optional[str] = Field(title="MQTT TLS CA Certificates") tls_ca_certs: Optional[str] = Field(None, title="MQTT TLS CA Certificates")
tls_client_cert: Optional[str] = Field(title="MQTT TLS Client Certificate") tls_client_cert: Optional[str] = Field(None, title="MQTT TLS Client Certificate")
tls_client_key: Optional[str] = Field(title="MQTT TLS Client Key") tls_client_key: Optional[str] = Field(None, title="MQTT TLS Client Key")
tls_insecure: Optional[bool] = Field(title="MQTT TLS Insecure") tls_insecure: Optional[bool] = Field(None, title="MQTT TLS Insecure")
@validator("password", pre=True, always=True) @field_validator("password")
def validate_password(cls, v, values): def user_requires_pass(cls, v, info: ValidationInfo):
if (v is None) != (values["user"] is None): print(f"doing a check where {v} is None and {info.data['user']} is None")
if (v is None) != (info.data["user"] is None):
raise ValueError("Password must be provided with username.") raise ValueError("Password must be provided with username.")
return v return v
@ -186,18 +194,19 @@ class PtzAutotrackConfig(FrigateBaseModel):
title="Internal value used for PTZ movements based on the speed of your camera's motor.", title="Internal value used for PTZ movements based on the speed of your camera's motor.",
) )
enabled_in_config: Optional[bool] = Field( enabled_in_config: Optional[bool] = Field(
title="Keep track of original state of autotracking." None, title="Keep track of original state of autotracking."
) )
@validator("movement_weights", pre=True) @field_validator("movement_weights", mode="before")
@classmethod
def validate_weights(cls, v): def validate_weights(cls, v):
if v is None: if v is None:
return None return None
if isinstance(v, str): if isinstance(v, str):
weights = list(map(float, v.split(","))) weights = list(map(str, map(float, v.split(","))))
elif isinstance(v, list): elif isinstance(v, list):
weights = [float(val) for val in v] weights = [str(float(val)) for val in v]
else: else:
raise ValueError("Invalid type for movement_weights") raise ValueError("Invalid type for movement_weights")
@ -210,8 +219,8 @@ class PtzAutotrackConfig(FrigateBaseModel):
class OnvifConfig(FrigateBaseModel): class OnvifConfig(FrigateBaseModel):
host: str = Field(default="", title="Onvif Host") host: str = Field(default="", title="Onvif Host")
port: int = Field(default=8000, title="Onvif Port") port: int = Field(default=8000, title="Onvif Port")
user: Optional[str] = Field(title="Onvif Username") user: Optional[str] = Field(None, title="Onvif Username")
password: Optional[str] = Field(title="Onvif Password") password: Optional[str] = Field(None, title="Onvif Password")
autotracking: PtzAutotrackConfig = Field( autotracking: PtzAutotrackConfig = Field(
default_factory=PtzAutotrackConfig, default_factory=PtzAutotrackConfig,
title="PTZ auto tracking config.", title="PTZ auto tracking config.",
@ -242,6 +251,7 @@ class EventsConfig(FrigateBaseModel):
title="List of required zones to be entered in order to save the event.", title="List of required zones to be entered in order to save the event.",
) )
objects: Optional[List[str]] = Field( objects: Optional[List[str]] = Field(
None,
title="List of objects to be detected in order to save the event.", title="List of objects to be detected in order to save the event.",
) )
retain: RetainConfig = Field( retain: RetainConfig = Field(
@ -296,7 +306,7 @@ class RecordConfig(FrigateBaseModel):
default_factory=RecordPreviewConfig, title="Recording Preview Config" default_factory=RecordPreviewConfig, title="Recording Preview Config"
) )
enabled_in_config: Optional[bool] = Field( enabled_in_config: Optional[bool] = Field(
title="Keep track of original state of recording." None, title="Keep track of original state of recording."
) )
@ -324,8 +334,17 @@ class MotionConfig(FrigateBaseModel):
title="Delay for updating MQTT with no motion detected.", title="Delay for updating MQTT with no motion detected.",
) )
enabled_in_config: Optional[bool] = Field( enabled_in_config: Optional[bool] = Field(
title="Keep track of original state of motion detection." None, title="Keep track of original state of motion detection."
) )
raw_mask: Union[str, List[str]] = ""
@field_serializer("mask", when_used="json")
def serialize_mask(self, value: Any, info):
return self.raw_mask
@field_serializer("raw_mask", when_used="json")
def serialize_raw_mask(self, value: Any, info):
return None
class RuntimeMotionConfig(MotionConfig): class RuntimeMotionConfig(MotionConfig):
@ -348,19 +367,25 @@ class RuntimeMotionConfig(MotionConfig):
super().__init__(**config) super().__init__(**config)
def dict(self, **kwargs): def dict(self, **kwargs):
ret = super().dict(**kwargs) ret = super().model_dump(**kwargs)
if "mask" in ret: if "mask" in ret:
ret["mask"] = ret["raw_mask"] ret["mask"] = ret["raw_mask"]
ret.pop("raw_mask") ret.pop("raw_mask")
return ret return ret
class Config: @field_serializer("mask", when_used="json")
arbitrary_types_allowed = True def serialize_mask(self, value: Any, info):
extra = Extra.ignore return self.raw_mask
@field_serializer("raw_mask", when_used="json")
def serialize_raw_mask(self, value: Any, info):
return None
model_config = ConfigDict(arbitrary_types_allowed=True, extra="ignore")
class StationaryMaxFramesConfig(FrigateBaseModel): class StationaryMaxFramesConfig(FrigateBaseModel):
default: Optional[int] = Field(title="Default max frames.", ge=1) default: Optional[int] = Field(None, title="Default max frames.", ge=1)
objects: Dict[str, int] = Field( objects: Dict[str, int] = Field(
default_factory=dict, title="Object specific max frames." default_factory=dict, title="Object specific max frames."
) )
@ -368,10 +393,12 @@ class StationaryMaxFramesConfig(FrigateBaseModel):
class StationaryConfig(FrigateBaseModel): class StationaryConfig(FrigateBaseModel):
interval: Optional[int] = Field( interval: Optional[int] = Field(
None,
title="Frame interval for checking stationary objects.", title="Frame interval for checking stationary objects.",
gt=0, gt=0,
) )
threshold: Optional[int] = Field( threshold: Optional[int] = Field(
None,
title="Number of frames without a position change for an object to be considered stationary", title="Number of frames without a position change for an object to be considered stationary",
ge=1, ge=1,
) )
@ -382,17 +409,21 @@ class StationaryConfig(FrigateBaseModel):
class DetectConfig(FrigateBaseModel): class DetectConfig(FrigateBaseModel):
height: Optional[int] = Field(title="Height of the stream for the detect role.") height: Optional[int] = Field(
width: Optional[int] = Field(title="Width of the stream for the detect role.") None, title="Height of the stream for the detect role."
)
width: Optional[int] = Field(None, title="Width of the stream for the detect role.")
fps: int = Field( fps: int = Field(
default=5, title="Number of frames per second to process through detection." default=5, title="Number of frames per second to process through detection."
) )
enabled: bool = Field(default=True, title="Detection Enabled.") enabled: bool = Field(default=True, title="Detection Enabled.")
min_initialized: Optional[int] = Field( min_initialized: Optional[int] = Field(
title="Minimum number of consecutive hits for an object to be initialized by the tracker." None,
title="Minimum number of consecutive hits for an object to be initialized by the tracker.",
) )
max_disappeared: Optional[int] = Field( max_disappeared: Optional[int] = Field(
title="Maximum number of frames the object can dissapear before detection ends." None,
title="Maximum number of frames the object can dissapear before detection ends.",
) )
stationary: StationaryConfig = Field( stationary: StationaryConfig = Field(
default_factory=StationaryConfig, default_factory=StationaryConfig,
@ -426,8 +457,18 @@ class FilterConfig(FrigateBaseModel):
default=0.5, title="Minimum detection confidence for object to be counted." default=0.5, title="Minimum detection confidence for object to be counted."
) )
mask: Optional[Union[str, List[str]]] = Field( mask: Optional[Union[str, List[str]]] = Field(
None,
title="Detection area polygon mask for this filter configuration.", title="Detection area polygon mask for this filter configuration.",
) )
raw_mask: Union[str, List[str]] = ""
@field_serializer("mask", when_used="json")
def serialize_mask(self, value: Any, info):
return self.raw_mask
@field_serializer("raw_mask", when_used="json")
def serialize_raw_mask(self, value: Any, info):
return None
class AudioFilterConfig(FrigateBaseModel): class AudioFilterConfig(FrigateBaseModel):
@ -440,8 +481,8 @@ class AudioFilterConfig(FrigateBaseModel):
class RuntimeFilterConfig(FilterConfig): class RuntimeFilterConfig(FilterConfig):
mask: Optional[np.ndarray] mask: Optional[np.ndarray] = None
raw_mask: Optional[Union[str, List[str]]] raw_mask: Optional[Union[str, List[str]]] = None
def __init__(self, **config): def __init__(self, **config):
mask = config.get("mask") mask = config.get("mask")
@ -453,15 +494,13 @@ class RuntimeFilterConfig(FilterConfig):
super().__init__(**config) super().__init__(**config)
def dict(self, **kwargs): def dict(self, **kwargs):
ret = super().dict(**kwargs) ret = super().model_dump(**kwargs)
if "mask" in ret: if "mask" in ret:
ret["mask"] = ret["raw_mask"] ret["mask"] = ret["raw_mask"]
ret.pop("raw_mask") ret.pop("raw_mask")
return ret return ret
class Config: model_config = ConfigDict(arbitrary_types_allowed=True, extra="ignore")
arbitrary_types_allowed = True
extra = Extra.ignore
# this uses the base model because the color is an extra attribute # this uses the base model because the color is an extra attribute
@ -531,9 +570,11 @@ class AudioConfig(FrigateBaseModel):
listen: List[str] = Field( listen: List[str] = Field(
default=DEFAULT_LISTEN_AUDIO, title="Audio to listen for." default=DEFAULT_LISTEN_AUDIO, title="Audio to listen for."
) )
filters: Optional[Dict[str, AudioFilterConfig]] = Field(title="Audio filters.") filters: Optional[Dict[str, AudioFilterConfig]] = Field(
None, title="Audio filters."
)
enabled_in_config: Optional[bool] = Field( enabled_in_config: Optional[bool] = Field(
title="Keep track of original state of audio detection." None, title="Keep track of original state of audio detection."
) )
num_threads: int = Field(default=2, title="Number of detection threads", ge=1) num_threads: int = Field(default=2, title="Number of detection threads", ge=1)
@ -660,7 +701,8 @@ class CameraInput(FrigateBaseModel):
class CameraFfmpegConfig(FfmpegConfig): class CameraFfmpegConfig(FfmpegConfig):
inputs: List[CameraInput] = Field(title="Camera inputs.") inputs: List[CameraInput] = Field(title="Camera inputs.")
@validator("inputs") @field_validator("inputs")
@classmethod
def validate_roles(cls, v): def validate_roles(cls, v):
roles = [role for i in v for role in i.roles] roles = [role for i in v for role in i.roles]
roles_set = set(roles) roles_set = set(roles)
@ -690,7 +732,7 @@ class SnapshotsConfig(FrigateBaseModel):
default_factory=list, default_factory=list,
title="List of required zones to be entered in order to save a snapshot.", title="List of required zones to be entered in order to save a snapshot.",
) )
height: Optional[int] = Field(title="Snapshot image height.") height: Optional[int] = Field(None, title="Snapshot image height.")
retain: RetainConfig = Field( retain: RetainConfig = Field(
default_factory=RetainConfig, title="Snapshot retention." default_factory=RetainConfig, title="Snapshot retention."
) )
@ -727,7 +769,7 @@ class TimestampStyleConfig(FrigateBaseModel):
format: str = Field(default=DEFAULT_TIME_FORMAT, title="Timestamp format.") format: str = Field(default=DEFAULT_TIME_FORMAT, title="Timestamp format.")
color: ColorConfig = Field(default_factory=ColorConfig, title="Timestamp color.") color: ColorConfig = Field(default_factory=ColorConfig, title="Timestamp color.")
thickness: int = Field(default=2, title="Timestamp thickness.") thickness: int = Field(default=2, title="Timestamp thickness.")
effect: Optional[TimestampEffectEnum] = Field(title="Timestamp effect.") effect: Optional[TimestampEffectEnum] = Field(None, title="Timestamp effect.")
class CameraMqttConfig(FrigateBaseModel): class CameraMqttConfig(FrigateBaseModel):
@ -755,8 +797,7 @@ class CameraLiveConfig(FrigateBaseModel):
class RestreamConfig(BaseModel): class RestreamConfig(BaseModel):
class Config: model_config = ConfigDict(extra="allow")
extra = Extra.allow
class CameraUiConfig(FrigateBaseModel): class CameraUiConfig(FrigateBaseModel):
@ -767,7 +808,7 @@ class CameraUiConfig(FrigateBaseModel):
class CameraConfig(FrigateBaseModel): class CameraConfig(FrigateBaseModel):
name: Optional[str] = Field(title="Camera name.", regex=REGEX_CAMERA_NAME) name: Optional[str] = Field(None, title="Camera name.", pattern=REGEX_CAMERA_NAME)
enabled: bool = Field(default=True, title="Enable camera.") enabled: bool = Field(default=True, title="Enable camera.")
ffmpeg: CameraFfmpegConfig = Field(title="FFmpeg configuration for the camera.") ffmpeg: CameraFfmpegConfig = Field(title="FFmpeg configuration for the camera.")
best_image_timeout: int = Field( best_image_timeout: int = Field(
@ -775,6 +816,7 @@ class CameraConfig(FrigateBaseModel):
title="How long to wait for the image with the highest confidence score.", title="How long to wait for the image with the highest confidence score.",
) )
webui_url: Optional[str] = Field( webui_url: Optional[str] = Field(
None,
title="URL to visit the camera directly from system page", title="URL to visit the camera directly from system page",
) )
zones: Dict[str, ZoneConfig] = Field( zones: Dict[str, ZoneConfig] = Field(
@ -798,7 +840,9 @@ class CameraConfig(FrigateBaseModel):
audio: AudioConfig = Field( audio: AudioConfig = Field(
default_factory=AudioConfig, title="Audio events configuration." default_factory=AudioConfig, title="Audio events configuration."
) )
motion: Optional[MotionConfig] = Field(title="Motion detection configuration.") motion: Optional[MotionConfig] = Field(
None, title="Motion detection configuration."
)
detect: DetectConfig = Field( detect: DetectConfig = Field(
default_factory=DetectConfig, title="Object detection configuration." default_factory=DetectConfig, title="Object detection configuration."
) )
@ -983,7 +1027,7 @@ def verify_valid_live_stream_name(
"""Verify that a restream exists to use for live view.""" """Verify that a restream exists to use for live view."""
if ( if (
camera_config.live.stream_name camera_config.live.stream_name
not in frigate_config.go2rtc.dict().get("streams", {}).keys() not in frigate_config.go2rtc.model_dump().get("streams", {}).keys()
): ):
return ValueError( return ValueError(
f"No restream with name {camera_config.live.stream_name} exists for camera {camera_config.name}." f"No restream with name {camera_config.live.stream_name} exists for camera {camera_config.name}."
@ -1108,7 +1152,7 @@ class FrigateConfig(FrigateBaseModel):
default_factory=AudioConfig, title="Global Audio events configuration." default_factory=AudioConfig, title="Global Audio events configuration."
) )
motion: Optional[MotionConfig] = Field( motion: Optional[MotionConfig] = Field(
title="Global motion detection configuration." None, title="Global motion detection configuration."
) )
detect: DetectConfig = Field( detect: DetectConfig = Field(
default_factory=DetectConfig, title="Global object tracking configuration." default_factory=DetectConfig, title="Global object tracking configuration."
@ -1121,7 +1165,7 @@ class FrigateConfig(FrigateBaseModel):
def runtime_config(self, plus_api: PlusApi = None) -> FrigateConfig: def runtime_config(self, plus_api: PlusApi = None) -> FrigateConfig:
"""Merge camera config with globals.""" """Merge camera config with globals."""
config = self.copy(deep=True) config = self.model_copy(deep=True)
# MQTT user/password substitutions # MQTT user/password substitutions
if config.mqtt.user or config.mqtt.password: if config.mqtt.user or config.mqtt.password:
@ -1140,7 +1184,7 @@ class FrigateConfig(FrigateBaseModel):
config.ffmpeg.hwaccel_args = auto_detect_hwaccel() config.ffmpeg.hwaccel_args = auto_detect_hwaccel()
# Global config to propagate down to camera level # Global config to propagate down to camera level
global_config = config.dict( global_config = config.model_dump(
include={ include={
"audio": ..., "audio": ...,
"birdseye": ..., "birdseye": ...,
@ -1157,8 +1201,10 @@ class FrigateConfig(FrigateBaseModel):
) )
for name, camera in config.cameras.items(): for name, camera in config.cameras.items():
merged_config = deep_merge(camera.dict(exclude_unset=True), global_config) merged_config = deep_merge(
camera_config: CameraConfig = CameraConfig.parse_obj( camera.model_dump(exclude_unset=True), global_config
)
camera_config: CameraConfig = CameraConfig.model_validate(
{"name": name, **merged_config} {"name": name, **merged_config}
) )
@ -1203,7 +1249,7 @@ class FrigateConfig(FrigateBaseModel):
) )
# Default min_initialized configuration # Default min_initialized configuration
min_initialized = camera_config.detect.fps / 2 min_initialized = int(camera_config.detect.fps / 2)
if camera_config.detect.min_initialized is None: if camera_config.detect.min_initialized is None:
camera_config.detect.min_initialized = min_initialized camera_config.detect.min_initialized = min_initialized
@ -1267,7 +1313,7 @@ class FrigateConfig(FrigateBaseModel):
# Set runtime filter to create masks # Set runtime filter to create masks
camera_config.objects.filters[object] = RuntimeFilterConfig( camera_config.objects.filters[object] = RuntimeFilterConfig(
frame_shape=camera_config.frame_shape, frame_shape=camera_config.frame_shape,
**filter.dict(exclude_unset=True), **filter.model_dump(exclude_unset=True),
) )
# Convert motion configuration # Convert motion configuration
@ -1279,7 +1325,7 @@ class FrigateConfig(FrigateBaseModel):
camera_config.motion = RuntimeMotionConfig( camera_config.motion = RuntimeMotionConfig(
frame_shape=camera_config.frame_shape, frame_shape=camera_config.frame_shape,
raw_mask=camera_config.motion.mask, raw_mask=camera_config.motion.mask,
**camera_config.motion.dict(exclude_unset=True), **camera_config.motion.model_dump(exclude_unset=True),
) )
camera_config.motion.enabled_in_config = camera_config.motion.enabled camera_config.motion.enabled_in_config = camera_config.motion.enabled
@ -1309,12 +1355,16 @@ class FrigateConfig(FrigateBaseModel):
config.model.check_and_load_plus_model(plus_api) config.model.check_and_load_plus_model(plus_api)
for key, detector in config.detectors.items(): for key, detector in config.detectors.items():
detector_config: DetectorConfig = parse_obj_as(DetectorConfig, detector) adapter = TypeAdapter(DetectorConfig)
model_dict = (
detector if isinstance(detector, dict) else detector.model_dump()
)
detector_config: DetectorConfig = adapter.validate_python(model_dict)
if detector_config.model is None: if detector_config.model is None:
detector_config.model = config.model detector_config.model = config.model
else: else:
model = detector_config.model model = detector_config.model
schema = ModelConfig.schema()["properties"] schema = ModelConfig.model_json_schema()["properties"]
if ( if (
model.width != schema["width"]["default"] model.width != schema["width"]["default"]
or model.height != schema["height"]["default"] or model.height != schema["height"]["default"]
@ -1328,8 +1378,8 @@ class FrigateConfig(FrigateBaseModel):
"Customizing more than a detector model path is unsupported." "Customizing more than a detector model path is unsupported."
) )
merged_model = deep_merge( merged_model = deep_merge(
detector_config.model.dict(exclude_unset=True), detector_config.model.model_dump(exclude_unset=True),
config.model.dict(exclude_unset=True), config.model.model_dump(exclude_unset=True),
) )
if "path" not in merged_model: if "path" not in merged_model:
@ -1338,7 +1388,7 @@ class FrigateConfig(FrigateBaseModel):
elif detector_config.type == "edgetpu": elif detector_config.type == "edgetpu":
merged_model["path"] = "/edgetpu_model.tflite" merged_model["path"] = "/edgetpu_model.tflite"
detector_config.model = ModelConfig.parse_obj(merged_model) detector_config.model = ModelConfig.model_validate(merged_model)
detector_config.model.check_and_load_plus_model( detector_config.model.check_and_load_plus_model(
plus_api, detector_config.type plus_api, detector_config.type
) )
@ -1347,7 +1397,8 @@ class FrigateConfig(FrigateBaseModel):
return config return config
@validator("cameras") @field_validator("cameras")
@classmethod
def ensure_zones_and_cameras_have_different_names(cls, v: Dict[str, CameraConfig]): def ensure_zones_and_cameras_have_different_names(cls, v: Dict[str, CameraConfig]):
zones = [zone for camera in v.values() for zone in camera.zones.keys()] zones = [zone for camera in v.values() for zone in camera.zones.keys()]
for zone in zones: for zone in zones:
@ -1365,9 +1416,9 @@ class FrigateConfig(FrigateBaseModel):
elif config_file.endswith(".json"): elif config_file.endswith(".json"):
config = json.loads(raw_config) config = json.loads(raw_config)
return cls.parse_obj(config) return cls.model_validate(config)
@classmethod @classmethod
def parse_raw(cls, raw_config): def parse_raw(cls, raw_config):
config = load_config_with_no_duplicates(raw_config) config = load_config_with_no_duplicates(raw_config)
return cls.parse_obj(config) return cls.model_validate(config)

View File

@ -7,7 +7,7 @@ from typing import Dict, Optional, Tuple
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import requests import requests
from pydantic import BaseModel, Extra, Field from pydantic import BaseModel, ConfigDict, Field
from pydantic.fields import PrivateAttr from pydantic.fields import PrivateAttr
from frigate.plus import PlusApi from frigate.plus import PlusApi
@ -35,8 +35,10 @@ class ModelTypeEnum(str, Enum):
class ModelConfig(BaseModel): class ModelConfig(BaseModel):
path: Optional[str] = Field(title="Custom Object detection model path.") path: Optional[str] = Field(None, title="Custom Object detection model path.")
labelmap_path: Optional[str] = Field(title="Label map for custom object detector.") labelmap_path: Optional[str] = Field(
None, title="Label map for custom object detector."
)
width: int = Field(default=320, title="Object detection model input width.") width: int = Field(default=320, title="Object detection model input width.")
height: int = Field(default=320, title="Object detection model input height.") height: int = Field(default=320, title="Object detection model input height.")
labelmap: Dict[int, str] = Field( labelmap: Dict[int, str] = Field(
@ -132,17 +134,15 @@ class ModelConfig(BaseModel):
for key, val in enumerate(enabled_labels): for key, val in enumerate(enabled_labels):
self._colormap[val] = tuple(int(round(255 * c)) for c in cmap(key)[:3]) self._colormap[val] = tuple(int(round(255 * c)) for c in cmap(key)[:3])
class Config: model_config = ConfigDict(extra="forbid", protected_namespaces=())
extra = Extra.forbid
class BaseDetectorConfig(BaseModel): class BaseDetectorConfig(BaseModel):
# the type field must be defined in all subclasses # the type field must be defined in all subclasses
type: str = Field(default="cpu", title="Detector Type") type: str = Field(default="cpu", title="Detector Type")
model: ModelConfig = Field( model: Optional[ModelConfig] = Field(
default=None, title="Detector specific model configuration." default=None, title="Detector specific model configuration."
) )
model_config = ConfigDict(
class Config: extra="allow", arbitrary_types_allowed=True, protected_namespaces=()
extra = Extra.allow )
arbitrary_types_allowed = True

View File

@ -247,9 +247,9 @@ class AudioEventMaintainer(threading.Thread):
def handle_detection(self, label: str, score: float) -> None: def handle_detection(self, label: str, score: float) -> None:
if self.detections.get(label): if self.detections.get(label):
self.detections[label][ self.detections[label]["last_detection"] = (
"last_detection" datetime.datetime.now().timestamp()
] = datetime.datetime.now().timestamp() )
else: else:
self.requestor.send_data(f"{self.config.name}/audio/{label}", "ON") self.requestor.send_data(f"{self.config.name}/audio/{label}", "ON")

View File

@ -115,12 +115,12 @@ PRESETS_HW_ACCEL_ENCODE_BIRDSEYE = {
"preset-rk-h265": "ffmpeg -hide_banner {0} -c:v hevc_rkmpp -profile:v high {1}", "preset-rk-h265": "ffmpeg -hide_banner {0} -c:v hevc_rkmpp -profile:v high {1}",
"default": "ffmpeg -hide_banner {0} -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency {1}", "default": "ffmpeg -hide_banner {0} -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency {1}",
} }
PRESETS_HW_ACCEL_ENCODE_BIRDSEYE[ PRESETS_HW_ACCEL_ENCODE_BIRDSEYE["preset-nvidia-h264"] = (
"preset-nvidia-h264" PRESETS_HW_ACCEL_ENCODE_BIRDSEYE[FFMPEG_HWACCEL_NVIDIA]
] = PRESETS_HW_ACCEL_ENCODE_BIRDSEYE[FFMPEG_HWACCEL_NVIDIA] )
PRESETS_HW_ACCEL_ENCODE_BIRDSEYE[ PRESETS_HW_ACCEL_ENCODE_BIRDSEYE["preset-nvidia-h265"] = (
"preset-nvidia-h265" PRESETS_HW_ACCEL_ENCODE_BIRDSEYE[FFMPEG_HWACCEL_NVIDIA]
] = PRESETS_HW_ACCEL_ENCODE_BIRDSEYE[FFMPEG_HWACCEL_NVIDIA] )
PRESETS_HW_ACCEL_ENCODE_TIMELAPSE = { PRESETS_HW_ACCEL_ENCODE_TIMELAPSE = {
"preset-rpi-64-h264": "ffmpeg -hide_banner {0} -c:v h264_v4l2m2m -pix_fmt yuv420p {1}", "preset-rpi-64-h264": "ffmpeg -hide_banner {0} -c:v h264_v4l2m2m -pix_fmt yuv420p {1}",
@ -136,9 +136,9 @@ PRESETS_HW_ACCEL_ENCODE_TIMELAPSE = {
"preset-rk-h265": "ffmpeg -hide_banner {0} -c:v hevc_rkmpp -profile:v high {1}", "preset-rk-h265": "ffmpeg -hide_banner {0} -c:v hevc_rkmpp -profile:v high {1}",
"default": "ffmpeg -hide_banner {0} -c:v libx264 -preset:v ultrafast -tune:v zerolatency {1}", "default": "ffmpeg -hide_banner {0} -c:v libx264 -preset:v ultrafast -tune:v zerolatency {1}",
} }
PRESETS_HW_ACCEL_ENCODE_TIMELAPSE[ PRESETS_HW_ACCEL_ENCODE_TIMELAPSE["preset-nvidia-h264"] = (
"preset-nvidia-h264" PRESETS_HW_ACCEL_ENCODE_TIMELAPSE[FFMPEG_HWACCEL_NVIDIA]
] = PRESETS_HW_ACCEL_ENCODE_TIMELAPSE[FFMPEG_HWACCEL_NVIDIA] )
# encoding of previews is only done on CPU due to comparable encode times and better quality from libx264 # encoding of previews is only done on CPU due to comparable encode times and better quality from libx264
PRESETS_HW_ACCEL_ENCODE_PREVIEW = { PRESETS_HW_ACCEL_ENCODE_PREVIEW = {

View File

@ -917,9 +917,9 @@ def event_snapshot(id):
else: else:
response.headers["Cache-Control"] = "no-store" response.headers["Cache-Control"] = "no-store"
if download: if download:
response.headers[ response.headers["Content-Disposition"] = (
"Content-Disposition" f"attachment; filename=snapshot-{id}.jpg"
] = f"attachment; filename=snapshot-{id}.jpg" )
return response return response
@ -1106,9 +1106,9 @@ def event_clip(id):
if download: if download:
response.headers["Content-Disposition"] = "attachment; filename=%s" % file_name response.headers["Content-Disposition"] = "attachment; filename=%s" % file_name
response.headers["Content-Length"] = os.path.getsize(clip_path) response.headers["Content-Length"] = os.path.getsize(clip_path)
response.headers[ response.headers["X-Accel-Redirect"] = (
"X-Accel-Redirect" f"/clips/{file_name}" # nginx: https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_ignore_headers
] = f"/clips/{file_name}" # nginx: https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_ignore_headers )
return response return response
@ -1384,7 +1384,7 @@ def end_event(event_id):
@bp.route("/config") @bp.route("/config")
def config(): def config():
config = current_app.frigate_config.dict() config = current_app.frigate_config.model_dump(mode="json", exclude_none=True)
# remove the mqtt password # remove the mqtt password
config["mqtt"].pop("password", None) config["mqtt"].pop("password", None)
@ -1404,9 +1404,9 @@ def config():
config["plus"] = {"enabled": current_app.plus_api.is_active()} config["plus"] = {"enabled": current_app.plus_api.is_active()}
for detector, detector_config in config["detectors"].items(): for detector, detector_config in config["detectors"].items():
detector_config["model"][ detector_config["model"]["labelmap"] = (
"labelmap" current_app.frigate_config.model.merged_labelmap
] = current_app.frigate_config.model.merged_labelmap )
return jsonify(config) return jsonify(config)
@ -1811,9 +1811,9 @@ def get_recordings_storage_usage():
total_mb = recording_stats["total"] total_mb = recording_stats["total"]
camera_usages: dict[ camera_usages: dict[str, dict] = (
str, dict current_app.storage_maintainer.calculate_camera_usages()
] = current_app.storage_maintainer.calculate_camera_usages() )
for camera_name in camera_usages.keys(): for camera_name in camera_usages.keys():
if camera_usages.get(camera_name, {}).get("usage"): if camera_usages.get(camera_name, {}).get("usage"):
@ -2001,9 +2001,9 @@ def recording_clip(camera_name, start_ts, end_ts):
if download: if download:
response.headers["Content-Disposition"] = "attachment; filename=%s" % file_name response.headers["Content-Disposition"] = "attachment; filename=%s" % file_name
response.headers["Content-Length"] = os.path.getsize(path) response.headers["Content-Length"] = os.path.getsize(path)
response.headers[ response.headers["X-Accel-Redirect"] = (
"X-Accel-Redirect" f"/cache/{file_name}" # nginx: https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_ignore_headers
] = f"/cache/{file_name}" # nginx: https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_ignore_headers )
return response return response

View File

@ -297,12 +297,12 @@ class PtzAutoTracker:
self.ptz_metrics[camera][ self.ptz_metrics[camera][
"ptz_max_zoom" "ptz_max_zoom"
].value = camera_config.onvif.autotracking.movement_weights[1] ].value = camera_config.onvif.autotracking.movement_weights[1]
self.intercept[ self.intercept[camera] = (
camera camera_config.onvif.autotracking.movement_weights[2]
] = camera_config.onvif.autotracking.movement_weights[2] )
self.move_coefficients[ self.move_coefficients[camera] = (
camera camera_config.onvif.autotracking.movement_weights[3:]
] = camera_config.onvif.autotracking.movement_weights[3:] )
else: else:
camera_config.onvif.autotracking.enabled = False camera_config.onvif.autotracking.enabled = False
self.ptz_metrics[camera]["ptz_autotracker_enabled"].value = False self.ptz_metrics[camera]["ptz_autotracker_enabled"].value = False
@ -603,9 +603,9 @@ class PtzAutoTracker:
) ** self.zoom_factor[camera] ) ** self.zoom_factor[camera]
if "original_target_box" not in self.tracked_object_metrics[camera]: if "original_target_box" not in self.tracked_object_metrics[camera]:
self.tracked_object_metrics[camera][ self.tracked_object_metrics[camera]["original_target_box"] = (
"original_target_box" self.tracked_object_metrics[camera]["target_box"]
] = self.tracked_object_metrics[camera]["target_box"] )
( (
self.tracked_object_metrics[camera]["valid_velocity"], self.tracked_object_metrics[camera]["valid_velocity"],

View File

@ -126,9 +126,9 @@ class OnvifController:
logger.debug(f"Onvif config for {camera_name}: {ptz_config}") logger.debug(f"Onvif config for {camera_name}: {ptz_config}")
service_capabilities_request = ptz.create_type("GetServiceCapabilities") service_capabilities_request = ptz.create_type("GetServiceCapabilities")
self.cams[camera_name][ self.cams[camera_name]["service_capabilities_request"] = (
"service_capabilities_request" service_capabilities_request
] = service_capabilities_request )
fov_space_id = next( fov_space_id = next(
( (
@ -244,9 +244,9 @@ class OnvifController:
supported_features.append("zoom-r") supported_features.append("zoom-r")
try: try:
# get camera's zoom limits from onvif config # get camera's zoom limits from onvif config
self.cams[camera_name][ self.cams[camera_name]["relative_zoom_range"] = (
"relative_zoom_range" ptz_config.Spaces.RelativeZoomTranslationSpace[0]
] = ptz_config.Spaces.RelativeZoomTranslationSpace[0] )
except Exception: except Exception:
if ( if (
self.config.cameras[camera_name].onvif.autotracking.zooming self.config.cameras[camera_name].onvif.autotracking.zooming
@ -263,9 +263,9 @@ class OnvifController:
supported_features.append("zoom-a") supported_features.append("zoom-a")
try: try:
# get camera's zoom limits from onvif config # get camera's zoom limits from onvif config
self.cams[camera_name][ self.cams[camera_name]["absolute_zoom_range"] = (
"absolute_zoom_range" ptz_config.Spaces.AbsoluteZoomPositionSpace[0]
] = ptz_config.Spaces.AbsoluteZoomPositionSpace[0] )
self.cams[camera_name]["zoom_limits"] = configs.ZoomLimits self.cams[camera_name]["zoom_limits"] = configs.ZoomLimits
except Exception: except Exception:
if self.config.cameras[camera_name].onvif.autotracking.zooming: if self.config.cameras[camera_name].onvif.autotracking.zooming:
@ -282,9 +282,9 @@ class OnvifController:
and configs.DefaultRelativePanTiltTranslationSpace is not None and configs.DefaultRelativePanTiltTranslationSpace is not None
): ):
supported_features.append("pt-r-fov") supported_features.append("pt-r-fov")
self.cams[camera_name][ self.cams[camera_name]["relative_fov_range"] = (
"relative_fov_range" ptz_config.Spaces.RelativePanTiltTranslationSpace[fov_space_id]
] = ptz_config.Spaces.RelativePanTiltTranslationSpace[fov_space_id] )
self.cams[camera_name]["features"] = supported_features self.cams[camera_name]["features"] = supported_features

View File

@ -1,6 +1,7 @@
import json import json
import os import os
import unittest import unittest
from unittest.mock import patch
import numpy as np import numpy as np
from pydantic import ValidationError from pydantic import ValidationError
@ -70,7 +71,9 @@ class TestConfig(unittest.TestCase):
assert runtime_config.detectors["cpu"].type == DetectorTypeEnum.cpu assert runtime_config.detectors["cpu"].type == DetectorTypeEnum.cpu
assert runtime_config.detectors["cpu"].model.width == 320 assert runtime_config.detectors["cpu"].model.width == 320
def test_detector_custom_model_path(self): @patch("frigate.detectors.detector_config.load_labels")
def test_detector_custom_model_path(self, mock_labels):
mock_labels.return_value = {}
config = { config = {
"detectors": { "detectors": {
"cpu": { "cpu": {
@ -110,7 +113,7 @@ class TestConfig(unittest.TestCase):
assert runtime_config.detectors["openvino"].model.path == "/etc/hosts" assert runtime_config.detectors["openvino"].model.path == "/etc/hosts"
assert runtime_config.model.width == 512 assert runtime_config.model.width == 512
assert runtime_config.detectors["cpu"].model.width == 512 assert runtime_config.detectors["cpu"].model.width == 320
assert runtime_config.detectors["edgetpu"].model.width == 160 assert runtime_config.detectors["edgetpu"].model.width == 160
assert runtime_config.detectors["openvino"].model.width == 512 assert runtime_config.detectors["openvino"].model.width == 512

View File

@ -41,9 +41,9 @@ class TestFfmpegPresets(unittest.TestCase):
assert self.default_ffmpeg == frigate_config.dict(exclude_unset=True) assert self.default_ffmpeg == frigate_config.dict(exclude_unset=True)
def test_ffmpeg_hwaccel_preset(self): def test_ffmpeg_hwaccel_preset(self):
self.default_ffmpeg["cameras"]["back"]["ffmpeg"][ self.default_ffmpeg["cameras"]["back"]["ffmpeg"]["hwaccel_args"] = (
"hwaccel_args" "preset-rpi-64-h264"
] = "preset-rpi-64-h264" )
frigate_config = FrigateConfig(**self.default_ffmpeg) frigate_config = FrigateConfig(**self.default_ffmpeg)
frigate_config.cameras["back"].create_ffmpeg_cmds() frigate_config.cameras["back"].create_ffmpeg_cmds()
assert "preset-rpi-64-h264" not in ( assert "preset-rpi-64-h264" not in (
@ -54,9 +54,9 @@ class TestFfmpegPresets(unittest.TestCase):
) )
def test_ffmpeg_hwaccel_not_preset(self): def test_ffmpeg_hwaccel_not_preset(self):
self.default_ffmpeg["cameras"]["back"]["ffmpeg"][ self.default_ffmpeg["cameras"]["back"]["ffmpeg"]["hwaccel_args"] = (
"hwaccel_args" "-other-hwaccel args"
] = "-other-hwaccel args" )
frigate_config = FrigateConfig(**self.default_ffmpeg) frigate_config = FrigateConfig(**self.default_ffmpeg)
frigate_config.cameras["back"].create_ffmpeg_cmds() frigate_config.cameras["back"].create_ffmpeg_cmds()
assert "-other-hwaccel args" in ( assert "-other-hwaccel args" in (
@ -64,9 +64,9 @@ class TestFfmpegPresets(unittest.TestCase):
) )
def test_ffmpeg_hwaccel_scale_preset(self): def test_ffmpeg_hwaccel_scale_preset(self):
self.default_ffmpeg["cameras"]["back"]["ffmpeg"][ self.default_ffmpeg["cameras"]["back"]["ffmpeg"]["hwaccel_args"] = (
"hwaccel_args" "preset-nvidia-h264"
] = "preset-nvidia-h264" )
self.default_ffmpeg["cameras"]["back"]["detect"] = { self.default_ffmpeg["cameras"]["back"]["detect"] = {
"height": 1920, "height": 1920,
"width": 2560, "width": 2560,
@ -85,9 +85,9 @@ class TestFfmpegPresets(unittest.TestCase):
def test_default_ffmpeg_input_arg_preset(self): def test_default_ffmpeg_input_arg_preset(self):
frigate_config = FrigateConfig(**self.default_ffmpeg) frigate_config = FrigateConfig(**self.default_ffmpeg)
self.default_ffmpeg["cameras"]["back"]["ffmpeg"][ self.default_ffmpeg["cameras"]["back"]["ffmpeg"]["input_args"] = (
"input_args" "preset-rtsp-generic"
] = "preset-rtsp-generic" )
frigate_preset_config = FrigateConfig(**self.default_ffmpeg) frigate_preset_config = FrigateConfig(**self.default_ffmpeg)
frigate_config.cameras["back"].create_ffmpeg_cmds() frigate_config.cameras["back"].create_ffmpeg_cmds()
frigate_preset_config.cameras["back"].create_ffmpeg_cmds() frigate_preset_config.cameras["back"].create_ffmpeg_cmds()
@ -98,9 +98,9 @@ class TestFfmpegPresets(unittest.TestCase):
) )
def test_ffmpeg_input_preset(self): def test_ffmpeg_input_preset(self):
self.default_ffmpeg["cameras"]["back"]["ffmpeg"][ self.default_ffmpeg["cameras"]["back"]["ffmpeg"]["input_args"] = (
"input_args" "preset-rtmp-generic"
] = "preset-rtmp-generic" )
frigate_config = FrigateConfig(**self.default_ffmpeg) frigate_config = FrigateConfig(**self.default_ffmpeg)
frigate_config.cameras["back"].create_ffmpeg_cmds() frigate_config.cameras["back"].create_ffmpeg_cmds()
assert "preset-rtmp-generic" not in ( assert "preset-rtmp-generic" not in (
@ -131,9 +131,9 @@ class TestFfmpegPresets(unittest.TestCase):
) )
def test_ffmpeg_output_record_preset(self): def test_ffmpeg_output_record_preset(self):
self.default_ffmpeg["cameras"]["back"]["ffmpeg"]["output_args"][ self.default_ffmpeg["cameras"]["back"]["ffmpeg"]["output_args"]["record"] = (
"record" "preset-record-generic-audio-aac"
] = "preset-record-generic-audio-aac" )
frigate_config = FrigateConfig(**self.default_ffmpeg) frigate_config = FrigateConfig(**self.default_ffmpeg)
frigate_config.cameras["back"].create_ffmpeg_cmds() frigate_config.cameras["back"].create_ffmpeg_cmds()
assert "preset-record-generic-audio-aac" not in ( assert "preset-record-generic-audio-aac" not in (
@ -144,9 +144,9 @@ class TestFfmpegPresets(unittest.TestCase):
) )
def test_ffmpeg_output_record_not_preset(self): def test_ffmpeg_output_record_not_preset(self):
self.default_ffmpeg["cameras"]["back"]["ffmpeg"]["output_args"][ self.default_ffmpeg["cameras"]["back"]["ffmpeg"]["output_args"]["record"] = (
"record" "-some output"
] = "-some output" )
frigate_config = FrigateConfig(**self.default_ffmpeg) frigate_config = FrigateConfig(**self.default_ffmpeg)
frigate_config.cameras["back"].create_ffmpeg_cmds() frigate_config.cameras["back"].create_ffmpeg_cmds()
assert "-some output" in ( assert "-some output" in (

View File

@ -20,6 +20,7 @@ Some examples (model - class or model name)::
> migrator.add_default(model, field_name, default) > migrator.add_default(model, field_name, default)
""" """
import peewee as pw import peewee as pw
SQL = pw.SQL SQL = pw.SQL

View File

@ -20,6 +20,7 @@ Some examples (model - class or model name)::
> migrator.add_default(model, field_name, default) > migrator.add_default(model, field_name, default)
""" """
import peewee as pw import peewee as pw
SQL = pw.SQL SQL = pw.SQL

View File

@ -20,6 +20,7 @@ Some examples (model - class or model name)::
> migrator.add_default(model, field_name, default) > migrator.add_default(model, field_name, default)
""" """
import peewee as pw import peewee as pw
SQL = pw.SQL SQL = pw.SQL

View File

@ -20,6 +20,7 @@ Some examples (model - class or model name)::
> migrator.add_default(model, field_name, default) > migrator.add_default(model, field_name, default)
""" """
import peewee as pw import peewee as pw
SQL = pw.SQL SQL = pw.SQL

View File

@ -20,6 +20,7 @@ Some examples (model - class or model name)::
> migrator.add_default(model, field_name, default) > migrator.add_default(model, field_name, default)
""" """
import peewee as pw import peewee as pw
SQL = pw.SQL SQL = pw.SQL

View File

@ -20,6 +20,7 @@ Some examples (model - class or model name)::
> migrator.add_default(model, field_name, default) > migrator.add_default(model, field_name, default)
""" """
import peewee as pw import peewee as pw
SQL = pw.SQL SQL = pw.SQL