Initial load

This commit is contained in:
Eric Loyd
2026-09-06 12:08:05 -04:00
commit 3c8d0674ef
25 changed files with 1763 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
[submodule "sunwait"]
path = sunwait
url = http://github.com/risacher/sunwait
+209
View File
@@ -0,0 +1,209 @@
#!/usr/bin/env bash
myCommand=""
debug=""
PODip="192.168.0.151"
PODport="32000"
PPBAkey=""
PPBAname=""
MQTT_HOST="192.168.0.6"
MQTT_USER="" # optional
MQTT_PASS="" # optional
myDevice="all"
do_help() {
cat << HELP_EOF
Usage: $0 <command> [OPTIONS]
command is one of:
--ip) IP of the PPBA API server
--port) Port of the PPBA API server
--init) Starts the Unity driver (must do this first)
--start) Starts the PPBA driver (do this after --init)
--mqtt) Send HA MQTT discovery information to broker
--stats) Send MQTT stats to broker
--power [on|off] <DEV>) Powers on a device (see DEV, below)
-d|--debug) Print debug information
-h|--help) print this help information
You can put multiple commands on the command line; they will be executed sequentially.
They are scanned for in the order listed above.
Thus, you can do "$0 --init --start --mqtt" for a one-liner startup script
DEV is a device name:
all | quad | var | dew1 | dew2 | autodew
HELP_EOF
exit
}
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|quad*|All|all) do_api "/Driver/PPBAdvance/Power/Hub/Off?DriverUniqueKey=${PPBAkey}" PUT "Turning Off Quad Power..." | jq '.status';;&
Var|var*|All|all) do_api "/Driver/PPBAdvance/Power/Variable/Off?DriverUniqueKey=${PPBAkey}" PUT "Turning Off Variable Power..." | jq '.status';;&
Dew1|dew1|All|all) do_api "/Driver/PPBAdvance/Dew/1/Off?DriverUniqueKey=${PPBAkey}" PUT "Turning Off Dew Heater 1..." | jq '.status';;&
Dew2|dew2|All|all) do_api "/Driver/PPBAdvance/Dew/2/Off?DriverUniqueKey=${PPBAkey}" PUT "Turning Off Dew Heater 2..." | jq '.status';;&
Auto*|auto*|All|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|quad*|All|all) do_api "/Driver/PPBAdvance/Power/Hub/On?DriverUniqueKey=${PPBAkey}" PUT "Turning on Quad Power..." | jq '.status';;&
Var*|var*|All|all) do_api "/Driver/PPBAdvance/Power/Variable/On?DriverUniqueKey=${PPBAkey}" PUT "Turning on Variable Power..." | jq '.status';;&
Dew1|dew1|All|all) do_api "/Driver/PPBAdvance/Dew/1/On?DriverUniqueKey=${PPBAkey}" PUT "Turning on Dew Heater 1..." | jq '.status';;&
Dew2|dew2|All|all) do_api "/Driver/PPBAdvance/Dew/2/On?DriverUniqueKey=${PPBAkey}" PUT "Turning on Dew Heater 2..." | jq '.status';;&
Auto*|auto|All|all) do_api "/Driver/PPBAdvance/Dew/Auto/On?DriverUniqueKey=${PPBAkey}" POST "Turning on Auto Dew..." | jq '.status';;
esac
}
mqtt_pub() {
topic="$1"
message="$2"
do_debug "mosquitto_pub -h $MQTT_HOST -t \"PPBA/status/$topic\" -m \"$message\""
mosquitto_pub -h $MQTT_HOST -t "PPBA/status/$topic" -m "$message" -r
}
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}")
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
exit
}
while [ -n "$1" ]; do
case "$1" in
-h|--help) do_help; shift 1;;
--ip) PODip="$2"; shift 2;;
--port) PODport="$2"; shift 2;;
--init*) do_init; shift 1;;
--start) do_start; shift 1;;
--mqtt*) do_mqtt; shift 1;;
--stat*) do_stats; 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;;
-d|--debug) debug="true"; shift 1;;
*) shift 1;;
esac
done
case "$myCommand" in
poweroff) do_poweroff;;
poweron) do_poweron;;
# *) do_help;;
esac
exit
Executable
+11
View File
@@ -0,0 +1,11 @@
#!/bin/sh
# Updates our crontab file so that it has all the stuff we need
cat << EOF | crontab
# Updates Nagios day/night time includes for current seasonal sunrise/sunset
01 00 * * * \${HOME}/NWC2026-AstropotaPOD/daynight.sh
# MQTT Stats from PPBA
* * * * * \${HOME}/NWC2026-AstropotaPOD/AstropotaPOD.sh --stats
EOF
+24
View File
@@ -0,0 +1,24 @@
#!/bin/sh
# Updates our crontab file so that it has all the stuff we need
InfluxToken="InfluxDB Token Goes Here"
flairID="Flair Client ID Goes Here"
flairCS="Flair Client Secret Goes Here"
cat << EOF | crontab
@reboot ${HOME}/NWC2026-AstropotaPOD/start_gearman.sh
@reboot ${HOME}/NWC2026-AstropotaPOD/AstropotaPOD.sh --init --start --mqtt
# Get AstropotaPOD Stats and set up the MQTT HA integration information
0 * * * * ${HOME}/NWC2026-AstropotaPOD/AstropotaPOD.sh --mqtt
* * * * * ${HOME}/NWC2026-AstropotaPOD/AstropotaPOD.sh --stats
# Pull stats and throw into InfluxDB
* * * * * INFLUX_TOKEN=${InfluxToken} ${HOME}/NWC2026-AstropotaPOD/telegraf.sh --local
*/2 * * * * INFLUX_TOKEN=${InfluxToken} ${HOME}/NWC2026-AstropotaPOD/telegraf.sh --ppba
*/3 * * * * INFLUX_TOKEN=${InfluxToken} ${HOME}/NWC2026-AstropotaPOD/telegraf.sh --iss
*/15 * * * * INFLUX_TOKEN=${InfluxToken} ${HOME}/NWC2026-AstropotaPOD/telegraf.sh --weather
# Grab Puck temp stats and throw into MQTT (which will then be pulled into Nagios via a check_mqtt plugin)
*/5 * * * * FLAIR_CLIENT_ID="${flairID}" FLAIR_CLIENT_SECRET="${flairCS}" ${HOME}/NWC2026-AstropotaPOD/flair2MQTT.sh bridge
EOF
Executable
+188
View File
@@ -0,0 +1,188 @@
#!/bin/bash
. /home/eloyd/NWC2026-AstropotaPOD/AstropotaUtils.sh
# Default states, minimums and maximums, etc
mqtt_host="192.168.0.6"
debug=""
min=""
max=""
warn=""
crit=""
exitCode="0"
statusText="OK"
# Definitions for where the AstropotaPOD is located (using Nagios Global Headquarters)
LAT="44.973N"
LON="93.155W"
TARGET_TZ="EDT"
sunwait="/home/eloyd/NWC2026-AstropotaPOD/sunwait"
do_debug() {
[ -z "$debug" ] && return
echo "DEBUG: $*" >&2
}
do_help() {
echo "Usage: $0 -w <warn> -c <crit> -m <min> -M <max> [-d] (kp | flux | sun | puck <field> | NINA <topic> | weather <field> | ppba <field>)"
exit
}
while getopts "w:c:m:M:hd" opt; do
case "$opt" in
w) warn="$OPTARG";;
c) crit="$OPTARG";;
m) min="$OPTARG";;
M) max="$OPTARG";;
h) do_help;;
d) debug="true";;
esac
done
shift $((OPTIND - 1))
command="$@"
[ "$warn" = "-c" -o "$warn" = "ignored" ] && warn=""
[ "$crit" = "ignored" ] && crit=""
[ "$min" = "-M" -o "$min" = "ignored" ] && min=""
[ "$max" = "ignored" ] && max=""
do_debug "We'll be looking for: $command [-w $warn] [-c $crit] [-m $min] [-M $max]"
get_weather() {
do_debug "In get_weather"
do_debug "mosquitto_sub -W 5 -h $mqtt_host -t AstropotaPOD/weather/$1 -C 1"
mosquitto_sub -W 5 -h $mqtt_host -t AstropotaPOD/weather/$1 -C 1
}
convert_to_time_t() {
theTime="$1"
whatDay="${2:-today}"
theTZ="${3:-EDT}"
do_debug "convert_to_time_t: theTime=$theTime, whatDay=$whatDay"
NOW=$(date +%s) # Now in time_t
theDate=$(date -d "$whatDay" +"%d-%b-%Y")
do_debug "theDate=$theDate"
nextEvent=$(date -d "$theDate $theTime ${theTZ}" +%s)
do_debug "nextEvent=$nextEvent"
echo "$nextEvent"
}
# Sunwait will get today's rise (rise) today's set (set) or tomorrow's rise (tomorrow)
get_sunrise() {
do_debug "In get_sunrise"
NOW=$(date +%s)
do_debug "NOW=$NOW"
dawnTime=$($sunwait list 1 nautical rise $LAT $LON | tail -1)
nextDawn=$(convert_to_time_t $dawnTime)
# If it's 2am, then NOW is less than DAWN, we don't need to do anything.
# If it's 2pm, then NOW is greater than DAWN, so we need to get tomorrow's dawn.
if [ "$NOW" -gt "$nextDawn" ]; then
do_debug "Need to look at tomorrow's dawn"
dawnTime=$($sunwait list 2 nautical rise $LAT $LON | tail -1)
nextDawn=$(convert_to_time_t $dawnTime tomorrow)
do_debug "Tomorrow's dawn is nextDawn=$nextDawn"
else
do_debug "Dawn is still in the future for today, so the next dawn is today's dawn: $nextDawn"
fi
do_debug "dawnTime=$dawnTime, nextDawn=$nextDawn"
duskTime=$($sunwait list nautical set $LAT $LON)
nextDusk=$(date -d "${dateToday} ${duskTime} ${TARGET_TZ}" +%s)
do_debug "duskTime=$duskTime, nextDusk=$nextDusk"
echo "nextDusk=$nextDusk, nextDawn=$nextDawn"
}
get_mqtt() {
do_debug "In get_mqtt"
do_debug "mosquitto_sub -W 5 -h $mqtt_host -t \"$1\" -C 1"
mosquitto_sub -W 5 -h $mqtt_host -t "$1" -C 1
}
get_puck() {
do_debug "In get_puck"
do_debug "mosquitto_sub -W 5 -h $mqtt_host -t AstropotaPOD/sensor/AstropotaPOD-e9af/$1 -C 1"
mosquitto_sub -W 5 -h $mqtt_host -t AstropotaPOD/sensor/AstropotaPOD-e9af/$1 -C 1
}
get_ppba() {
do_debug "In get_ppba"
do_debug "mosquitto_sub -W 5 -h $mqtt_host -t PPBA/status/$1 -C 1"
mosquitto_sub -W 5 -h $mqtt_host -t PPBA/status/$1 -C 1
}
get_NINA() {
do_debug "In get_NINA"
do_debug "mosquitto_sub -W 5 -h $mqtt_host -t Astro/NINA/$1 -C 1"
mosquitto_sub -W 5 -h $mqtt_host -t Astro/NINA/$1 -C 1
}
get_suntime() {
do_debug "In get_suntime"
${sunwait} poll nautical $LAT $LON >/dev/null
sunState=$?
do_debug "sunState=$sunState"
exitCode=0
statusText="OK"
case "$sunState" in
2) exitCode=1; statusText="WARNING - Sky is too bright for imaging.";;
3) exitCode=0; statusText="OK - Sky is dark. Safe to image deep-sky targets.";;
*) exitCode=3; statusText="UNKNOWN - invalid code returned from sunwait command";;
esac
sunRise=$(get_sunrise)
echo "${statusText} | IsDark=$exitCode, $sunRise;;;;"
exit $exitCode
}
# main
value=""
do_debug "command is $command"
case "$command" in
f*|F*) command="10MeV-Flux"; value=$(curl -s "https://services.swpc.noaa.gov/json/goes/primary/integral-protons-7-day.json" | jq '. | map(select(.energy == ">=10 MeV")) | last | .flux');;
k*|K*) command="Kp-Index"; value=$(curl -s https://services.swpc.noaa.gov/products/noaa-planetary-k-index.json | jq '[last][].Kp');;
n*|N*) command="$2"; value=$(get_NINA $2);;
s*|S*) command="Suntime"; get_suntime;;
t*|T*) command="SunRiseSunSet"; value=$(get_sunrise);;
w*|W*) command="$2"; value=$(get_weather $2);;
mqtt*) command="$2"; value=$(get_mqtt $2);;
ppba*) command="$2"; value=$(get_ppba $2);;
puck*) command="$2"; value=$(get_puck $2);;
*) echo "unknown command was $command";;
esac
# --- FLOATING POINT FORMATTING ---
# If value is not empty/unknown, check its formatting
if [ "$value" != "?" ]; then
# 1. First, check if it contains RA/Dec formatting symbols (: or °)
# If it does, bypass formatting completely to preserve the raw coordinate string
if [[ "$value" == *":"* ]] || [[ "$value" == *"°"* ]]; then
value="$value"
# 2. Otherwise, check if it's a strict decimal number and pad/round to 4 spaces
elif [[ "$value" =~ ^-?[0-9]+\.[0-9]+$ ]]; then
value=$(printf "%.4f" "$value")
fi
fi
# CRITICAL check must always run first to ensure it takes precedence
statusText=$(check_threshold "$value" "$warn" "$crit")
exitCode=$?
echo "$command $statusText: $value|$command=$value;$warn;$crit;$min;$max"
exit $exitCode
# --- NAGIOS OUTPUT STAGE ---
# Because check_threshold already figured out the severity, we just match the exit code
case $EXIT_CODE in
0)
echo "OK - Value is $value | $command=$value;$warn;$crit;$min;$max"
exit 0
;;
1)
echo "WARNING - Value $value is outside of warning range ($warn) | $command=$value;$warn;$crit;$min;$max"
exit 1
;;
2)
echo "CRITICAL - Value $value is outside of critical range ($crit) | $command=$value;$warn;$crit;$min;$max"
exit 2
;;
*)
echo "UNKNOWN - Received unexpected response from check engine | $command=$value;$warn;$crit;$min;$max"
exit 3
;;
esac
Executable
+152
View File
@@ -0,0 +1,152 @@
#!/bin/bash
hostname=""
community=""
counter=""
perfName=""
iname=""
inum=""
debug=""
min=""
max=""
warn=""
crit=""
exitCode="0"
statusText="OK"
# snmpwalk -v 2c -c public 192.168.0.1 1.3.6.1.2.1.2.2.1.2 | sed -e "s/^IF-MIB::ifDescr.//" | awk '{print $NF, $1}' | while read iname inum; do echo "$iname"; for oid in 1.3.6.1.2.1.2.2.1.5 1.3.6.1.2.1.2.2.1.10 1.3.6.1.2.1.2.2.1.16 1.3.6.1.2.1.2.2.1.20; do snmpget -v 2c -c public 192.168.0.1 ${oid}.${inum}; done; done
# current bandwidth snmpwalk -v 2c -c public 192.168.1.1 1.3.6.1.2.1.2.2.1.5
# octets received snmpwalk -v 2c -c public 192.168.1.1 1.3.6.1.2.1.2.2.1.10
# octets sent snmpwalk -v 2c -c public 192.168.1.1 1.3.6.1.2.1.2.2.1.16
# error octets 1.3.6.1.2.1.2.2.1.20
do_debug() {
[ -z "$debug" ] && return
echo "DEBUG: $*" >&2
}
do_help() {
echo "Usage: $0 -H <hostname> -C <community> [-I <interface name> | -i <inum>) -t (b|r|s|e) [-w <warn>] [-c <crit>]"
# -w <warn> -c <crit> -m <min> -M <max> [-d] (kp | flux)"
exit
}
while getopts "dhC:H:I:i:t:w:c:" opt; do
case "$opt" in
H) hostname="$OPTARG";;
I) iname="$OPTARG";;
i) inum="$OPTARG";;
t) counter="$OPTARG";;
C) community="$OPTARG";;
w) warn="$OPTARG";;
c) crit="$OPTARG";;
h) do_help;;
d) debug="true";;
esac
done
shift $((OPTIND - 1))
command="$@"
do_debug "host=$hostname iname=$iname inum=$inum counter=$counter warn=$warn crit=$crit"
[ -z "$hostname" ] && echo "No hostname specified." && exit 0
[ -z "$community" ] && echo "No community specified." && exit 0
check_threshold() {
local val=$1
local range=$2
local alert_inside=0
local start=0
local end="inf"
# Handle empty thresholds safely
if [[ -z "$range" ]]; then return 0; fi
# Check for the inside-range prefix '@'
if [[ "$range" == @* ]]; then
alert_inside=1
range="${range#@}" # Strip the '@'
fi
# Parse start and end values out of the range colon (:) syntax
if [[ "$range" == *:* ]]; then
start="${range%%:*}"
end="${range#*:}"
# Handle implicit defaults
[[ -z "$start" ]] && start=0
[[ "$start" == "~" ]] && start="-inf"
[[ -z "$end" ]] && end="inf"
else
# No colon: Shorthand for 0:X
start=0
end="$range"
fi
# Evaluate logic using bc (handles floats and floating point math)
local outside=0
# Check lower boundary; inclusive of endpoints
if [[ "$start" != "-inf" ]]; then
if (( $(echo "$val < $start" | bc -l) )); then outside=1; fi
fi
# Check upper boundary; inclusive of endpoints
if [[ "$end" != "inf" ]]; then
if (( $(echo "$val > $end" | bc -l) )); then outside=1; fi
fi
# Return true/false based on whether the @ symbol inverted the logic
if [[ $alert_inside -eq 1 ]]; then
# Alert if INSIDE the range
if [[ $outside -eq 0 ]]; then return 1; else return 0; fi
else
# Alert if OUTSIDE the range (Standard Nagios Behavior)
if [[ $outside -eq 1 ]]; then return 1; else return 0; fi
fi
}
do_snmp() {
myNum="$1"
myOid="$2"
myName="$3"
do_debug "checking $hostname for $myName (inum: $myNum) on $myOid:"
output=$(snmpget -v 2c -c $community $hostname ${myOid}.${myNum} | awk '{print $NF}')
if [ -z "$output" ]; then
echo "UNKNOWN: snmpget error"
exit 3
else
echo "$output"
fi
}
do_stats() {
myNum="$1"
myName="$2"
myOid=""
do_debug "do_stats: myNum=$myNum myName=$myName myOid=$myOid"
case "$counter" in
b) counter="Bandwidth"; perfName="'bw_bytes'"; myOid="1.3.6.1.2.1.2.2.1.5";;
r) counter="Received"; perfName="'bytes_recv'"; myOid="1.3.6.1.2.1.2.2.1.10";;
s) counter="Sent"; perfName="'bytes_sent'"; myOid="1.3.6.1.2.1.2.2.1.16";;
e) counter="Errors"; perfName="'bytes_errors'"; myOid="1.3.6.1.2.1.2.2.1.20";;
*) echo "Invalid counter type specified."; exit 0;;
esac
do_debug "do_stats: do_snmp $myNum $myOid $myName"
value=$(do_snmp $myNum $myOid $myName)
# CRITICAL check must always run first to ensure it takes precedence
if ! check_threshold "$value" "$crit"; then
exitCode=2
statusText="CRITICAL"
elif ! check_threshold "$value" "$warn"; then
exitCode=1
statusText="WARNING"
fi
# [ "$value" != "?" ] && value=$(printf "%.4f" "$value")
echo "$myName $counter $statusText: $value|$perfName=${value}B;$warn;$crit;$min;$max"
exit $exitCode
}
if [ -n "$inum" ]; then
do_debug "Specified inum=$inum"
do_stats $inum
else
do_debug "Did not specify an inum"
do_debug "snmpwalk -v 2c -c $community $hostname 1.3.6.1.2.1.2.2.1.2"
snmpwalk -v 2c -c $community $hostname 1.3.6.1.2.1.2.2.1.2 | sed -e "s/^IF-MIB::ifDescr.//" | awk '{print $1, $NF}' | while read myNum myName; do
[ -z "$iname" -a -z "$inum" ] && echo "iname: $myName (inum: $myNum)" && continue # Show interfaces if we didn't specify one
[ "$iname" != "$myName" ] && continue
do_debug "Found $iname (ifnum: $myNum)"
do_stats $myNum
done
fi
Executable
+22
View File
@@ -0,0 +1,22 @@
#!/bin/bash
# check_iss.sh
API="http://api.open-notify.org/iss-now.json"
# Query the public ISS live API
DATA=$(curl --connect-timeout 5 --max-time 15 -s "$API")
if [ "$?" -gt 0 ]; then
echo "UNKNOWN - $API returned no data or serious error: $DATA"
exit 3
fi
if [ $(echo "$DATA" | jq -r '.message') != "success" ]; then
echo "UNKNOWN - $API timed out"
exit 3
fi
# Extract coordinates
echo $DATA | jq -r '.iss_position | .latitude + " " + .longitude' | \
(read LAT LON; \
echo "OK - ISS Location: Lat $LAT, Lon $LON | ISS_lat=$LAT;;;; ISS_lon=$LON;;;;")
exit 0
Executable
+106
View File
@@ -0,0 +1,106 @@
#!/bin/bash
# ==========================================================================
# UNIVERSAL KASA HS103 NAGIOS PLUGIN
# Usage: ./check_kasa.sh <IP_ADDRESS> <relay_state|rssi|on_time|sw_ver|mac>
# ==========================================================================
plugIP=""
plugPort="9999"
metric=""
while getopts "H:m:P:" opt; do
case "$opt" in
H) plugIP="$OPTARG";;
P) plugPort="$OPTARG";;
m) metric="$OPTARG";;
esac
done
shift $((OPTIND - 1))
restOfLine="$@"
if [ -z "$plugIP" ] || [ -z "$metric" ]; then
echo "UNKNOWN - Missing arguments. Usage: $0 <IP> <metric>"
echo " metric: relay_state rssi on_time sw_ver hw_ver mac model"
exit 3
fi
# Pre-encrypted XOR payload for get_sysinfo
# NOTE: Why this value is what it is is beyond the scope of this script
PAYLOAD="AAAAI9Dw0qHYq9+61/XPtJS20bTAn+yV5o/hh+jK8J7rh+vLtpbr"
# Fetch raw binary stream as clean decimal strings
RAW_STREAM=$(echo "$PAYLOAD" | base64 -d | nc -w 3 "$plugIP" "$plugPort" | od -An -v -t u1)
if [ -z "$RAW_STREAM" ]; then
echo "CRITICAL - No network response from HS103 at $plugIP"
exit 2
fi
# XOR Decryption Loop
DECRYPTED_JSON=""
KEY=171
for BYTE in $RAW_STREAM; do
if [ "$BYTE" -eq 0 ] && [ -z "$DECRYPTED_JSON" ]; then
continue
fi
OUTPUT=$(( BYTE ^ KEY ))
KEY=$BYTE
DECRYPTED_JSON+="$(printf "\\$(printf '%03o' "$OUTPUT")")"
done
# Clean out trailing encryption anomalies
CLEAN_JSON=$(echo "$DECRYPTED_JSON" | grep -o '{.*}' | sed 's/.$//')
# Extract the alias name universally for clean alert messaging
ALIAS=$(echo "$CLEAN_JSON" | jq -r '.get_sysinfo.alias')
# Dynamic Evaluation Engine based on your CLI parameter Choice
case "$metric" in
relay_state)
VALUE=$(echo "$CLEAN_JSON" | jq '.get_sysinfo.relay_state')
if [ "$VALUE" -eq 1 ]; then
echo "OK - ${ALIAS} Relay is ON | relay_state=1;;;;"
exit 0
else
echo "CRITICAL - ${ALIAS} Relay is OFF | relay_state=0;;;;"
exit 2
fi
;;
rssi)
VALUE=$(echo "$CLEAN_JSON" | jq '.get_sysinfo.rssi')
# Standard Wi-Fi signal threshold parameters
if [ "$VALUE" -ge -70 ]; then
echo "OK - ${ALIAS} Wi-Fi Signal Strength: ${VALUE} dBm | rssi=${VALUE};-75;-85;;"
exit 0
elif [ "$VALUE" -lt -70 ] && [ "$VALUE" -ge -82 ]; then
echo "WARNING - ${ALIAS} Wi-Fi Signal Weak: ${VALUE} dBm | rssi=${VALUE};-75;-85;;"
exit 1
else
echo "CRITICAL - ${ALIAS} Wi-Fi Signal Dropping Out: ${VALUE} dBm | rssi=${VALUE};-75;-85;;"
exit 2
fi
;;
on_time)
VALUE=$(echo "$CLEAN_JSON" | jq '.get_sysinfo.on_time')
# Converts raw seconds to readable hours for clear telemetry status
HOURS=$(echo "scale=2; $VALUE / 3600" | bc)
echo "OK - ${ALIAS} Continuous Active Runtime: ${HOURS} Hours | on_time=${VALUE};;;;"
exit 0
;;
sw_ver|hw_ver|mac|model)
# Dynamic handling for inventory/metadata parameters
VALUE=$(echo "$CLEAN_JSON" | jq -r ".get_sysinfo.${metric}")
echo "OK - ${ALIAS} ${metric}: ${VALUE}"
exit 0
;;
*)
echo "UNKNOWN - Unsupported metric: ${metric}. Choose: relay_state, rssi, on_time, sw_ver, hw_ver, mac, model"
exit 3
;;
esac
Executable
+48
View File
@@ -0,0 +1,48 @@
#!/bin/bash
# Grab parameters with built-in fallbacks
SEARCH_REGEX="${1:-Starlink}"
LIMIT_COUNT="${2:-1}"
# Convert search string to lowercase for the safety pool check
LOWER_SEARCH=$(echo "$SEARCH_REGEX" | tr '[:upper:]' '[:lower:]')
# Define an intelligent data pool cap based on user request
if [[ "$LOWER_SEARCH" == *"starlink"* || "$LOWER_SEARCH" == *"oneweb"* || "$LOWER_SEARCH" == *"|"* ]]; then
# For specific trackers, fetch a wider baseline pool (at least 20)
FETCH_LIMIT=$(( LIMIT_COUNT > 20 ? LIMIT_COUNT : 20 ))
else
# Otherwise, trust the user input directly
FETCH_LIMIT=$LIMIT_COUNT
fi
# Hard-clamp the final fetch request strictly to 100 (the API's absolute max ceiling)
if [ "$FETCH_LIMIT" -gt 100 ]; then
FETCH_LIMIT=100
fi
# Fetch the upcoming space manifest using the stable 2.3.0 production API
DATA=$(curl -s "https://ll.thespacedevs.com/2.3.0/launches/upcoming/?limit=${FETCH_LIMIT}")
# Defensive Check: Ensure the API returned a valid JSON payload containing "results"
if ! echo "$DATA" | jq -e '.results' >/dev/null 2>&1; then
echo "CRITICAL - API Error: Received invalid response or rate limit from server | launch_alert=0"
exit 2
fi
# Filter the data pool locally using your regex, then slice cleanly to your display limit
LAUNCH_INFO=$(echo "$DATA" | jq -r --arg regex "$SEARCH_REGEX" --argjson limit "$LIMIT_COUNT" '
.results |
map(select(.name | test($regex; "i"))) |
.[0:$limit] |
.[] |
"\(.name) @ \(.net)"')
if [ ! -z "$LAUNCH_INFO" ]; then
# Output matches cleanly formatted for both CLI tracking and Nagios perf-piping
echo "OK - Target Approaching: $LAUNCH_INFO" | tr -d "|" | sed '$s/$/ | launch_alert=1/'
exit 0
else
echo "OK - No flights matching pattern [$SEARCH_REGEX] found in the upcoming queue | launch_alert=0"
exit 0
fi
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Executable
+357
View File
@@ -0,0 +1,357 @@
#!/usr/bin/env python
"""
SYNOPSIS
"""
import sys
import optparse
import traceback
import ssl
# Python 2/3 Compatibility imports
try:
import json
except ImportError:
import simplejson as json
try:
import urllib.request
import urllib.parse
import urllib.error
except ImportError:
import urllib2
import urllib
try:
urlencode = urllib.parse.urlencode
except AttributeError:
urlencode = urllib.urlencode
try:
urlopen = urllib.request.urlopen
except AttributeError:
urlopen = urllib2.urlopen
try:
urlquote = urllib.parse.quote
except AttributeError:
urlquote = urllib.quote
try:
urlerror = urllib.error.URLError
except AttributeError:
urlerror = urllib2.URLError
try:
httperror = urllib.error.HTTPError
except AttributeError:
httperror = urllib2.HTTPError
import shlex
import re
import signal
__VERSION__ = '1.2.4'
class ConnectionError(Exception):
error_output_prefix = "UNKNOWN: An error occurred connecting to API. "
pass
class URLError(ConnectionError):
def __init__(self, error_message):
self.error_message = ConnectionError.error_output_prefix + "(Connection error: '" + error_message + "')"
class HTTPError(ConnectionError):
def __init__(self, error_message):
self.error_message = ConnectionError.error_output_prefix + "(HTTP error: '" + error_message + "')"
def parse_args():
version = 'check_ncpa.py, Version %s' % __VERSION__
parser = optparse.OptionParser()
parser.add_option("-H", "--hostname", help="The hostname to be connected to.")
parser.add_option("-M", "--metric", default='',
help="The metric to check, this is defined on client "
"system. This would also be the plugin name in the "
"plugins directory. Do not attach arguments to it, "
"use the -a directive for that. DO NOT INCLUDE the api/ "
"instruction.")
parser.add_option("-P", "--port", default=5693, type="int",
help="Port to use to connect to the client.")
parser.add_option("-w", "--warning", default=None, type="str",
help="Warning value to be passed for the check.")
parser.add_option("-c", "--critical", default=None, type="str",
help="Critical value to be passed for the check.")
parser.add_option("-u", "--units", default=None,
help="The unit prefix (k, Ki, M, Mi, G, Gi, T, Ti) for b and B unit "
"types which calculates the value returned.")
parser.add_option("-n", "--unit", default=None,
help="Overrides the unit with whatever unit you define. "
"Does not perform calculations. This changes the unit of measurement only.")
parser.add_option("-a", "--arguments", default=None,
help="Arguments for the plugin to be run. Not necessary "
"unless you're running a custom plugin. Given in the same "
"as you would call from the command line. Example: -a '-w 10 -c 20 -f /usr/local'")
parser.add_option("-t", "--token", default='',
help="The token for connecting.")
parser.add_option("-T", "--timeout", default=55, type="int",
help="Enforced timeout, will terminate plugins after "
"this amount of seconds. [%default]")
parser.add_option("-d", "--delta", action='store_true',
help="Signals that this check is a delta check and a "
"local state will kept.")
parser.add_option("-l", "--list", action='store_true',
help="List all values under a given node. Do not perform "
"a check.")
parser.add_option("-v", "--verbose", action='store_true',
help='Print more verbose error messages.')
parser.add_option("-D", "--debug", action='store_true',
help='Print LOTS of error messages. Used mostly for debugging.')
parser.add_option("-V", "--version", action='store_true',
help='Print version number of plugin.')
parser.add_option("-q", "--queryargs", default=None,
help='Extra query arguments to pass in the NCPA URL.')
parser.add_option("-s", "--secure", action='store_true', default=False,
help='Require successful certificate verification. Does not work on Python < 2.7.9.')
parser.add_option("-p", "--performance", action='store_true', default=False,
help='Print performance data even when there is none. '
'Will print data matching the return code of this script')
options, _ = parser.parse_args()
if options.version:
print(version)
sys.exit(0)
if options.arguments and options.metric and not 'plugin' in options.metric:
parser.print_help()
parser.error('You cannot specify arguments without running a custom plugin.')
if not options.hostname:
parser.print_help()
parser.error("Hostname is required for use.")
elif not options.metric and not options.list:
parser.print_help()
parser.error('No metric given, if you want to list all possible items '
'use --list.')
options.metric = re.sub(r'^/?(api/)?', '', options.metric)
return options
# ~ The following are all helper functions. I would normally split these out into
# ~ a new module but this needs to be portable.
def get_url_from_options(options):
host_part = get_host_part_from_options(options)
arguments = get_arguments_from_options(options)
return '%s?%s' % (host_part, arguments)
def get_host_part_from_options(options):
"""Gets the address that will be queries for the JSON.
"""
hostname = options.hostname
port = options.port
if not options.metric is None:
metric = urlquote(options.metric)
else:
metric = ''
arguments = get_check_arguments_from_options(options)
if not metric and not arguments:
api_address = 'https://%s:%d/api' % (hostname, port)
else:
api_address = 'https://%s:%d/api/%s/%s' % (hostname, port, metric, arguments)
return api_address
def get_check_arguments_from_options(options):
"""Gets the escaped URL for plugin arguments to be added
to the end of the host URL. This is different from the get_arguments_from_options
in that this is meant for the syntax when the user is calling a check, whereas the below
is when GET arguments need to be added.
"""
arguments = options.arguments
if arguments is None:
return ''
else:
lex = shlex.shlex(arguments)
lex.whitespace_split = True
arguments = '/'.join([urlquote(x, safe='') for x in lex])
return arguments
def get_arguments_from_options(options, **kwargs):
"""Returns the http query arguments. If there is a list variable specified,
it will return the arguments necessary to query for a list.
"""
# Note: Changed back to units due to the units being what is passed via the
# API call which can confuse people if they don't match
arguments = { 'token': options.token,
'units': options.units }
if not options.list:
arguments['warning'] = options.warning
arguments['critical'] = options.critical
arguments['delta'] = options.delta
arguments['check'] = 1
arguments['unit'] = options.unit
args = list((k, v) for k, v in list(arguments.items()) if v is not None)
# Get the options (comma separated)
if options.queryargs:
# for each comma, perform lookahead, split if we aren't inside quotes.
arguments_list = re.split(''',(?=(?:[^'"]|'[^']*'|"[^"]*")*$)''', options.queryargs)
for argument in arguments_list:
key, value = argument.split('=', 1)
if value is not None:
args.append((key, value))
#~ Encode the items in the dictionary that are not None
return urlencode(args)
def get_json(options):
"""Get the page given by the options. This will call down the url and
encode its finding into a Python object (from JSON).
"""
url = get_url_from_options(options)
if options.verbose:
print('Connecting to: ' + url)
try:
try:
ctx = ssl.create_default_context()
if not options.secure:
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
ret = urlopen(url, context=ctx)
except AttributeError:
ret = urlopen(url)
except httperror as e:
try:
raise HTTPError('{0} {1}'.format(e.code, e.reason))
except AttributeError:
raise HTTPError('{0}'.format(e.code))
except urlerror as e:
raise URLError('{0}'.format(e.reason))
ret = ret.read()
if options.verbose:
print('File returned contained:\n' + ret.decode('utf-8'))
arr = json.loads(ret)
if options.list:
return arr
# Fix for NCPA < 2
if 'value' in arr:
arr = arr['value']
# We need to flip the returncode and stdout
if isinstance(arr['stdout'], int) and not isinstance(arr['returncode'], int):
tmp = arr['returncode']
arr['returncode'] = arr['stdout']
arr['stdout'] = tmp
# If we recieve and error, return critical and give out error text
elif 'error' in arr:
arr['stdout'] = 'CRITICAL: %s' % arr['error']
arr['returncode'] = 2
return arr
def run_check(info_json):
"""Run a check against the remote host.
"""
if 'stdout' in info_json and 'returncode' in info_json:
return info_json['stdout'], info_json['returncode']
elif 'error' in info_json:
return info_json['error'], 3
def show_list(info_json):
"""Show the list of available options.
"""
return json.dumps(info_json, indent=4), 0
def timeout_handler(threshold):
def wrapped(signum, frames):
stdout = "UNKNOWN: Execution exceeded timeout threshold of %ds" % threshold
print(stdout)
sys.exit(3)
return wrapped
def main():
options = parse_args()
# We need to ensure that we will only execute for a certain amount of
# seconds.
signal.signal(signal.SIGALRM, timeout_handler(options.timeout))
signal.alarm(options.timeout)
try:
if options.version:
stdout = 'The version of this plugin is %s' % __VERSION__
return stdout, 0
info_json = get_json(options)
if options.list:
return show_list(info_json)
else:
stdout, returncode = run_check(info_json)
if options.performance and stdout.find("|") == -1:
stdout = "{0} | 'status'={1};1;2;;".format(stdout, returncode)
return stdout, returncode
except (HTTPError, URLError) as e:
if options.debug:
return 'The stack trace:\n' + traceback.format_exc(), 3
elif options.verbose:
return 'An error occurred:\n' + str(e.error_message), 3
else:
return e.error_message, 3
except Exception as e:
if options.debug:
return 'The stack trace:\n' + traceback.format_exc(), 3
elif options.verbose:
return 'An error occurred:\n' + str(e), 3
else:
return 'UNKNOWN: Error occurred while running the plugin. Use the verbose flag for more details.', 3
if __name__ == "__main__":
stdout, returncode = main()
if sys.version_info[0] < 3:
print(unicode(stdout).encode('utf-8'))
else:
print(stdout.encode().decode('utf-8'))
sys.exit(returncode)
BIN
View File
Binary file not shown.
+38
View File
@@ -0,0 +1,38 @@
define command {
command_name AstropotaNotify-Host
command_line /home/nwc2026/NWC2026-AstropotaPOD/notify-by-ntfy.sh -t host -ha "$HOSTALIAS$" -h "$HOSTNAME$" -hs "$HOSTSTATE$" -ld "$LONGDATETIME$" -nt "$NOTIFICATIONTYPE$" -C "$CONTACTPAGER$"
}
define command {
command_name AstropotaNotify-Service
command_line /home/nwc2026/NWC2026-AstropotaPOD/notify-by-ntfy.sh -t service -ha "$HOSTALIAS$" -h "$HOSTNAME$" -hs "$HOSTSTATE$" -ld "$LONGDATETIME$" -nt "$NOTIFICATIONTYPE$" -s "$SERVICEDESC$" -ss "$SERVICESTATE$" -C "$CONTACTPAGER$"
}
define command {
command_name check_astro
command_line /home/nwc2026/NWC2026-AstropotaPOD/check_astro -w $ARG2$ -c $ARG3$ -m $ARG4$ -M $ARG5$ $ARG1$ $ARG8$
}
define command {
command_name check_dd_wrt
command_line /home/nwc2026/NWC2026-AstropotaPOD/check_dd_wrt -H $HOSTADDRESS$ -C $ARG1$ -i $ARG2$ -t $ARG3$ $ARG4$ $ARG8$
}
define command {
command_name check_iss
command_line /home/nwc2026/NWC2026-AstropotaPOD/check_iss
}
define command {
command_name check_launch
command_line /home/nwc2026/NWC2026-AstropotaPOD/check_launch $ARG1$ $ARG2$ $ARG8$
}
define command {
command_name check_kasa
command_line /home/nwc2026/NWC2026-AstropotaPOD/check_kasa -H $HOSTADDRESS$ -m $ARG1$ $ARG8$
}
define command {
command_name MQTT-PODalert
command_line /usr/bin/mosquitto_pub -h ssdnode-1.bitnetix.com -t "nagios/alerts" -m "$ARG2$" $ARG8$
}
define command {
command_name ping-other-host
command_line $USER1$/check_icmp -H $ARG1$ -w "$ARG2$" -c "$ARG3$" -p 5 $ARG8$
}
Executable
+30
View File
@@ -0,0 +1,30 @@
#!/bin/bash
# Set your exact location (Nagios Global Headquarters)
LAT="44.973N"
LON="93.155W"
# sunwait outputs values in clean 24-hour HH:MM format
# We use nautical because the program actually shows the end of nautical, which is the start of astronomical
DAWN=$(${HOME}/NWC2026-AstropotaPOD/sunwait list dawn nautical $LAT $LON)
DUSK=$(${HOME}/NWC2026-AstropotaPOD/sunwait list dusk nautical $LAT $LON)
# From here, it's Nagios API time
# Your Nagios XI API Details
XI_URL="http://192.168.123.16/nagiosxi/api/v1"
API_KEY="YOUR NAGIOS XI API KEY"
# Build the new time strings
# Day string becomes: "05:42-20:14"
# Night string becomes: "00:00-05:42,20:14-24:00"
DAY_RANGE="${DAWN}-${DUSK}"
NIGHT_RANGE="00:00-${DAWN},${DUSK}-24:00"
# Ensure our Timeperiods exist the way we want
curl -XPOST "${XI_URL}/config/timeperiod/AstropotaPOD-day?apikey=${API_KEY}&pretty=1" \
-d "timeperiod_name=AstropotaPOD-day&alias=Local+Daytime+recalculated+via+cron&sunday=${DAY_RANGE}&monday=${DAY_RANGE}&tuesday=${DAY_RANGE}&wednesday=${DAY_RANGE}&thursday=${DAY_RANGE}&friday=${DAY_RANGE}&saturday=${DAY_RANGE}"
curl -XPOST "${XI_URL}/config/timeperiod/AstropotaPOD-night?apikey=${API_KEY}&pretty=1" \
-d "timeperiod_name=AstropotaPOD-night&alias=Local+Nighttime+recalculated+via+cron&sunday=${NIGHT_RANGE}&monday=${NIGHT_RANGE}&tuesday=${NIGHT_RANGE}&wednesday=${NIGHT_RANGE}&thursday=${NIGHT_RANGE}&friday=${NIGHT_RANGE}&saturday=${NIGHT_RANGE}"
# Tell Nagios XI to safely apply the changes to the engine
curl -XPOST "${XI_URL}/system/applyconfig?apikey=${API_KEY}"
Executable
+89
View File
@@ -0,0 +1,89 @@
#!/bin/bash
# --- CONFIGURATION FROM ENVIRONMENT VARIABLES ---
CLIENT_ID="${FLAIR_CLIENT_ID}"
CLIENT_SECRET="${FLAIR_CLIENT_SECRET}"
if [ -z "$CLIENT_ID" ] || [ -z "$CLIENT_SECRET" ]; then
echo "Error: Missing environment variables." >&2
echo "Please set FLAIR_CLIENT_ID and FLAIR_CLIENT_SECRET before running this script." >&2
exit 1
fi
MQTT_BROKER="192.168.0.6"
MQTT_PORT="1883"
# Matching your requested structure
MQTT_TOPIC_BASE="AstropotaPOD/sensor"
# --- HELPER: GET OAUTH2 TOKEN ---
get_flair_token() {
local token_response
token_response=$(curl -s -X POST "https://api.flair.co/oauth2/token" \
-d "grant_type=client_credentials" \
-d "client_id=${CLIENT_ID}" \
-d "client_secret=${CLIENT_SECRET}")
echo "$token_response" | jq -r '.access_token // empty'
}
# --- FETCH REFRESHED TOKEN ---
TOKEN=$(get_flair_token)
if [ -z "$TOKEN" ] || [ "$TOKEN" == "null" ]; then
echo "Failed to authenticate with Flair." >&2
exit 1
fi
# --- FETCH AND PUBLISH TO MQTT ---
PAYLOAD=$(curl -s -X GET "https://api.flair.co/api/puck2s" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Accept: application/json")
do_pub() {
mosquitto_pub -h "$MQTT_BROKER" -p "$MQTT_PORT" -t "${TARGET_TOPIC}/${1}" -m "$2" -r
}
while read -r puck_data; do
if [ -z "$puck_data" ]; then continue; fi
# Grab the human-readable name from your payload (e.g., "AstropotaPOD-e9af")
puck_name=$(echo "$puck_data" | jq -r '.attributes.name // empty')
# Fallback to ID just in case a device doesn't have a custom name assigned
if [ -z "$puck_name" ] || [ "$puck_name" == "null" ]; then
puck_name=$(echo "$puck_data" | jq -r '.id')
fi
# Extract metrics from payload attributes
temp_c=$(echo "$puck_data" | jq -r '.attributes."current-temperature-c" // empty')
humidity=$(echo "$puck_data" | jq -r '.attributes."current-humidity" // empty')
wifi_rssi=$(echo "$puck_data" | jq -r '.attributes."current-wifi-rssi" // empty')
power_source=$(echo "$puck_data" | jq -r '.attributes."power-source" // empty')
reporting_interval=$(echo "$puck_data" | jq -r '.attributes."reporting-interval-ds" // empty')
firmware=$(echo "$puck_data" | jq -r '.attributes."firmware-version" // empty')
beacon_ms=$(echo "$puck_data" | jq -r '.attributes."beacon-interval-ms" // empty')
voltage=$(echo "$puck_data" | jq -r '.attributes.voltage')
last_voltage=$(echo "$puck_data" | jq -r '.attributes."last-reported-voltage"')
if [ "$voltage" == "0" ] || [ "$voltage" == "0.0" ]; then
battery="$last_voltage"
else
battery="$voltage"
fi
# Dynamic target base matching your requested pattern: AstropotaPOD/sensor/AstropotaPOD-e9af/
TARGET_TOPIC="${MQTT_TOPIC_BASE}/${puck_name}"
# Blast sub-topics cleanly to Mosquitto
[ -n "$temp_c" ] && do_pub "temperature" "$temp_c"
[ -n "$humidity" ] && do_pub "humidity" "$humidity"
[ -n "$wifi_rssi" ] && do_pub "wifi_rssi" "$wifi_rssi"
[ -n "$power_source" ] && do_pub "power_source" "$power_source"
[ -n "$battery" ] && do_pub "battery_voltage" "$battery"
[ -n "$reporting_interval" ] && do_pub "reporting_interval" "$reporting_interval"
[ -n "$firmware" ] && do_pub "firmware_version" "$firmware"
[ -n "$beacon_ms" ] && do_pub "beacon_interval" "$beacon_ms"
# echo "Published to MQTT under: ${TARGET_TOPIC}/"
done < <(echo "$PAYLOAD" | jq -c '.data[]')
+91
View File
@@ -0,0 +1,91 @@
#!/bin/bash
# This assumes a BLANK Raspberry Pi Zero W that we can install our AstropotaPOD stuff onto
do_toilet() {
echo "$*" | toilet -f standard -F metal
}
# 1. Update
echo "Updating repositories and ensuring we have the software we need..."
sudo apt -y update && sudo apt -y dist-upgrade && sudo apt -y autoremove
# 2. Make sure we have the software we need
echo "Installing toilet...I know. Bear with me."
sudo apt -y install toilet figlet
do_toilet "Pre-reqs"
sudo apt -y install git snmp jq bc mosquitto-clients
sudo sed -i 's/^AcceptEnv LANG LC_\*/#AcceptEnv LANG LC_\*/g' /etc/ssh/sshd_config
sudo systemctl restart ssh
do_toilet "Pips (via apt)"
sudo apt -y install python3-requests python3-paho-mqtt
# 3. Go get some stuff
do_toilet "Log dir"
cd
mkdir logs
#do_toilet "Getting NWC2026-AstropotaPOD"
#[ ! -d "NWC2026-AstropotaPOD" ] && git clone https://git.everwatch.global/eloyd/NWC2026-AstropotaPOD
# 4. Install Nagios Plugins
do_toilet "Nagios Plugins"
if [ ! -f "/usr/local/nagios/libexec/check_http" ]; then
cd
tar xfz NWC2026-AstropotaPOD/nagios-plugins-2.5.tar.gz
cd nagios-plugins-2.5
./configure
make && sudo make install
fi
# 5. Install NCPA
do_toilet "NCPA Client"
if [ ! -f "/usr/local/ncpa/etc/ncpa.cfg" ]; then
cd; cd NWC2026-AstropotaPOD
sudo apt -y install ./ncpa_3.3.1-1_armhf.deb
sudo sed -i -e "s/^community_string.*/community_string=AstropotamusWasHere/" /usr/local/ncpa/etc/ncpa.cfg
sudo systemctl restart ncpa
fi
# 6. Install Nagios Mod Gearman
do_toilet "NMG Worker"
if [ ! -f "/usr/local/bin/nagios-mod-gearman-worker" ]; then
cd
git clone https://github.com/NagiosEnterprises/nagios-mod-gearman
cd nagios-mod-gearman
sudo apt -y install automake libncurses-dev gearman libgearman-dev help2man dctrl-tools libperl-dev g++ libltdl-dev pkgconf make dpkg-dev debhelper libssl-dev
sudo ./autogen.sh
sudo ./configure
sudo make install
fi
# 7. Install check_ncpa clients
do_toilet "NCPA Clients"
cd; cd NWC2026-AstropotaPOD
sudo cp check_ncpa.py /usr/local/nagios/libexec
arch=$(uname -m)
file="check_ncpa.${arch}"
if [ -r "$file" ]; then
echo "Will install $file, since it's here..."
echo "==> Installing ${file} to /usr/local/nagios/libexec..."
sudo install -d /usr/local/nagios/libexec
sudo install -m 0755 ${file} /usr/local/nagios/libexec/check_ncpa
echo "==> Installation complete!"
echo "Installed to: /usr/local/nagios/libexec/check_ncpa"
else
echo ""
echo "###"
echo "###"
echo "###"
echo "### IMPORTANT:"
echo "###"
echo "### You must extract, compile, and install the check_ncpa_c.2.Mar-05-26.tgz beta code to /usr/local/nagios/libexec"
echo "###"
echo "### Failure to do so will result in service check failures that may be hard to diagnose."
echo "###"
echo "###"
echo "###"
echo ""
fi
# Test
#/usr/local/bin/nagios-mod-gearman-worker --debug=2 --server=192.168.123.16 --key=AstropotamusWasHere --hosts --services --hostgroup=PODhosts --servicegroup=PODservices
+75
View File
@@ -0,0 +1,75 @@
/* ==========================================================================
1. SIDEBAR CANVAS: Unified Deep Space Background
========================================================================== */
/* Targets the primary sidebar structural container and its underlying list */
html body.parent div.parentpage div#leftnav,
html body.parent div.parentpage .leftnav-container,
html body.parent div.parentpage ul.navside {
background-color: #02040a !important;
/* Diagonal 45-degree gradient matches the sweep pattern of the top header */
background: linear-gradient(45deg,
#02040a 0%,
#060c1f 40%,
#0b1736 100%
) !important;
border-right: 1px solid #1e3a8a !important; /* Low-contrast sapphire right boundary */
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.5) !important;
}
/* ==========================================================================
2. CATEGORY HEADERS: Structural Menu Sections
========================================================================== */
/* Style rules for static section headers (e.g., "Views", "Dashboards", "System") */
html body.parent div.parentpage .menu-section-title,
html body.parent div.parentpage ul.navside li.navside-head {
color: #3b82f6 !important; /* Prominent observatory focus blue */
font-weight: 600 !important;
text-transform: uppercase !important;
letter-spacing: 0.5px !important;
border-bottom: 1px solid rgba(37, 99, 235, 0.15) !important;
padding-bottom: 4px !important;
margin-top: 12px !important;
}
/* ==========================================================================
3. NAVIGATION LINKS: Base Typography & Spacing Continuity
========================================================================== */
/* Maps target rules for left-hand menu lists while locking the native font layout */
html body.parent div.parentpage ul.navside li a,
html body.parent div.parentpage .leftnav-container ul li a {
display: block !important;
color: #cbd5e1 !important; /* Clean slate-white match with top navigation */
text-decoration: none !important;
/* Custom cubic-bezier ensures uniform physics curves with top header changes */
transition: transform 0.15s cubic-bezier(0.25, 1, 0.5, 1), color 0.15s ease, background-color 0.15s ease !important;
}
/* ==========================================================================
4. NAVIGATION INTERACTION: Responsive Left-Hand Menu Pop
========================================================================== */
/* Triggers a physical pop response when hovering over left nav elements */
html body.parent div.parentpage ul.navside li a:hover,
html body.parent div.parentpage .leftnav-container ul li a:hover {
color: #ffffff !important; /* Snaps text to clean white */
background-color: rgba(255, 255, 255, 0.04) !important; /* Low-opacity selection frame */
/* Horizontal push (+4px to the right) functions better in vertical menus than a vertical lift */
transform: translateX(4px) !important;
text-shadow: 0 0 8px rgba(255, 255, 255, 0.3) !important;
}
/* ==========================================================================
5. MENU TREE UTILITIES: Expand/Collapse Arrow Tweaks
========================================================================== */
/* Adjusts standard navigation arrow structures to use the accent blue tint */
html body.parent div.parentpage ul.navside li i.fa-chevron-right,
html body.parent div.parentpage ul.navside li i.fa-chevron-down,
html body.parent div.parentpage .leftnav-container .fa-caret-right {
color: #1d4ed8 !important; /* Integrated sapphire blue arrow indicators */
transition: color 0.15s ease !important;
}
html body.parent div.parentpage ul.navside li a:hover i {
color: #60a5fa !important; /* Highlights arrows on active parent item hover */
}
Binary file not shown.
Binary file not shown.
+100
View File
@@ -0,0 +1,100 @@
#!/bin/bash
notify_host="https://notify.astropotamus.com"
chanDefault="POD"
alertType=""
msgTitle=""
message=""
tag="exclamation"
priority="3"
state=""
testMode=""
### Define command for service
### define command {
### command_name notify-service-by-ntfy
### command_line $USER1$/notify-by-ntfy.sh -t service -ha "$HOSTALIAS$" -h "$HOSTNAME$" -hs "$HOSTSTATE$" -ld "$LONGDATETIME$" -nt "$NOTIFICATIONTYPE$" -s "$SERVICEDESC$" -ss "$SERVICESTATE$"
### }
while [ -n "$1" ]; do
case "$1" in
--test) testMode="true"; shift 1;;
-C|--channel) channel="$2"; shift 1;;
-t|--type) alertType="$2"; shift 2;;
-ha) NAGIOS_HOSTALIAS="$2"; shift 2;;
-h) NAGIOS_HOSTNAME="$2"; shift 2;;
-hs) NAGIOS_HOSTSTATE="$2"; shift 2;;
-ld) NAGIOS_LONGDATETIME="$2"; shift 2;;
-nt) NAGIOS_NOTIFICATIONTYPE="$2"; shift 2;;
-s) NAGIOS_SERVICEDESC="$2"; shift 2;;
-ss) NAGIOS_SERVICESTATE="$2"; shift 2;;
*) shift 1;;
esac
done
[ -z "$channel" ] && channel="$chanDefault"
case "$NAGIOS_NOTIFICATIONTYPE" in
PROBLEM) tag="bangbang";;
RECOVERY) tag="ok";;
ACKNOWLEDGEMENT) tag="ok_hand";;
FLAPPINGSTART) tag="hatched_chick";;
FLAPPINGSTOP) tag="hourglass_flowing_sand";;
DOWNTIMESTART) tag="sleeping_bed";;
DOWNTIMEEND) tag="sunny";;
*) tag="exclamation";;
esac
case "$NAGIOS_HOSTSTATE" in
0) tag="$tag,+1";;
1) tag="$tag,-1";;
2) tag="$tag,-1";;
3) tag="$tag,question";;
esac
case "$NAGIOS_SERVICESTATE" in
0) tag="$tag,+1";;
1) tag="$tag,-1";;
2) tag="$tag,warning";;
3) tag="$tag,question";;
esac
do_notify() {
[ -n "$1" ] && channel="${channel}-${1}"
curl -s \
-H "Title: ${msgTitle}" \
-H "Tags: ${tag}" \
-H "Priority: ${priority}" \
-d "${message}" \
${notify_host}/${channel}
}
do_host() {
msgTitle="HOST ${NAGIOS_NOTIFICATIONTYPE}"
message="$NAGIOS_NOTIFICATIONTYPE $NAGIOS_HOSTNAME is $NAGIOS_HOSTSTATE $NAGIOS_LONGDATETIME"
do_notify "Hosts"
}
do_service() {
msgTitle="SERVICE ${NAGIOS_NOTIFICATIONTYPE}"
message="$NAGIOS_NOTIFICATIONTYPE $NAGIOS_SERVICEDESC @ $NAGIOS_HOSTALIAS is $NAGIOS_SERVICESTATE $NAGIOS_LONGDATETIME"
do_notify "Services"
}
if [ -n "$testMode" ]; then
NAGIOS_NOTIFICATIONTYPE="Test"
NAGIOS_HOSTNAME="FakeHost"
NAGIOS_HOSTSTATE="OK"
NAGIOS_SERVICEDESC="FakeService"
NAGIOS_HOSTALIAS="FakeHostAlias"
NAGIOS_SERVICESTATE="OK"
NAGIOS_LONGDATETIME="Today"
fi
case "$alertType" in
h|host|host) do_host;;
s|svc|service) do_service;;
*) shift 1;;
esac
exit
+46
View File
@@ -0,0 +1,46 @@
#!/bin/bash
action="start"
xihost="192.168.123.16"
key="NRDP Key for Nagios Server"
logFile="${HOME}/logs/gearman.log"
start_options="--server=${xihost} --key=${key}"
start_options+=" --min-worker=2"
start_options+=" --max-worker=5"
start_options+=" --spawn-rate=1"
start_options+=" --load_limit1=1"
start_options+=" --load_limit5=2"
start_options+=" --load_limit15=3"
start_options+=" --idle-timeout=600"
start_options+=" --hosts --services --hostgroup=PODhosts --servicegroup=PODservices --logmode=file --logfile=${logFile} --debug=1"
while [ -n "$1" ]; do
case "$1" in
--start) action="start"; shift 1;;
--stop) action="stop"; shift 1;;
--restart) action="restart"; shift 1;;
*) shift 1;;
esac
done
do_stop() {
echo "Stopping nagios-mod-gearman-worker..."
killall /usr/local/bin/nagios-mod-gearman-worker
echo "Done."
}
do_start() {
echo "Starting nagios-mod-gearman-worker..."
/usr/local/bin/nagios-mod-gearman-worker -d $start_options
echo "Done."
}
do_restart() {
do_stop
sleep 5
do_start
}
do_${action}
Submodule
+1
Submodule sunwait added at 151d8340a7
Executable
+104
View File
@@ -0,0 +1,104 @@
#!/bin/bash
if [ -z "$INFLUX_TOKEN" ]; then
echo "WARNING: You need to set an INFLUX_TOKEN environment variable or else it won't actually work."
fi
# This is hardcoded here because it's easier. Find it on your own. :-)
PPBAkey="00e086b2-85b9-4562-a6bd-6d5a68ad2c26"
fluxHost="192.168.0.6"
fluxPort="8086"
ppbaHost="192.168.0.151"
ppbaPort="32000"
do_help() {
echo "Help coming soon. I promise. Do not panic!"
exit
}
do_preface() {
cat << PREFACE_EOF
[global_tags]
[agent]
interval = "10s"
round_interval = true
metric_batch_size = 1000
metric_buffer_limit = 10000
collection_jitter = "0s"
flush_interval = "10s"
flush_jitter = "0s"
precision = "0s"
[[outputs.influxdb_v2]]
urls = ["http://${fluxHost}:${fluxPort}"]
token = "$INFLUX_TOKEN"
organization = "Astropotamus"
bucket = "AstropotaPOD"
PREFACE_EOF
}
do_postface() {
cat << POSTFACE_EOF
POSTFACE_EOF
}
do_ppba() {
do_preface
cat << PPBA_EOF
[[inputs.http]]
urls = [
"http://${ppbaHost}:${ppbaPort}/Driver/PPBAdvance/Report?DriverUniqueKey=${PPBAkey}"
]
name_override = "PPBA"
data_format = "json"
tagexclude = ["url", "host"]
json_query="data.message"
json_string_fields=["dewHubStatus_hub_?_current_isOverCurrent","powerHubStatus_state","powerVariablePortStatus_state"]
PPBA_EOF
do_postface
}
do_weather() {
do_preface
cat << WEATHER_EOF
[[inputs.http]]
urls=["https://api.openweathermap.org/data/2.5/weather?id=5126015&units=metric&appid=043d58f75a4ffe7cf5d414ead183cb7f"]
data_format = "json"
tag_keys = [ "coord_lon", "coord_lat", "sys_country", "id", "name" ]
json_string_fields = [ "weather_0_main", "weather_0_description" ]
json_time_key = "dt"
json_time_format = "unix"
name_override = "openweathermap"
WEATHER_EOF
do_postface
}
do_local() {
do_preface
cat << LOCAL_EOF
[[inputs.cpu]]
percpu = true
totalcpu = true
collect_cpu_time = false
report_active = false
core_tags = false
[[inputs.mem]]
[[inputs.system]]
LOCAL_EOF
do_postface
}
tmpFile=$(mktemp)
while [ -n "$1" ]; do
case "$1" in
-h|--help) do_help;;
--ppba) do_ppba > $tmpFile; shift 1;;
--weather) do_weather > $tmpFile; shift 1;;
--iss) rm $tmpFile; echo "Not yet."; exit;;
--local) do_local > $tmpFile; shift 1;;
*) shift 1;;
esac
done
telegraf --config $tmpFile --once --quiet
rm $tmpFile
+69
View File
@@ -0,0 +1,69 @@
/* ==========================================
1. THE CANVAS: Elongated Diagonal Sky Wash
========================================== */
html body.parent div.parentpage div#header.parenthead {
width: 100% !important;
background-color: #02040a !important;
/* 45-degree angle stretches the colors sideways across your 1706px screen */
background: linear-gradient(45deg,
#02040a 0%,
#0b1736 35%,
#1e3a8a 50%,
#0b1736 65%,
#02040a 100%
) !important;
border-bottom: 2px solid #2563eb !important; /* Sharp blue accent baseline */
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.7) !important;
}
/* ==========================================
2. TEXT INTERACTION: Increased Lift
========================================== */
html body.parent div.parentpage div#header.parenthead .mainmenu div a,
html body.parent div.parentpage div#header.parenthead #config-menulink span a {
display: inline-block !important;
/* Uses a spring-like ease out instead of a flat linear animation */
transition: transform 0.15s cubic-bezier(0.25, 1, 0.5, 1), color 0.15s ease !important;
}
html body.parent div.parentpage div#header.parenthead .mainmenu div a:hover,
html body.parent div.parentpage div#header.parenthead #config-menulink span a:hover {
color: #ffffff !important;
transform: translateY(-3px) !important; /* Doubled the lift distance for a distinct reaction */
text-shadow: 0 0 10px rgba(255, 255, 255, 0.4); /* Subtle text clarity boost */
text-decoration: none !important;
}
/* ==========================================
3. BUTTON INTERACTION: Solid Scale Pop
========================================== */
html body.parent div.parentpage div#header.parenthead button.btn,
html body.parent div.parentpage div#header.parenthead .btn-primary {
transition: transform 0.15s cubic-bezier(0.25, 1, 0.5, 1), box-shadow 0.15s ease, filter 0.15s ease !important;
}
html body.parent div.parentpage div#header.parenthead button.btn:hover,
html body.parent div.parentpage div#header.parenthead .btn-primary:hover {
transform: translateY(-3px) scale(1.04) !important; /* Increased lift and expansion scale */
box-shadow: 0 0 15px rgba(37, 99, 235, 0.6) !important; /* Clear highlight glow underneath */
filter: brightness(1.2) !important;
}
/* ==========================================
4. UTILITY ICONS: Quick Response
========================================== */
html body.parent div.parentpage div#header.parenthead .header-right a i,
html body.parent div.parentpage div#header.parenthead #profile-button i,
html body.parent div.parentpage div#header.parenthead #help-button i {
display: inline-block !important;
transition: transform 0.12s ease, color 0.12s ease !important;
}
html body.parent div.parentpage div#header.parenthead .header-right a i:hover,
html body.parent div.parentpage div#header.parenthead #profile-button:hover i,
html body.parent div.parentpage div#header.parenthead #help-button:hover i {
color: #60a5fa !important;
transform: scale(1.2) translateY(-1px) !important; /* Visually pops forward and up slightly */
}