4 trap destroy SIGINT SIGTERM ERR EXIT
7 script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd -P)
8 icon="${script_dir}/assets/elgato.png"
13 declare action="usage"
22 declare call='curl --silent --show-error --location --header "Accept: application/json" --request'
23 declare devices="/elgato/lights"
24 declare accessory_info="/elgato/accessory-info"
25 declare settings="/elgato/lights/settings"
27 if [ ! -r "${icon}" ]; then icon=sunny; fi
30 if [ $silent -eq 0 ]; then
31 notify-send -i "$icon" "Key Light Controller" "$1"
48 Usage: $(basename "${BASH_SOURCE[0]}") [-h] [-f <value>] [-l <value>] [-p] [-s] [-t <value>][-v] [--<option>] [--<option> <value>] <action>
50 Elgato Lights controller. Works for Key Light and Key Light Air.
53 list List available lights
54 status Get state of lights
56 off Turn all lights off
57 temperature Set temperature level (260-470)
58 brightness Set brightness level (0-100)
59 increase Increases brightness by 10
60 decrease Decreases brightness by 10
63 json Renders output as JSON (default)
64 simple Renders output as JSON array of single level objects with subarrays as .(dot) notation JSON
65 flat Renders output as fully flattened single level JSON with .(dot) notation JSON
66 html Renders output as basic html table
67 csv Renders output as csv
68 table Renders output as a printed table
69 pair Renders output as flattened key=value pairs
74 -h, --help Print this help and exit
75 -f, --format Set output format
76 -l, --limit <list> Limit top level output fields to the specified comma separated list
77 -p, --pretty Pretty print console output
78 -s, --silent Supress notifications
79 -t, --target <filter> Only perform action on devices where value matches filter
80 -v, --verbose Print script debug info
86 # default values of variables set from params
96 limit=$(eval echo "\| { ${2-} } ")
99 -p | --pretty) pretty=1 ;;
100 -v | --verbose) set -x ;;
101 -s | --silent) silent=1 ;;
106 -?*) die "Unknown option: $1" ;;
114 # check required params and arguments
115 declare -A actions=([help]=1 [list]=1 [status]=1 [on]=1 [off]=1)
116 [[ ${#args[@]} -ne 1 ]] && die "Incorrect argument count"
118 #[[ ($silent -eq 1) && ($pretty -eq 1) ]] && die "Cannot use silent and pretty options simultaneously"
120 [[ -n "${actions[${args[0]}]}" ]] && action="${args[0]}"
127 if ! command -v $var &>/dev/null; then
128 die "Dependency $var was not found, please install and try again"
133 default_light_properties() {
134 # Default values for json type enforcement
151 t=$(eval echo "'[.[] $limit| select($target)]'")
152 f=$(eval echo "'[.[] | select($target)]'")
154 lights_json=$(echo "${lights[@]}" | jq -c -s "$t")
155 full_json=$(echo "${lights[@]}" | jq -c -s "$f")
156 simple_json=$(echo "${lights_json}" | jq -c '.[] | reduce ( tostream | select(length==2) | .[0] |= [join(".")] ) as [$p,$v] ({}; setpath($p; $v)) ')
157 simple_json=$(echo "${simple_json}" | jq -c -s '.') # slurp it to make it an array
158 flat_json=$(echo "${lights_json}" | jq -c -s '.[] | reduce ( tostream | select(length==2) | .[0] |= [join(".")] ) as [$p,$v] ({}; setpath($p; $v)) ')
164 # Mange user requested output format
166 json) print_json "$lights_json" ;;
167 simple) print_json "$simple_json" ;;
168 flat) print_json "$flat_json" ;;
169 table) print_structured '@tsv' ;;
170 csv) print_structured '@csv' ;;
171 pair) print_structured 'pairs' ;;
173 -?*) die "Unknown output format (-f/--format): $format" ;;
179 # Manage pretty printing
180 if [[ $pretty -eq 1 ]]; then
181 echo "${1-}" | jq '.'
183 echo "${1-}" | jq -c -M '.'
192 # Handle csv and table printing
193 query="(.[0] | keys_unsorted | map(ascii_upcase)), (.[] | [.[]])|${1-@csv}"
195 # Handle printing as key value pairs
196 if [[ ${1} == 'pairs' ]]; then
197 query='.[] | "--------------",(to_entries[] | [.key, "=", .value] | @tsv)'
200 # Manage pretty printing
201 if [[ $pp -eq 1 ]]; then
202 echo "${simple_json}" | jq --raw-output "$query" | column -t -s$'\t' | sed -e 's/"//g'
204 if [[ ${1} == 'pairs' ]]; then
205 echo "${simple_json}" | jq -r "$query" | sed -e 's/\t//g'
207 echo "${simple_json}" | jq -r "$query"
213 data=$(print_structured '@csv' 1)
220 if $print_header; then
221 echo "<tr><th>${d//,/<\/th><th>}</th></tr>"
225 echo "<tr><td>${d//,/</td><td>}</td></tr>"
234 readarray -t data < <(echo "${full_json}" | jq -c '.[] | {displayName, url, numberOfLights, lights}')
237 x=$(echo "${1}" | tr 01 10) # "flip the bit"
239 for d in "${data[@]}"; do
240 query_old="[.lights[] | select(.on==${x})] | length"
241 count_found=$(echo "${d}" | jq "$query_old")
243 # Don't send to lights already in wanted state
244 if [[ $count_found -eq 0 ]]; then continue; fi
246 # Extract relevant data and create new json object
247 url=$(echo "${d}" | jq '.url')
248 dn=$(echo "${d}" | jq -r '.displayName')
249 l=$(echo "${d}" | jq -c 'del(.url, .displayName)' | jq ". | .lights[].on = ${1}")
252 if eval "${call} PUT -d '${l}' ${url}${devices}" >/dev/null; then updated+=("$dn"); fi
255 # Text representation of new state
257 [[ $1 -eq 0 ]] && state="OFF"
260 if [[ ${#updated[*]} -gt 0 ]]; then
261 n="Turned $state ${#updated[@]} lights:\n\n"
262 for i in "${updated[@]}"; do
271 # Scan the network for Elgato devices
273 readarray -t avahi < <(avahi-browse -d local _elg._tcp --resolve -t -p | grep -v "^\+")
287 default_light_properties
289 for l in "${avahi[@]}"; do
290 IFS=';' read -ra data <<<"$l" # split line into array
292 # Gather information about the light
293 device=$(echo "${data[3]}" | sed -e 's/\\032/ /g') # fix avahi output
295 [[ ${data[7]} =~ fe80 ]] && ipv6=${data[7]} || ipv4=${data[7]}
297 txt=$(eval echo "${data[9]}") # eval to strip quotes
298 [[ $txt =~ mf=([^[[:space:]]*]*) ]] && manufacturer=${BASH_REMATCH[1]}
299 [[ $txt =~ id=([^[[:space:]]*]*) ]] && mac=${BASH_REMATCH[1]}
300 [[ $txt =~ md=.+[[:space:]]([^[[:space:]]*]*)[[:space:]]id= ]] && sku=${BASH_REMATCH[1]}
302 # Get information from the light
303 url="http://$ipv4:$port"
305 declare protocol="--ipv4"
306 if [[ $ipv4 == "N/A" ]]; then
307 # Workaround: Ignoring ipv6 as Elgato miss-announces addressing and is not accepting requests
308 # properly for v6. Will not change to filter only on ipv4 from avahi, as that can cause us to only end
309 # up with an ipv6 address even though it was announced as ipv4, which in turn means we cannot communicate.
311 # Remove above and uncomment below if a future update fixes ipv6 announcement and requests
313 #url="http://[$ip]:$port"
316 cfg=$(eval "${call} GET $protocol ${url}${settings}") >/dev/null
317 info=$(eval "${call} GET $protocol ${url}${accessory_info}") >/dev/null
318 light=$(eval "${call} GET $protocol ${url}${devices}") >/dev/null
321 --arg dev "$device" \
322 --arg hn "$hostname" \
325 --argjson port "$port" \
326 --arg mf "$manufacturer" \
330 --argjson cfg "$cfg" \
331 '{device: $dev, manufacturer: $mf, hostname: $hn, url: $url, ipv4: $ipv4, ipv6: $ipv6,
332 port: $port, mac: $mac, sku: $sku, settings: $cfg}')
334 # Store the light as json and merge info + light into base object
335 lights["$device"]=$(echo "$info $light $json" | jq -s '. | add')
337 # Reset for next light as we are processing the last avahi line
338 default_light_properties
343 # Quit if script is run by root
344 [[ "$EUID" -eq 0 ]] && die "Not allowed to run as root"
346 # Manage user parameters
349 # Make sure dependencies are installed
350 dependencies avahi-browse curl notify-send jq
354 # Fail if we cannot find lights
355 [[ ${#lights[@]} -eq 0 ]] && die "No lights found"
366 -?*) die "Unknown action" ;;