#!/usr/bin/env bash
#
# Innerscene fixture control (Bash) - set color temperature & brightness over Wi-Fi.
# Requires: curl and openssl (both standard on macOS and Linux).
#
#   ./fixture_control.sh 192.168.1.42 4500 0.5           # 4500 K at 50%
#   ./fixture_control.sh 192.168.1.42 4500 0.5 mypass    # with a custom password
#
# -----------------------------------------------------------------------------
# Wi-Fi HTTP authentication
# -----------------------------------------------------------------------------
# Fixture firmware 2.0.253 added a per-request authentication gate on the Wi-Fi
# (LAN) HTTP interface, and it is ON BY DEFAULT. On 2.0.253+ every request must be
# signed (an installer can turn it off via "Require Password over Wi-Fi" on the
# developer screen). Firmware older than 2.0.253 has no gate.
#
# This script is backward compatible: it sends the request unsigned first, and
# only runs the handshake if the fixture answers 401. So the same code works on
# older firmware, on 2.0.253+ with the gate on (signs after the 401), and on
# 2.0.253+ with the gate off (no signing needed).
#
# Default password is "innerscene" (the fixture's Wi-Fi setup AP password).
# -----------------------------------------------------------------------------
set -euo pipefail

IP="${1:?Usage: fixture_control.sh <ip> <cct> <intensity> [password]}"
CCT="${2:?missing cct}"
INTENSITY="${3:?missing intensity}"
PASSWORD="${4:-innerscene}"
BASE="http://$IP"

SID=""; KSIG_HEX=""; CTR=0

hmac_hex() { # hmac_hex <key-macopt> ; message on stdin -> hex mac on stdout
  openssl dgst -sha256 -mac HMAC -macopt "$1" -hex | sed 's/.*= //'
}

json_field() { # json_field <name> <json> -> hex value of "name":"<hex>"
  echo "$2" | grep -oE "\"$1\":\"[0-9a-f]+\"" | head -n1 | sed -E "s/.*:\"([0-9a-f]+)\"/\1/"
}

authenticate() {
  local challenge nonce salt response verify tmp
  challenge="$(curl -s "$BASE/auth/request")"
  SID="$(json_field sid "$challenge")"
  nonce="$(json_field nonce "$challenge")"
  salt="$(json_field salt "$challenge")"

  # response = HMAC(password, nonce||salt). Write the raw bytes to a temp file
  # so binary (including null bytes) survives the pipe intact.
  tmp="$(mktemp)"; printf '%s%s' "$nonce" "$salt" | xxd -r -p > "$tmp"
  response="$(hmac_hex "key:$PASSWORD" < "$tmp")"

  verify="$(curl -s "$BASE/auth/verify2?sid=$SID&response=$response")"
  if ! echo "$verify" | grep -q '"a":1'; then
    rm -f "$tmp"
    echo "fixture rejected the password (default is 'innerscene')" >&2
    exit 1
  fi

  # K_sig = HMAC(password, nonce||salt||"a2-sig-v1").
  printf '%s%s' "$nonce" "$salt" | xxd -r -p > "$tmp"
  printf 'a2-sig-v1' >> "$tmp"
  KSIG_HEX="$(hmac_hex "key:$PASSWORD" < "$tmp")"
  rm -f "$tmp"
  CTR=0
}

# GET a path; sign if we hold a session; on 401 authenticate and retry once.
fx_get() {
  local path="$1" code body hdrs=()
  if [ -n "$KSIG_HEX" ]; then
    CTR=$((CTR + 1))
    # canonical = METHOD "\n" URI "\n" CTR "\n" CONTENT_LENGTH (0 for GET)
    local sig
    sig="$(printf 'GET\n%s\n%s\n0' "$path" "$CTR" | hmac_hex "hexkey:$KSIG_HEX")"
    hdrs=(-H "X-Sig-Sid: $SID" -H "X-Sig-Ctr: $CTR" -H "X-Sig: $sig")
  fi
  body="$(curl -s -w '\n%{http_code}' "${hdrs[@]}" "$BASE$path")"
  code="${body##*$'\n'}"; body="${body%$'\n'*}"
  if [ "$code" = "401" ]; then
    KSIG_HEX=""; authenticate; fx_get "$path"; return
  fi
  echo "$body"
}

echo "Before : $(fx_get /getStatus)"
echo "Set    : $(fx_get "/setCCT?cct=$CCT&i=$INTENSITY")  ($CCT K, $INTENSITY)"
echo "After  : $(fx_get /getStatus)"
