Skip to content

Commit

Permalink
Inverter REST tests and fixes (#1818)
Browse files Browse the repository at this point in the history
* Test cases file

* Add files via upload

* Misc

* [pre-commit.ci lite] apply automatic fixes

* Updated

* [pre-commit.ci lite] apply automatic fixes

---------

Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
  • Loading branch information
springfall2008 and pre-commit-ci-lite[bot] authored Dec 30, 2024
1 parent fc924fd commit f0553d3
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 32 deletions.
81 changes: 50 additions & 31 deletions apps/predbat/inverter.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import requests
from datetime import datetime, timedelta
from config import INVERTER_DEF, MINUTE_WATT, TIME_FORMAT, TIME_FORMAT_OCTOPUS, INVERTER_TEST, SOLAX_SOLIS_MODES_NEW, TIME_FORMAT_SECONDS, SOLAX_SOLIS_MODES
from utils import calc_percent_limit, dp0, dp2
from utils import calc_percent_limit, dp0, dp2, dp3


class Inverter:
Expand Down Expand Up @@ -139,13 +139,15 @@ def __init__(self, base, id=0, quiet=False, rest_postCommand=None, rest_getData=
self.battery_rate_max_charge_scaled = 0
self.battery_rate_max_discharge_scaled = 0
self.battery_power = 0
self.battery_voltage = 52.0
self.pv_power = 0
self.load_power = 0
self.rest_api = None
self.in_calibration = False
self.firmware_version = "Unknown"
self.givtcp_version = "n/a"
self.rest_v3 = False
self.serial_number = "Unknown"
self.count_register_writes = 0
self.created_attributes = {}
self.track_charge_start = "00:00:00"
Expand Down Expand Up @@ -228,9 +230,10 @@ def __init__(self, base, id=0, quiet=False, rest_postCommand=None, rest_getData=
else:
self.givtcp_version = self.rest_data.get("Stats", {}).get("GivTCP_Version", "Unknown")
self.firmware_version = self.rest_data.get("raw", {}).get("invertor", {}).get("firmware_version", "Unknown")
self.serial_number = self.rest_data.get("raw", {}).get("invertor", {}).get("serial_number", "Unknown")
if self.givtcp_version.startswith("3"):
self.rest_v3 = True
self.log("Inverter {} GivTCP Version: {} Firmware: {}".format(self.id, self.givtcp_version, self.firmware_version))
self.log("Inverter {} GivTCP Version: {} Firmware: {} serial {}".format(self.id, self.givtcp_version, self.firmware_version, self.serial_number))

# Timed pause support?
if self.inv_has_timed_pause:
Expand All @@ -250,31 +253,47 @@ def __init__(self, base, id=0, quiet=False, rest_postCommand=None, rest_getData=
ivtime = None
if self.rest_data and ("Invertor_Details" in self.rest_data):
idetails = self.rest_data["Invertor_Details"]
self.soc_max = float(idetails["Battery_Capacity_kWh"])
self.nominal_capacity = self.soc_max
if "raw" in self.rest_data:
raw_data = self.rest_data["raw"]
invname = "invertor"
if invname not in raw_data:
invname = "inverter"
if invname in raw_data and "battery_nominal_capacity" in raw_data[invname]:
self.nominal_capacity = float(raw_data[invname]["battery_nominal_capacity"]) / 19.53125 # XXX: Where does 19.53125 come from? I back calculated but why that number...
if "Battery_Capacity_kWh" in idetails:
self.soc_max = float(idetails["Battery_Capacity_kWh"])
self.nominal_capacity = self.soc_max
self.soc_max *= self.battery_scaling
self.soc_max = dp3(self.soc_max)

if self.rest_data and ("raw" in self.rest_data):
raw_data = self.rest_data["raw"]

# for V3 the inverter details is now named after the serial number
if self.serial_number in self.rest_data:
idetails = self.rest_data[self.serial_number]
if "Battery_Capacity_kWh" in idetails:
self.soc_max = float(idetails["Battery_Capacity_kWh"])
self.nominal_capacity = self.soc_max
self.soc_max *= self.battery_scaling
self.soc_max = dp3(self.soc_max)

# Battery capacity nominal
battery_capacity_nominal = raw_data.get("invertor", {}).get("battery_nominal_capacity", None)
if battery_capacity_nominal:
if self.rest_v3:
self.nominal_capacity = float(battery_capacity_nominal)
else:
self.nominal_capacity = float(battery_capacity_nominal) / 19.53125 # XXX: Where does 19.53125 come from? I back calculated but why that number...

if self.base.battery_capacity_nominal:
if abs(self.soc_max - self.nominal_capacity) > 1.0:
# XXX: Weird workaround for battery reporting wrong capacity issue
self.base.log("Warn: REST data reports Battery Capacity kWh as {} but nominal indicates {} - using nominal".format(self.soc_max, self.nominal_capacity))
self.soc_max = self.nominal_capacity
if invname in raw_data and "soc_force_adjust" in raw_data[invname]:
soc_force_adjust = raw_data[invname]["soc_force_adjust"]
if soc_force_adjust:
try:
soc_force_adjust = int(soc_force_adjust)
except ValueError:
soc_force_adjust = 0
if (soc_force_adjust > 0) and (soc_force_adjust < 7):
self.in_calibration = True
self.log("Warn: Inverter is in calibration mode {}, Predbat will not function correctly and will be disabled".format(soc_force_adjust))
self.soc_max *= self.battery_scaling
self.soc_max = self.nominal_capacity * self.battery_scaling

soc_force_adjust = raw_data.get("invertor", {}).get("soc_force_adjust", None)
if soc_force_adjust:
try:
soc_force_adjust = int(soc_force_adjust)
except ValueError:
soc_force_adjust = 0
if (soc_force_adjust > 0) and (soc_force_adjust < 7):
self.in_calibration = True
self.log("Warn: Inverter is in calibration mode {}, Predbat will not function correctly and will be disabled".format(soc_force_adjust))

# Max battery rate
if "Invertor_Max_Bat_Rate" in idetails:
Expand All @@ -295,10 +314,6 @@ def __init__(self, base, id=0, quiet=False, rest_postCommand=None, rest_getData=
self.soc_max = self.base.get_arg("soc_max", default=10.0, index=self.id) * self.battery_scaling
self.nominal_capacity = self.soc_max

self.battery_voltage = 52.0
if "battery_voltage" in self.base.args:
self.base.get_arg("battery_voltage", index=self.id, default=52.0)

if self.inverter_type in ["GE", "GEC", "GEE"]:
self.battery_rate_max_raw = self.base.get_arg("charge_rate", attribute="max", index=self.id, default=2600.0)
elif "battery_rate_max" in self.base.args:
Expand Down Expand Up @@ -790,12 +805,12 @@ def update_status(self, minutes_now, quiet=False):
self.discharge_rate_now = max(self.discharge_rate_now * self.base.battery_rate_max_scaling_discharge, self.battery_rate_min)

if self.rest_data:
self.soc_kw = self.rest_data["Power"]["Power"]["SOC_kWh"] * self.battery_scaling
self.soc_kw = dp3(self.rest_data["Power"]["Power"]["SOC_kWh"] * self.battery_scaling)
else:
if "soc_percent" in self.base.args:
self.soc_kw = self.base.get_arg("soc_percent", default=0.0, index=self.id) * self.soc_max / 100.0
self.soc_kw = dp3(self.base.get_arg("soc_percent", default=0.0, index=self.id) * self.soc_max / 100.0)
else:
self.soc_kw = self.base.get_arg("soc_kw", default=0.0, index=self.id) * self.battery_scaling
self.soc_kw = dp3(self.base.get_arg("soc_kw", default=0.0, index=self.id) * self.battery_scaling)

if self.soc_max <= 0.0:
self.soc_percent = 0
Expand All @@ -809,6 +824,10 @@ def update_status(self, minutes_now, quiet=False):
self.battery_power = float(ppdetails.get("Battery_Power", 0.0))
self.pv_power = float(ppdetails.get("PV_Power", 0.0))
self.load_power = float(ppdetails.get("Load_Power", 0.0))
if self.rest_v3:
self.battery_voltage = float(ppdetails.get("Battery_Voltage", 0.0))
else:
self.battery_voltage = self.base.get_arg("battery_voltage", default=52.0, index=self.id)
else:
self.battery_power = self.base.get_arg("battery_power", default=0.0, index=self.id)
self.pv_power = self.base.get_arg("pv_power", default=0.0, index=self.id)
Expand All @@ -817,7 +836,7 @@ def update_status(self, minutes_now, quiet=False):
for i in range(1, self.inv_num_load_entities):
self.load_power += self.base.get_arg(f"load_power_{i}", default=0.0, index=self.id)

self.battery_voltage = self.base.get_arg("battery_voltage", default=52.0, index=self.id)
self.battery_voltage = self.base.get_arg("battery_voltage", default=52.0, index=self.id)

if not quiet:
self.base.log(
Expand Down
2 changes: 1 addition & 1 deletion apps/predbat/predbat.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
import asyncio
import json

THIS_VERSION = "v8.8.18"
THIS_VERSION = "v8.8.19"

# fmt: off
PREDBAT_FILES = ["predbat.py", "config.py", "prediction.py", "gecloud.py","utils.py", "inverter.py", "ha.py", "download.py", "unit_test.py", "web.py", "predheat.py", "futurerate.py", "octopus.py", "solcast.py","execute.py", "plan.py", "fetch.py", "output.py", "userinterface.py"]
Expand Down
130 changes: 130 additions & 0 deletions apps/predbat/unit_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,93 @@ def test_adjust_battery_target(test_name, ha, inv, dummy_rest, prev_soc, soc, is
return failed


def test_inverter_rest_template(
test_name,
my_predbat,
filename,
assert_soc_max=9.52,
assert_soc=0,
assert_voltage=52,
assert_inverter_limit=3600,
assert_battery_rate_max=2600,
assert_serial_number="Unknown",
assert_pv_power=0,
assert_load_power=0,
assert_charge_start_time_minutes=0,
assert_charge_end_time_minutes=0,
assert_charge_enable=False,
assert_discharge_start_time_minutes=0,
assert_discharge_end_time_minutes=0,
assert_discharge_enable=False,
assert_pause_start_time_minutes=0,
assert_pause_end_time_minutes=0,
assert_nominal_capacity=9.52,
):
failed = False
print("**** Running Test: {} ****".format(test_name))
dummy_rest = DummyRestAPI()
my_predbat.args["givtcp_rest"] = "dummy"

dummy_rest.rest_data = {}
with open(filename, "r") as file:
dummy_rest.rest_data = json.load(file)

my_predbat.restart_active = True
inv = Inverter(my_predbat, 0, rest_postCommand=dummy_rest.dummy_rest_postCommand, rest_getData=dummy_rest.dummy_rest_getData, quiet=False)
inv.sleep = dummy_sleep

inv.update_status(my_predbat.minutes_now)
my_predbat.restart_active = False

if assert_soc_max != inv.soc_max:
print("ERROR: SOC Max should be {} got {}".format(assert_soc_max, inv.soc_max))
failed = True
if assert_soc != inv.soc_kw:
print("ERROR: SOC should be {} got {}".format(assert_soc, inv.soc_kw))
failed = True
if assert_voltage != inv.battery_voltage:
print("ERROR: Voltage should be {} got {}".format(assert_voltage, inv.battery_voltage))
failed = True
if assert_inverter_limit != inv.inverter_limit * MINUTE_WATT:
print("ERROR: Inverter limit should be {} got {}".format(assert_inverter_limit, inv.inverter_limit * MINUTE_WATT))
failed = True
if assert_battery_rate_max != inv.battery_rate_max_raw:
print("ERROR: Battery rate max should be {} got {}".format(assert_battery_rate_max, inv.battery_rate_max_raw))
failed = True
if assert_serial_number != inv.serial_number:
print("ERROR: Serial number should be {} got {}".format(assert_serial_number, inv.serial_number))
failed = True
if assert_pv_power != inv.pv_power:
print("ERROR: PV power should be {} got {}".format(assert_pv_power, inv.pv_power))
failed = True
if assert_load_power != inv.load_power:
print("ERROR: Load power should be {} got {}".format(assert_load_power, inv.load_power))
failed = True
if assert_charge_start_time_minutes != inv.charge_start_time_minutes:
print("ERROR: Charge start time should be {} got {}".format(assert_charge_start_time_minutes, inv.charge_start_time_minutes))
failed = True
if assert_charge_end_time_minutes != inv.charge_end_time_minutes:
print("ERROR: Discharge end time should be {} got {}".format(assert_charge_end_time_minutes, inv.charge_end_time_minutes))
failed = True
if assert_charge_enable != inv.charge_enable_time:
print("ERROR: Charge enable should be {} got {}".format(assert_charge_enable, inv.charge_enable_time))
failed = True
if assert_discharge_start_time_minutes != inv.discharge_start_time_minutes:
print("ERROR: Discharge start time should be {} got {}".format(assert_discharge_start_time_minutes, inv.discharge_start_time_minutes))
failed = True
if assert_discharge_end_time_minutes != inv.discharge_end_time_minutes:
print("ERROR: Discharge end time should be {} got {}".format(assert_discharge_end_time_minutes, inv.discharge_end_time_minutes))
failed = True
if assert_discharge_enable != inv.discharge_enable_time:
print("ERROR: Discharge enable should be {} got {}".format(assert_discharge_enable, inv.discharge_enable_time))
failed = True
if assert_nominal_capacity != inv.nominal_capacity:
print("ERROR: Nominal capacity should be {} got {}".format(assert_nominal_capacity, inv.nominal_capacity))
failed = True

return failed


def test_inverter_update(
test_name,
my_predbat,
Expand Down Expand Up @@ -1135,6 +1222,9 @@ def run_inverter_tests():
expect_load_power=2.0,
expect_soc_kwh=6.0,
)
if failed:
return failed

failed |= test_inverter_update(
"update2",
my_predbat,
Expand All @@ -1150,6 +1240,46 @@ def run_inverter_tests():
expect_load_power=2.5,
expect_soc_kwh=6.6,
)
if failed:
return failed

failed |= test_inverter_rest_template(
"rest1",
my_predbat,
filename="cases/rest_v2.json",
assert_soc_max=9.523,
assert_soc=3.333,
assert_pv_power=10,
assert_load_power=624,
assert_charge_start_time_minutes=1410,
assert_charge_end_time_minutes=1770,
assert_discharge_start_time_minutes=1380,
assert_discharge_end_time_minutes=1441,
assert_discharge_enable=False,
assert_charge_enable=True,
assert_nominal_capacity=9.5232,
)
if failed:
return failed
failed |= test_inverter_rest_template(
"rest2",
my_predbat,
filename="cases/rest_v3.json",
assert_voltage=53.65,
assert_battery_rate_max=3600,
assert_serial_number="EA2303G082",
assert_soc=7.62,
assert_pv_power=247.0,
assert_load_power=197.0,
assert_charge_start_time_minutes=1440,
assert_charge_end_time_minutes=1440,
assert_discharge_start_time_minutes=5,
assert_discharge_end_time_minutes=91,
assert_discharge_enable=True,
assert_nominal_capacity=9.52,
)
if failed:
return failed

my_predbat.args["givtcp_rest"] = None
dummy_rest = DummyRestAPI()
Expand Down
Binary file added coverage/cases.tgz
Binary file not shown.

0 comments on commit f0553d3

Please sign in to comment.