From b83878afff7a38f66759573390ff61cb376fefc5 Mon Sep 17 00:00:00 2001 From: Eric Loyd Date: Tue, 9 Dec 2025 14:55:35 -0500 Subject: [PATCH] Even more control, now with MQTT and GoVee --- AstropotaPOD.sh | 222 ++++++++++++++++++++++++++++++++++++++++++++++++ govee.sh | 180 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 402 insertions(+) create mode 100755 AstropotaPOD.sh create mode 100755 govee.sh diff --git a/AstropotaPOD.sh b/AstropotaPOD.sh new file mode 100755 index 0000000..e6d94ae --- /dev/null +++ b/AstropotaPOD.sh @@ -0,0 +1,222 @@ +#!/usr/bin/env bash + +myCommand="" +debug="" +PODip="192.168.0.72" +PODport="32000" +PPBAkey="" +PPBAname="" +myDevice="all" + +do_help() { + cat << HELP_EOF +Usage: $0 [OPTIONS] + command is one of: + --mqtt) Send HA MQTT discovery information to broker + --stats) Send MQTT stats to broker + --init) Not sure + --start) Starts the PPBA driver + --power [on|off] ) Powers on a device (see DEV, below) + --ip) IP of the PPBA API server + --port) Port of the PPBA API server + -d|--debug) Print debug information + -h|--help) print this help information + + DEV is a device name: + all | quad | var | dew1 | dew2 | autodew +HELP_EOF + exit +} + +while [ -n "$1" ]; do + case "$1" in + -h|--help) do_help; shift 1;; + --mqtt*) myCommand="mqtt"; shift 1;; + --stat*) myCommand="stats"; shift 1;; + --init*) myCommand="init"; shift 1;; + --start) myCommand="start"; shift 1;; + --power) myCommand="power${2:-on}"; myDevice="$3"; shift 3;; + --poweron) myCommand="poweron"; myDevice="$2"; shift 2;; + --poweroff) myCommand="poweroff"; myDevice="$2"; shift 2;; + --ip) PODip="$2"; shift 2;; + --port) PODport="$2"; shift 2;; + -d|--debug) debug="true"; shift 1;; + *) shift 1;; + esac +done + + +do_debug() { + [ -n "$debug" ] && echo "DEBUG: $*" >&2 +} + +do_api() { + ep="${1#/}" + method="${2:-GET}" + notes="${3}" + url="http://${PODip}:${PODport}/${ep}" + do_debug "curl -s -X ${method} \"$url\"" + [ -n "$notes" ] && echo "$notes" >&2 + curl -s -X ${method} "$url" +} + +do_getKey() { + # Get POD PPBA unique key and name + PPBAkey=$(do_api "/Server/DeviceManager/Connected" | jq -r '.data[] | select (.name =="PPBAdvance") | .uniqueKey') + PPBAname=$(do_api "/Server/DeviceManager/Device/${PPBAkey}/ProfileName" | jq -r '.data') + do_debug "PPBAkey=$PPBAkey, PPBAname=$PPBAname" + if [ -z "$PPBAkey" -o -z "$PPBAname" ]; then + # The PPBA is not powered on or cannot be found + do_debug "The PPBA is not powered on or cannot be found" + exit + fi +} + +do_init() { + do_getKey + do_api "/Server/Start" PUT "Starting server..." | jq '.status' + sleep 2 +} + +do_start() { + do_getKey + do_api "/Driver/PPBAdvance/Start?DriverUniqueKey=${PPBAkey}" OPTIONS "Starting PPBAdvance driver..." | jq '.status' + do_api "/Driver/PPBAdvance/Power/Hub/Off?DriverUniqueKey=${PPBAkey}" PUT "Turning off Quad Power..." | jq '.status' + do_api "/Driver/PPBAdvance/Power/Variable/Off?DriverUniqueKey=${PPBAkey}" PUT "Turning off Variable Power..." | jq '.status' + do_api "/Driver/PPBAdvance/Dew/1/Off?DriverUniqueKey=${PPBAkey}" PUT "Turning off Dew Heater 1..." | jq '.status' + do_api "/Driver/PPBAdvance/Dew/2/Off?DriverUniqueKey=${PPBAkey}" PUT "Turning off Dew Heater 2..." | jq '.status' + do_api "/Driver/PPBAdvance/Dew/Auto/Off?DriverUniqueKey=${PPBAkey}" POST "Turning off Auto Dew..." | jq '.status' +} + +do_poweroff() { + do_getKey + case "$myDevice" in + quad*|all) do_api "/Driver/PPBAdvance/Power/Hub/Off?DriverUniqueKey=${PPBAkey}" PUT "Turning Off Quad Power..." | jq '.status';;& + var*|all) do_api "/Driver/PPBAdvance/Power/Variable/Off?DriverUniqueKey=${PPBAkey}" PUT "Turning Off Variable Power..." | jq '.status';;& + dew1|all) do_api "/Driver/PPBAdvance/Dew/1/Off?DriverUniqueKey=${PPBAkey}" PUT "Turning Off Dew Heater 1..." | jq '.status';;& + dew2|all) do_api "/Driver/PPBAdvance/Dew/2/Off?DriverUniqueKey=${PPBAkey}" PUT "Turning Off Dew Heater 2..." | jq '.status';;& + autodew|all) do_api "/Driver/PPBAdvance/Dew/Auto/Off?DriverUniqueKey=${PPBAkey}" POST "Turning Off Auto Dew..." | jq '.status';; + esac +} + +do_poweron() { + do_getKey + case "$myDevice" in + quad*|all) do_api "/Driver/PPBAdvance/Power/Hub/On?DriverUniqueKey=${PPBAkey}" PUT "Turning on Quad Power..." | jq '.status';;& + var*|all) do_api "/Driver/PPBAdvance/Power/Variable/On?DriverUniqueKey=${PPBAkey}" PUT "Turning on Variable Power..." | jq '.status';;& + dew1|all) do_api "/Driver/PPBAdvance/Dew/1/On?DriverUniqueKey=${PPBAkey}" PUT "Turning on Dew Heater 1..." | jq '.status';;& + dew2|all) do_api "/Driver/PPBAdvance/Dew/2/On?DriverUniqueKey=${PPBAkey}" PUT "Turning on Dew Heater 2..." | jq '.status';;& + autodew|all) do_api "/Driver/PPBAdvance/Dew/Auto/On?DriverUniqueKey=${PPBAkey}" POST "Turning on Auto Dew..." | jq '.status';; + esac +} + + +mqtt_pub() { + topic="$1" + message="$2" + mosquitto_pub -h ssdnode-1.bitnetix.com -t "PPBA/status/$topic" -m "$message" +} + +do_stats() { + do_getKey + stats=$(do_api "/Driver/PPBAdvance/Report?DriverUniqueKey=${PPBAkey}") + for stat in "voltage" "current" "quadCurrent" "power" "temperature" "humidity" "dewPoint" "isOverCurrent" "averageAmps" "ampsPerHour" "wattPerHour" "upTime"; do + val=$(echo "$stats" | jq -r ".data.message.$stat") + mqtt_pub "$stat" "$val" + done + for stat in "powerHubStatus" "powerVariablePortStatus" "ppbA_DualUSB2Status"; do + val=$(echo "$stats" | jq -r ".data.message.$stat.state") + mqtt_pub "$stat" "$val" + done +} + +### +### + +do_mqtt() { + do_getKey + JSON_DATA=$(do_api "/Driver/PPBAdvance/Report?DriverUniqueKey=${PPBAkey}") + MQTT_HOST="ssdnode-1.bitnetix.com" # <-- change this! + MQTT_USER="" # optional + MQTT_PASS="" # optional + DEVICE_ID="ppbadv_gen2" + DISCOVERY_PREFIX="homeassistant" + STATE_TOPIC="$DEVICE_ID/state" + STATE_TOPIC="PPBA/status" + POLL_INTERVAL=30 # seconds + + publish_discovery() { + echo "Publishing Home Assistant MQTT discovery configs..." + # Helper to publish a single discovery config message + publish_sensor() { + local ENTITY="$1" + local NAME="$2" + local UNIT="$3" + local DEVICE_CLASS="$4" + local VALUE_TEMPLATE="{{ value_json.$ENTITY }}" + local TOPIC="$DISCOVERY_PREFIX/sensor/$DEVICE_ID/${ENTITY}/config" + local PAYLOAD="{ \ + \"name\": \"$NAME\", \ + \"state_topic\": \"$STATE_TOPIC/${ENTITY}\", \ + \"unique_id\": \"${DEVICE_ID}_${ENTITY}\", \ + \"device\": { \ + \"identifiers\": [\"$DEVICE_ID\"], \ + \"manufacturer\": \"Pegasus Astro\", \ + \"model\": \"Pocket Powerbox Advance Gen2\", \ + \"name\": \"PPB Advance Gen2\" \ + }" + + # Optional fields + if [ -n "$UNIT" ]; then + PAYLOAD="$PAYLOAD, \"unit_of_measurement\": \"$UNIT\"" + fi + if [ -n "$DEVICE_CLASS" ]; then + PAYLOAD="$PAYLOAD, \"device_class\": \"$DEVICE_CLASS\"" + fi + + PAYLOAD="$PAYLOAD }" + mosquitto_pub -h "$MQTT_HOST" -u "$MQTT_USER" -P "$MQTT_PASS" \ + -t "$TOPIC" -m "$PAYLOAD" -r + } + # Sensors + publish_sensor "voltage" "PPB Voltage" "V" "voltage" + publish_sensor "current" "PPB Current" "A" "current" + publish_sensor "temperature" "PPB Temperature" "°C" "temperature" + publish_sensor "humidity" "PPB Humidity" "%" "humidity" + publish_sensor "dewPoint" "PPB Dew Point" "°C" "" + publish_sensor "averageAmps" "PPB Average Amps" "A" "" + publish_sensor "ampsPerHour" "PPB Amps Per Hour" "Ah" "" + publish_sensor "wattPerHour" "PPB Watt Per Hour" "Wh" "" + publish_sensor "upTime" "PPB Uptime" "" "" + publish_sensor "powerHubStatus" "PPB Power Hub Status" "" "" + publish_sensor "powerVariablePortStatus" "PPB Variable Port" "" "" + publish_sensor "ppbA_DualUSB2Status" "PPB Dual USB2 Status" "" "" + } + publish_discovery + +### echo "Starting PPBADV → MQTT polling loop..." +### while true; do +### if [ -n "$JSON_DATA" ]; then +### mosquitto_pub -h "$MQTT_HOST" -u "$MQTT_USER" -P "$MQTT_PASS" \ +### -t "$STATE_TOPIC" -m "$JSON_DATA" +### else +### echo "WARNING: Failed to fetch API data from PPBADV Gen2" +### fi +### +### sleep "$POLL_INTERVAL" +### done + exit +} + +do_debug "Command = $myCommand" +case "$myCommand" in + mqtt) do_mqtt;; + stats) do_stats;; + init) do_init;; + start) do_start;; + poweroff) do_poweroff;; + poweron) do_poweron;; + *) do_help;; +esac + +exit diff --git a/govee.sh b/govee.sh new file mode 100755 index 0000000..1874f91 --- /dev/null +++ b/govee.sh @@ -0,0 +1,180 @@ +#!/usr/bin/env bash +# govee.sh - small CLI wrapper for Govee "control device" API +# Requirements: curl, jq, uuidgen (or openssl) +# Usage examples at bottom. + +API_BASE="${GOVEE_API_BASE:-https://openapi.api.govee.com}" +CONTROL_ENDPOINT="${API_BASE}/router/api/v1/device/control" +#API_KEY="${GOVEE_API_KEY:-}" +API_KEY="933194fa-d341-4362-b371-de290cb99a58" + +mySKU="H6110" +myID="20:11:A4:C1:38:9B:CB:54" + +if [[ -z "$API_KEY" ]]; then + echo "ERROR: Set GOVEE_API_KEY environment variable (export GOVEE_API_KEY=xxxx)" + exit 2 +fi + +get_info() { + local entry="router/api/v1/user/devices" + json=$(curl -s -k ${API_BASE}/${entry} \ + -H "Content-Type: application/json" \ + -H "Govee-API-Key: ${API_KEY}") + sku=$(echo "$json" | jq -r ".data[].sku") + device=$(echo "$json" | jq -r ".data[].device") +} + +# helpers +uuid() { + if command -v uuidgen >/dev/null 2>&1; then + uuidgen + else + # fallback + date +%s%N | sha1sum | cut -c1-32 + fi +} + +# convert hex/rrggbb or r,g,b to integer (0..16777215) +rgb_to_int() { + local in="$1" + if [[ "$in" =~ ^#?[0-9A-Fa-f]{6}$ ]]; then + # hex + in="${in#'#'}" + printf "%d\n" "$((0x$in))" + return + fi + # r,g,b + IFS=',' read -r r g b <<< "$in" + r=${r:-0}; g=${g:-0}; b=${b:-0} + # validate numeric + for val in "$r" "$g" "$b"; do + if ! [[ "$val" =~ ^[0-9]+$ ]] || (( val < 0 )) || (( val > 255 )); then + echo "ERR" # caller should handle + return 1 + fi + done + echo $(( (r << 16) + (g << 8) + b )) +} + +# build and send the control request +_govee_control() { + local sku="$1"; shift + local device="$1"; shift + local type="$1"; shift + local instance="$1"; shift + local value="$1" + + local reqid + reqid="$(uuid)" + + local body + body=$(jq -n \ + --arg rid "$reqid" \ + --arg sku "$sku" \ + --arg device "$device" \ + --arg type "$type" \ + --arg instance "$instance" \ + --argjson value "$value" \ + '{requestId:$rid, payload: { sku:$sku, device:$device, capability: { type:$type, instance:$instance, value:$value } } }') + + curl -sS -X POST "$CONTROL_ENDPOINT" \ + -H "Content-Type: application/json" \ + -H "Govee-API-Key: $API_KEY" \ + -d "$body" +} + +print_usage() { + cat < [options] + +Commands: + power Turn device on or off + toggle <0|1> + brightness <0..100> Set brightness (range depends on device) + color_rgb Set RGB color (hex like ff8800 or "255,100,0") + temp_k Set color temperature (e.g. 2700) + mode Set a mode or scene by numeric value + raw Accepts raw JSON payload from stdin and sends it + +Examples: + ./govee.sh power H605C "64:09:C5:32:37:36:2D:13" on + ./govee.sh brightness H605C "64:09:C5:32:37:36:2D:13" 50 + ./govee.sh color_rgb H605C "64:09:C5:32:37:36:2D:13" ff0088 + ./govee.sh color_rgb H605C "64:09:C5:32:37:36:2D:13" 255,0,136 + +Note: You must know device SKU and device id (from /user/devices endpoint). +EOF +} + +if (( $# == 0 )); then + print_usage + exit 1 +fi + +### cmd="$1"; shift +cmdline="" +while [ -n "$1" ]; do + case "$1" in + --sku) mySKU="$2"; shift 2;; + --dev|--id) myID="$2"; shift 2;; + -c|--cmd) cmd="$2"; shift 2;; + *) break;; + esac +done + +case "$cmd" in + po*) + # args: sku device on|off + sku="$mySKU"; device="$myID"; state="$1" + if [[ -z "$sku" || -z "$device" || -z "$state" ]]; then print_usage; exit 1; fi + if [[ "$state" == "on" ]]; then val=1; else val=0; fi + _govee_control "$sku" "$device" "devices.capabilities.on_off" "powerSwitch" "$val" | jq . + ;; + + to*) + # sku device instance 0|1 + sku="$mySKU"; device="$myID"; instance="$1"; val="$2" + _govee_control "$sku" "$device" "devices.capabilities.toggle" "$instance" "$val" | jq . + ;; + + br*) + sku="$mySKU"; device="$myID"; val="$1" + if [[ -z "$val" ]]; then print_usage; exit 1; fi + _govee_control "$sku" "$device" "devices.capabilities.range" "brightness" "$val" | jq . + ;; + + co*) + sku="$mySKU"; device="$myID"; color="$1" + if [[ -z "$color" ]]; then print_usage; exit 1; fi + rgbint=$(rgb_to_int "$color") || { echo "Bad color"; exit 1; } + _govee_control "$sku" "$device" "devices.capabilities.color_setting" "colorRgb" "$rgbint" | jq . + ;; + + te*) + sku="$mySKU"; device="$myID"; kelvin="$1" + if [[ -z "$kelvin" ]]; then print_usage; exit 1; fi + _govee_control "$sku" "$device" "devices.capabilities.color_setting" "colorTemperatureK" "$kelvin" | jq . + ;; + + mo*) + sku="$mySKU"; device="$myID"; instance="$1"; val="$2" + _govee_control "$sku" "$device" "devices.capabilities.mode" "$instance" "$val" | jq . + ;; + + raw) + # read raw payload JSON from stdin (should include payload object) + body=$(cat -) + if [[ -z "$body" ]]; then echo "Provide JSON on stdin"; exit 1; fi + curl -sS -X POST "$CONTROL_ENDPOINT" \ + -H "Content-Type: application/json" \ + -H "Govee-API-Key: $API_KEY" \ + -d "$body" | jq . + ;; + + *) + print_usage + exit 1 + ;; +esac +