mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-05 10:45:21 +03:00
Update YAML file from URL query parameters in frigate/http.py and add functionality to save motion masks, zones, and object masks in CameraMap.jsx
This commit is contained in:
parent
ee9bac3500
commit
0bed28d175
@ -44,7 +44,7 @@ from frigate.util import (
|
||||
get_tz_modifiers,
|
||||
restart_frigate,
|
||||
vainfo_hwaccel,
|
||||
update_yaml_file,
|
||||
update_yaml_from_url,
|
||||
)
|
||||
from frigate.version import VERSION
|
||||
|
||||
@ -1019,10 +1019,36 @@ def config_set():
|
||||
if os.path.isfile(config_file_yaml):
|
||||
config_file = config_file_yaml
|
||||
|
||||
for key, value in request.args:
|
||||
logging.debug(f"Update config key {key} to {value}")
|
||||
keys = key.split(".")
|
||||
update_yaml_file(config_file, keys, value)
|
||||
with open(config_file, "r") as f:
|
||||
old_raw_config = f.read()
|
||||
f.close()
|
||||
|
||||
try:
|
||||
update_yaml_from_url(config_file, request.url)
|
||||
with open(config_file, "r") as f:
|
||||
new_raw_config = f.read()
|
||||
f.close()
|
||||
# Validate the config schema
|
||||
try:
|
||||
FrigateConfig.parse_raw(new_raw_config)
|
||||
except Exception:
|
||||
with open(config_file, "w") as f:
|
||||
f.write(old_raw_config)
|
||||
f.close()
|
||||
return make_response(
|
||||
jsonify(
|
||||
{
|
||||
"success": False,
|
||||
"message": f"\nConfig Error:\n\n{str(traceback.format_exc())}",
|
||||
}
|
||||
),
|
||||
400,
|
||||
)
|
||||
except Exception as e:
|
||||
logging.error(f"Error updating config: {e}")
|
||||
return "Error updating config", 500
|
||||
|
||||
return "Config successfully updated", 200
|
||||
|
||||
|
||||
@bp.route("/config/schema.json")
|
||||
|
||||
@ -1213,18 +1213,64 @@ def get_video_properties(url, get_duration=False):
|
||||
return result
|
||||
|
||||
|
||||
def update_yaml_from_url(file_path, url):
|
||||
parsed_url = urllib.parse.urlparse(url)
|
||||
query_string = urllib.parse.parse_qs(parsed_url.query, keep_blank_values=True)
|
||||
|
||||
for key_path_str, new_value_list in query_string.items():
|
||||
key_path = key_path_str.split(".")
|
||||
for i in range(len(key_path)):
|
||||
try:
|
||||
index = int(key_path[i])
|
||||
key_path[i] = (key_path[i - 1], index)
|
||||
key_path.pop(i - 1)
|
||||
except ValueError:
|
||||
pass
|
||||
new_value = new_value_list[0]
|
||||
update_yaml_file(file_path, key_path, new_value)
|
||||
|
||||
|
||||
def update_yaml_file(file_path, key_path, new_value):
|
||||
yaml = YAML()
|
||||
with open(file_path, "r") as f:
|
||||
data = yaml.safe_load(f)
|
||||
data = yaml.load(f)
|
||||
|
||||
temp = data
|
||||
for key in key_path[:-1]:
|
||||
if key not in temp:
|
||||
temp[key] = {}
|
||||
temp = temp[key]
|
||||
|
||||
temp[key_path[-1]] = new_value
|
||||
if isinstance(key, tuple):
|
||||
if key[0] not in temp:
|
||||
temp[key[0]] = [{}] * max(1, key[1] + 1)
|
||||
elif len(temp[key[0]]) <= key[1]:
|
||||
temp[key[0]] += [{}] * (key[1] - len(temp[key[0]]) + 1)
|
||||
temp = temp[key[0]][key[1]]
|
||||
else:
|
||||
if key not in temp:
|
||||
temp[key] = {}
|
||||
temp = temp[key]
|
||||
print(new_value)
|
||||
last_key = key_path[-1]
|
||||
if new_value == "":
|
||||
print(last_key)
|
||||
if isinstance(last_key, tuple):
|
||||
del temp[last_key[0]][last_key[1]]
|
||||
else:
|
||||
del temp[last_key]
|
||||
else:
|
||||
if isinstance(last_key, tuple):
|
||||
if last_key[0] not in temp:
|
||||
temp[last_key[0]] = [{}] * max(1, last_key[1] + 1)
|
||||
elif len(temp[last_key[0]]) <= last_key[1]:
|
||||
temp[last_key[0]] += [{}] * (last_key[1] - len(temp[last_key[0]]) + 1)
|
||||
temp[last_key[0]][last_key[1]] = new_value
|
||||
else:
|
||||
if (
|
||||
last_key in temp
|
||||
and isinstance(temp[last_key], dict)
|
||||
and isinstance(new_value, dict)
|
||||
):
|
||||
temp[last_key].update(new_value)
|
||||
else:
|
||||
temp[last_key] = new_value
|
||||
|
||||
with open(file_path, "w") as f:
|
||||
yaml.dump(data, f)
|
||||
|
||||
@ -7,7 +7,7 @@ import { useResizeObserver } from '../hooks';
|
||||
import { useCallback, useMemo, useRef, useState } from 'preact/hooks';
|
||||
import { useApiHost } from '../api';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import axios from 'axios';
|
||||
export default function CameraMasks({ camera }) {
|
||||
const { data: config } = useSWR('config');
|
||||
const apiHost = useApiHost();
|
||||
@ -101,6 +101,23 @@ export default function CameraMasks({ camera }) {
|
||||
${motionMaskPoints.map((mask) => ` - ${polylinePointsToPolyline(mask)}`).join('\n')}`);
|
||||
}, [motionMaskPoints]);
|
||||
|
||||
const handleSaveMotionMasks = useCallback(async () => {
|
||||
try {
|
||||
const queryParameters = motionMaskPoints
|
||||
.map((mask, index) => `cameras.${camera}.motion.mask.${index}=${polylinePointsToPolyline(mask)}`)
|
||||
.join('&');
|
||||
const endpoint = `config/set?${queryParameters}`;
|
||||
const response = await axios.put(endpoint);
|
||||
if (response.status === 200) {
|
||||
// handle successful response
|
||||
}
|
||||
} catch (error) {
|
||||
// handle error
|
||||
console.error(error);
|
||||
}
|
||||
}, [motionMaskPoints]);
|
||||
|
||||
|
||||
// Zone methods
|
||||
const handleEditZone = useCallback(
|
||||
(key) => {
|
||||
@ -131,9 +148,47 @@ ${motionMaskPoints.map((mask) => ` - ${polylinePointsToPolyline(mask)}`).jo
|
||||
${Object.keys(zonePoints)
|
||||
.map(
|
||||
(zoneName) => ` ${zoneName}:
|
||||
coordinates: ${polylinePointsToPolyline(zonePoints[zoneName])}`
|
||||
)
|
||||
.join('\n')}`);
|
||||
coordinates: ${polylinePointsToPolyline(zonePoints[zoneName])}`).join('\n')}`);
|
||||
|
||||
if (window.navigator.clipboard && window.navigator.clipboard.writeText) {
|
||||
// Use Clipboard API if available
|
||||
window.navigator.clipboard.writeText(textToCopy).catch((err) => {
|
||||
console.error('Failed to copy text: ', err);
|
||||
});
|
||||
} else {
|
||||
// Fallback to document.execCommand('copy')
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = textToCopy;
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
|
||||
try {
|
||||
const successful = document.execCommand('copy');
|
||||
if (!successful) {
|
||||
throw new Error('Failed to copy text');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to copy text: ', err);
|
||||
}
|
||||
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
}, [zonePoints]);
|
||||
|
||||
const handleSaveZones = useCallback(async () => {
|
||||
try {
|
||||
const queryParameters = Object.keys(zonePoints)
|
||||
.map((zoneName, index) => `cameras.${camera}.zones.${zoneName}.coordinates.${index}=${polylinePointsToPolyline(zonePoints[zoneName])}`)
|
||||
.join('&');
|
||||
const endpoint = `config/set?${queryParameters}`;
|
||||
const response = await axios.put(endpoint);
|
||||
if (response.status === 200) {
|
||||
// handle successful response
|
||||
}
|
||||
} catch (error) {
|
||||
// handle error
|
||||
console.error(error);
|
||||
}
|
||||
}, [zonePoints]);
|
||||
|
||||
// Object methods
|
||||
@ -175,6 +230,23 @@ ${Object.keys(objectMaskPoints)
|
||||
.join('\n')}`);
|
||||
}, [objectMaskPoints]);
|
||||
|
||||
const handleSaveObjectMasks = useCallback(async () => {
|
||||
try {
|
||||
const queryParameters = Object.keys(objectMaskPoints)
|
||||
.filter((objectName) => objectMaskPoints[objectName].length > 0)
|
||||
.map((objectName, index) => `cameras.${camera}.objects.filters.${objectName}.mask.${index}=${polylinePointsToPolyline(objectMaskPoints[objectName])}`)
|
||||
.join('&');
|
||||
const endpoint = `config/set?${queryParameters}`;
|
||||
const response = await axios.put(endpoint);
|
||||
if (response.status === 200) {
|
||||
// handle successful response
|
||||
}
|
||||
} catch (error) {
|
||||
// handle error
|
||||
console.error(error);
|
||||
}
|
||||
}, [objectMaskPoints]);
|
||||
|
||||
const handleAddToObjectMask = useCallback(
|
||||
(key) => {
|
||||
const newObjectMaskPoints = { ...objectMaskPoints, [key]: [...objectMaskPoints[key], []] };
|
||||
@ -246,6 +318,7 @@ ${Object.keys(objectMaskPoints)
|
||||
editing={editing}
|
||||
title="Motion masks"
|
||||
onCopy={handleCopyMotionMasks}
|
||||
onSave={handleSaveMotionMasks}
|
||||
onCreate={handleAddMask}
|
||||
onEdit={handleEditMask}
|
||||
onRemove={handleRemoveMask}
|
||||
@ -258,6 +331,7 @@ ${Object.keys(objectMaskPoints)
|
||||
editing={editing}
|
||||
title="Zones"
|
||||
onCopy={handleCopyZones}
|
||||
onSave={handleSaveZones}
|
||||
onCreate={handleAddZone}
|
||||
onEdit={handleEditZone}
|
||||
onRemove={handleRemoveZone}
|
||||
@ -272,6 +346,7 @@ ${Object.keys(objectMaskPoints)
|
||||
title="Object masks"
|
||||
onAdd={handleAddToObjectMask}
|
||||
onCopy={handleCopyObjectMasks}
|
||||
onSave={handleSaveObjectMasks}
|
||||
onCreate={handleAddObjectMask}
|
||||
onEdit={handleEditObjectMask}
|
||||
onRemove={handleRemoveObjectMask}
|
||||
@ -407,6 +482,7 @@ function MaskValues({
|
||||
title,
|
||||
onAdd,
|
||||
onCopy,
|
||||
onSave,
|
||||
onCreate,
|
||||
onEdit,
|
||||
onRemove,
|
||||
@ -455,6 +531,15 @@ function MaskValues({
|
||||
[onAdd]
|
||||
);
|
||||
|
||||
|
||||
const handleSave = useCallback(
|
||||
(event) => {
|
||||
const { key } = event.target.dataset;
|
||||
onSave(key);
|
||||
},
|
||||
[onAdd]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden" onMouseOver={handleMousein} onMouseOut={handleMouseout}>
|
||||
<div className="flex space-x-4">
|
||||
@ -463,6 +548,7 @@ function MaskValues({
|
||||
</Heading>
|
||||
<Button onClick={onCopy}>Copy</Button>
|
||||
<Button onClick={onCreate}>Add</Button>
|
||||
<Button onClick={onSave}>Save</Button>
|
||||
</div>
|
||||
<pre className="relative overflow-auto font-mono text-gray-900 dark:text-gray-100 rounded bg-gray-100 dark:bg-gray-800 p-2">
|
||||
{yamlPrefix}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user