forked from nsahq/elgato-key-light-linux
-
Notifications
You must be signed in to change notification settings - Fork 0
/
keylights.sh
executable file
·348 lines (284 loc) · 10.3 KB
/
keylights.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
#!/bin/bash
set -Eeuo pipefail
trap destroy SIGINT SIGTERM ERR EXIT
# Settings
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd -P)
icon="${script_dir}/assets/elgato.png"
# Declarations
declare -i silent=0
declare -i pretty=0
declare action="usage"
declare target='.'
declare limit=""
declare format="json"
declare -A lights
declare lights_json
declare full_json
declare simple_json
declare flat_json
declare call='curl --silent --show-error --location --header "Accept: application/json" --request'
declare devices="/elgato/lights"
declare accessory_info="/elgato/accessory-info"
declare settings="/elgato/lights/settings"
if [ ! -r "${icon}" ]; then icon=sunny; fi
notify() {
if [ $silent -eq 0 ]; then
notify-send -i "${icon}" "Key Light Controller" "${1}"
fi
}
die() {
echo >&2 -e "${1-}"
exit "${2-1}"
}
destroy() {
code=$?
exit ${code}
}
usage() {
cat <<EOF
Usage: $(basename "${BASH_SOURCE[0]}") [-h] [-f <value>] [-l <value>] [-p] [-s] [-t <value>][-v] [--<option>] [--<option> <value>] <action>
Elgato Lights controller. Works for Key Light and Key Light Air.
Available actions:
list List available lights
status Get state of lights
on Turn all lights on
off Turn all lights off
temperature Set temperature level (260-470)
brightness Set brightness level (0-100)
increase Increases brightness by 10
decrease Decreases brightness by 10
Available formats:
json Renders output as JSON (default)
simple Renders output as JSON array of single level objects with subarrays as .(dot) notation JSON
flat Renders output as fully flattened single level JSON with .(dot) notation JSON
html Renders output as basic html table
csv Renders output as csv
table Renders output as a printed table
pair Renders output as flattened key=value pairs
Available options:
-h, --help Print this help and exit
-f, --format Set output format
-l, --limit <list> Limit top level output fields to the specified comma separated list
-p, --pretty Pretty print console output
-s, --silent Supress notifications
-t, --target <filter> Only perform action on devices where value matches filter
-v, --verbose Print script debug info
EOF
exit
}
parse_params() {
# default values of variables set from params
while :; do
case "${1-}" in
-h | --help) usage ;;
-f | --format)
format="${2-}"
shift
;;
-l | --limit)
limit=$(eval echo "\| { ${2-} } ")
shift
;;
-p | --pretty) pretty=1 ;;
-v | --verbose) set -x ;;
-s | --silent) silent=1 ;;
-t | --target)
target="${2-}"
shift
;;
-?*) die "Unknown option: $1" ;;
*) break ;;
esac
shift
done
args=("$@")
# check required params and arguments
declare -A actions=([help]=1 [list]=1 [status]=1 [on]=1 [off]=1)
[[ ${#args[@]} -ne 1 ]] && die "Incorrect argument count"
#[[ ($silent -eq 1) && ($pretty -eq 1) ]] && die "Cannot use silent and pretty options simultaneously"
[[ "${args[0]}" == "increase" ]] && die "Action not yet implemented"
[[ "${args[0]}" == "decrease" ]] && die "Action not yet implemented"
[[ "${args[0]}" == "brightness" ]] && die "Action not yet implemented"
[[ "${args[0]}" == "temperature" ]] && die "Action not yet implemented"
[[ "${args[0]}" == "status" ]] && limit="| { 'displayName,lights' } "
[[ -n "${actions[${args[0]}]}" ]] && action="${args[0]}"
return 0
}
dependencies() {
for var in "$@"; do
if ! command -v ${var} &>/dev/null; then
die "Dependency ${var} was not found, please install and try again"
fi
done
}
produce_json() {
t=$(eval echo "'[.[] ${limit} | select(${target})]'")
f=$(eval echo "'[.[] | select(${target})]'")
lights_json=$(echo "${lights[@]}" | jq -c -s "$t")
full_json=$(echo "${lights[@]}" | jq -c -s "$f")
simple_json=$(echo "${lights_json}" | jq -c '.[] | reduce ( tostream | select(length==2) | .[0] |= [join(".")] ) as [$p,$v] ({}; setpath($p; $v)) ')
simple_json=$(echo "${simple_json}" | jq -c -s '.') # slurp it to make it an array
flat_json=$(echo "${lights_json}" | jq -c -s '.[] | reduce ( tostream | select(length==2) | .[0] |= [join(".")] ) as [$p,$v] ({}; setpath($p; $v)) ')
}
output() {
# Mange user requested output format
case ${format} in
json) print_json "${lights_json}" ;;
simple) print_json "${simple_json}" ;;
flat) print_json "${flat_json}" ;;
table) print_structured '@tsv' ;;
csv) print_structured '@csv' ;;
pair) print_structured 'pairs' ;;
html) print_html ;;
-?*) die "Unknown output format (-f/--format): ${format}" ;;
esac
}
print_json() {
# Manage pretty printing
if [[ $pretty -eq 1 ]]; then
echo "${1-}" | jq '.'
else
echo "${1-}" | jq -c -M '.'
fi
exit 0
}
print_structured() {
pp=${2-$pretty}
# Handle csv and table printing
query="(.[0] | keys_unsorted | map(ascii_upcase)), (.[] | [.[]])|${1-@csv}"
# Handle printing as key value pairs
if [[ ${1} == 'pairs' ]]; then
query='.[] | "--------------",(to_entries[] | [.key, "=", .value] | @tsv)'
fi
# Manage pretty printing
if [[ $pp -eq 1 ]]; then
echo "${simple_json}" | jq --raw-output "${query}" | column -t -s$'\t' | sed -e 's/"//g'
else
if [[ ${1} == 'pairs' ]]; then
echo "${simple_json}" | jq -r "${query}" | sed -e 's/\t//g'
else
echo "${simple_json}" | jq -r "${query}"
fi
fi
}
print_html() {
data=$(print_structured '@csv' 1)
html="
<table>
$(
print_header=true
while read d; do
if ${print_header}; then
echo "<tr><th>${d//,/<\/th><th>}</th></tr>"
print_header=false
continue
fi
echo "<tr><td>${d//,/</td><td>}</td></tr>"
done <<<"${data}"
)
</table>"
echo "${html}"
}
set_state() {
declare -a data
readarray -t data < <(echo "${full_json}" | jq -c '.[] | {displayName, url, numberOfLights, lights}')
declare -a updated
x=$(echo "${1}" | tr 01 10) # "flip the bit"
for d in "${data[@]}"; do
query_old="[.lights[] | select(.on==${x})] | length"
count_found=$(echo "${d}" | jq "${query_old}")
# Don't send to lights already in wanted state
if [[ ${count_found} -eq 0 ]]; then continue; fi
# Extract relevant data and create new json object
url=$(echo "${d}" | jq '.url')
dn=$(echo "${d}" | jq -r '.displayName')
l=$(echo "${d}" | jq -c 'del(.url, .displayName)' | jq ". | .lights[].on = ${1}")
# Send command
if eval "${call} PUT -d '${l}' ${url}${devices}" >/dev/null; then updated+=("${dn}"); fi
done
# Text representation of new state
state="ON"
[[ ${1} -eq 0 ]] && state="OFF"
# Send notification
if [[ ${#updated[*]} -gt 0 ]]; then
n="Turned ${state} ${#updated[@]} lights:\n\n"
for i in "${updated[@]}"; do
n+="${i}\n"
done
notify "${n}"
fi
}
find_lights() {
# Scan the network for Elgato devices
declare -a avahi
readarray -t avahi < <(avahi-browse -d local _elg._tcp --resolve -t -p | grep -v "^\+")
for l in "${avahi[@]}"; do
declare ipv4="N/A"
declare ipv6="N/A"
declare cfg="{}"
declare url="N/A"
declare info="{}"
declare light="{}"
IFS=';' read -ra data <<<"$l" # split line into array
# Gather information about the light
device="${data[3]//\\032/ }"
port="${data[8]}"
hostname="${data[6]}"
if [[ ${data[7]} =~ fe80 ]]; then ipv6=${data[7]}; else ipv4=${data[7]}; fi
txt=$(eval echo "${data[9]}") # eval to strip quotes
[[ $txt =~ mf=([^[[:space:]]*]*) ]] && manufacturer=${BASH_REMATCH[1]}
[[ $txt =~ id=([^[[:space:]]*]*) ]] && mac=${BASH_REMATCH[1]}
[[ $txt =~ md=.+[[:space:]]([^[[:space:]]*]*)[[:space:]]id= ]] && sku=${BASH_REMATCH[1]}
url="http://${ipv4}:${port}"
protocol="--ipv4"
if [[ ${ipv4} == "N/A" ]]; then
# Workaround: Ignoring ipv6 as Elgato miss-announces addressing and is not accepting requests
# properly for v6. Will not change to filter only on ipv4 from avahi, as that can cause us to only end
# up with an ipv6 address even though it was announced as ipv4, which in turn means we cannot communicate.
continue
# Remove above and uncomment below if a future update fixes ipv6 announcement and requests
#protocol="--ipv6"
#url="http://[$ip]:$port"
fi
# Get information from the light
cfg=$(eval "${call} GET ${protocol} ${url}${settings}") >/dev/null
info=$(eval "${call} GET ${protocol} ${url}${accessory_info}") >/dev/null
light=$(eval "${call} GET ${protocol} ${url}${devices}") >/dev/null
# Build json
json=$(jq -n \
--arg dev "${device}" \
--arg hn "${hostname}" \
--arg ipv4 "${ipv4}" \
--arg ipv6 "${ipv6}" \
--argjson port "${port}" \
--arg mf "${manufacturer}" \
--arg mac "${mac}" \
--arg sku "${sku}" \
--arg url "${url}" \
--argjson cfg "${cfg}" \
'{device: $dev, manufacturer: $mf, hostname: $hn, url: $url, ipv4: $ipv4, ipv6: $ipv6,
port: $port, mac: $mac, sku: $sku, settings: $cfg}')
# Store the light as json and merge info + light into base object
lights["${device}"]=$(echo "${info} ${light} ${json}" | jq -s '. | add')
done
}
# Quit if script is run by root
[[ "${EUID}" -eq 0 ]] && die "Not allowed to run as root"
# Manage user parameters
parse_params "$@"
# Make sure dependencies are installed
dependencies avahi-browse curl notify-send jq
find_lights
# Fail if we cannot find lights
[[ ${#lights[@]} -eq 0 ]] && die "No lights found"
produce_json
# Dispatch actions
case ${action} in
usage) usage ;;
help) usage ;;
list) output ;;
status) output ;;
on) set_state 1 ;;
off) set_state 0 ;;
-?*) die "Unknown action" ;;
esac