#!/bin/bash
-#set -x
-set -o nounset
set -Eeuo pipefail
trap destroy SIGINT SIGTERM ERR EXIT
# Settings
-temp_file="/tmp/elgatokeylights"
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd -P)
icon="${script_dir}/assets/elgato.png"
declare -i silent=0
declare -i pretty=0
declare action="usage"
-declare target=""
+declare target='.'
+declare limit=""
+declare format="json"
declare -A lights
declare lights_json
-declare call="curl --silent --show-error --location --header 'Accept: application/json' --request"
+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() {
- echo "mm"
if [ $silent -eq 0 ]; then
notify-send -i "$icon" "Key Light Controller" "$1"
fi
}
-error() {
- echo >&2 -e "${1-}"
-}
-
die() {
- local msg=$1
- local code=${2-1} # default exit status 1
- error "$msg"
- exit "$code"
+ echo >&2 -e "${1-}"
+ exit "${2-1}"
}
destroy() {
code=$?
- rm "$temp_file" 2>/dev/null
exit ${code}
}
usage() {
cat <<EOF
-Usage: $(basename "${BASH_SOURCE[0]}") [-h] [-l] [-p] [-s] [-v] <action>
+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.
status Get state of lights
on Turn all lights on
off Turn all lights off
- temperature Set temperature level (260-470)
+ 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
--p, --pretty Pretty print console output
--v, --verbose Print script debug info
--f, --flag Some flag description
--s, --silent Supress notifications
+-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
}
while :; do
case "${1-}" in
-h | --help) usage ;;
- -p | --pretty) pretty=1;;
+ -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=1
- target="${2-""}"
+ -t | --target)
+ target="${2-}"
shift
;;
-?*) die "Unknown option: $1" ;;
args=("$@")
# check required params and arguments
- declare -A actions=([help]=1 [list]=1)
- [[ ${#args[@]} -ne 1 ]] && die "Incorrect action count, 1 allowed"
+ 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"
- [[ ($silent -eq 1) && ($pretty -eq 1) ]] && die "Cannot use silent and pretty options simultaneously"
-
[[ -n "${actions[${args[0]}]}" ]] && action="${args[0]}"
-
+
return 0
}
dependencies() {
for var in "$@"; do
if ! command -v $var &>/dev/null; then
- error "Dependency $var was not found, please install and try again"
+ die "Dependency $var was not found, please install and try again"
fi
done
+}
+
+default_light_properties() {
+ # Default values for json type enforcement
+ device="N/A"
+ hostname="N/A"
+ manufacturer="N/A"
+ ipv4="N/A"
+ ipv6="N/A"
+ port=0
+ mac="N/A"
+ sku="N/A"
+ cfg="{}"
+ url="{}"
+ info="{}"
+ light="{}"
}
produce_json() {
- declare json
- for l in "${!lights[@]}"; do
- json+="${lights[$l]},"
- done
+ 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)) ')
+
+}
- lights_json="[${json%,}]"
+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 '.'
+ echo "${1-}" | jq '.'
else
- echo "$1"|jq -c -M '.'
+ echo "${1-}" | jq -c -M '.'
fi
-
+
exit 0
}
-print_status() {
- die "To be implemented"
+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() {
die "To be implemented"
}
-
find_lights() {
# Scan the network for Elgato devices
- avahi-browse -d local _elg._tcp --resolve -t | grep -v "^\+" >"$temp_file"
-
- # Declaration for json type forcing
- declare device="N/A"
- declare hostname="N/A"
- declare manufacturer="N/A"
- declare ip="N/A"
- declare -i port=0
- declare mac="N/A"
- declare sku="N/A"
-
- cat "$temp_file" > tmp
- while read -r line; do
+ declare -a avahi
+ readarray -t avahi < <(avahi-browse -d local _elg._tcp --resolve -t -p | grep -v "^\+")
+
+ declare device
+ declare hostname
+ declare manufacturer
+ declare ipv4
+ declare ipv6
+ declare -i port
+ declare mac
+ declare sku
+ declare cfg
+ declare url
+ declare info
+ declare light
+ default_light_properties
+
+ for l in "${avahi[@]}"; do
+ IFS=';' read -ra data <<<"$l" # split line into array
# Gather information about the light
- if [[ ($line == =*) && ($line =~ IPv4[[:space:]](.+)[[:space:]]_elg) ]]; then
- device=$(eval echo "${BASH_REMATCH[1]}") # eval to strip whitespace
- elif [[ $line =~ hostname.+\[(.+)\] ]]; then hostname=${BASH_REMATCH[1]};
- elif [[ $line =~ address.+\[(.+)\] ]]; then ip=${BASH_REMATCH[1]};
- elif [[ $line =~ port.+\[(.+)\] ]]; then port=${BASH_REMATCH[1]};
- elif [[ $line =~ txt.+\[(.+)\] ]]; then
- txt=$(eval echo "${BASH_REMATCH[1]}") # eval to strip single and double quotes
-
- if [[ $txt =~ mf=([^[[:space:]]*]*) ]]; then manufacturer=${BASH_REMATCH[1]}; fi
- if [[ $txt =~ id=([^[[:space:]]*]*) ]]; then mac=${BASH_REMATCH[1]}; fi
- if [[ $txt =~ md=.+[[:space:]]([^[[:space:]]*]*)[[:space:]]id= ]]; then sku=${BASH_REMATCH[1]}; fi
-
-
- # Get information from the light
- declare cfg="{}"
- declare url="{}"
- declare info="{}"
- declare light="{}"
- if [[ ! (-z $ip) && ! (-z $port) ]]; then
- url="http://$ip:$port"
- cfg=$(eval "${call} GET ${url}${settings}") > /dev/null
- info=$(eval "${call} GET ${url}${accessory_info}") > /dev/null
- light=$(eval "${call} GET ${url}${devices}") > /dev/null
- fi
- # Store the light as json
- lights["$ip"]=$( jq -n \
- --arg dev "$device" \
- --arg hn "$hostname" \
- --arg ip "$ip" \
- --arg port "$port" \
- --arg mf "$manufacturer" \
- --arg mac "$mac" \
- --arg sku "$sku" \
- --arg url "$url" \
- --argjson light "$light" \
- --argjson cfg "$cfg" \
- --argjson info "$info" \
- '{device: $dev, manufacturer: $mf, hostname: $hn, url: $url, ip: $ip,
- port: $port, mac: $mac, sku: $sku, light: $light, settings: $cfg, info: $info}' )
-
- # Reset for next light
- declare {device,hostname,manufacturer,url,ip,mac,protocol,sku,cfg}="N/A"
- declare port=0
+ device=$(echo "${data[3]}" | sed -e 's/\\032/ /g') # fix avahi output
+ hostname=${data[6]}
+ [[ ${data[7]} =~ fe80 ]] && ipv6=${data[7]} || ipv4=${data[7]}
+ port=${data[8]}
+ 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]}
+
+ # Get information from the light
+ url="http://$ipv4:$port"
+
+ declare 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
- done <"$temp_file"
-
- rm "$temp_file" 2>/dev/null
+
+ 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
+
+ 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')
+
+ # Reset for next light as we are processing the last avahi line
+ default_light_properties
+
+ done
}
+# Quit if script is run by root
+[[ "$EUID" -eq 0 ]] && die "Not allowed to run as root"
+
# Manage user parameters
parse_params "$@"
find_lights
# Fail if we cannot find lights
-[[ ${#lights[@]} -eq 0 ]] && error "No lights found" 1
+[[ ${#lights[@]} -eq 0 ]] && die "No lights found"
produce_json
# Dispatch actions
-[[ $action == "usage" ]] && usage
-[[ $action == "list" ]] && print_json "${lights_json}"
-[[ $action == "status" ]] && status
-[[ $action == "on" ]] && set_state 1
-[[ $action == "off" ]] && set_state 0
-
-
-
-
-
-
-
+case $action in
+usage) usage ;;
+list) output ;;
+status) status ;;
+on) set_state 1 ;;
+off) set_state 0 ;;
+-?*) die "Unknown action" ;;
+esac