config.py 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182
  1. from __future__ import annotations
  2. import base64
  3. import dataclasses
  4. import datetime
  5. import json
  6. import logging
  7. import os
  8. from typing import Any, Dict, List, Optional, Tuple, Union
  9. import matplotlib.pyplot as plt
  10. import numpy as np
  11. import voluptuous as vol
  12. import yaml
  13. from frigate.const import BASE_DIR, RECORD_DIR, CLIPS_DIR, CACHE_DIR
  14. from frigate.util import create_mask
  15. logger = logging.getLogger(__name__)
  16. # TODO: Identify what the default format to display timestamps is
  17. DEFAULT_TIME_FORMAT = "%m/%d/%Y %H:%M:%S"
  18. # German Style:
  19. # DEFAULT_TIME_FORMAT = "%d.%m.%Y %H:%M:%S"
  20. DEFAULT_TRACKED_OBJECTS = ["person"]
  21. DEFAULT_RGB_COLOR = {"red": 255, "green": 255, "blue": 255}
  22. DEFAULT_DETECTORS = {"coral": {"type": "edgetpu", "device": "usb"}}
  23. DETECTORS_SCHEMA = vol.Schema(
  24. {
  25. vol.Required(str): {
  26. vol.Required("type", default="edgetpu"): vol.In(["cpu", "edgetpu"]),
  27. vol.Optional("device", default="usb"): str,
  28. vol.Optional("num_threads", default=3): int,
  29. }
  30. }
  31. )
  32. @dataclasses.dataclass(frozen=True)
  33. class DetectorConfig:
  34. type: str
  35. device: str
  36. num_threads: int
  37. @classmethod
  38. def build(cls, config) -> DetectorConfig:
  39. return DetectorConfig(config["type"], config["device"], config["num_threads"])
  40. def to_dict(self) -> Dict[str, Any]:
  41. return dataclasses.asdict(self)
  42. MQTT_SCHEMA = vol.Schema(
  43. {
  44. vol.Required("host"): str,
  45. vol.Optional("port", default=1883): int,
  46. vol.Optional("topic_prefix", default="frigate"): str,
  47. vol.Optional("client_id", default="frigate"): str,
  48. vol.Optional("stats_interval", default=60): int,
  49. vol.Inclusive("user", "auth"): str,
  50. vol.Inclusive("password", "auth"): str,
  51. vol.Optional("tls_ca_certs"): str,
  52. vol.Optional("tls_client_cert"): str,
  53. vol.Optional("tls_client_key"): str,
  54. vol.Optional("tls_insecure"): bool,
  55. }
  56. )
  57. @dataclasses.dataclass(frozen=True)
  58. class MqttConfig:
  59. host: str
  60. port: int
  61. topic_prefix: str
  62. client_id: str
  63. stats_interval: int
  64. user: Optional[str]
  65. password: Optional[str]
  66. tls_ca_certs: Optional[str]
  67. tls_client_cert: Optional[str]
  68. tls_client_key: Optional[str]
  69. tls_insecure: Optional[bool]
  70. @classmethod
  71. def build(cls, config) -> MqttConfig:
  72. return MqttConfig(
  73. config["host"],
  74. config["port"],
  75. config["topic_prefix"],
  76. config["client_id"],
  77. config["stats_interval"],
  78. config.get("user"),
  79. config.get("password"),
  80. config.get("tls_ca_certs"),
  81. config.get("tls_client_cert"),
  82. config.get("tls_client_key"),
  83. config.get("tls_insecure"),
  84. )
  85. def to_dict(self) -> Dict[str, Any]:
  86. return dataclasses.asdict(self)
  87. RETAIN_SCHEMA = vol.Schema(
  88. {vol.Required("default", default=10): int, "objects": {str: int}}
  89. )
  90. @dataclasses.dataclass(frozen=True)
  91. class RetainConfig:
  92. default: int
  93. objects: Dict[str, int]
  94. @classmethod
  95. def build(cls, config, global_config={}) -> RetainConfig:
  96. return RetainConfig(
  97. config.get("default", global_config.get("default")),
  98. config.get("objects", global_config.get("objects", {})),
  99. )
  100. def to_dict(self) -> Dict[str, Any]:
  101. return dataclasses.asdict(self)
  102. CLIPS_SCHEMA = vol.Schema(
  103. {
  104. vol.Optional("max_seconds", default=300): int,
  105. vol.Optional("retain", default={}): RETAIN_SCHEMA,
  106. }
  107. )
  108. @dataclasses.dataclass(frozen=True)
  109. class ClipsConfig:
  110. max_seconds: int
  111. retain: RetainConfig
  112. @classmethod
  113. def build(cls, config) -> ClipsConfig:
  114. return ClipsConfig(
  115. config["max_seconds"],
  116. RetainConfig.build(config["retain"]),
  117. )
  118. def to_dict(self) -> Dict[str, Any]:
  119. return {
  120. "max_seconds": self.max_seconds,
  121. "retain": self.retain.to_dict(),
  122. }
  123. MOTION_SCHEMA = vol.Schema(
  124. {
  125. "mask": vol.Any(str, [str]),
  126. "threshold": vol.Range(min=1, max=255),
  127. "contour_area": int,
  128. "delta_alpha": float,
  129. "frame_alpha": float,
  130. "frame_height": int,
  131. }
  132. )
  133. @dataclasses.dataclass(frozen=True)
  134. class MotionConfig:
  135. raw_mask: Union[str, List[str]]
  136. mask: np.ndarray
  137. threshold: int
  138. contour_area: int
  139. delta_alpha: float
  140. frame_alpha: float
  141. frame_height: int
  142. @classmethod
  143. def build(cls, config, global_config, frame_shape) -> MotionConfig:
  144. raw_mask = config.get("mask")
  145. if raw_mask:
  146. mask = create_mask(frame_shape, raw_mask)
  147. else:
  148. mask = np.zeros(frame_shape, np.uint8)
  149. mask[:] = 255
  150. return MotionConfig(
  151. raw_mask,
  152. mask,
  153. config.get("threshold", global_config.get("threshold", 25)),
  154. config.get("contour_area", global_config.get("contour_area", 100)),
  155. config.get("delta_alpha", global_config.get("delta_alpha", 0.2)),
  156. config.get("frame_alpha", global_config.get("frame_alpha", 0.2)),
  157. config.get(
  158. "frame_height", global_config.get("frame_height", frame_shape[0] // 6)
  159. ),
  160. )
  161. def to_dict(self) -> Dict[str, Any]:
  162. return {
  163. "mask": self.raw_mask,
  164. "threshold": self.threshold,
  165. "contour_area": self.contour_area,
  166. "delta_alpha": self.delta_alpha,
  167. "frame_alpha": self.frame_alpha,
  168. "frame_height": self.frame_height,
  169. }
  170. GLOBAL_DETECT_SCHEMA = vol.Schema({"max_disappeared": int})
  171. DETECT_SCHEMA = GLOBAL_DETECT_SCHEMA.extend(
  172. {vol.Optional("enabled", default=True): bool}
  173. )
  174. @dataclasses.dataclass
  175. class DetectConfig:
  176. enabled: bool
  177. max_disappeared: int
  178. @classmethod
  179. def build(cls, config, global_config, camera_fps) -> DetectConfig:
  180. return DetectConfig(
  181. config["enabled"],
  182. config.get(
  183. "max_disappeared", global_config.get("max_disappeared", camera_fps * 5)
  184. ),
  185. )
  186. def to_dict(self) -> Dict[str, Any]:
  187. return {
  188. "enabled": self.enabled,
  189. "max_disappeared": self.max_disappeared,
  190. }
  191. ZONE_FILTER_SCHEMA = vol.Schema(
  192. {
  193. str: {
  194. "min_area": int,
  195. "max_area": int,
  196. "threshold": float,
  197. }
  198. }
  199. )
  200. FILTER_SCHEMA = ZONE_FILTER_SCHEMA.extend(
  201. {
  202. str: {
  203. "min_score": float,
  204. "mask": vol.Any(str, [str]),
  205. }
  206. }
  207. )
  208. @dataclasses.dataclass(frozen=True)
  209. class FilterConfig:
  210. min_area: int
  211. max_area: int
  212. threshold: float
  213. min_score: float
  214. mask: Optional[np.ndarray]
  215. raw_mask: Union[str, List[str]]
  216. @classmethod
  217. def build(
  218. cls, config, global_config={}, global_mask=None, frame_shape=None
  219. ) -> FilterConfig:
  220. raw_mask = []
  221. if global_mask:
  222. if isinstance(global_mask, list):
  223. raw_mask += global_mask
  224. elif isinstance(global_mask, str):
  225. raw_mask += [global_mask]
  226. config_mask = config.get("mask")
  227. if config_mask:
  228. if isinstance(config_mask, list):
  229. raw_mask += config_mask
  230. elif isinstance(config_mask, str):
  231. raw_mask += [config_mask]
  232. mask = create_mask(frame_shape, raw_mask) if raw_mask else None
  233. return FilterConfig(
  234. min_area=config.get("min_area", global_config.get("min_area", 0)),
  235. max_area=config.get("max_area", global_config.get("max_area", 24000000)),
  236. threshold=config.get("threshold", global_config.get("threshold", 0.7)),
  237. min_score=config.get("min_score", global_config.get("min_score", 0.5)),
  238. mask=mask,
  239. raw_mask=raw_mask,
  240. )
  241. def to_dict(self) -> Dict[str, Any]:
  242. return {
  243. "min_area": self.min_area,
  244. "max_area": self.max_area,
  245. "threshold": self.threshold,
  246. "min_score": self.min_score,
  247. "mask": self.raw_mask,
  248. }
  249. ZONE_SCHEMA = {
  250. str: {
  251. vol.Required("coordinates"): vol.Any(str, [str]),
  252. vol.Optional("filters", default={}): ZONE_FILTER_SCHEMA,
  253. }
  254. }
  255. @dataclasses.dataclass(frozen=True)
  256. class ZoneConfig:
  257. filters: Dict[str, FilterConfig]
  258. coordinates: Union[str, List[str]]
  259. contour: np.ndarray
  260. color: Tuple[int, int, int]
  261. @classmethod
  262. def build(cls, config, color: Tuple[int, int, int]) -> ZoneConfig:
  263. coordinates = config["coordinates"]
  264. if isinstance(coordinates, list):
  265. contour = np.array(
  266. [[int(p.split(",")[0]), int(p.split(",")[1])] for p in coordinates]
  267. )
  268. elif isinstance(coordinates, str):
  269. points = coordinates.split(",")
  270. contour = np.array(
  271. [[int(points[i]), int(points[i + 1])] for i in range(0, len(points), 2)]
  272. )
  273. else:
  274. contour = np.array([])
  275. return ZoneConfig(
  276. {name: FilterConfig.build(c) for name, c in config["filters"].items()},
  277. coordinates,
  278. contour,
  279. color=color,
  280. )
  281. def to_dict(self) -> Dict[str, Any]:
  282. return {
  283. "filters": {k: f.to_dict() for k, f in self.filters.items()},
  284. "coordinates": self.coordinates,
  285. }
  286. def filters_for_all_tracked_objects(object_config):
  287. for tracked_object in object_config.get("track", DEFAULT_TRACKED_OBJECTS):
  288. if not "filters" in object_config:
  289. object_config["filters"] = {}
  290. if not tracked_object in object_config["filters"]:
  291. object_config["filters"][tracked_object] = {}
  292. return object_config
  293. OBJECTS_SCHEMA = vol.Schema(
  294. vol.All(
  295. filters_for_all_tracked_objects,
  296. {
  297. "track": [str],
  298. "mask": vol.Any(str, [str]),
  299. vol.Optional("filters", default={}): FILTER_SCHEMA,
  300. },
  301. )
  302. )
  303. @dataclasses.dataclass(frozen=True)
  304. class ObjectConfig:
  305. track: List[str]
  306. filters: Dict[str, FilterConfig]
  307. raw_mask: Optional[Union[str, List[str]]]
  308. @classmethod
  309. def build(cls, config, global_config, frame_shape) -> ObjectConfig:
  310. track = config.get("track", global_config.get("track", DEFAULT_TRACKED_OBJECTS))
  311. raw_mask = config.get("mask")
  312. return ObjectConfig(
  313. track,
  314. {
  315. name: FilterConfig.build(
  316. config["filters"].get(name, {}),
  317. global_config["filters"].get(name, {}),
  318. raw_mask,
  319. frame_shape,
  320. )
  321. for name in track
  322. },
  323. raw_mask,
  324. )
  325. def to_dict(self) -> Dict[str, Any]:
  326. return {
  327. "track": self.track,
  328. "mask": self.raw_mask,
  329. "filters": {k: f.to_dict() for k, f in self.filters.items()},
  330. }
  331. BIRDSEYE_SCHEMA = vol.Schema(
  332. {
  333. vol.Optional("enabled", default=True): bool,
  334. vol.Optional("width", default=1280): int,
  335. vol.Optional("height", default=720): int,
  336. vol.Optional("quality", default=8): vol.Range(min=1, max=31),
  337. vol.Optional("mode", default="objects"): vol.In(
  338. ["objects", "motion", "continuous"]
  339. ),
  340. }
  341. )
  342. @dataclasses.dataclass(frozen=True)
  343. class BirdseyeConfig:
  344. enabled: bool
  345. width: int
  346. height: int
  347. quality: int
  348. mode: str
  349. @classmethod
  350. def build(cls, config) -> BirdseyeConfig:
  351. return BirdseyeConfig(
  352. config["enabled"],
  353. config["width"],
  354. config["height"],
  355. config["quality"],
  356. config["mode"],
  357. )
  358. def to_dict(self) -> Dict[str, Any]:
  359. return dataclasses.asdict(self)
  360. FFMPEG_GLOBAL_ARGS_DEFAULT = ["-hide_banner", "-loglevel", "warning"]
  361. FFMPEG_INPUT_ARGS_DEFAULT = [
  362. "-avoid_negative_ts",
  363. "make_zero",
  364. "-fflags",
  365. "+genpts+discardcorrupt",
  366. "-rtsp_transport",
  367. "tcp",
  368. "-stimeout",
  369. "5000000",
  370. "-use_wallclock_as_timestamps",
  371. "1",
  372. ]
  373. DETECT_FFMPEG_OUTPUT_ARGS_DEFAULT = ["-f", "rawvideo", "-pix_fmt", "yuv420p"]
  374. RTMP_FFMPEG_OUTPUT_ARGS_DEFAULT = ["-c", "copy", "-f", "flv"]
  375. SAVE_CLIPS_FFMPEG_OUTPUT_ARGS_DEFAULT = [
  376. "-f",
  377. "segment",
  378. "-segment_time",
  379. "10",
  380. "-segment_format",
  381. "mp4",
  382. "-reset_timestamps",
  383. "1",
  384. "-strftime",
  385. "1",
  386. "-c",
  387. "copy",
  388. "-an",
  389. ]
  390. RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT = [
  391. "-f",
  392. "segment",
  393. "-segment_time",
  394. "60",
  395. "-segment_format",
  396. "mp4",
  397. "-reset_timestamps",
  398. "1",
  399. "-strftime",
  400. "1",
  401. "-c",
  402. "copy",
  403. "-an",
  404. ]
  405. GLOBAL_FFMPEG_SCHEMA = vol.Schema(
  406. {
  407. vol.Optional("global_args", default=FFMPEG_GLOBAL_ARGS_DEFAULT): vol.Any(
  408. str, [str]
  409. ),
  410. vol.Optional("hwaccel_args", default=[]): vol.Any(str, [str]),
  411. vol.Optional("input_args", default=FFMPEG_INPUT_ARGS_DEFAULT): vol.Any(
  412. str, [str]
  413. ),
  414. vol.Optional("output_args", default={}): {
  415. vol.Optional("detect", default=DETECT_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(
  416. str, [str]
  417. ),
  418. vol.Optional("record", default=RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(
  419. str, [str]
  420. ),
  421. vol.Optional(
  422. "clips", default=SAVE_CLIPS_FFMPEG_OUTPUT_ARGS_DEFAULT
  423. ): vol.Any(str, [str]),
  424. vol.Optional("rtmp", default=RTMP_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(
  425. str, [str]
  426. ),
  427. },
  428. }
  429. )
  430. def each_role_used_once(inputs):
  431. roles = [role for i in inputs for role in i["roles"]]
  432. roles_set = set(roles)
  433. if len(roles) > len(roles_set):
  434. raise ValueError
  435. return inputs
  436. def detect_is_required(inputs):
  437. roles = [role for i in inputs for role in i["roles"]]
  438. if not "detect" in roles:
  439. raise ValueError
  440. return inputs
  441. CAMERA_FFMPEG_SCHEMA = vol.Schema(
  442. {
  443. vol.Required("inputs"): vol.All(
  444. [
  445. {
  446. vol.Required("path"): str,
  447. vol.Required("roles"): ["detect", "clips", "record", "rtmp"],
  448. "global_args": vol.Any(str, [str]),
  449. "hwaccel_args": vol.Any(str, [str]),
  450. "input_args": vol.Any(str, [str]),
  451. }
  452. ],
  453. vol.Msg(each_role_used_once, msg="Each input role may only be used once"),
  454. vol.Msg(detect_is_required, msg="The detect role is required"),
  455. ),
  456. "global_args": vol.Any(str, [str]),
  457. "hwaccel_args": vol.Any(str, [str]),
  458. "input_args": vol.Any(str, [str]),
  459. "output_args": {
  460. vol.Optional("detect", default=DETECT_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(
  461. str, [str]
  462. ),
  463. vol.Optional("record", default=RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(
  464. str, [str]
  465. ),
  466. vol.Optional(
  467. "clips", default=SAVE_CLIPS_FFMPEG_OUTPUT_ARGS_DEFAULT
  468. ): vol.Any(str, [str]),
  469. vol.Optional("rtmp", default=RTMP_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(
  470. str, [str]
  471. ),
  472. },
  473. }
  474. )
  475. @dataclasses.dataclass(frozen=True)
  476. class CameraFfmpegConfig:
  477. inputs: List[CameraInput]
  478. output_args: Dict[str, List[str]]
  479. @classmethod
  480. def build(cls, config, global_config):
  481. output_args = config.get("output_args", global_config["output_args"])
  482. output_args = {
  483. k: v if isinstance(v, list) else v.split(" ")
  484. for k, v in output_args.items()
  485. }
  486. return CameraFfmpegConfig(
  487. [CameraInput.build(i, config, global_config) for i in config["inputs"]],
  488. output_args,
  489. )
  490. @dataclasses.dataclass(frozen=True)
  491. class CameraInput:
  492. path: str
  493. roles: List[str]
  494. global_args: List[str]
  495. hwaccel_args: List[str]
  496. input_args: List[str]
  497. @classmethod
  498. def build(cls, ffmpeg_input, camera_config, global_config) -> CameraInput:
  499. return CameraInput(
  500. ffmpeg_input["path"],
  501. ffmpeg_input["roles"],
  502. CameraInput._extract_args(
  503. "global_args", ffmpeg_input, camera_config, global_config
  504. ),
  505. CameraInput._extract_args(
  506. "hwaccel_args", ffmpeg_input, camera_config, global_config
  507. ),
  508. CameraInput._extract_args(
  509. "input_args", ffmpeg_input, camera_config, global_config
  510. ),
  511. )
  512. @staticmethod
  513. def _extract_args(name, ffmpeg_input, camera_config, global_config):
  514. args = ffmpeg_input.get(name, camera_config.get(name, global_config[name]))
  515. return args if isinstance(args, list) else args.split(" ")
  516. def ensure_zones_and_cameras_have_different_names(cameras):
  517. zones = [zone for camera in cameras.values() for zone in camera["zones"].keys()]
  518. for zone in zones:
  519. if zone in cameras.keys():
  520. raise ValueError
  521. return cameras
  522. def ensure_timeformat_is_legit(format_string):
  523. datetime.datetime.now().strftime(format_string)
  524. return format_string
  525. RGB_COLOR_SCHEMA = vol.Schema(
  526. {
  527. vol.Required("red"): vol.Range(min=0, max=255),
  528. vol.Required("green"): vol.Range(min=0, max=255),
  529. vol.Required("blue"): vol.Range(min=0, max=255),
  530. }
  531. )
  532. CAMERAS_SCHEMA = vol.Schema(
  533. vol.All(
  534. {
  535. str: {
  536. vol.Required("ffmpeg"): CAMERA_FFMPEG_SCHEMA,
  537. vol.Required("height"): int,
  538. vol.Required("width"): int,
  539. "fps": int,
  540. vol.Optional("best_image_timeout", default=60): int,
  541. vol.Optional("zones", default={}): ZONE_SCHEMA,
  542. vol.Optional("clips", default={}): {
  543. vol.Optional("enabled", default=False): bool,
  544. vol.Optional("pre_capture", default=5): int,
  545. vol.Optional("post_capture", default=5): int,
  546. vol.Optional("required_zones", default=[]): [str],
  547. "objects": [str],
  548. vol.Optional("retain", default={}): RETAIN_SCHEMA,
  549. },
  550. vol.Optional("record", default={}): {
  551. "enabled": bool,
  552. "retain_days": int,
  553. },
  554. vol.Optional("rtmp", default={}): {
  555. vol.Required("enabled", default=True): bool,
  556. },
  557. vol.Optional("snapshots", default={}): {
  558. vol.Optional("enabled", default=False): bool,
  559. vol.Optional("clean_copy", default=True): bool,
  560. vol.Optional("timestamp", default=False): bool,
  561. vol.Optional("bounding_box", default=False): bool,
  562. vol.Optional("crop", default=False): bool,
  563. vol.Optional("required_zones", default=[]): [str],
  564. "height": int,
  565. vol.Optional("retain", default={}): RETAIN_SCHEMA,
  566. },
  567. vol.Optional("mqtt", default={}): {
  568. vol.Optional("enabled", default=True): bool,
  569. vol.Optional("timestamp", default=True): bool,
  570. vol.Optional("bounding_box", default=True): bool,
  571. vol.Optional("crop", default=True): bool,
  572. vol.Optional("height", default=270): int,
  573. vol.Optional("required_zones", default=[]): [str],
  574. },
  575. vol.Optional("objects", default={}): OBJECTS_SCHEMA,
  576. vol.Optional("motion", default={}): MOTION_SCHEMA,
  577. vol.Optional("detect", default={}): DETECT_SCHEMA,
  578. vol.Optional("timestamp_style", default={}): {
  579. vol.Optional("position", default="tl"): vol.In(
  580. ["tl", "tr", "bl", "br"]
  581. ),
  582. vol.Optional(
  583. "format", default=DEFAULT_TIME_FORMAT
  584. ): ensure_timeformat_is_legit,
  585. vol.Optional("color", default=DEFAULT_RGB_COLOR): RGB_COLOR_SCHEMA,
  586. vol.Optional("scale", default=1.0): float,
  587. vol.Optional("thickness", default=2): int,
  588. vol.Optional("effect", default=None): vol.In(
  589. [None, "solid", "shadow"]
  590. ),
  591. },
  592. }
  593. },
  594. vol.Msg(
  595. ensure_zones_and_cameras_have_different_names,
  596. msg="Zones cannot share names with cameras",
  597. ),
  598. )
  599. )
  600. @dataclasses.dataclass
  601. class CameraSnapshotsConfig:
  602. enabled: bool
  603. clean_copy: bool
  604. timestamp: bool
  605. bounding_box: bool
  606. crop: bool
  607. required_zones: List[str]
  608. height: Optional[int]
  609. retain: RetainConfig
  610. @classmethod
  611. def build(cls, config, global_config) -> CameraSnapshotsConfig:
  612. return CameraSnapshotsConfig(
  613. enabled=config["enabled"],
  614. clean_copy=config["clean_copy"],
  615. timestamp=config["timestamp"],
  616. bounding_box=config["bounding_box"],
  617. crop=config["crop"],
  618. required_zones=config["required_zones"],
  619. height=config.get("height"),
  620. retain=RetainConfig.build(
  621. config["retain"], global_config["snapshots"]["retain"]
  622. ),
  623. )
  624. def to_dict(self) -> Dict[str, Any]:
  625. return {
  626. "enabled": self.enabled,
  627. "clean_copy": self.clean_copy,
  628. "timestamp": self.timestamp,
  629. "bounding_box": self.bounding_box,
  630. "crop": self.crop,
  631. "height": self.height,
  632. "retain": self.retain.to_dict(),
  633. "required_zones": self.required_zones,
  634. }
  635. @dataclasses.dataclass
  636. class CameraMqttConfig:
  637. enabled: bool
  638. timestamp: bool
  639. bounding_box: bool
  640. crop: bool
  641. height: int
  642. required_zones: List[str]
  643. @classmethod
  644. def build(cls, config) -> CameraMqttConfig:
  645. return CameraMqttConfig(
  646. config["enabled"],
  647. config["timestamp"],
  648. config["bounding_box"],
  649. config["crop"],
  650. config.get("height"),
  651. config["required_zones"],
  652. )
  653. def to_dict(self) -> Dict[str, Any]:
  654. return dataclasses.asdict(self)
  655. @dataclasses.dataclass
  656. class TimestampStyleConfig:
  657. position: str
  658. format: str
  659. color: Tuple[int, int, int]
  660. scale: float
  661. thickness: int
  662. effect: str
  663. @classmethod
  664. def build(cls, config) -> TimestampStyleConfig:
  665. return TimestampStyleConfig(
  666. config["position"],
  667. config["format"],
  668. (config["color"]["red"], config["color"]["green"], config["color"]["blue"]),
  669. config["scale"],
  670. config["thickness"],
  671. config["effect"],
  672. )
  673. def to_dict(self) -> Dict[str, Any]:
  674. return dataclasses.asdict(self)
  675. @dataclasses.dataclass
  676. class CameraClipsConfig:
  677. enabled: bool
  678. pre_capture: int
  679. post_capture: int
  680. required_zones: List[str]
  681. objects: Optional[List[str]]
  682. retain: RetainConfig
  683. @classmethod
  684. def build(cls, config, global_config) -> CameraClipsConfig:
  685. return CameraClipsConfig(
  686. enabled=config["enabled"],
  687. pre_capture=config["pre_capture"],
  688. post_capture=config["post_capture"],
  689. required_zones=config["required_zones"],
  690. objects=config.get("objects"),
  691. retain=RetainConfig.build(
  692. config["retain"],
  693. global_config["clips"]["retain"],
  694. ),
  695. )
  696. def to_dict(self) -> Dict[str, Any]:
  697. return {
  698. "enabled": self.enabled,
  699. "pre_capture": self.pre_capture,
  700. "post_capture": self.post_capture,
  701. "objects": self.objects,
  702. "retain": self.retain.to_dict(),
  703. "required_zones": self.required_zones,
  704. }
  705. @dataclasses.dataclass
  706. class CameraRtmpConfig:
  707. enabled: bool
  708. @classmethod
  709. def build(cls, config) -> CameraRtmpConfig:
  710. return CameraRtmpConfig(config["enabled"])
  711. def to_dict(self) -> Dict[str, Any]:
  712. return dataclasses.asdict(self)
  713. @dataclasses.dataclass(frozen=True)
  714. class CameraConfig:
  715. name: str
  716. ffmpeg: CameraFfmpegConfig
  717. height: int
  718. width: int
  719. fps: Optional[int]
  720. best_image_timeout: int
  721. zones: Dict[str, ZoneConfig]
  722. clips: CameraClipsConfig
  723. record: RecordConfig
  724. rtmp: CameraRtmpConfig
  725. snapshots: CameraSnapshotsConfig
  726. mqtt: CameraMqttConfig
  727. objects: ObjectConfig
  728. motion: MotionConfig
  729. detect: DetectConfig
  730. timestamp_style: TimestampStyleConfig
  731. @property
  732. def frame_shape(self) -> Tuple[int, int]:
  733. return self.height, self.width
  734. @property
  735. def frame_shape_yuv(self) -> Tuple[int, int]:
  736. return self.height * 3 // 2, self.width
  737. @property
  738. def ffmpeg_cmds(self) -> List[Dict[str, List[str]]]:
  739. ffmpeg_cmds = []
  740. for ffmpeg_input in self.ffmpeg.inputs:
  741. ffmpeg_cmd = self._get_ffmpeg_cmd(ffmpeg_input)
  742. if ffmpeg_cmd is None:
  743. continue
  744. ffmpeg_cmds.append({"roles": ffmpeg_input.roles, "cmd": ffmpeg_cmd})
  745. return ffmpeg_cmds
  746. @classmethod
  747. def build(cls, name, config, global_config) -> CameraConfig:
  748. colors = plt.cm.get_cmap("tab10", len(config["zones"]))
  749. zones = {
  750. name: ZoneConfig.build(z, tuple(round(255 * c) for c in colors(idx)[:3]))
  751. for idx, (name, z) in enumerate(config["zones"].items())
  752. }
  753. frame_shape = config["height"], config["width"]
  754. return CameraConfig(
  755. name=name,
  756. ffmpeg=CameraFfmpegConfig.build(config["ffmpeg"], global_config["ffmpeg"]),
  757. height=config["height"],
  758. width=config["width"],
  759. fps=config.get("fps"),
  760. best_image_timeout=config["best_image_timeout"],
  761. zones=zones,
  762. clips=CameraClipsConfig.build(config["clips"], global_config),
  763. record=RecordConfig.build(config["record"], global_config["record"]),
  764. rtmp=CameraRtmpConfig.build(config["rtmp"]),
  765. snapshots=CameraSnapshotsConfig.build(config["snapshots"], global_config),
  766. mqtt=CameraMqttConfig.build(config["mqtt"]),
  767. objects=ObjectConfig.build(
  768. config.get("objects", {}), global_config["objects"], frame_shape
  769. ),
  770. motion=MotionConfig.build(
  771. config["motion"], global_config["motion"], frame_shape
  772. ),
  773. detect=DetectConfig.build(
  774. config["detect"], global_config["detect"], config.get("fps", 5)
  775. ),
  776. timestamp_style=TimestampStyleConfig.build(config["timestamp_style"]),
  777. )
  778. def _get_ffmpeg_cmd(self, ffmpeg_input):
  779. ffmpeg_output_args = []
  780. if "detect" in ffmpeg_input.roles:
  781. ffmpeg_output_args = (
  782. self.ffmpeg.output_args["detect"] + ffmpeg_output_args + ["pipe:"]
  783. )
  784. if self.fps:
  785. ffmpeg_output_args = ["-r", str(self.fps)] + ffmpeg_output_args
  786. if "rtmp" in ffmpeg_input.roles and self.rtmp.enabled:
  787. ffmpeg_output_args = (
  788. self.ffmpeg.output_args["rtmp"]
  789. + [f"rtmp://127.0.0.1/live/{self.name}"]
  790. + ffmpeg_output_args
  791. )
  792. if "clips" in ffmpeg_input.roles:
  793. ffmpeg_output_args = (
  794. self.ffmpeg.output_args["clips"]
  795. + [f"{os.path.join(CACHE_DIR, self.name)}-%Y%m%d%H%M%S.mp4"]
  796. + ffmpeg_output_args
  797. )
  798. if "record" in ffmpeg_input.roles and self.record.enabled:
  799. ffmpeg_output_args = (
  800. self.ffmpeg.output_args["record"]
  801. + [f"{os.path.join(RECORD_DIR, self.name)}-%Y%m%d%H%M%S.mp4"]
  802. + ffmpeg_output_args
  803. )
  804. # if there arent any outputs enabled for this input
  805. if len(ffmpeg_output_args) == 0:
  806. return None
  807. cmd = (
  808. ["ffmpeg"]
  809. + ffmpeg_input.global_args
  810. + ffmpeg_input.hwaccel_args
  811. + ffmpeg_input.input_args
  812. + ["-i", ffmpeg_input.path]
  813. + ffmpeg_output_args
  814. )
  815. return [part for part in cmd if part != ""]
  816. def to_dict(self) -> Dict[str, Any]:
  817. return {
  818. "name": self.name,
  819. "height": self.height,
  820. "width": self.width,
  821. "fps": self.fps,
  822. "best_image_timeout": self.best_image_timeout,
  823. "zones": {k: z.to_dict() for k, z in self.zones.items()},
  824. "clips": self.clips.to_dict(),
  825. "record": self.record.to_dict(),
  826. "rtmp": self.rtmp.to_dict(),
  827. "snapshots": self.snapshots.to_dict(),
  828. "mqtt": self.mqtt.to_dict(),
  829. "objects": self.objects.to_dict(),
  830. "motion": self.motion.to_dict(),
  831. "detect": self.detect.to_dict(),
  832. "frame_shape": self.frame_shape,
  833. "ffmpeg_cmds": [
  834. {"roles": c["roles"], "cmd": " ".join(c["cmd"])}
  835. for c in self.ffmpeg_cmds
  836. ],
  837. }
  838. FRIGATE_CONFIG_SCHEMA = vol.Schema(
  839. {
  840. vol.Optional("database", default={}): {
  841. vol.Optional("path", default=os.path.join(BASE_DIR, "frigate.db")): str
  842. },
  843. vol.Optional("model", default={"width": 320, "height": 320}): {
  844. vol.Required("width"): int,
  845. vol.Required("height"): int,
  846. },
  847. vol.Optional("detectors", default=DEFAULT_DETECTORS): DETECTORS_SCHEMA,
  848. "mqtt": MQTT_SCHEMA,
  849. vol.Optional("logger", default={}): {
  850. vol.Optional("default", default="info"): vol.In(
  851. ["info", "debug", "warning", "error", "critical"]
  852. ),
  853. vol.Optional("logs", default={}): {
  854. str: vol.In(["info", "debug", "warning", "error", "critical"])
  855. },
  856. },
  857. vol.Optional("snapshots", default={}): {
  858. vol.Optional("retain", default={}): RETAIN_SCHEMA
  859. },
  860. vol.Optional("clips", default={}): CLIPS_SCHEMA,
  861. vol.Optional("record", default={}): {
  862. vol.Optional("enabled", default=False): bool,
  863. vol.Optional("retain_days", default=30): int,
  864. },
  865. vol.Optional("ffmpeg", default={}): GLOBAL_FFMPEG_SCHEMA,
  866. vol.Optional("objects", default={}): OBJECTS_SCHEMA,
  867. vol.Optional("motion", default={}): MOTION_SCHEMA,
  868. vol.Optional("detect", default={}): GLOBAL_DETECT_SCHEMA,
  869. vol.Optional("birdseye", default={}): BIRDSEYE_SCHEMA,
  870. vol.Required("cameras", default={}): CAMERAS_SCHEMA,
  871. vol.Optional("environment_vars", default={}): {str: str},
  872. }
  873. )
  874. @dataclasses.dataclass(frozen=True)
  875. class DatabaseConfig:
  876. path: str
  877. @classmethod
  878. def build(cls, config) -> DatabaseConfig:
  879. return DatabaseConfig(config["path"])
  880. def to_dict(self) -> Dict[str, Any]:
  881. return dataclasses.asdict(self)
  882. @dataclasses.dataclass(frozen=True)
  883. class ModelConfig:
  884. width: int
  885. height: int
  886. @classmethod
  887. def build(cls, config) -> ModelConfig:
  888. return ModelConfig(config["width"], config["height"])
  889. def to_dict(self) -> Dict[str, Any]:
  890. return dataclasses.asdict(self)
  891. @dataclasses.dataclass(frozen=True)
  892. class LoggerConfig:
  893. default: str
  894. logs: Dict[str, str]
  895. @classmethod
  896. def build(cls, config) -> LoggerConfig:
  897. return LoggerConfig(
  898. config["default"].upper(),
  899. {k: v.upper() for k, v in config["logs"].items()},
  900. )
  901. def to_dict(self) -> Dict[str, Any]:
  902. return dataclasses.asdict(self)
  903. @dataclasses.dataclass(frozen=True)
  904. class SnapshotsConfig:
  905. retain: RetainConfig
  906. @classmethod
  907. def build(cls, config) -> SnapshotsConfig:
  908. return SnapshotsConfig(RetainConfig.build(config["retain"]))
  909. def to_dict(self) -> Dict[str, Any]:
  910. return {"retain": self.retain.to_dict()}
  911. @dataclasses.dataclass
  912. class RecordConfig:
  913. enabled: bool
  914. retain_days: int
  915. @classmethod
  916. def build(cls, config, global_config) -> RecordConfig:
  917. return RecordConfig(
  918. config.get("enabled", global_config["enabled"]),
  919. config.get("retain_days", global_config["retain_days"]),
  920. )
  921. def to_dict(self) -> Dict[str, Any]:
  922. return dataclasses.asdict(self)
  923. class FrigateConfig:
  924. def __init__(self, config_file=None, config=None) -> None:
  925. if config is None and config_file is None:
  926. raise ValueError("config or config_file must be defined")
  927. elif not config_file is None:
  928. config = self._load_file(config_file)
  929. config = FRIGATE_CONFIG_SCHEMA(config)
  930. config = self._sub_env_vars(config)
  931. self._database = DatabaseConfig.build(config["database"])
  932. self._model = ModelConfig.build(config["model"])
  933. self._detectors = {
  934. name: DetectorConfig.build(d) for name, d in config["detectors"].items()
  935. }
  936. self._mqtt = MqttConfig.build(config["mqtt"])
  937. self._clips = ClipsConfig.build(config["clips"])
  938. self._snapshots = SnapshotsConfig.build(config["snapshots"])
  939. self._birdseye = BirdseyeConfig.build(config["birdseye"])
  940. self._cameras = {
  941. name: CameraConfig.build(name, c, config)
  942. for name, c in config["cameras"].items()
  943. }
  944. self._logger = LoggerConfig.build(config["logger"])
  945. self._environment_vars = config["environment_vars"]
  946. def _sub_env_vars(self, config):
  947. frigate_env_vars = {
  948. k: v for k, v in os.environ.items() if k.startswith("FRIGATE_")
  949. }
  950. if "password" in config["mqtt"]:
  951. config["mqtt"]["password"] = config["mqtt"]["password"].format(
  952. **frigate_env_vars
  953. )
  954. for camera in config["cameras"].values():
  955. for i in camera["ffmpeg"]["inputs"]:
  956. i["path"] = i["path"].format(**frigate_env_vars)
  957. return config
  958. def _load_file(self, config_file):
  959. with open(config_file) as f:
  960. raw_config = f.read()
  961. if config_file.endswith(".yml"):
  962. config = yaml.safe_load(raw_config)
  963. elif config_file.endswith(".json"):
  964. config = json.loads(raw_config)
  965. return config
  966. def to_dict(self) -> Dict[str, Any]:
  967. return {
  968. "database": self.database.to_dict(),
  969. "model": self.model.to_dict(),
  970. "detectors": {k: d.to_dict() for k, d in self.detectors.items()},
  971. "mqtt": self.mqtt.to_dict(),
  972. "clips": self.clips.to_dict(),
  973. "snapshots": self.snapshots.to_dict(),
  974. "birdseye": self.birdseye.to_dict(),
  975. "cameras": {k: c.to_dict() for k, c in self.cameras.items()},
  976. "logger": self.logger.to_dict(),
  977. "environment_vars": self._environment_vars,
  978. }
  979. @property
  980. def database(self) -> DatabaseConfig:
  981. return self._database
  982. @property
  983. def model(self) -> ModelConfig:
  984. return self._model
  985. @property
  986. def detectors(self) -> Dict[str, DetectorConfig]:
  987. return self._detectors
  988. @property
  989. def logger(self) -> LoggerConfig:
  990. return self._logger
  991. @property
  992. def mqtt(self) -> MqttConfig:
  993. return self._mqtt
  994. @property
  995. def clips(self) -> ClipsConfig:
  996. return self._clips
  997. @property
  998. def snapshots(self) -> SnapshotsConfig:
  999. return self._snapshots
  1000. @property
  1001. def birdseye(self) -> BirdseyeConfig:
  1002. return self._birdseye
  1003. @property
  1004. def cameras(self) -> Dict[str, CameraConfig]:
  1005. return self._cameras
  1006. @property
  1007. def environment_vars(self):
  1008. return self._environment_vars