From 0bed28d175228603895ea5529230704eeff1f0c5 Mon Sep 17 00:00:00 2001 From: Sergey Krashevich Date: Mon, 26 Jun 2023 13:53:54 +0300 Subject: [PATCH] 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 --- frigate/http.py | 36 ++++++++++++-- frigate/util.py | 58 +++++++++++++++++++--- web/src/routes/CameraMap.jsx | 94 ++++++++++++++++++++++++++++++++++-- 3 files changed, 173 insertions(+), 15 deletions(-) diff --git a/frigate/http.py b/frigate/http.py index 11fc21710..9577b97d0 100644 --- a/frigate/http.py +++ b/frigate/http.py @@ -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") diff --git a/frigate/util.py b/frigate/util.py index e98fe9d4f..73df5fbce 100755 --- a/frigate/util.py +++ b/frigate/util.py @@ -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) diff --git a/web/src/routes/CameraMap.jsx b/web/src/routes/CameraMap.jsx index ca77ec56e..20e9a343d 100644 --- a/web/src/routes/CameraMap.jsx +++ b/web/src/routes/CameraMap.jsx @@ -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 (
@@ -463,6 +548,7 @@ function MaskValues({ +
         {yamlPrefix}