diff --git a/luasrc/model/cbi/frigate.lua b/luasrc/model/cbi/frigate.lua index 6a86144..7039e65 100644 --- a/luasrc/model/cbi/frigate.lua +++ b/luasrc/model/cbi/frigate.lua @@ -34,6 +34,7 @@ o.default = "/dev/crypto" o = s:option(Value, "storage", "Storage Path") o.default = "./frigate/storage" + -- MQTT Configuration s = m:section(NamedSection, "mqtt", "frigate_config", "MQTT Configuration") s.addremove = false @@ -68,8 +69,18 @@ s.novaluetext = "There are no cameras configured yet." o = s:option(Value, "name", "Camera Name") o = s:option(Value, "path", "RTSP Path") o = s:option(Value, "roles", "Roles") + o = s:option(Flag, "record", "Enable Recording") +o.default = "0" +o.rmempty = false + o = s:option(Flag, "snapshots", "Enable Snapshots") -o = s:option(Value, "mask", "Mask") +o.default = "0" +o.rmempty = false + +-- Add the new flag for Overwrite Frigate Config +o = s:option(Flag, "overwrite_cfg", "Overwrite Frigate Config") +o.default = "1" +o.rmempty = false return m \ No newline at end of file diff --git a/root/etc/config/frigate b/root/etc/config/frigate index 163f1ee..5d56463 100644 --- a/root/etc/config/frigate +++ b/root/etc/config/frigate @@ -14,27 +14,11 @@ config frigate_config 'tpu' option type 'edgetpu' option device 'usb' -config camera_config 'cam1' +config camera_config option name 'living_room' option path 'rtsp://admin:password@192.168.70.173:554/cam/realmonitor?channel=1&subtype=0&authbasic=64' option roles 'detect' option record '1' option snapshots '1' - option mask '0,461,3,0,1919,0,1919,843,1699,492,1344,458,1346,336,973,317,869,375,866,432' - -config camera_config 'cam2' - option name 'upstairs_bedroom' - option path 'rtsp://admin:password@192.168.70.173:554/cam/realmonitor?channel=1&subtype=0&authbasic=64' - option roles 'detect' - option record '1' - option snapshots '1' - option mask '0,461,3,0,1919,0,1919,843,1699,492,1344,458,1346,336,973,317,869,375,866,432' - -config camera_config 'cam3' - option name 'kitchen' - option path 'rtsp://admin:password@192.168.70.173:554/cam/realmonitor?channel=1&subtype=0&authbasic=64' - option roles 'detect' - option record '1' - option snapshots '1' - option mask '0,461,3,0,1919,0,1919,843,1699,492,1344,458,1346,336,973,317,869,375,866,432' + option overwrite_cfg '1' diff --git a/root/usr/libexec/apps/frigate/frigate.sh b/root/usr/libexec/apps/frigate/frigate.sh index 97ece0c..64e18bd 100755 --- a/root/usr/libexec/apps/frigate/frigate.sh +++ b/root/usr/libexec/apps/frigate/frigate.sh @@ -4,9 +4,20 @@ APP_NAME="frigate" ACTION="${1}" shift 1 +#set -x -get_image() { - do_install_detail +fetch_and_validate_uci () { + local config_name=$1 + local config_attr=$2 + + local result=$(uci get frigate.$config_name.$config_attr 2>/dev/null) + if [ -z "$result" ]; then + echo "Error: $config_name not found in configuration." >&2 + exit 1 + fi + + echo "Fetched $config_name from configuration: $result" >&2 + echo $result } do_install_detail() { @@ -14,44 +25,34 @@ do_install_detail() { local usbcoral hwaccel storage mqtt host coral type device echo "Fetching port from configuration..." - port=$(uci get frigate.docker.port 2>/dev/null) - [ -z "$port" ] && { echo "Error: Port not found in configuration."; exit 1; } - + port=$(fetch_and_validate_uci docker port) + echo "Fetching image name from configuration..." - IMAGE_NAME=$(uci get frigate.docker.image 2>/dev/null) - [ -z "$IMAGE_NAME" ] && { echo "Error: Image name not found in configuration."; exit 1; } + IMAGE_NAME=$(fetch_and_validate_uci docker image) echo "Fetching USB Coral path from configuration..." - usbcoral=$(uci get frigate.docker.usbcoral 2>/dev/null) - [ -z "$usbcoral" ] && { echo "Error: USB Coral path not found in configuration."; exit 1; } + usbcoral=$(fetch_and_validate_uci docker usbcoral) echo "Fetching hardware acceleration path from configuration..." - hwaccel=$(uci get frigate.docker.hwaccel 2>/dev/null) - [ -z "$hwaccel" ] && { echo "Error: Hardware acceleration path not found in configuration."; exit 1; } + hwaccel=$(fetch_and_validate_uci docker hwaccel) echo "Fetching storage path from configuration..." - storage=$(uci get frigate.docker.storage 2>/dev/null) - [ -z "$storage" ] && { echo "Error: Storage path not found in configuration."; exit 1; } + storage=$(fetch_and_validate_uci docker storage) echo "Fetching MQTT status from configuration..." - mqtt=$(uci get frigate.mqtt.enabled 2>/dev/null) - [ -z "$mqtt" ] && { echo "Error: MQTT status not found in configuration."; exit 1; } + mqtt=$(fetch_and_validate_uci mqtt enabled) echo "Fetching MQTT host from configuration..." - host=$(uci get frigate.mqtt.host 2>/dev/null) - [ -z "$host" ] && { echo "Error: MQTT host not found in configuration."; exit 1; } + host=$(fetch_and_validate_uci mqtt host) echo "Fetching Coral status from configuration..." - coral=$(uci get frigate.tpu.device 2>/dev/null) - [ -z "$coral" ] && { echo "Error: Coral status not found in configuration."; exit 1; } + coral=$(fetch_and_validate_uci tpu device) echo "Fetching Coral type from configuration..." - type=$(uci get frigate.tpu.type 2>/dev/null) - [ -z "$type" ] && { echo "Error: Coral type not found in configuration."; exit 1; } + type=$(fetch_and_validate_uci tpu type) echo "Fetching Coral device from configuration..." - device=$(uci get frigate.tpu.device 2>/dev/null) - [ -z "$device" ] && { echo "Error: Coral device not found in configuration."; exit 1; } + device=$(fetch_and_validate_uci tpu device) LAN_IP=$(uci get network.lan.ipaddr) LAN_IP="${LAN_IP%/*}" @@ -102,14 +103,13 @@ EOF # yq eval ".usbcoral = \"$usbcoral\"" -i /opt/docker2/compose/frigate/config.yml # yq eval ".hwaccel = \"$hwaccel\"" -i /opt/docker2/compose/frigate/config.yml # yq eval ".media.storage = \"$storage\"" -i /opt/docker2/compose/frigate/config.yml + yq eval '. head_comment="WARNING: Values labeled OVERWRITTEN_BY_ROUTER are auto-managed by Private Router. Uncheck \"Overwrite Frigate Config\" in camera settings to edit manually."' -i /opt/docker2/compose/frigate/config.yml + yq eval ".mqtt.enabled = $mqtt" -i /opt/docker2/compose/frigate/config.yml yq eval ".mqtt.host = \"$host\"" -i /opt/docker2/compose/frigate/config.yml yq eval ".detectors.coral.type = \"$type\"" -i /opt/docker2/compose/frigate/config.yml yq eval ".detectors.coral.device = \"$coral\"" -i /opt/docker2/compose/frigate/config.yml - # Initialize cameras structure - yq eval ".cameras = {}" -i /opt/docker2/compose/frigate/config.yml - # Write each camera's configuration to config.yml local camera_index=0 while : ; do @@ -117,45 +117,7 @@ EOF local name=$(uci get frigate.@camera_config[$camera_index].name 2>/dev/null | tr ' ' '_') # Exit loop if no more cameras are found [[ -z "$name" ]] && break - - # Retrieve the rest of the camera settings here... - local path=$(uci get frigate.@camera_config[$camera_index].path 2>/dev/null) - local roles=$(uci get frigate.@camera_config[$camera_index].roles 2>/dev/null) - local record=$(uci get frigate.@camera_config[$camera_index].record 2>/dev/null) - local snapshots=$(uci get frigate.@camera_config[$camera_index].snapshots 2>/dev/null) - local mask=$(uci get frigate.@camera_config[$camera_index].mask 2>/dev/null) - - # Initialize camera structure - yq eval ".cameras.$name = {}" -i /opt/docker2/compose/frigate/config.yml - - # Initialize ffmpeg and inputs for each camera - yq eval ".cameras.$name.ffmpeg = {}" -i /opt/docker2/compose/frigate/config.yml - yq eval ".cameras.$name.ffmpeg.inputs = []" -i /opt/docker2/compose/frigate/config.yml - - # Add path and roles to the inputs - yq eval ".cameras.$name.ffmpeg.inputs[0].path = \"$path\"" -i /opt/docker2/compose/frigate/config.yml - yq eval ".cameras.$name.ffmpeg.inputs[0].roles = [\"detect\"]" -i /opt/docker2/compose/frigate/config.yml - - # Add other camera settings - yq eval ".cameras.$name.detect = {}" -i /opt/docker2/compose/frigate/config.yml - yq eval ".cameras.$name.detect.width = 1280" -i /opt/docker2/compose/frigate/config.yml - yq eval ".cameras.$name.detect.height = 720" -i /opt/docker2/compose/frigate/config.yml - - yq eval ".cameras.$name.record = {}" -i /opt/docker2/compose/frigate/config.yml - yq eval ".cameras.$name.record.enabled = $record" -i /opt/docker2/compose/frigate/config.yml - - yq eval ".cameras.$name.snapshots = {}" -i /opt/docker2/compose/frigate/config.yml - yq eval ".cameras.$name.snapshots.enabled = $snapshots" -i /opt/docker2/compose/frigate/config.yml - - # Check if the mask is not empty - if [ -n "$mask" ]; then - # Run the yq commands to update the YAML file - yq eval ".cameras.$name.motion = {}" -i /opt/docker2/compose/frigate/config.yml - yq eval ".cameras.$name.motion.mask = [\"$mask\"]" -i /opt/docker2/compose/frigate/config.yml - else - echo "Mask is empty, ignoring." - fi - + write_camera_to_yml $name camera_index=$((camera_index+1)) done @@ -178,6 +140,197 @@ usage() { echo " port frigate port" } +# Gets a specific setting for a camera. If no setting is provided, it retrieves the camera name. +get_uci_camera_value() { + local index=$1 + local setting=${2:-name} + uci get frigate.@camera_config[$index].$setting 2>/dev/null | tr ' ' '_' +} + +# Sets a specific setting for a camera +set_uci_camera_value() { + local name=$1 + local setting=$2 + local value=$3 + uci set frigate.@camera_config[@$name].$setting=$value + uci commit frigate +} + +write_camera_to_yml() { + local name=$1 + local camera_index="" + local lines=$(uci show frigate | grep '@camera_config') + IFS=$'\n' + for line in $lines + do + if [[ $line == *".name='$name'"* ]] + then + camera_index=$(echo $line | grep -E -o "@camera_config\[[0-9]+\]" | grep -E -o "[0-9]+" ) + break + fi + done + + if [[ -z "$camera_index" ]] + then + echo "Camera $name not found in UCI" + return + fi + + # Retrieves the rest of the camera settings from UCI... + local path=$(uci get frigate.@camera_config[$camera_index].path 2>/dev/null) + local roles=$(uci get frigate.@camera_config[$camera_index].roles 2>/dev/null) + local record=$(uci get frigate.@camera_config[$camera_index].record 2>/dev/null) + local snapshots=$(uci get frigate.@camera_config[$camera_index].snapshots 2>/dev/null) + + # Initialize camera structure in YML + yq eval ".cameras.$name = {}" -i /opt/docker2/compose/frigate/config.yml + + # Initialize ffmpeg and inputs for each camera in YML + yq eval ".cameras.$name.ffmpeg = {}" -i /opt/docker2/compose/frigate/config.yml + yq eval ".cameras.$name.ffmpeg.inputs = []" -i /opt/docker2/compose/frigate/config.yml + + # Add path and roles to the inputs in YML + yq eval ".cameras.$name.ffmpeg.inputs[0].path = \"$path\"" -i /opt/docker2/compose/frigate/config.yml + yq eval ".cameras.$name.ffmpeg.inputs[0].roles = [\"detect\"]" -i /opt/docker2/compose/frigate/config.yml + # Add other camera settings to YML + yq eval ".cameras.$name.detect = {}" -i /opt/docker2/compose/frigate/config.yml + yq eval ".cameras.$name.detect.width = 1280" -i /opt/docker2/compose/frigate/config.yml + yq eval ".cameras.$name.detect.height = 720" -i /opt/docker2/compose/frigate/config.yml + + yq eval ".cameras.$name.record = {}" -i /opt/docker2/compose/frigate/config.yml + yq eval ".cameras.$name.record.enabled = $record" -i /opt/docker2/compose/frigate/config.yml + yq eval ".cameras.$name.snapshots = {}" -i /opt/docker2/compose/frigate/config.yml + yq eval ".cameras.$name.snapshots.enabled = $snapshots" -i /opt/docker2/compose/frigate/config.yml + + add_warning_comments $name +} + +get_camera_index_by_name() { + local name=$1 + local camera_index=0 + while : ; do + local current_name=$(get_uci_camera_value $camera_index) + [[ "$current_name" == "$name" ]] && echo $camera_index && break + [[ -z "$current_name" ]] && break + camera_index=$((camera_index+1)) + done +} + +# Toggles the line comment for a specific camera attribute in the YML +add_warning_comments() { + local name=$1 + yq eval '(.cameras.'$name' | key) line_comment="DO NOT REMOVE - Managed by PrivateRouter Script"' -i /opt/docker2/compose/frigate/config.yml + + yq eval '(.cameras.'$name'.ffmpeg.inputs[0].path | key) line_comment="OVERWRITTEN_BY_ROUTER"' -i /opt/docker2/compose/frigate/config.yml + + yq eval '(.cameras.'$name'.ffmpeg.inputs[0].roles | key) line_comment="OVERWRITTEN_BY_ROUTER"' -i /opt/docker2/compose/frigate/config.yml + + yq eval '.cameras.'$name'.record.enabled line_comment="OVERWRITTEN_BY_ROUTER"' -i /opt/docker2/compose/frigate/config.yml + + yq eval '.cameras.'$name'.snapshots.enabled line_comment="OVERWRITTEN_BY_ROUTER"' -i /opt/docker2/compose/frigate/config.yml +} +remove_warning_comments() { + local name=$1 + + yq eval '.cameras.'$name'.ffmpeg.inputs[0].path line_comment=""' -i /opt/docker2/compose/frigate/config.yml + + yq eval '(.cameras.'$name'.ffmpeg.inputs[0].roles | key) line_comment=""' -i /opt/docker2/compose/frigate/config.yml + + yq eval '.cameras.'$name'.record.enabled line_comment=""' -i /opt/docker2/compose/frigate/config.yml + + yq eval '.cameras.'$name'.snapshots.enabled line_comment=""' -i /opt/docker2/compose/frigate/config.yml +} + +update_camera_in_yml() { + local name=$1 + + # Get the index of the camera with name "$name" + local camera_index=$(get_camera_index_by_name $name) + + # Fetch and Update the specific settings for this camera from UCI -> YAML + + # For each attribute you update, apply warning comment + local path=$(get_uci_camera_value $camera_index "path") + yq eval '.cameras.'$name'.ffmpeg.inputs[0].path = "'$path'"' -i /opt/docker2/compose/frigate/config.yml + + + local roles="[\"detect\"]" # adjust as necessary + yq eval '.cameras.'$name'.ffmpeg.inputs[0].roles = '$roles'' -i /opt/docker2/compose/frigate/config.yml + + + local record=$(get_uci_camera_value $camera_index "record") + yq eval '.cameras.'$name'.record.enabled = '$record'' -i /opt/docker2/compose/frigate/config.yml + + + local snapshots=$(get_uci_camera_value $camera_index "snapshots") + yq eval '.cameras.'$name'.snapshots.enabled = '$snapshots'' -i /opt/docker2/compose/frigate/config.yml + + add_warning_comments $name +} + +sync_camera_config() { + echo "Syncing camera config..." + + # Initializing the camera index + camera_index=0 + + # Fetching all cameras from frigate YML + yml_cameras=$(yq eval '... comments="" | .cameras | keys' /opt/docker2/compose/frigate/config.yml | sed 's/- //g' | xargs) + + echo "Cameras fetched from YML: $yml_cameras" + + # Loop through all camera configs in UCI using index + while true; do + # Try to fetch camera configuration + local camera_name=$(get_uci_camera_value $camera_index) + local overwrite_cfg=$(get_uci_camera_value $camera_index "overwrite_cfg") + + # Exit loop if no more cameras are found + [ -z "$camera_name" ] && break + + # Only proceed if overwrite_cfg is enabled (set to 1) + if [ "$overwrite_cfg" -eq 1 ] + then + if ! echo "$yml_cameras" | grep -q -w "$camera_name"; then + echo "UCI Camera $camera_name not found in YML, adding..." + write_camera_to_yml $camera_name + else + echo "UCI Camera $camera_name found in YML, updating..." + update_camera_in_yml $camera_name + fi + else + echo "Skipping camera $camera_name, overwrite is set to false." + remove_warning_comments $camera_name + fi + + # Add to uci_cameras + uci_cameras="${uci_cameras} ${camera_name}" + + # Increment camera index + camera_index=$((camera_index+1)) + done + + # Crosschecking and removing orphaned entry in YML not present in UCI config + for yml_camera in $yml_cameras; do + # Get the comment of this camera + local comment=$(yq ".cameras.$yml_camera | key | line_comment" /opt/docker2/compose/frigate/config.yml) + comment=$(echo $comment | awk '{print tolower($0)}') # convert to lowercase using awk + comment=$(echo $comment | tr -d ' ') # remove whitespaces + + if ! echo "$uci_cameras" | grep -q -w "$yml_camera"; then + # If the comment is either non-existent or doesn't contain "donotremove-managedbyprivaterouterscript", keep it + if [ -z "$comment" ] || [[ "$comment" != *"donotremove-managedbyprivaterouterscript"* ]]; then + echo "Manual camera $yml_camera found in YML. Keeping it since it's manually added." + else + echo "Auto generated camera $yml_camera found in YML, but not in UCI , removing..." + yq eval "del(.cameras.$yml_camera)" -i /opt/docker2/compose/frigate/config.yml + fi + fi + done + + echo "Camera config syncing completed!" +} + case "${ACTION}" in "install" | "upgrade") do_install_detail @@ -198,6 +351,7 @@ case "${ACTION}" in CONTAINER_IDS=$(docker ps -a --filter "name=${APP_NAME}" --format '{{.ID}}') for ID in $CONTAINER_IDS; do docker "${ACTION}" "${ID}" + sync_camera_config done ;; "status")