mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-02 04:46: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)
|
||||
except ValueError:
|
||||
logger.warning(f"Unable to parse trusted network: {proxy}")
|
||||
continue
|
||||
trusted_proxies.append(network)
|
||||
|
||||
# return the first remote address that is not trusted
|
||||
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})")
|
||||
trusted = False
|
||||
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):
|
||||
# TODO: ideally this would set secure as well, but that requires TLS
|
||||
# SameSite is intentionally left unset (browsers default to Lax). Setting
|
||||
# SameSite=Lax/Strict would stop the cookie from being sent in cross-origin
|
||||
# iframes, breaking embedded views such as the Home Assistant Frigate card.
|
||||
# CSRF is instead mitigated by requiring a custom X-CSRF-TOKEN header, which
|
||||
# cross-origin pages cannot set without a CORS preflight that Frigate never
|
||||
# grants (see check_csrf in api/fastapi_app.py).
|
||||
# Starlette sets SameSite=Lax by default. The cookie is still sent to
|
||||
# same-site iframes (e.g. Home Assistant on the same host or domain), but
|
||||
# not to cross-site ones. CSRF is also mitigated by requiring a custom
|
||||
# X-CSRF-TOKEN header, which cross-origin pages cannot set without a CORS
|
||||
# preflight that Frigate never grants (see check_csrf in api/fastapi_app.py).
|
||||
response.set_cookie(
|
||||
key=cookie_name,
|
||||
value=encoded_jwt,
|
||||
|
||||
@@ -782,7 +782,9 @@ class Dispatcher:
|
||||
try:
|
||||
payload = int(payload)
|
||||
except ValueError:
|
||||
f"Received unsupported value for motion contour area: {payload}"
|
||||
logger.warning(
|
||||
f"Received unsupported value for motion contour area: {payload}"
|
||||
)
|
||||
return
|
||||
|
||||
motion_settings = self.config.cameras[camera_name].motion
|
||||
@@ -799,7 +801,9 @@ class Dispatcher:
|
||||
try:
|
||||
payload = int(payload)
|
||||
except ValueError:
|
||||
f"Received unsupported value for motion threshold: {payload}"
|
||||
logger.warning(
|
||||
f"Received unsupported value for motion threshold: {payload}"
|
||||
)
|
||||
return
|
||||
|
||||
motion_settings = self.config.cameras[camera_name].motion
|
||||
@@ -814,7 +818,9 @@ class Dispatcher:
|
||||
def _on_global_notification_command(self, payload: str) -> None:
|
||||
"""Callback for global notification topic."""
|
||||
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
|
||||
|
||||
notification_settings = self.config.notifications
|
||||
|
||||
@@ -176,6 +176,7 @@ class LicensePlateProcessingMixin:
|
||||
"""
|
||||
input_shape = [3, 48, 320]
|
||||
num_images = len(images)
|
||||
outputs: list[np.ndarray] = []
|
||||
|
||||
for index in range(0, num_images, self.batch_size):
|
||||
input_h, input_w = input_shape[1], input_shape[2]
|
||||
@@ -195,11 +196,11 @@ class LicensePlateProcessingMixin:
|
||||
norm_image = norm_image[np.newaxis, :]
|
||||
norm_images.append(norm_image)
|
||||
|
||||
try:
|
||||
outputs = self.model_runner.recognition_model(norm_images) # type: ignore[arg-type]
|
||||
except Exception as e:
|
||||
logger.warning(f"Error running LPR recognition model: {e}")
|
||||
return [], []
|
||||
try:
|
||||
outputs.extend(self.model_runner.recognition_model(norm_images)) # type: ignore[arg-type]
|
||||
except Exception as e:
|
||||
logger.warning(f"Error running LPR recognition model: {e}")
|
||||
return [], []
|
||||
|
||||
return self.ctc_decoder(outputs)
|
||||
|
||||
|
||||
@@ -357,6 +357,9 @@ class Embeddings:
|
||||
|
||||
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 = (
|
||||
Event.select()
|
||||
.order_by(Event.start_time.desc())
|
||||
|
||||
@@ -1397,9 +1397,6 @@ class PtzAutoTracker:
|
||||
def is_autotracking(self, camera: str):
|
||||
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):
|
||||
if camera not in self.config.cameras:
|
||||
return
|
||||
@@ -1538,8 +1535,6 @@ class PtzAutoTracker:
|
||||
# returns camera to preset after timeout when tracking is over
|
||||
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
|
||||
if not self.ptz_metrics[camera].motor_stopped.is_set():
|
||||
await self.onvif.get_camera_status(camera)
|
||||
|
||||
@@ -966,6 +966,10 @@ class OnvifController:
|
||||
}
|
||||
else:
|
||||
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:
|
||||
logger.error(
|
||||
f"Error during ONVIF initialization for {camera_name}: {e}"
|
||||
|
||||
@@ -1375,7 +1375,7 @@ class RecordingExporter(threading.Thread):
|
||||
|
||||
if preview.end_time > self.end_time:
|
||||
playlist_lines.append(
|
||||
f"outpoint {int(preview.end_time - self.end_time)}"
|
||||
f"outpoint {int(self.end_time - preview.start_time)}"
|
||||
)
|
||||
|
||||
ffmpeg_input = (
|
||||
|
||||
@@ -257,6 +257,31 @@ class TestMigrateConfigFile(unittest.TestCase):
|
||||
self.assertEqual(migrated["models"][0]["devices"], ["openvino:GPU"])
|
||||
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):
|
||||
migrated = self._migrate(
|
||||
"mqtt:\n"
|
||||
|
||||
@@ -191,7 +191,7 @@ def migrate_frigate_config(config_file: str):
|
||||
|
||||
if previous_version < "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:
|
||||
yaml.dump(new_config, f)
|
||||
previous_version = "0.14"
|
||||
@@ -209,35 +209,35 @@ def migrate_frigate_config(config_file: str):
|
||||
|
||||
if previous_version < "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:
|
||||
yaml.dump(new_config, f)
|
||||
previous_version = "0.15-0"
|
||||
|
||||
if previous_version < "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:
|
||||
yaml.dump(new_config, f)
|
||||
previous_version = "0.15-1"
|
||||
|
||||
if previous_version < "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:
|
||||
yaml.dump(new_config, f)
|
||||
previous_version = "0.16-0"
|
||||
|
||||
if previous_version < "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:
|
||||
yaml.dump(new_config, f)
|
||||
previous_version = "0.17-0"
|
||||
|
||||
if previous_version < "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:
|
||||
yaml.dump(new_config, f)
|
||||
previous_version = "0.18-0"
|
||||
|
||||
@@ -1162,7 +1162,9 @@
|
||||
"actions": "Actions",
|
||||
"noRoles": "No custom roles found.",
|
||||
"editCameras": "Edit Cameras",
|
||||
"deleteRole": "Delete Role"
|
||||
"deleteRole": "Delete Role",
|
||||
"cameraCount_one": "{{count}} camera",
|
||||
"cameraCount_other": "{{count}} cameras"
|
||||
},
|
||||
"toast": {
|
||||
"success": {
|
||||
|
||||
@@ -627,7 +627,11 @@ export default function NotificationsSettingsExtras({
|
||||
<SettingsGroupCard title={t("notification.deviceSpecific")}>
|
||||
<div className={cn("space-y-2", isAdmin && "md:max-w-[50%]")}>
|
||||
<Button
|
||||
aria-label={t("notification.registerDevice")}
|
||||
aria-label={
|
||||
registration != null
|
||||
? t("notification.unregisterDevice")
|
||||
: t("notification.registerDevice")
|
||||
}
|
||||
className="w-full md:w-auto"
|
||||
disabled={!shouldFetchPubKey || publicKey == undefined}
|
||||
onClick={() => {
|
||||
|
||||
@@ -218,7 +218,7 @@ export default function InputWithTags({
|
||||
}
|
||||
|
||||
return current_suggestions.filter((suggestion) =>
|
||||
suggestion.toLowerCase().startsWith(currentWord),
|
||||
suggestion.toLowerCase().startsWith(currentWord.toLowerCase()),
|
||||
);
|
||||
},
|
||||
[inputValue, suggestions, currentFilterType],
|
||||
|
||||
@@ -631,7 +631,7 @@ export function SpeedFilterContent({
|
||||
const value = e.target.value;
|
||||
|
||||
if (value) {
|
||||
setSpeedRange(parseInt(value), maxSpeed ?? 1.0);
|
||||
setSpeedRange(parseInt(value), maxSpeed ?? 150);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -287,7 +287,6 @@ function ConfigEditor() {
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={!hasChanges}
|
||||
className="flex items-center gap-2"
|
||||
aria-label={t("saveAndRestart")}
|
||||
onClick={handleSaveAndRestart}
|
||||
@@ -300,7 +299,6 @@ function ConfigEditor() {
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={!hasChanges}
|
||||
className="flex items-center gap-2"
|
||||
aria-label={t("saveOnly")}
|
||||
onClick={() => onHandleSaveConfig("saveonly")}
|
||||
|
||||
@@ -657,7 +657,9 @@ export default function AuthenticationView({
|
||||
</Badge>
|
||||
) : roleData.cameras.length > 5 ? (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{roleData.cameras.length} cameras
|
||||
{t("roles.table.cameraCount", {
|
||||
count: roleData.cameras.length,
|
||||
})}
|
||||
</Badge>
|
||||
) : (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
|
||||
@@ -661,7 +661,10 @@ export default function ProfilesView({
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => setAddDialogOpen(false)}
|
||||
onClick={() => {
|
||||
setAddDialogOpen(false);
|
||||
addForm.reset();
|
||||
}}
|
||||
disabled={addingProfile}
|
||||
>
|
||||
{t("button.cancel", { ns: "common" })}
|
||||
|
||||
Reference in New Issue
Block a user