#!/bin/sh
#
# Turn WLED installations in the C4 hackspace on/off, change brightness or
# switch between presets.
#
# Author: Shy
# License: CC0

NAME="c4wled.sh"
COMMAND="c4wled"
VERSION="0.2.0"

exit_code=0
host_list=

# Shell specific options.
case "$(readlink /proc/$$/exe)" in
    */zsh) setopt SH_WORD_SPLIT ;;
esac

# Make the exit status of command pipelines the status of the last command
# with non-zero exit status - if supported by this shell.
(set -o pipefail 2>/dev/null) && set -o pipefail

print_usage() {
    echo "\
Usage: $COMMAND -f(norcenter)|-w(wohnzimmer)|-a(ll) on|off
       $COMMAND -f|-w|-a 1-17|list
       $COMMAND -f|-w|-a -b [1-255]"
}

print_help() {
    print_usage
    echo "
Instance selection:
    -f,--fnordcenter        select fnordcenter
    -w,--wohnzimmer         select wohnzimmer
    -a,--all                select all known instances

Commands:
    on|off                  switch on/off
    1-17                    switch preset
    l, list                 list presets
    -b,--brightness [1-255] get or set brightness

Exit status:
    0   no failure
    1   operating error
    2   child command error
    4   unexpected API response"
}

curl_fetch() {
    curl --silent --show-error --fail --ipv4 --max-filesize 32768 \
         --max-redirs 0 --header 'Accept: application/json' "$1"
}

curl_send() {
    curl --silent --show-error --fail --ipv4 --max-filesize 1024 \
         --max-redirs 0 --header 'Accept: application/json' \
         --header 'Content-Type: application/json' --data "$1" "$2"
}

active_preset() {
    # Find number of active preset. API returns -1 if none is active, we return
    # an empty string.
    curl_fetch "http://$1/json" | \
        # -1 will be a no-match.
        sed -n 's/.*"ps"[[:space:]]*:[[:space:]]*\(1\?[0-9]\).*/\1/p'
}

current_brightness() {
    # Find current brightness.
    curl_fetch "http://$1/json" | \
        sed -n 's/.*"state"[[:space:]]*:[[:space:]]*{[^}]*"bri"[[:space:]]*:[[:space:]]*\([0-9]\{1,3\}\).*/\1/p'
}

if test $# -eq 0; then
    print_help
    exit 0
fi

# Parse instance argument and store hostnames.
while test $# -gt 0; do
    case "$1" in
        -h|--help)
            print_help
            exit 0
        ;;
        -v|--version)
            echo "$NAME version $VERSION"
            exit 0
        ;;
        -a|--all)
            host_list="wled-fnordcenter.local wled-wohnzimmer.local"
        ;;
        -f|--fnordcenter)
            host_list="$host_list wled-fnordcenter.local"
        ;;
        -w|--wohnzimmer)
            host_list="$host_list wled-wohnzimmer.local"
        ;;
        *) # Unknown instance parameter.
            test -n "$host_list" && break
            echo "Error: not a valid instance option (\"$1\")." >&2
            print_usage
            exit 1
        ;;
    esac
    shift
done

# Parse and execute command argument against given instances.
for wled_host in $host_list; do
    case "$1" in
        on) # Switch on.
            api_resp=$(curl_send '{"on":true}' "http://$wled_host/json")
        ;;
        off) # Switch off.
            api_resp=$(curl_send '{"on":false}' "http://$wled_host/json")
        ;;
        -b|--brightness) # Set brightness.
            if test -z "$2"; then
                # No parameter given. Print current brightness.
                echo "Brightness of $wled_host:"
                current_brightness "$wled_host"
            else
                # Try to convert 3rd parameter to an integer between 1 an 255.
                bright=$(printf '%u' "$2" 2>/dev/null)
                if test $? -gt 0; then # Conversion failed.
                    echo "Error: unable to parse parameter for brightness." >&2
                    exit 1
                elif test "$bright" -lt 1 -o "$bright" -gt 255; then
                    echo "Error: parameter for brightness is out of range." >&2
                    exit 1
                fi
                api_resp=$(curl_send "{'bri':$bright}" "http://$wled_host/json")
            fi
        ;;
        [1-9]|1[0-7]) # Switch to preset.
            api_resp=$(curl_send "{'ps':$1}" "http://$wled_host/json")
        ;;
        l|list) # List presets.
            test -t 1 && printf 'connecting ...\r'
            active_ps=$(active_preset "$wled_host")
            curl_fetch "http://$wled_host/presets.json" | \
                # Insert newline in front of every preset slice.
                # (Start of loop.)
                # Match number and name of first preset.
                # Replace in number: name format.
                # Match and mark active preset.
                # Right-align number.
                # Print out up to next newline.
                # Delete up to next newline.
                # Process next preset or end of loop.
                # Prepend description.
                sed --sandbox -n '
                    s/"1\?[0-9]"[[:space:]]*:/\n&/g
                    :loop
                    /^"\(1\?[0-9]\)"[[:space:]]*:[^\n]*"n"[[:space:]]*:[[:space:]]*"\([[:alnum:] _-]*\)"[^\n]*/{
                        s//\1: \2/
                        s/^'"$active_ps"':[^\n]*/& */
                        s/^[0-9]:/ &/
                        P
                    }
                    s/^[^\n]*\n//
                    t loop
                    i\
'"Presets on $wled_host:" | \
                # Sort.
                sort -n -b -t : -k 1,1
        ;;
        "") # Missing command.
            echo "Error: missing command." >&2
            print_usage
            exit 1
        ;;
        *) # Unknown command.
            echo "Error: unknown command \"$1\"." >&2
            print_usage
            exit 1
        ;;
    esac

    # Check curl (or maybe sed) exit status
    child_exit=$?
    if test $child_exit -ne 0; then
        echo "Error: curl or sed exited with exit code $child_exit." >&2
        exit_code=$((exit_code|2))
    fi

    # Check API response.
    if test -n "$api_resp" && echo "$api_resp" | grep -qv '{\s*"success"\s*:\s*true\s*}'; then
        echo "Error: unexpected response from WLED API." >&2
        exit_code=$((exit_code|4))
    fi
done

exit $exit_code