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"
20 declare call='curl --silent --show-error --location --header "Accept: application/json" --request'
21 declare devices="/elgato/lights"
22 declare accessory_info="/elgato/accessory-info"
23 declare settings="/elgato/lights/settings"
25 if [ ! -r "${icon}" ]; then icon=sunny; fi
28 if [ $silent -eq 0 ]; then
29 notify-send -i "$icon" "Key Light Controller" "$1"
46 Usage: $(basename "${BASH_SOURCE[0]}") [-h] [-f <value>] [-l] [-p] [-s] [-t <value>][-v] [--<option>] [--<option> <value>] <action>
48 Elgato Lights controller. Works for Key Light and Key Light Air.
51 list List available lights
52 status Get state of lights
54 off Turn all lights off
55 temperature Set temperature level (260-470)
56 brightness Set brightness level (0-100)
57 increase Increases brightness by 10
58 decrease Decreases brightness by 10
61 json Renders output as JSON (default)
62 simple Renders output as JSON array of single level objects with subarrays as .(dot) notation JSON
63 flat Renders output as fully flattened single level JSON with .(dot) notation JSON
64 html Renders output as basic html table
65 csv Renders output as csv
66 table Renders output as a printed table
67 pair Renders output as flattened key=value pairs
72 -h, --help Print this help and exit
73 -f --format Set output format
74 -p, --pretty Pretty print console output
75 -s, --silent Supress notifications
76 -t, --target Only perform action on devices where value matches filter
77 -v, --verbose Print script debug info
83 # default values of variables set from params
92 -p | --pretty) pretty=1 ;;
93 -v | --verbose) set -x ;;
94 -s | --silent) silent=1 ;;
99 -?*) die "Unknown option: $1" ;;
107 # check required params and arguments
108 declare -A actions=([help]=1 [list]=1 [status]=1 [on]=1 [off]=1)
109 [[ ${#args[@]} -ne 1 ]] && die "Incorrect argument count"
111 #[[ ($silent -eq 1) && ($pretty -eq 1) ]] && die "Cannot use silent and pretty options simultaneously"
113 [[ -n "${actions[${args[0]}]}" ]] && action="${args[0]}"
120 if ! command -v $var &>/dev/null; then
121 die "Dependency $var was not found, please install and try again"
126 default_light_properties() {
127 # Default values for json type enforcement
144 t=$(eval echo "'[.[] | select($target)]'")
146 lights_json=$(echo "${lights[@]}" | jq -c -s "$t")
147 simple_json=$(echo "${lights_json}" | jq -c '.[] | reduce ( tostream | select(length==2) | .[0] |= [join(".")] ) as [$p,$v] ({}; setpath($p; $v)) ')
148 simple_json=$(echo "${simple_json}" | jq -c -s '.') # slurp it to make it an array
149 flat_json=$(echo "${lights_json}" | jq -c -s '.[] | reduce ( tostream | select(length==2) | .[0] |= [join(".")] ) as [$p,$v] ({}; setpath($p; $v)) ')
155 # Mange user requested output format
157 json) print_json "$lights_json" ;;
158 simple) print_json "$simple_json" ;;
159 flat) print_json "$flat_json" ;;
160 table) print_structured '@tsv' ;;
161 csv) print_structured '@csv' ;;
162 pair) print_structured 'pairs' ;;
164 -?*) die "Unknown output format (-f/--format): $format" ;;
170 # Manage pretty printing
171 if [[ $pretty -eq 1 ]]; then
172 echo "${1-}" | jq '.'
174 echo "${1-}" | jq -c -M '.'
182 # Handle csv and table printing
183 query="(.[0] | keys_unsorted | map(ascii_upcase)), (.[] | [.[]])|${1-@csv}"
185 # Handle printing as key value pairs
186 if [[ ${1} == 'pairs' ]]; then
187 query='.[] | "--------------",(to_entries[] | [.key, "=", .value] | @tsv)'
190 # Manage pretty printing
191 if [[ $pp -eq 1 ]]; then
192 echo "${simple_json}" | jq --raw-output "$query" | column -t -s$'\t' | sed -e 's/"//g'
194 echo "${simple_json}" | jq -r "$query"
199 data=$(print_structured '@csv' 1)
206 if $print_header; then
207 echo "<tr><th>${d//,/<\/th><th>}</th></tr>"
211 echo "<tr><td>${d//,/</td><td>}</td></tr>"
220 die "To be implemented"
224 # Scan the network for Elgato devices
226 readarray -t avahi < <(avahi-browse -d local _elg._tcp --resolve -t -p | grep -v "^\+")
240 default_light_properties
242 for l in "${avahi[@]}"; do
243 IFS=';' read -ra data <<<"$l" # split line into array
245 # Gather information about the light
246 device=$(echo "${data[3]}" | sed -e 's/\\032/ /g') # fix avahi output
248 [[ ${data[7]} =~ fe80 ]] && ipv6=${data[7]} || ipv4=${data[7]}
250 txt=$(eval echo "${data[9]}") # eval to strip quotes
251 [[ $txt =~ mf=([^[[:space:]]*]*) ]] && manufacturer=${BASH_REMATCH[1]}
252 [[ $txt =~ id=([^[[:space:]]*]*) ]] && mac=${BASH_REMATCH[1]}
253 [[ $txt =~ md=.+[[:space:]]([^[[:space:]]*]*)[[:space:]]id= ]] && sku=${BASH_REMATCH[1]}
255 # Get information from the light
256 url="http://$ipv4:$port"
258 declare protocol="--ipv4"
259 if [[ $ipv4 == "N/A" ]]; then
260 # Workaround: Ignoring ipv6 as Elgato miss-announces addressing and is not accepting requests
261 # properly for v6. Will not change to filter only on ipv4 from avahi, as that can cause us to only end
262 # up with an ipv6 address even though it was announced as ipv4, which in turn means we cannot communicate.
264 # Remove above and uncomment below if a future update fixes ipv6 announcement and requests
266 #url="http://[$ip]:$port"
269 cfg=$(eval "${call} GET $protocol ${url}${settings}") >/dev/null
270 info=$(eval "${call} GET $protocol ${url}${accessory_info}") >/dev/null
271 light=$(eval "${call} GET $protocol ${url}${devices}") >/dev/null
274 --arg dev "$device" \
275 --arg hn "$hostname" \
278 --argjson port "$port" \
279 --arg mf "$manufacturer" \
283 --argjson cfg "$cfg" \
284 '{device: $dev, manufacturer: $mf, hostname: $hn, url: $url, ipv4: $ipv4, ipv6: $ipv6,
285 port: $port, mac: $mac, sku: $sku, settings: $cfg}')
287 # Store the light as json and merge info + light into base object
288 lights["$device"]=$(echo "$info $light $json" | jq -s '. | add')
290 # Reset for next light as we are processing the last avahi line
291 default_light_properties
296 # Manage user parameters
299 # Make sure dependencies are installed
300 dependencies avahi-browse curl notify-send jq
304 # Fail if we cannot find lights
305 [[ ${#lights[@]} -eq 0 ]] && die "No lights found"
316 -?*) die "Unknown action" ;;