]> git.vanrenterghem.biz Git - elgato-keylight-script.git/blob - keylights.sh
Co-authored-by: JonssonViktor <JonssonViktor@users.noreply.github.com>
[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 format="json"
16 declare -A lights
17 declare lights_json
18 declare simple_json
19 declare flat_json
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
27 notify() {
28     if [ $silent -eq 0 ]; then
29         notify-send -i "$icon" "Key Light Controller" "$1"
30     fi
31 }
33 die() {
34     echo >&2 -e "${1-}"
35     exit "${2-1}"
36 }
38 destroy() {
39     code=$?
41     exit ${code}
42 }
44 usage() {
45     cat <<EOF
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.
50 Available actions:
51     list        List available lights
52     status      Get state of lights
53     on          Turn all lights on
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
60 Available formats:
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
70 Available options:
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
78 EOF
79     exit
80 }
82 parse_params() {
83     # default values of variables set from params
85     while :; do
86         case "${1-}" in
87         -h | --help) usage ;;
88         -f | --format)
89             format="${2-}"
90             shift
91             ;;
92         -p | --pretty) pretty=1 ;;
93         -v | --verbose) set -x ;;
94         -s | --silent) silent=1 ;;
95         -t | --target)
96             target="${2-}"
97             shift
98             ;;
99         -?*) die "Unknown option: $1" ;;
100         *) break ;;
101         esac
102         shift
103     done
105     args=("$@")
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]}"
115     return 0
118 dependencies() {
119     for var in "$@"; do
120         if ! command -v $var &>/dev/null; then
121             die "Dependency $var was not found, please install and try again"
122         fi
123     done
126 default_light_properties() {
127     # Default values for json type enforcement
128     device="N/A"
129     hostname="N/A"
130     manufacturer="N/A"
131     ipv4="N/A"
132     ipv6="N/A"
133     port=0
134     mac="N/A"
135     sku="N/A"
136     cfg="{}"
137     url="{}"
138     info="{}"
139     light="{}"
143 produce_json() {
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)) ')
153 output() {
155     # Mange user requested output format
156     case $format in
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' ;;
163     html) print_html ;;
164     -?*) die "Unknown output format (-f/--format): $format" ;;
165     esac
168 print_json() {
170     # Manage pretty printing
171     if [[ $pretty -eq 1 ]]; then
172         echo "${1-}" | jq '.'
173     else
174         echo "${1-}" | jq -c -M '.'
175     fi
177     exit 0
180 print_structured() {
181     pp=${2-$pretty}
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)'
188     fi
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'
193     else
194         echo "${simple_json}" | jq -r "$query"
195     fi
198 print_html() {
199     data=$(print_structured '@csv' 1)
201     html="
202     <table>
203     $(
204         print_header=true
205         while read d; do
206             if $print_header; then
207                 echo "<tr><th>${d//,/<\/th><th>}</th></tr>"
208                 print_header=false
209                 continue
210             fi
211             echo "<tr><td>${d//,/</td><td>}</td></tr>"
212         done <<<"${data}"
213     )
214     </table>"
215     echo "$html"
218 set_state() {
219     new_state=$1
220     die "To be implemented"
223 find_lights() {
224     # Scan the network for Elgato devices
225     declare -a avahi
226     readarray -t avahi < <(avahi-browse -d local _elg._tcp --resolve -t -p | grep -v "^\+")
228     declare device
229     declare hostname
230     declare manufacturer
231     declare ipv4
232     declare ipv6
233     declare -i port
234     declare mac
235     declare sku
236     declare cfg
237     declare url
238     declare info
239     declare light
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
247         hostname=${data[6]}
248         [[ ${data[7]} =~ fe80 ]] && ipv6=${data[7]} || ipv4=${data[7]}
249         port=${data[8]}
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.
263             continue
264             # Remove above and uncomment below if a future update fixes ipv6 announcement and requests
265             #protocol="--ipv6"
266             #url="http://[$ip]:$port"
267         fi
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
273         json=$(jq -n \
274             --arg dev "$device" \
275             --arg hn "$hostname" \
276             --arg ipv4 "$ipv4" \
277             --arg ipv6 "$ipv6" \
278             --argjson port "$port" \
279             --arg mf "$manufacturer" \
280             --arg mac "$mac" \
281             --arg sku "$sku" \
282             --arg url "$url" \
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
293     done
296 # Manage user parameters
297 parse_params "$@"
299 # Make sure dependencies are installed
300 dependencies avahi-browse curl notify-send jq
302 find_lights
304 # Fail if we cannot find lights
305 [[ ${#lights[@]} -eq 0 ]] && die "No lights found"
307 produce_json
309 # Dispatch actions
310 case $action in
311 usage) usage ;;
312 list) output ;;
313 status) status ;;
314 on) set_state 1 ;;
315 off) set_state 0 ;;
316 -?*) die "Unknown action" ;;
317 esac