]> git.vanrenterghem.biz Git - elgato-keylight-script.git/blobdiff - keylights.sh
Add root check
[elgato-keylight-script.git] / keylights.sh
index 6c028542290f438637bdc2e8c887ff2904ea9784..64036324e20ea4cf521636342ad99bf317750b0f 100755 (executable)
@@ -1,12 +1,9 @@
 #!/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"
 
@@ -14,10 +11,15 @@ 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"
@@ -25,33 +27,25 @@ 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.
 
@@ -60,18 +54,30 @@ Available actions:
     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
 }
@@ -82,11 +88,19 @@ parse_params() {
     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" ;;
@@ -98,46 +112,121 @@ parse_params() {
     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() {
@@ -145,73 +234,82 @@ 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 "$@"
 
@@ -221,20 +319,16 @@ dependencies avahi-browse curl notify-send jq
 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