The code below describes an application to manage the state of charge of a battery system. It continuously checks the battery’s SOC and adjusts the power setpoint to charge or discharge the battery towards a target SOC, constrained by predefined minimum and maximum SOC levels. The power setpoint is limited by the maximum import/export capabilities of the converter and the charge/discharge limits of the battery. Commands are then sent to the converter to adjust its power output accordingly. The script runs in an infinite loop with a specified delay between each iteration, and it handles exceptions and graceful shutdown on user interruption.
target-soc.py
import time
from pplapp import Pplapp
from dotenv import load_dotenv
import os

load_dotenv()

# Constants:
startupDelay = 5
executionDelay = 5

POWER = 10000
MINSOC = 20
MAXSOC = 90
TARGETSOC = 75

batteryId = "battery1"
converterId = "converter1"

# Main function:
def batteryTargetSOC(app):
  # Check if target SOC is within limits
  targetSoc = limit(TARGETSOC, MINSOC, MAXSOC)

  # Battery reported State of Charge
  soc = int(float(app.getMeasurements(batteryId, "measure.ports.port1.soc")))

  if soc < targetSoc:
    powerSetpoint = -POWER # Charge the battery, need to set negative power setpoint to converter port
  elif soc > targetSoc:
    powerSetpoint = POWER # Discharge the battery, need to set positive power setpoint to converter port
  else:
    powerSetpoint = 0

  # Check if power setpoint is within limits of the converter
  converterImportPowerMax = int(app.getMeasurements(converterId, "measure.ports.port2.power.import.max"))
  converterExportPowerMax = int(app.getMeasurements(converterId, "measure.ports.port2.power.export.max"))

  powerSetpoint = limit(powerSetpoint, converterExportPowerMax, converterImportPowerMax)

  # Check if power setpoint is within limits of the battery
  batteryChargePowerMax = int(app.getMeasurements(batteryId, "measure.ports.port1.power.charge.max"))
  batteryDischargePowerMax = int(app.getMeasurements(batteryId, "measure.ports.port1.power.discharge.max"))

  powerSetpoint = limit(powerSetpoint, -batteryChargePowerMax, batteryDischargePowerMax)

  setPower(app, powerSetpoint)

  print(f"Battery SOC: {soc}% - Target SOC: {targetSoc}%")
  print("Power Setpoint: " + str(powerSetpoint) + "W")

# Helper functions:
def limit(setpoint, minimum, maximum):
  return max(min(setpoint, maximum), minimum)

def setPower(app, powerSetpoint):
  commands = {
    "control.ports.port2.method": "constant-power",
    "control.ports.port2.power": str(powerSetpoint)
  }
  app.setCommands(converterId, commands)

def disableBatteryPort(app):
  commands = {
    "control.ports.port2.method": "disabled",
    "control.ports.port2.power": str(0)
  }
  app.setCommands(converterId, commands)

def main():
  try:
    ipAddress = "192.168.1.10"
    username = os.getenv("NATS_USERNAME")
    password = os.getenv("NATS_PASSWORD")

    if not username or not password:
      raise ValueError("NATS username or password not set in environment variables")

    app = Pplapp(ipAddress, username, password)

    time.sleep(startupDelay)

    while True:
      batteryTargetSOC(app)
      time.sleep(executionDelay)

  except Exception as e:
    print(f"Failed to initialize battery Target SOC: {e}")

  except KeyboardInterrupt:
    disableBatteryPort(app)

    time.sleep(executionDelay)

    app.connectToNats = False

if __name__ == "__main__":
  main()