#!/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