#!/bin/bash
# Variables
BAT_CHRG_HOSTNAME="obkbat"
BAT_CHRG_READ="http://obkbat.local/index"
INV_PWR_HOSTNAME="obkinv"
INV_PWR_READ="http://obkinv.local/index"
BMS_BAT_HOSTNAME="hackberrypi"
BMS_BAT_READ="http://hackberrypi.local:8000/status"
retry() {
local max_attempts="$1" delay="$2"
shift 2
local attempt=1
until "$@"; do
if (( attempt >= max_attempts )); then
date +"%d.%m.%Y %H:%M:%S Error: '$*' finally failed"
return 1
fi
sleep "$delay"
((attempt++))
done
}
get_data() {
# Get Data
BAT_CHRG_DATA=$(retry 3 15 curl -sf --max-time 5 "$BAT_CHRG_READ")
INV_PWR_DATA=$(retry 3 15 curl -sf --max-time 5 "$INV_PWR_READ")
BMS_BAT_DATA=$(retry 3 15 curl -sf --max-time 30 "$BMS_BAT_READ")
# Error Handling
if [[ -z "$BAT_CHRG_DATA" || -z "$INV_PWR_DATA" ]]; then
date +"%d.%m.%Y %H:%M:%S Error [get_data]: No connection to OBK"
bms_switch "off" "$BMS_BAT_HOSTNAME"
exit 1
fi
if [[ -z "$BMS_BAT_DATA" ]]; then
date +"%d.%m.%Y %H:%M:%S Error [get_data]: No connection to Battery"
obk_switch "off" "$INV_PWR_HOSTNAME"
obk_switch "off" "$BAT_CHRG_HOSTNAME"
exit 1
fi
}
get_state() {
# Get current OpenBK states
BAT_CHRG_SWITCH_STATE=$(echo "$BAT_CHRG_DATA" | grep -Po "
\K[^<]+")
INV_PWR_SWITCH_STATE=$(echo "$INV_PWR_DATA" | grep -Po " | \K[^<]+")
# Get BMS states
BMS_DISCHARGE=$(echo "$BMS_BAT_DATA" | jq -r '.dischrg_mosfet')
BMS_CHARGE=$(echo "$BMS_BAT_DATA" | jq -r '.chrg_mosfet')
if [[ "$BMS_DISCHARGE" == "true" || "$BMS_CHARGE" == "true" ]]; then
BMS_OUTPUT_STATE="on"
else
BMS_OUTPUT_STATE="off"
fi
# Error Handling
if [[ "$BAT_CHRG_SWITCH_STATE" != @(ON|OFF) || \
"$INV_PWR_SWITCH_STATE" != @(ON|OFF) || \
"$BMS_DISCHARGE" != @(true|false) || \
"$BMS_CHARGE" != @(true|false) ]]; then
echo "$BMS_BAT_DATA"
date +"%d.%m.%Y %H:%M:%S Error [get_state]: Failed to parse one or more switch states." >&2
obk_switch "off" "$INV_PWR_HOSTNAME"
obk_switch "off" "$BAT_CHRG_HOSTNAME"
bms_switch "off" "$BMS_BAT_HOSTNAME"
exit 1
fi
# Logic - Get current State
COMBINATION="${BAT_CHRG_SWITCH_STATE,,},${INV_PWR_SWITCH_STATE,,},${BMS_OUTPUT_STATE}"
case "$COMBINATION" in
"off,off,off") SYSTEM_STATE="OFF" ;;
"off,off,on") SYSTEM_STATE="POWER" ;;
"on,off,on") SYSTEM_STATE="CHARGING" ;;
"off,on,on") SYSTEM_STATE="DISCHARGING" ;;
*) SYSTEM_STATE="UNKNOWN" ;;
esac
date +"%d.%m.%Y %H:%M:%S $SYSTEM_STATE"
}
obk_switch() {
local SWITCH="$1"
local HOSTNAME="$2"
if [[ "$SWITCH" != @(on|off) ]]; then
date +"%d.%m.%Y %H:%M:%S Error [bms]: Unsupported value"
return 1
elif [[ "$SWITCH" == "on" ]]; then
response=$(retry 3 15 curl -sf "http://${HOSTNAME}.local/cm?cmnd=POWER%20ON" --max-time 5 | jq -r '.POWER')
else
response=$(retry 3 15 curl -sf "http://${HOSTNAME}.local/cm?cmnd=POWER%20OFF" --max-time 5 | jq -r '.POWER')
fi
if [[ "$response" != @(ON|OFF) ]]; then
date +"%d.%m.%Y %H:%M:%S Error [obk]: Switch not successful"
bms_switch "off" "$BMS_BAT_HOSTNAME"
return 1
fi
}
bms_switch() {
local SWITCH="$1"
local HOSTNAME="$2"
if [[ "$SWITCH" != @(on|off) ]]; then
date +"%d.%m.%Y %H:%M:%S Error [bms]: Unsupported value"
return 1
elif [[ "$SWITCH" == "on" ]]; then
response=$(retry 3 15 curl -sf -X 'POST' \
"http://${HOSTNAME}.local:8000/discharge_a_charge?on=true" \
-H 'accept: application/json' \
-d '' --max-time 20 | jq -r '.discharging')
else
response=$(retry 3 15 curl -sf -X 'POST' \
"http://${HOSTNAME}.local:8000/discharge_a_charge?on=false" \
-H 'accept: application/json' \
-d '' --max-time 20 | jq -r '.discharging')
fi
if [[ "$response" != @(true|false) ]]; then
date +"%d.%m.%Y %H:%M:%S Error [bms]: Switch not successful"
obk_switch "off" "$INV_PWR_HOSTNAME"
obk_switch "off" "$BAT_CHRG_HOSTNAME"
return 1
fi
}
set_state() {
local TARGET_STATE="$1"
# Check if the state would change
if [[ "$TARGET_STATE" == "$SYSTEM_STATE" ]]; then
return 0
fi
# Set inital state
if [[ "$SYSTEM_STATE" != "OFF" ]]; then
bms_switch "off" "$BMS_BAT_HOSTNAME" || return 1
obk_switch "off" "$INV_PWR_HOSTNAME" || return 1
obk_switch "off" "$BAT_CHRG_HOSTNAME" || return 1
fi
# Logic - Set target State
case "$TARGET_STATE" in
"OFF")
;;
"POWER")
bms_switch "on" "$BMS_BAT_HOSTNAME" || return 1
;;
"CHARGING")
obk_switch "on" "$BAT_CHRG_HOSTNAME" || return 1
bms_switch "on" "$BMS_BAT_HOSTNAME" || return 1
;;
"DISCHARGING")
obk_switch "on" "$INV_PWR_HOSTNAME" || return 1
bms_switch "on" "$BMS_BAT_HOSTNAME" || return 1
;;
*)
date +"%d.%m.%Y %H:%M:%S Error [set_state]: Unknown state '$TARGET_STATE'"
return 1
;;
esac
# Update the Batterie state for the db
get_data
date +"%d.%m.%Y %H:%M:%S $TARGET_STATE"
}
main() {
# Threshoulds
BATTERY_LOW="20"
SWITCH_TO_DISCHARGING="100"
SWITCH_TO_ABSOLUTE="4800"
CHARGE_ON_ABSOLUTE="2000"
CHARGE_OFF_ABSOLUTE="1500"
# Data
FORECAST_POWER=$(cat /var/www/html/control)
BATTERY_LEVEL=$(echo "$BMS_BAT_DATA" | jq -r '.battery_level')
BATTERY_POWER=$(echo "$BMS_BAT_DATA" | jq -r '.power | round')
BATTERY_CHARGING=$(echo "$BMS_BAT_DATA" | jq -r '.battery_charging')
SOLAR_POWER=$(mariadb photovoltaik --password=XXX -N -e "
SELECT ROUND(
IFNULL((SELECT AVG(power) FROM inverter1 WHERE timestamp >= NOW() - INTERVAL 15 MINUTE), 0) +
IFNULL((SELECT AVG(power) FROM inverter2 WHERE timestamp >= NOW() - INTERVAL 15 MINUTE), 0) +
IFNULL((SELECT AVG(power) FROM inverter3 WHERE timestamp >= NOW() - INTERVAL 15 MINUTE), 0) +
IFNULL((SELECT AVG(power) FROM inverter4 WHERE timestamp >= NOW() - INTERVAL 15 MINUTE), 0) +
IFNULL((SELECT AVG(power) FROM inverter5 WHERE timestamp >= NOW() - INTERVAL 15 MINUTE), 0)
, 0);
")
# Error Handling
if [[ ! "$SOLAR_POWER" =~ ^[0-9]+$ ]]; then
date +"%d.%m.%Y %H:%M:%S Error [power_avg]: not a valid integer"
set_state "OFF"
return 1
fi
if [[ ! "$FORECAST_POWER" =~ ^[0-9]+$ ]]; then
date +"%d.%m.%Y %H:%M:%S Error [forecast_power]: not a valid integer"
set_state "OFF"
return 1
fi
if [[ ! "$BATTERY_LEVEL" =~ ^[0-9]+$ ]]; then
date +"%d.%m.%Y %H:%M:%S Error [battery_level]: not a valid integer"
set_state "OFF"
return 1
fi
if [[ "$BATTERY_CHARGING" != "true" && "$BATTERY_CHARGING" != "false" ]]; then
echo "$(date +"%d.%m.%Y %H:%M:%S") Error [battery_charging]: not a valid boolean"
set_state "OFF"
return 1
fi
# Discharging
if (( SOLAR_POWER <= SWITCH_TO_DISCHARGING )); then
if (( BATTERY_LEVEL >= BATTERY_LOW )); then
set_state "DISCHARGING"
else
set_state "OFF"
fi
# Charging
elif (( FORECAST_POWER <= SWITCH_TO_ABSOLUTE )); then
if (( SOLAR_POWER >= CHARGE_ON_ABSOLUTE )); then
set_state "CHARGING"
elif (( SOLAR_POWER < CHARGE_OFF_ABSOLUTE )); then
set_state "OFF"
fi
else
if (( SOLAR_POWER >= FORECAST_POWER )); then
set_state "CHARGING"
elif (( SOLAR_POWER < CHARGE_OFF_ABSOLUTE )); then
set_state "OFF"
fi
fi
# Refresh BMS data
BATTERY_LEVEL=$(echo "$BMS_BAT_DATA" | jq -r '.battery_level')
BATTERY_POWER=$(echo "$BMS_BAT_DATA" | jq -r '.power | round')
BATTERY_CHARGING=$(echo "$BMS_BAT_DATA" | jq -r '.battery_charging')
# Redis - Update JSON
local key="metrics"
# Initialize default key structure ONLY if it doesn't exist yet
redis-cli JSON.SET "$key" $ '{"solar":{},"water":{},"batterie":{"level":0,"power":0,"charging":false}}' NX >/dev/null
# Set Timestamp
redis-cli SET metrics:last_updated $(date +%s) >/dev/null
redis-cli JSON.MERGE "$key" $ "$(cat </dev/null
}
get_data
get_state
main
|