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:
Sergey Krashevich 2023-06-26 13:53:54 +03:00
parent ee9bac3500
commit 0bed28d175
No known key found for this signature in database
GPG Key ID: 625171324E7D3856
3 changed files with 173 additions and 15 deletions

View File

@ -44,7 +44,7 @@ from frigate.util import (
get_tz_modifiers, get_tz_modifiers,
restart_frigate, restart_frigate,
vainfo_hwaccel, vainfo_hwaccel,
update_yaml_file, update_yaml_from_url,
) )
from frigate.version import VERSION from frigate.version import VERSION
@ -1019,10 +1019,36 @@ def config_set():
if os.path.isfile(config_file_yaml): if os.path.isfile(config_file_yaml):
config_file = config_file_yaml config_file = config_file_yaml
for key, value in request.args: with open(config_file, "r") as f:
logging.debug(f"Update config key {key} to {value}") old_raw_config = f.read()
keys = key.split(".") f.close()
update_yaml_file(config_file, keys, value)
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") @bp.route("/config/schema.json")

View File

@ -1213,18 +1213,64 @@ def get_video_properties(url, get_duration=False):
return result 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): def update_yaml_file(file_path, key_path, new_value):
yaml = YAML() yaml = YAML()
with open(file_path, "r") as f: with open(file_path, "r") as f:
data = yaml.safe_load(f) data = yaml.load(f)
temp = data temp = data
for key in key_path[:-1]: for key in key_path[:-1]:
if key not in temp: if isinstance(key, tuple):
temp[key] = {} if key[0] not in temp:
temp = temp[key] temp[key[0]] = [{}] * max(1, key[1] + 1)
elif len(temp[key[0]]) <= key[1]:
temp[key_path[-1]] = new_value 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: with open(file_path, "w") as f:
yaml.dump(data, f) yaml.dump(data, f)

View File

@ -7,7 +7,7 @@ import { useResizeObserver } from '../hooks';
import { useCallback, useMemo, useRef, useState } from 'preact/hooks'; import { useCallback, useMemo, useRef, useState } from 'preact/hooks';
import { useApiHost } from '../api'; import { useApiHost } from '../api';
import useSWR from 'swr'; import useSWR from 'swr';
import axios from 'axios';
export default function CameraMasks({ camera }) { export default function CameraMasks({ camera }) {
const { data: config } = useSWR('config'); const { data: config } = useSWR('config');
const apiHost = useApiHost(); const apiHost = useApiHost();
@ -101,6 +101,23 @@ export default function CameraMasks({ camera }) {
${motionMaskPoints.map((mask) => ` - ${polylinePointsToPolyline(mask)}`).join('\n')}`); ${motionMaskPoints.map((mask) => ` - ${polylinePointsToPolyline(mask)}`).join('\n')}`);
}, [motionMaskPoints]); }, [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 // Zone methods
const handleEditZone = useCallback( const handleEditZone = useCallback(
(key) => { (key) => {
@ -131,9 +148,47 @@ ${motionMaskPoints.map((mask) => ` - ${polylinePointsToPolyline(mask)}`).jo
${Object.keys(zonePoints) ${Object.keys(zonePoints)
.map( .map(
(zoneName) => ` ${zoneName}: (zoneName) => ` ${zoneName}:
coordinates: ${polylinePointsToPolyline(zonePoints[zoneName])}` coordinates: ${polylinePointsToPolyline(zonePoints[zoneName])}`).join('\n')}`);
)
.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]); }, [zonePoints]);
// Object methods // Object methods
@ -175,6 +230,23 @@ ${Object.keys(objectMaskPoints)
.join('\n')}`); .join('\n')}`);
}, [objectMaskPoints]); }, [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( const handleAddToObjectMask = useCallback(
(key) => { (key) => {
const newObjectMaskPoints = { ...objectMaskPoints, [key]: [...objectMaskPoints[key], []] }; const newObjectMaskPoints = { ...objectMaskPoints, [key]: [...objectMaskPoints[key], []] };
@ -246,6 +318,7 @@ ${Object.keys(objectMaskPoints)
editing={editing} editing={editing}
title="Motion masks" title="Motion masks"
onCopy={handleCopyMotionMasks} onCopy={handleCopyMotionMasks}
onSave={handleSaveMotionMasks}
onCreate={handleAddMask} onCreate={handleAddMask}
onEdit={handleEditMask} onEdit={handleEditMask}
onRemove={handleRemoveMask} onRemove={handleRemoveMask}
@ -258,6 +331,7 @@ ${Object.keys(objectMaskPoints)
editing={editing} editing={editing}
title="Zones" title="Zones"
onCopy={handleCopyZones} onCopy={handleCopyZones}
onSave={handleSaveZones}
onCreate={handleAddZone} onCreate={handleAddZone}
onEdit={handleEditZone} onEdit={handleEditZone}
onRemove={handleRemoveZone} onRemove={handleRemoveZone}
@ -272,6 +346,7 @@ ${Object.keys(objectMaskPoints)
title="Object masks" title="Object masks"
onAdd={handleAddToObjectMask} onAdd={handleAddToObjectMask}
onCopy={handleCopyObjectMasks} onCopy={handleCopyObjectMasks}
onSave={handleSaveObjectMasks}
onCreate={handleAddObjectMask} onCreate={handleAddObjectMask}
onEdit={handleEditObjectMask} onEdit={handleEditObjectMask}
onRemove={handleRemoveObjectMask} onRemove={handleRemoveObjectMask}
@ -407,6 +482,7 @@ function MaskValues({
title, title,
onAdd, onAdd,
onCopy, onCopy,
onSave,
onCreate, onCreate,
onEdit, onEdit,
onRemove, onRemove,
@ -455,6 +531,15 @@ function MaskValues({
[onAdd] [onAdd]
); );
const handleSave = useCallback(
(event) => {
const { key } = event.target.dataset;
onSave(key);
},
[onAdd]
);
return ( return (
<div className="overflow-hidden" onMouseOver={handleMousein} onMouseOut={handleMouseout}> <div className="overflow-hidden" onMouseOver={handleMousein} onMouseOut={handleMouseout}>
<div className="flex space-x-4"> <div className="flex space-x-4">
@ -463,6 +548,7 @@ function MaskValues({
</Heading> </Heading>
<Button onClick={onCopy}>Copy</Button> <Button onClick={onCopy}>Copy</Button>
<Button onClick={onCreate}>Add</Button> <Button onClick={onCreate}>Add</Button>
<Button onClick={onSave}>Save</Button>
</div> </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"> <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} {yamlPrefix}