]> git.vanrenterghem.biz Git - elgato-keylight-script.git/blob - keylights.sh
Add feature to limit output and select order
[elgato-keylight-script.git] / keylights.sh
1 #!/bin/bash
2 set -Eeuo pipefail
4 trap destroy SIGINT SIGTERM ERR EXIT
6 # Settings
7 script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd -P)
8 icon="${script_dir}/assets/elgato.png"
10 # Declarations
11 declare -i silent=0
12 declare -i pretty=0
13 declare action="usage"
14 declare target='.'
15 declare limit=""
16 declare format="json"
17 declare -A lights
18 declare lights_json
19 declare full_json
20 declare simple_json
21 declare flat_json
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
29 notify() {
30     if [ $silent -eq 0 ]; then
31         notify-send -i "$icon" "Key Light Controller" "$1"
32     fi
33 }
35 die() {
36     echo >&2 -e "${1-}"
37     exit "${2-1}"
38 }
40 destroy() {
41     code=$?
43     exit ${code}
44 }
46 usage() {
47     cat <<EOF
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.
52 Available actions:
53     list        List available lights
54     status      Get state of lights
55     on          Turn all lights on
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
62 Available formats:
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
72 Available options:
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
81 EOF
82     exit
83 }
85 parse_params() {
86     # default values of variables set from params
88     while :; do
89         case "${1-}" in
90         -h | --help) usage ;;
91         -f | --format)
92             format="${2-}"
93             shift
94             ;;
95         -l | --limit)
96             limit=$(eval echo "\| {${2-}} ")
97             shift
98             ;;
99         -p | --pretty) pretty=1 ;;
100         -v | --verbose) set -x ;;
101         -s | --silent) silent=1 ;;
102         -t | --target)
103             target="${2-}"
104             shift
105             ;;
106         -?*) die "Unknown option: $1" ;;
107         *) break ;;
108         esac
109         shift
110     done
112     args=("$@")
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]}"
122     return 0
125 dependencies() {
126     for var in "$@"; do
127         if ! command -v $var &>/dev/null; then
128             die "Dependency $var was not found, please install and try again"
129         fi
130     done
133 default_light_properties() {
134     # Default values for json type enforcement
135     device="N/A"
136     hostname="N/A"
137     manufacturer="N/A"
138     ipv4="N/A"
139     ipv6="N/A"
140     port=0
141     mac="N/A"
142     sku="N/A"
143     cfg="{}"
144     url="{}"
145     info="{}"
146     light="{}"
150 produce_json() {
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)) ')
162 output() {
164     # Mange user requested output format
165     case $format in
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' ;;
172     html) print_html ;;
173     -?*) die "Unknown output format (-f/--format): $format" ;;
174     esac
177 print_json() {
179     # Manage pretty printing
180     if [[ $pretty -eq 1 ]]; then
181         echo "${1-}" | jq '.'
182     else
183         echo "${1-}" | jq -c -M '.'
184     fi
186     exit 0
189 print_structured() {
190     pp=${2-$pretty}
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)'
198     fi
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'
203     else
204         if [[ ${1} == 'pairs' ]]; then
205             echo "${simple_json}" | jq -r "$query" | sed -e 's/\t//g'
206         else
207         echo "${simple_json}" | jq -r "$query"
208     fi
209     fi
212 print_html() {
213     data=$(print_structured '@csv' 1)
215     html="
216     <table>
217     $(
218         print_header=true
219         while read d; do
220             if $print_header; then
221                 echo "<tr><th>${d//,/<\/th><th>}</th></tr>"
222                 print_header=false
223                 continue
224             fi
225             echo "<tr><td>${d//,/</td><td>}</td></tr>"
226         done <<<"${data}"
227     )
228     </table>"
229     echo "$html"
232 set_state() {
233     new_state=$1
234     die "To be implemented"
237 find_lights() {
238     # Scan the network for Elgato devices
239     declare -a avahi
240     readarray -t avahi < <(avahi-browse -d local _elg._tcp --resolve -t -p | grep -v "^\+")
242     declare device
243     declare hostname
244     declare manufacturer
245     declare ipv4
246     declare ipv6
247     declare -i port
248     declare mac
249     declare sku
250     declare cfg
251     declare url
252     declare info
253     declare light
254     default_light_properties
256     for l in "${avahi[@]}"; do
257         IFS=';' read -ra data <<<"$l" # split line into array
259         # Gather information about the light
260         device=$(echo "${data[3]}" | sed -e 's/\\032/ /g') # fix avahi output
261         hostname=${data[6]}
262         [[ ${data[7]} =~ fe80 ]] && ipv6=${data[7]} || ipv4=${data[7]}
263         port=${data[8]}
264         txt=$(eval echo "${data[9]}") # eval to strip quotes
265         [[ $txt =~ mf=([^[[:space:]]*]*) ]] && manufacturer=${BASH_REMATCH[1]}
266         [[ $txt =~ id=([^[[:space:]]*]*) ]] && mac=${BASH_REMATCH[1]}
267         [[ $txt =~ md=.+[[:space:]]([^[[:space:]]*]*)[[:space:]]id= ]] && sku=${BASH_REMATCH[1]}
269         # Get information from the light
270         url="http://$ipv4:$port"
272         declare protocol="--ipv4"
273         if [[ $ipv4 == "N/A" ]]; then
274             # Workaround: Ignoring ipv6 as Elgato miss-announces addressing and is not accepting requests
275             # properly for v6. Will not change to filter only on ipv4 from avahi, as that can cause us to only end
276             # up with an ipv6 address even though it was announced as ipv4, which in turn means we cannot communicate.
277             continue
278             # Remove above and uncomment below if a future update fixes ipv6 announcement and requests
279             #protocol="--ipv6"
280             #url="http://[$ip]:$port"
281         fi
283         cfg=$(eval "${call} GET $protocol ${url}${settings}") >/dev/null
284         info=$(eval "${call} GET $protocol ${url}${accessory_info}") >/dev/null
285         light=$(eval "${call} GET $protocol ${url}${devices}") >/dev/null
287         json=$(jq -n \
288             --arg dev "$device" \
289             --arg hn "$hostname" \
290             --arg ipv4 "$ipv4" \
291             --arg ipv6 "$ipv6" \
292             --argjson port "$port" \
293             --arg mf "$manufacturer" \
294             --arg mac "$mac" \
295             --arg sku "$sku" \
296             --arg url "$url" \
297             --argjson cfg "$cfg" \
298             '{device: $dev, manufacturer: $mf, hostname: $hn, url: $url, ipv4: $ipv4, ipv6: $ipv6, 
299                 port: $port, mac: $mac, sku: $sku, settings: $cfg}')
301         # Store the light as json and merge info + light into base object
302         lights["$device"]=$(echo "$info $light $json" | jq -s '. | add')
304         # Reset for next light as we are processing the last avahi line
305         default_light_properties
307     done
310 # Manage user parameters
311 parse_params "$@"
313 # Make sure dependencies are installed
314 dependencies avahi-browse curl notify-send jq
316 find_lights
318 # Fail if we cannot find lights
319 [[ ${#lights[@]} -eq 0 ]] && die "No lights found"
321 produce_json
323 # Dispatch actions
324 case $action in
325 usage) usage ;;
326 list) output ;;
327 status) status ;;
328 on) set_state 1 ;;
329 off) set_state 0 ;;
330 -?*) die "Unknown action" ;;
331 esac