#!/bin/sh
APP_NAME = "frigate"
ACTION = " ${ 1 } "
shift 1
set -x
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( ) {
local config port IMAGE_NAME LAN_IP
local usbcoral hwaccel storage mqtt host coral type device
echo "Fetching port from configuration..."
port = $( fetch_and_validate_uci docker port)
echo "Fetching image name from configuration..."
IMAGE_NAME = $( fetch_and_validate_uci docker image)
echo "Fetching USB Coral path from configuration..."
usbcoral = $( fetch_and_validate_uci docker usbcoral)
echo "Fetching hardware acceleration path from configuration..."
hwaccel = $( fetch_and_validate_uci docker hwaccel)
echo "Fetching storage path from configuration..."
storage = $( fetch_and_validate_uci docker storage)
echo "Fetching MQTT status from configuration..."
mqtt = $( fetch_and_validate_uci mqtt enabled)
echo "Fetching MQTT host from configuration..."
host = $( fetch_and_validate_uci mqtt host)
echo "Fetching Coral status from configuration..."
coral = $( fetch_and_validate_uci tpu device)
echo "Fetching Coral type from configuration..."
type = $( fetch_and_validate_uci tpu type )
echo "Fetching Coral device from configuration..."
device = $( fetch_and_validate_uci tpu device)
LAN_IP = $( uci get network.lan.ipaddr)
LAN_IP = " ${ LAN_IP %/* } "
[ -z " $port " ] && port = 1880
[ -z " $IMAGE_NAME " ] && IMAGE_NAME = "ghcr.io/blakeblackshear/frigate:stable"
rm -r /opt/docker2/compose/frigate 2>/dev/null
mkdir -p /opt/docker2/compose/frigate
touch /opt/docker2/compose/frigate/config.yml
touch /opt/docker2/compose/frigate/docker-compose.yml
cat > /opt/docker2/compose/frigate/docker-compose.yml <<EOF
version: "3.9"
services:
frigate:
container_name: $APP_NAME
privileged: true # this may not be necessary for all setups
restart: unless-stopped
image: $IMAGE_NAME
shm_size: "64mb" # update for your cameras based on calculation above
devices:
- $usbcoral :/dev/bus/usb # passes the USB Coral, needs to be modified for other versions
#- /dev/apex_0:/dev/apex_0 # passes a PCIe Coral, follow driver instructions here https://coral.ai/docs/m2/get-started/#2a-on-linux
- $hwaccel # for intel hwaccel, needs to be updated for your hardware
volumes:
#- ./frigate/etc/localtime:/etc/localtime:ro
- ./config.yml:/config/config.yml
- $storage :/media/frigate
#- type: tmpfs # Optional: 1GB of memory, reduces SSD/SD Card wear
# target: /tmp/cache
# tmpfs:
# size: 1000000000
ports:
- " $port :5000 "
- "8554:8554" # RTSP feeds
- "8555:8555/tcp" # WebRTC over tcp
- "8555:8555/udp" # WebRTC over udp
environment:
FRIGATE_RTSP_PASSWORD: "password"
EOF
# Initialize the base structure
echo "{}" > /opt/docker2/compose/frigate/config.yml
# Set global configurations
# 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: Any values marked as OVERWRITTEN_BY_ROUTER are managed by Private Router. Manual changes will overwritten unless you uncheck \"Overwrite Frigate Config\" in the frigate app camera settings."' -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
# Write each camera's configuration to config.yml
local camera_index = 0
while : ; do
# Try to fetch camera configuration
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
write_camera_to_yml $name
camera_index = $(( camera_index+1))
done
docker-compose -f /opt/docker2/compose/frigate/docker-compose.yml up -d --force-recreate
uci add shortcutmenu lists
uci set shortcutmenu.@lists[ -1] .webname= " $APP_NAME "
uci set shortcutmenu.@lists[ -1] .weburl= " $LAN_IP : $port "
uci set shortcutmenu.@lists[ -1] .webpath= "/"
uci commit shortcutmenu
}
usage( ) {
echo " usage: $0 sub-command "
echo "where sub-command is one of:"
echo " install Install frigate"
echo " upgrade Upgrade frigate"
echo " rm/start/stop/restart Remove/Start/Stop/Restart frigate"
echo " status frigate status"
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
}
# YML Helper Functions
# Retrieves a specific setting from the YML. If no setting_path is provided, it retrieves the camera name.
get_yml_value( ) {
local name = $1
local setting_path = ${ 2 :- }
yq eval " .cameras. $name $setting_path " /opt/docker2/compose/frigate/config.yml
}
# Sets a specific setting in the YML
set_yml_value( ) {
local name = $1
local setting_path = $2
local value = $3
# If the value starts with "{", "[", or is a number, we don't quote it; otherwise, we do.
case " $value " in
"{" *| "[" *| *[ 0-9] *)
# do nothing
; ;
*)
value = " \" $value \" "
; ;
esac
yq eval " .cameras. $name $setting_path = $value " -i /opt/docker2/compose/frigate/config.yml
}
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)
local mask = $( uci get frigate.@camera_config[ $camera_index ] .mask 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
overwrite_warning_comment $name "ffmpeg.inputs[0].path"
yq eval " .cameras. $name .ffmpeg.inputs[0].roles = [\"detect\"] " -i /opt/docker2/compose/frigate/config.yml
overwrite_warning_comment $name "ffmpeg.inputs[0].roles"
# 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
overwrite_warning_comment $name "record.enabled"
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
overwrite_warning_comment $name "snapshots.enabled"
# 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
overwrite_warning_comment $name "motion.mask"
else
echo "Mask is empty, ignoring."
fi
yq eval '(.cameras.Office | key) line_comment="DO NOT REMOVE - Managed by PrivateRouter Script"' -i /opt/docker2/compose/frigate/config.yml
}
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
overwrite_warning_comment( ) {
local name = $1
local attribute = $2
# Handle the special case of the list item
if [ [ $attribute = = "ffmpeg.inputs[0].roles" ] ] ; then
yq eval '(.cameras.' $name '.ffmpeg.inputs[0] | key) line_comment="OVERWRITTEN_BY_ROUTER"' -i /opt/docker2/compose/frigate/config.yml
else
yq eval '(.cameras.' $name '.' $attribute ' | key) line_comment="OVERWRITTEN_BY_ROUTER"' -i /opt/docker2/compose/frigate/config.yml
fi
}
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 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
yq eval '.cameras.' $name '.motion.mask 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" )
set_yml_value $name ".ffmpeg.inputs[0].path" " \" $path \" "
overwrite_warning_comment $name "ffmpeg.inputs[0].path"
local roles = $( get_uci_camera_value $camera_index "roles" )
set_yml_value $name ".ffmpeg.inputs[0].roles" $roles
overwrite_warning_comment $name "ffmpeg.inputs[0].roles"
local record = $( get_uci_camera_value $camera_index "record" )
set_yml_value $name ".record.enabled" $record
overwrite_warning_comment $name "record.enabled"
local snapshots = $( get_uci_camera_value $camera_index "snapshots" )
set_yml_value $name ".snapshots.enabled" $snapshots
overwrite_warning_comment $name "snapshots.enabled"
local mask = $( get_uci_camera_value $camera_index "mask" )
if [ -n " $mask " ] ; then
set_yml_value $name ".motion.mask" " [\" $mask \"] "
overwrite_warning_comment $name "motion.mask"
else
yq eval " del(.cameras. $name .motion) " -i /opt/docker2/compose/frigate/config.yml
fi
yq eval '(.cameras.Office | key) line_comment="DO NOT REMOVE - Managed by PrivateRouter Script"' -i /opt/docker2/compose/frigate/config.yml
}
sync_camera_config( ) {
echo "Syncing camera config..."
# Initializing the camera index
camera_index = 0
# Fetching all cameras from frigate YML
yml_cameras = $( yq eval '.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)
# Fix for word splitting
set -f
if ! echo $uci_cameras | grep -q -w " $yml_camera " ; then
# Trim leading and trailing whitespaces and converting to all lowercase.
comment = ${ comment ,, }
comment = ${ comment //[[ : blank : ]]/ }
# 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
set +f
done
echo "Camera config syncing completed!"
}
case " ${ ACTION } " in
"install" | "upgrade" )
do_install_detail
; ;
"rm" )
IMAGE_NAME = $( uci get frigate.@frigate[ 0] .image_name 2>/dev/null)
[ -z " $IMAGE_NAME " ] && IMAGE_NAME = "ghcr.io/blakeblackshear/frigate:stable"
CONTAINER_IDS = $( docker ps -a --filter " ancestor= ${ IMAGE_NAME } " --format '{{.ID}}' )
echo "Stopping and removing containers..."
for ID in $CONTAINER_IDS ; do
docker stop " $ID "
docker rm " $ID "
done
docker rmi -f " $IMAGE_NAME "
rm -r /opt/docker2/compose/frigate 2>/dev/null
; ;
"start" | "stop" | "restart" )
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" )
CONTAINER_NAME = $( docker ps -a --filter " name= ${ APP_NAME } " --format '{{.Names}}' )
CONTAINER_STATUS = $( docker ps --all --filter " name= ${ CONTAINER_NAME } " --format '{{.Status}}' | awk '/^Up/ { print "up " substr($0, 4) } !/^Up/ && /.+/ { print "down" }' )
if [ -z " $CONTAINER_NAME " ] ; then
echo " ${ APP_NAME } is not installed "
else
echo " ${ CONTAINER_STATUS } "
fi
; ;
"port" )
CONTAINER_NAME = $( docker ps -a --filter " name= ${ APP_NAME } " --format '{{.Names}}' )
# Fetch the port from UCI configuration
uci_port = $( uci get frigate.docker.port 2>/dev/null)
[ -z " $uci_port " ] && exit 1 # Exit silently if UCI port is not found
# Use the UCI port to filter the docker ps output and return only the external port
docker ps --all -f " name= ${ CONTAINER_NAME } " --format '{{.Ports}}' | grep -o " 0.0.0.0: $uci_port ->[0-9]*/tcp " | sed "s/0.0.0.0://;s/->[0-9]*\/tcp//"
; ;
esac