mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-07 07:12:50 +03:00
Miscellaneous fixes (#24528)
* revert disable save buttons when there are no changes in config editor * pass migrated config to each step in the config migration chain * use 150 as the default max when typing a min speed in the search filter * fix preview export outpoint to be relative to the start of the preview file * don't crash on invalid trusted proxy entries or non-ip forwarded hops * translate the camera count badge in the roles table * run every batch through the lpr recognition model * log rejected motion and notification mqtt payloads * log invalid addresses in x-forwarded-for * add test * remove unused autotracked_object_region * remove unreachable autotracker setup call in camera maintenance * apply onvif retry limit when initialization fails * fix reindex progress overcounting when there are fewer events than a batch * match the register device button aria label to its text * reset the add profile form on cancel * ignore case when filtering search suggestions * tweak comment
This commit is contained in:
+12
-7
@@ -324,11 +324,17 @@ def get_remote_addr(request: Request):
|
|||||||
network = ipaddress.ip_network(proxy)
|
network = ipaddress.ip_network(proxy)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
logger.warning(f"Unable to parse trusted network: {proxy}")
|
logger.warning(f"Unable to parse trusted network: {proxy}")
|
||||||
|
continue
|
||||||
trusted_proxies.append(network)
|
trusted_proxies.append(network)
|
||||||
|
|
||||||
# return the first remote address that is not trusted
|
# return the first remote address that is not trusted
|
||||||
for addr in route:
|
for addr in route:
|
||||||
ip = ipaddress.ip_address(addr.strip())
|
try:
|
||||||
|
ip = ipaddress.ip_address(addr.strip())
|
||||||
|
except ValueError:
|
||||||
|
logger.debug("Invalid address in X-Forwarded-For header")
|
||||||
|
return direct_addr or "127.0.0.1"
|
||||||
|
|
||||||
logger.debug(f"Checking {ip} (v{ip.version})")
|
logger.debug(f"Checking {ip} (v{ip.version})")
|
||||||
trusted = False
|
trusted = False
|
||||||
for trusted_proxy in trusted_proxies:
|
for trusted_proxy in trusted_proxies:
|
||||||
@@ -473,12 +479,11 @@ def create_encoded_jwt(user, role, expiration, secret):
|
|||||||
|
|
||||||
def set_jwt_cookie(response: Response, cookie_name, encoded_jwt, max_age, secure):
|
def set_jwt_cookie(response: Response, cookie_name, encoded_jwt, max_age, secure):
|
||||||
# TODO: ideally this would set secure as well, but that requires TLS
|
# TODO: ideally this would set secure as well, but that requires TLS
|
||||||
# SameSite is intentionally left unset (browsers default to Lax). Setting
|
# Starlette sets SameSite=Lax by default. The cookie is still sent to
|
||||||
# SameSite=Lax/Strict would stop the cookie from being sent in cross-origin
|
# same-site iframes (e.g. Home Assistant on the same host or domain), but
|
||||||
# iframes, breaking embedded views such as the Home Assistant Frigate card.
|
# not to cross-site ones. CSRF is also mitigated by requiring a custom
|
||||||
# CSRF is instead mitigated by requiring a custom X-CSRF-TOKEN header, which
|
# X-CSRF-TOKEN header, which cross-origin pages cannot set without a CORS
|
||||||
# cross-origin pages cannot set without a CORS preflight that Frigate never
|
# preflight that Frigate never grants (see check_csrf in api/fastapi_app.py).
|
||||||
# grants (see check_csrf in api/fastapi_app.py).
|
|
||||||
response.set_cookie(
|
response.set_cookie(
|
||||||
key=cookie_name,
|
key=cookie_name,
|
||||||
value=encoded_jwt,
|
value=encoded_jwt,
|
||||||
|
|||||||
@@ -782,7 +782,9 @@ class Dispatcher:
|
|||||||
try:
|
try:
|
||||||
payload = int(payload)
|
payload = int(payload)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
f"Received unsupported value for motion contour area: {payload}"
|
logger.warning(
|
||||||
|
f"Received unsupported value for motion contour area: {payload}"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
motion_settings = self.config.cameras[camera_name].motion
|
motion_settings = self.config.cameras[camera_name].motion
|
||||||
@@ -799,7 +801,9 @@ class Dispatcher:
|
|||||||
try:
|
try:
|
||||||
payload = int(payload)
|
payload = int(payload)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
f"Received unsupported value for motion threshold: {payload}"
|
logger.warning(
|
||||||
|
f"Received unsupported value for motion threshold: {payload}"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
motion_settings = self.config.cameras[camera_name].motion
|
motion_settings = self.config.cameras[camera_name].motion
|
||||||
@@ -814,7 +818,9 @@ class Dispatcher:
|
|||||||
def _on_global_notification_command(self, payload: str) -> None:
|
def _on_global_notification_command(self, payload: str) -> None:
|
||||||
"""Callback for global notification topic."""
|
"""Callback for global notification topic."""
|
||||||
if payload != "ON" and payload != "OFF":
|
if payload != "ON" and payload != "OFF":
|
||||||
f"Received unsupported value for all notification: {payload}"
|
logger.warning(
|
||||||
|
f"Received unsupported value for all notification: {payload}"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
notification_settings = self.config.notifications
|
notification_settings = self.config.notifications
|
||||||
|
|||||||
@@ -176,6 +176,7 @@ class LicensePlateProcessingMixin:
|
|||||||
"""
|
"""
|
||||||
input_shape = [3, 48, 320]
|
input_shape = [3, 48, 320]
|
||||||
num_images = len(images)
|
num_images = len(images)
|
||||||
|
outputs: list[np.ndarray] = []
|
||||||
|
|
||||||
for index in range(0, num_images, self.batch_size):
|
for index in range(0, num_images, self.batch_size):
|
||||||
input_h, input_w = input_shape[1], input_shape[2]
|
input_h, input_w = input_shape[1], input_shape[2]
|
||||||
@@ -195,11 +196,11 @@ class LicensePlateProcessingMixin:
|
|||||||
norm_image = norm_image[np.newaxis, :]
|
norm_image = norm_image[np.newaxis, :]
|
||||||
norm_images.append(norm_image)
|
norm_images.append(norm_image)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
outputs = self.model_runner.recognition_model(norm_images) # type: ignore[arg-type]
|
outputs.extend(self.model_runner.recognition_model(norm_images)) # type: ignore[arg-type]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Error running LPR recognition model: {e}")
|
logger.warning(f"Error running LPR recognition model: {e}")
|
||||||
return [], []
|
return [], []
|
||||||
|
|
||||||
return self.ctc_decoder(outputs)
|
return self.ctc_decoder(outputs)
|
||||||
|
|
||||||
|
|||||||
@@ -357,6 +357,9 @@ class Embeddings:
|
|||||||
|
|
||||||
self.requestor.send_data(UPDATE_EMBEDDINGS_REINDEX_PROGRESS, totals)
|
self.requestor.send_data(UPDATE_EMBEDDINGS_REINDEX_PROGRESS, totals)
|
||||||
|
|
||||||
|
# a single batch sends no progress, so the first message shows it nearly done
|
||||||
|
totals["processed_objects"] = 0
|
||||||
|
|
||||||
events = (
|
events = (
|
||||||
Event.select()
|
Event.select()
|
||||||
.order_by(Event.start_time.desc())
|
.order_by(Event.start_time.desc())
|
||||||
|
|||||||
@@ -1397,9 +1397,6 @@ class PtzAutoTracker:
|
|||||||
def is_autotracking(self, camera: str):
|
def is_autotracking(self, camera: str):
|
||||||
return self.tracked_object[camera] is not None
|
return self.tracked_object[camera] is not None
|
||||||
|
|
||||||
def autotracked_object_region(self, camera: str):
|
|
||||||
return self.tracked_object[camera]["region"]
|
|
||||||
|
|
||||||
def autotrack_object(self, camera: str, obj: TrackedObject):
|
def autotrack_object(self, camera: str, obj: TrackedObject):
|
||||||
if camera not in self.config.cameras:
|
if camera not in self.config.cameras:
|
||||||
return
|
return
|
||||||
@@ -1538,8 +1535,6 @@ class PtzAutoTracker:
|
|||||||
# returns camera to preset after timeout when tracking is over
|
# returns camera to preset after timeout when tracking is over
|
||||||
autotracker_config = self.config.cameras[camera].onvif.autotracking
|
autotracker_config = self.config.cameras[camera].onvif.autotracking
|
||||||
|
|
||||||
if not self.autotracker_init[camera]:
|
|
||||||
self._autotracker_setup(self.config.cameras[camera], camera)
|
|
||||||
# regularly update camera status
|
# regularly update camera status
|
||||||
if not self.ptz_metrics[camera].motor_stopped.is_set():
|
if not self.ptz_metrics[camera].motor_stopped.is_set():
|
||||||
await self.onvif.get_camera_status(camera)
|
await self.onvif.get_camera_status(camera)
|
||||||
|
|||||||
@@ -966,6 +966,10 @@ class OnvifController:
|
|||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
logger.warning(f"ONVIF initialization failed for {camera_name}")
|
logger.warning(f"ONVIF initialization failed for {camera_name}")
|
||||||
|
self.failed_cams[camera_name] = {
|
||||||
|
"retry_attempts": attempts + 1,
|
||||||
|
"last_attempt": time.time(),
|
||||||
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(
|
logger.error(
|
||||||
f"Error during ONVIF initialization for {camera_name}: {e}"
|
f"Error during ONVIF initialization for {camera_name}: {e}"
|
||||||
|
|||||||
@@ -1375,7 +1375,7 @@ class RecordingExporter(threading.Thread):
|
|||||||
|
|
||||||
if preview.end_time > self.end_time:
|
if preview.end_time > self.end_time:
|
||||||
playlist_lines.append(
|
playlist_lines.append(
|
||||||
f"outpoint {int(preview.end_time - self.end_time)}"
|
f"outpoint {int(self.end_time - preview.start_time)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
ffmpeg_input = (
|
ffmpeg_input = (
|
||||||
|
|||||||
@@ -257,6 +257,31 @@ class TestMigrateConfigFile(unittest.TestCase):
|
|||||||
self.assertEqual(migrated["models"][0]["devices"], ["openvino:GPU"])
|
self.assertEqual(migrated["models"][0]["devices"], ["openvino:GPU"])
|
||||||
self.assertNotIn("detectors", migrated)
|
self.assertNotIn("detectors", migrated)
|
||||||
|
|
||||||
|
def test_top_level_changes_survive_later_steps(self):
|
||||||
|
# 0.16 adds detect and 0.17 splits genai, both at the top level
|
||||||
|
migrated = self._migrate(
|
||||||
|
"mqtt:\n"
|
||||||
|
" enabled: false\n"
|
||||||
|
"genai:\n"
|
||||||
|
" provider: ollama\n"
|
||||||
|
" model: llava\n"
|
||||||
|
" prompt: describe it\n"
|
||||||
|
"cameras: {}\n"
|
||||||
|
"version: 0.15-1\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(migrated["version"], CURRENT_CONFIG_VERSION)
|
||||||
|
self.assertTrue(migrated["detect"]["enabled"])
|
||||||
|
self.assertEqual(migrated["objects"]["genai"], {"prompt": "describe it"})
|
||||||
|
self.assertEqual(
|
||||||
|
migrated["genai"]["default"],
|
||||||
|
{
|
||||||
|
"provider": "ollama",
|
||||||
|
"model": "llava",
|
||||||
|
"roles": ["descriptions", "chat"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
def test_a_migrated_config_is_left_alone(self):
|
def test_a_migrated_config_is_left_alone(self):
|
||||||
migrated = self._migrate(
|
migrated = self._migrate(
|
||||||
"mqtt:\n"
|
"mqtt:\n"
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ def migrate_frigate_config(config_file: str):
|
|||||||
|
|
||||||
if previous_version < "0.14":
|
if previous_version < "0.14":
|
||||||
logger.info(f"Migrating frigate config from {previous_version} to 0.14...")
|
logger.info(f"Migrating frigate config from {previous_version} to 0.14...")
|
||||||
new_config = migrate_014(config)
|
new_config = migrate_014(new_config)
|
||||||
with open(config_file, "w") as f:
|
with open(config_file, "w") as f:
|
||||||
yaml.dump(new_config, f)
|
yaml.dump(new_config, f)
|
||||||
previous_version = "0.14"
|
previous_version = "0.14"
|
||||||
@@ -209,35 +209,35 @@ def migrate_frigate_config(config_file: str):
|
|||||||
|
|
||||||
if previous_version < "0.15-0":
|
if previous_version < "0.15-0":
|
||||||
logger.info(f"Migrating frigate config from {previous_version} to 0.15-0...")
|
logger.info(f"Migrating frigate config from {previous_version} to 0.15-0...")
|
||||||
new_config = migrate_015_0(config)
|
new_config = migrate_015_0(new_config)
|
||||||
with open(config_file, "w") as f:
|
with open(config_file, "w") as f:
|
||||||
yaml.dump(new_config, f)
|
yaml.dump(new_config, f)
|
||||||
previous_version = "0.15-0"
|
previous_version = "0.15-0"
|
||||||
|
|
||||||
if previous_version < "0.15-1":
|
if previous_version < "0.15-1":
|
||||||
logger.info(f"Migrating frigate config from {previous_version} to 0.15-1...")
|
logger.info(f"Migrating frigate config from {previous_version} to 0.15-1...")
|
||||||
new_config = migrate_015_1(config)
|
new_config = migrate_015_1(new_config)
|
||||||
with open(config_file, "w") as f:
|
with open(config_file, "w") as f:
|
||||||
yaml.dump(new_config, f)
|
yaml.dump(new_config, f)
|
||||||
previous_version = "0.15-1"
|
previous_version = "0.15-1"
|
||||||
|
|
||||||
if previous_version < "0.16-0":
|
if previous_version < "0.16-0":
|
||||||
logger.info(f"Migrating frigate config from {previous_version} to 0.16-0...")
|
logger.info(f"Migrating frigate config from {previous_version} to 0.16-0...")
|
||||||
new_config = migrate_016_0(config)
|
new_config = migrate_016_0(new_config)
|
||||||
with open(config_file, "w") as f:
|
with open(config_file, "w") as f:
|
||||||
yaml.dump(new_config, f)
|
yaml.dump(new_config, f)
|
||||||
previous_version = "0.16-0"
|
previous_version = "0.16-0"
|
||||||
|
|
||||||
if previous_version < "0.17-0":
|
if previous_version < "0.17-0":
|
||||||
logger.info(f"Migrating frigate config from {previous_version} to 0.17-0...")
|
logger.info(f"Migrating frigate config from {previous_version} to 0.17-0...")
|
||||||
new_config = migrate_017_0(config)
|
new_config = migrate_017_0(new_config)
|
||||||
with open(config_file, "w") as f:
|
with open(config_file, "w") as f:
|
||||||
yaml.dump(new_config, f)
|
yaml.dump(new_config, f)
|
||||||
previous_version = "0.17-0"
|
previous_version = "0.17-0"
|
||||||
|
|
||||||
if previous_version < "0.18-0":
|
if previous_version < "0.18-0":
|
||||||
logger.info(f"Migrating frigate config from {previous_version} to 0.18-0...")
|
logger.info(f"Migrating frigate config from {previous_version} to 0.18-0...")
|
||||||
new_config = migrate_018_0(config)
|
new_config = migrate_018_0(new_config)
|
||||||
with open(config_file, "w") as f:
|
with open(config_file, "w") as f:
|
||||||
yaml.dump(new_config, f)
|
yaml.dump(new_config, f)
|
||||||
previous_version = "0.18-0"
|
previous_version = "0.18-0"
|
||||||
|
|||||||
@@ -1162,7 +1162,9 @@
|
|||||||
"actions": "Actions",
|
"actions": "Actions",
|
||||||
"noRoles": "No custom roles found.",
|
"noRoles": "No custom roles found.",
|
||||||
"editCameras": "Edit Cameras",
|
"editCameras": "Edit Cameras",
|
||||||
"deleteRole": "Delete Role"
|
"deleteRole": "Delete Role",
|
||||||
|
"cameraCount_one": "{{count}} camera",
|
||||||
|
"cameraCount_other": "{{count}} cameras"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"success": {
|
"success": {
|
||||||
|
|||||||
@@ -627,7 +627,11 @@ export default function NotificationsSettingsExtras({
|
|||||||
<SettingsGroupCard title={t("notification.deviceSpecific")}>
|
<SettingsGroupCard title={t("notification.deviceSpecific")}>
|
||||||
<div className={cn("space-y-2", isAdmin && "md:max-w-[50%]")}>
|
<div className={cn("space-y-2", isAdmin && "md:max-w-[50%]")}>
|
||||||
<Button
|
<Button
|
||||||
aria-label={t("notification.registerDevice")}
|
aria-label={
|
||||||
|
registration != null
|
||||||
|
? t("notification.unregisterDevice")
|
||||||
|
: t("notification.registerDevice")
|
||||||
|
}
|
||||||
className="w-full md:w-auto"
|
className="w-full md:w-auto"
|
||||||
disabled={!shouldFetchPubKey || publicKey == undefined}
|
disabled={!shouldFetchPubKey || publicKey == undefined}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ export default function InputWithTags({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return current_suggestions.filter((suggestion) =>
|
return current_suggestions.filter((suggestion) =>
|
||||||
suggestion.toLowerCase().startsWith(currentWord),
|
suggestion.toLowerCase().startsWith(currentWord.toLowerCase()),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[inputValue, suggestions, currentFilterType],
|
[inputValue, suggestions, currentFilterType],
|
||||||
|
|||||||
@@ -631,7 +631,7 @@ export function SpeedFilterContent({
|
|||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
|
|
||||||
if (value) {
|
if (value) {
|
||||||
setSpeedRange(parseInt(value), maxSpeed ?? 1.0);
|
setSpeedRange(parseInt(value), maxSpeed ?? 150);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -287,7 +287,6 @@ function ConfigEditor() {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
disabled={!hasChanges}
|
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
aria-label={t("saveAndRestart")}
|
aria-label={t("saveAndRestart")}
|
||||||
onClick={handleSaveAndRestart}
|
onClick={handleSaveAndRestart}
|
||||||
@@ -300,7 +299,6 @@ function ConfigEditor() {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
disabled={!hasChanges}
|
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
aria-label={t("saveOnly")}
|
aria-label={t("saveOnly")}
|
||||||
onClick={() => onHandleSaveConfig("saveonly")}
|
onClick={() => onHandleSaveConfig("saveonly")}
|
||||||
|
|||||||
@@ -657,7 +657,9 @@ export default function AuthenticationView({
|
|||||||
</Badge>
|
</Badge>
|
||||||
) : roleData.cameras.length > 5 ? (
|
) : roleData.cameras.length > 5 ? (
|
||||||
<Badge variant="outline" className="text-xs">
|
<Badge variant="outline" className="text-xs">
|
||||||
{roleData.cameras.length} cameras
|
{t("roles.table.cameraCount", {
|
||||||
|
count: roleData.cameras.length,
|
||||||
|
})}
|
||||||
</Badge>
|
</Badge>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
|
|||||||
@@ -661,7 +661,10 @@ export default function ProfilesView({
|
|||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setAddDialogOpen(false)}
|
onClick={() => {
|
||||||
|
setAddDialogOpen(false);
|
||||||
|
addForm.reset();
|
||||||
|
}}
|
||||||
disabled={addingProfile}
|
disabled={addingProfile}
|
||||||
>
|
>
|
||||||
{t("button.cancel", { ns: "common" })}
|
{t("button.cancel", { ns: "common" })}
|
||||||
|
|||||||
Reference in New Issue
Block a user