Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions apps/predbat/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,58 @@
"enable": "num_cars",
"enable_condition": "num_cars > 3",
},
{
"name": "car_charging_rate_4",
"friendly_name": "Car charging rate (Car 4)",
"type": "input_number",
"min": 1,
"max": 24,
"step": 0.10,
"unit": "kW",
"icon": "mdi:ev-station",
"default": 7.4,
"enable": "num_cars",
"enable_condition": "num_cars > 4",
},
{
"name": "car_charging_rate_5",
"friendly_name": "Car charging rate (Car 5)",
"type": "input_number",
"min": 1,
"max": 24,
"step": 0.10,
"unit": "kW",
"icon": "mdi:ev-station",
"default": 7.4,
"enable": "num_cars",
"enable_condition": "num_cars > 5",
},
{
"name": "car_charging_rate_6",
"friendly_name": "Car charging rate (Car 6)",
"type": "input_number",
"min": 1,
"max": 24,
"step": 0.10,
"unit": "kW",
"icon": "mdi:ev-station",
"default": 7.4,
"enable": "num_cars",
"enable_condition": "num_cars > 6",
},
{
"name": "car_charging_rate_7",
"friendly_name": "Car charging rate (Car 7)",
"type": "input_number",
"min": 1,
"max": 24,
"step": 0.10,
"unit": "kW",
"icon": "mdi:ev-station",
"default": 7.4,
"enable": "num_cars",
"enable_condition": "num_cars > 7",
},
{
"name": "car_charging_loss",
"friendly_name": "Car charging loss",
Expand Down Expand Up @@ -741,6 +793,90 @@
"default": 0.0,
"restore": False,
},
{
"name": "car_charging_manual_soc_4",
"friendly_name": "Car charging manual SOC (Car 4)",
"type": "switch",
"default": False,
"enable": "num_cars",
"enable_condition": "num_cars > 4",
},
{
"name": "car_charging_manual_soc_kwh_4",
"friendly_name": "Car manual SOC kWh (Car 4)",
"type": "input_number",
"min": 0,
"max": 100,
"step": 0.01,
"unit": "kWh",
"icon": "mdi:ev-station",
"enable": "car_charging_manual_soc_4",
"default": 0.0,
"restore": False,
},
{
"name": "car_charging_manual_soc_5",
"friendly_name": "Car charging manual SOC (Car 5)",
"type": "switch",
"default": False,
"enable": "num_cars",
"enable_condition": "num_cars > 5",
},
{
"name": "car_charging_manual_soc_kwh_5",
"friendly_name": "Car manual SOC kWh (Car 5)",
"type": "input_number",
"min": 0,
"max": 100,
"step": 0.01,
"unit": "kWh",
"icon": "mdi:ev-station",
"enable": "car_charging_manual_soc_5",
"default": 0.0,
"restore": False,
},
{
"name": "car_charging_manual_soc_6",
"friendly_name": "Car charging manual SOC (Car 6)",
"type": "switch",
"default": False,
"enable": "num_cars",
"enable_condition": "num_cars > 6",
},
{
"name": "car_charging_manual_soc_kwh_6",
"friendly_name": "Car manual SOC kWh (Car 6)",
"type": "input_number",
"min": 0,
"max": 100,
"step": 0.01,
"unit": "kWh",
"icon": "mdi:ev-station",
"enable": "car_charging_manual_soc_6",
"default": 0.0,
"restore": False,
},
{
"name": "car_charging_manual_soc_7",
"friendly_name": "Car charging manual SOC (Car 7)",
"type": "switch",
"default": False,
"enable": "num_cars",
"enable_condition": "num_cars > 7",
},
{
"name": "car_charging_manual_soc_kwh_7",
"friendly_name": "Car manual SOC kWh (Car 7)",
"type": "input_number",
"min": 0,
"max": 100,
"step": 0.01,
"unit": "kWh",
"icon": "mdi:ev-station",
"enable": "car_charging_manual_soc_7",
"default": 0.0,
"restore": False,
},
{
"name": "octopus_intelligent_charging",
"friendly_name": "Octopus Intelligent Charging",
Expand Down
2 changes: 1 addition & 1 deletion apps/predbat/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
INVERTER_MAX_RETRY_REST = 5 # Maximum number of retries for inverter REST commands
INVERTER_REST_TIMEOUT = 10 # Seconds to wait for a REST response before giving up (local network call, should be fast)
INVERTER_QUICK_UPDATE_SECONDS = 120 # Minimum seconds between quick inverter data updates
PREDBAT_MAX_CARS = 4 # Matches PK_MAX_CARS in prediction_kernel.cpp and the car_charging_rate_0..3 config items - the hard ceiling on num_cars
PREDBAT_MAX_CARS = 8 # Matches PK_MAX_CARS in prediction_kernel.cpp and the car_charging_rate/_1../_7 config items - the hard ceiling on num_cars
Comment thread
springfall2008 marked this conversation as resolved.

# 240v x 100 amps x 3 phases / 1000 to kW / 60 minutes in an hour is the maximum kWh in a 1 minute period
MAX_INCREMENT = 240 * 100 * 3 / 1000 / 60
Expand Down
4 changes: 2 additions & 2 deletions apps/predbat/prediction_kernel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@
// falling back. Bumping makes the loader reject it and use the Python engine, which is the whole
// point of the check.
#define PK_ABI_VERSION 4
#define PK_PARITY_REVISION 5
#define PK_MAX_CARS 4
#define PK_PARITY_REVISION 6
#define PK_MAX_CARS 8
#define PK_RUN_EVERY 5 // const.py RUN_EVERY

namespace {
Expand Down
8 changes: 4 additions & 4 deletions apps/predbat/prediction_kernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@

# Expected ABI/parity revisions of the shared library (see prediction_kernel.cpp)
KERNEL_ABI_VERSION = 4
KERNEL_PARITY_REVISION = 5
KERNEL_PARITY_REVISION = 6

# Maximum number of cars supported by the kernel (PK_MAX_CARS in prediction_kernel.cpp)
KERNEL_MAX_CARS = PREDBAT_MAX_CARS
Expand Down Expand Up @@ -97,8 +97,8 @@ class PkContext(ctypes.Structure):
("export_today_now", ctypes.c_double),
("iboost_today", ctypes.c_double),
("car_charging_loss", ctypes.c_double),
("car_charging_limit", ctypes.c_double * 4),
("car_charging_soc", ctypes.c_double * 4),
("car_charging_limit", ctypes.c_double * KERNEL_MAX_CARS),
("car_charging_soc", ctypes.c_double * KERNEL_MAX_CARS),
("iboost_max_energy", ctypes.c_double),
("iboost_max_power", ctypes.c_double),
("iboost_min_power", ctypes.c_double),
Expand Down Expand Up @@ -169,7 +169,7 @@ class PkResult(ctypes.Structure):
("metric_keep", ctypes.c_double),
("final_iboost", ctypes.c_double),
("final_carbon_g", ctypes.c_double),
("car_soc_next", ctypes.c_double * 4),
("car_soc_next", ctypes.c_double * KERNEL_MAX_CARS),
("iboost_next", ctypes.c_double),
("soc_min_minute", ctypes.c_int32),
("car_soc_next_valid", ctypes.c_int32),
Expand Down
Binary file modified apps/predbat/prediction_kernel_lib_aarch64.so
Binary file not shown.
Binary file modified apps/predbat/prediction_kernel_lib_armv7l.so
Binary file not shown.
Binary file modified apps/predbat/prediction_kernel_lib_darwin_arm64.so
Binary file not shown.
Binary file modified apps/predbat/prediction_kernel_lib_darwin_x86_64.so
Binary file not shown.
Binary file modified apps/predbat/prediction_kernel_lib_i686.so
Binary file not shown.
Binary file modified apps/predbat/prediction_kernel_lib_x86_64.so
Binary file not shown.
24 changes: 22 additions & 2 deletions apps/predbat/tests/test_fetch_config_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -363,11 +363,11 @@ def mock_expose_config(key, value):
# misconfiguration) must not be allowed through - car indices beyond the supported maximum
# have no HA config entity, which previously crashed get_car_charging_planned() with
# TypeError: float() argument must be a string or a real number, not 'NoneType'.
mock_config.config["num_cars"] = 6
mock_config.config["num_cars"] = 12

my_predbat.fetch_config_options()

assert my_predbat.num_cars == 4, "num_cars should be clamped to 4 (the supported maximum), got {}".format(my_predbat.num_cars)
assert my_predbat.num_cars == 8, "num_cars should be clamped to 8 (the supported maximum), got {}".format(my_predbat.num_cars)

print("✓ num_cars clamp test passed")

Expand All @@ -384,5 +384,25 @@ def mock_expose_config(key, value):
my_predbat.expose_config = original_expose_config
my_predbat.args = original_args

# Test 15: get_car_charging_planned resolves all 8 supported cars against the real
# CONFIG_ITEMS/config_index (not the MockConfigProvider above) - this is the exact code path
# that crashed with TypeError: float() argument must be a string or a real number, not
# 'NoneType' when num_cars exceeded the number of car_charging_rate_N entities defined.
print("\n*** Test 15: get_car_charging_planned resolves all 8 cars via real config_index ***")

original_num_cars = my_predbat.num_cars
my_predbat.num_cars = 8
my_predbat.get_car_charging_planned()

assert len(my_predbat.car_charging_rate) == 8, "car_charging_rate should have 8 entries, got {}".format(len(my_predbat.car_charging_rate))
for car_n, rate in enumerate(my_predbat.car_charging_rate):
assert rate == 7.4, "car {} charging rate should default to 7.4, got {}".format(car_n, rate)

# Restore real config state for any tests that run after this one
my_predbat.num_cars = original_num_cars
my_predbat.fetch_config_options()

print("✓ 8-car config resolution test passed")

print("\n**** All fetch_config_options tests passed! ****")
return False
11 changes: 8 additions & 3 deletions apps/predbat/tests/test_infra.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
# pylint: disable=attribute-defined-outside-init

from datetime import datetime, timedelta
from const import PREDBAT_MAX_CARS
from prediction import Prediction
from matplotlib import pyplot as plt
import asyncio
Expand Down Expand Up @@ -408,6 +409,10 @@ def get_default_config(self):
"car_charging_rate_1": 7.4,
"car_charging_rate_2": 7.4,
"car_charging_rate_3": 7.4,
"car_charging_rate_4": 7.4,
"car_charging_rate_5": 7.4,
"car_charging_rate_6": 7.4,
"car_charging_rate_7": 7.4,
Comment thread
springfall2008 marked this conversation as resolved.
"car_charging_battery_size_0": 100.0,
"car_charging_battery_size_1": 100.0,
"car_charging_limit_0": 100.0,
Expand Down Expand Up @@ -509,8 +514,8 @@ def reset_inverter(my_predbat):
my_predbat.num_cars = 0
my_predbat.car_charging_slots[0] = []
my_predbat.car_charging_from_battery = True
my_predbat.car_charging_limit = [100.0, 100.0, 100.0, 100.0]
my_predbat.car_charging_soc = [0, 0, 0, 0]
my_predbat.car_charging_limit = [100.0] * PREDBAT_MAX_CARS
my_predbat.car_charging_soc = [0] * PREDBAT_MAX_CARS
my_predbat.iboost_enable = False
my_predbat.iboost_solar = False
my_predbat.iboost_gas = False
Expand All @@ -523,7 +528,7 @@ def reset_inverter(my_predbat):
my_predbat.best_soc_keep = 0.0
my_predbat.carbon_enable = 0
my_predbat.inverter_soc_reset = True
my_predbat.car_charging_soc_next = [None for car_n in range(4)]
my_predbat.car_charging_soc_next = [None for car_n in range(PREDBAT_MAX_CARS)]
my_predbat.charge_limit_best = []
my_predbat.charge_window_best = []
my_predbat.export_limits_best = []
Expand Down
9 changes: 5 additions & 4 deletions apps/predbat/tests/test_octopus_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -1872,10 +1872,11 @@ def test_octopus_automatic_config_num_cars(my_predbat):
original_args = dict(my_predbat.args)

# Test 1: 5 registered devices, 2 suspended - only the 3 active devices should count towards
# num_cars and appear in the entity lists. Without this filter num_cars would be bumped to 5,
# which exceeds the 4 cars Predbat/the kernel support and previously crashed
# get_car_charging_planned() with TypeError: float() argument must be a string or a real number,
# not 'NoneType' (car_charging_rate_4 has no config entity).
# num_cars and appear in the entity lists. A stale/decommissioned device left in an Octopus
# account should not be treated as a car needing a charging slot (and, before
# fetch_config_options' num_cars clamp existed, an inflated count could push num_cars past
# what Predbat supports and crash get_car_charging_planned() with TypeError: float() argument
# must be a string or a real number, not 'NoneType').
print("\n*** Test 1: Suspended devices are excluded from num_cars and entity lists ***")
api = OctopusAPI(my_predbat, key="test-api-key", account_id="test-account", automatic=False)
api.intelligent_devices = {
Expand Down
Loading