Skip to content
Open
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
27 changes: 22 additions & 5 deletions apps/predbat/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -1076,19 +1076,31 @@ def get_charge_rate_kw(self, charge_window_n, minute_start, minute_relative_star
which stores a fixed reduced rate in the fractional part of export_limits_best, low power
charging (set_charge_low_power) throttles the rate dynamically minute-by-minute, so it has
to be recomputed the same way the prediction engine works it out (find_charge_rate()).

Returns (rate_kw, low_power): the rate achieved after the charge curve, and whether low power
charging asked for less than the maximum charge rate. The curve tapering a full rate charge
near the top of the battery is not low power - only the requested rate decides that.

The maximum is the one the prediction engine uses at the start of the slot: a hybrid inverter
whose DC charge rate is above its AC one can take the PV above the AC rate on top of it.
"""
window = self.charge_window_best[charge_window_n]
soc = self.predict_soc_best.get(minute_relative_start, self.soc_kw)
battery_rate_max_charge = self.battery_rate_max_charge
if self.inverter_hybrid and (self.battery_rate_max_charge_dc > battery_rate_max_charge):
pv_above = max((pv_forecast_minute_step.get(minute_relative_start, 0.0) / PREDICT_STEP) - battery_rate_max_charge, 0)
battery_rate_max_charge += min(self.battery_rate_max_charge_dc - battery_rate_max_charge, pv_above)
pv_window_kwh = 0.0
if self.set_charge_low_power:
window_end_rel = min(window["end"] - self.minutes_now, self.forecast_minutes)
# The window end is floored to a step boundary, as the prediction engine does
window_end_rel = min(max(((window["end"] - self.minutes_now) // PREDICT_STEP) * PREDICT_STEP, minute_relative_start), self.forecast_minutes)
pv_window_kwh = sum(pv_forecast_minute_step.get(m, 0.0) for m in range(minute_relative_start, window_end_rel, PREDICT_STEP))
Comment on lines +1095 to 1097
_, charge_rate_now_curve = find_charge_rate(
charge_rate_now, charge_rate_now_curve = find_charge_rate(
minute_start,
soc,
window,
self.charge_limit_best[charge_window_n],
self.battery_rate_max_charge,
battery_rate_max_charge,
self.soc_max,
self.battery_charge_power_curve,
self.set_charge_low_power,
Expand All @@ -1103,7 +1115,9 @@ def get_charge_rate_kw(self, charge_window_n, minute_start, minute_relative_star
low_power_pv_threshold_w=self.low_power_pv_threshold_w,
solar_full_rate=self.set_charge_low_power_solar_full_rate,
)
return dp2(charge_rate_now_curve * MINUTE_WATT / 1000.0)
# Compared in whole watts, find_charge_rate() steps the rate down in watts and converts back
low_power = dp0(charge_rate_now * MINUTE_WATT) < dp0(battery_rate_max_charge * MINUTE_WATT)
return dp2(charge_rate_now_curve * MINUTE_WATT / 1000.0), low_power

def publish_html_plan(self, pv_forecast_minute_step, pv_forecast_minute_step10, load_minutes_step, load_minutes_step10, end_record, publish=True, prediction=None, car_hold_minutes=None):
"""
Expand Down Expand Up @@ -1440,8 +1454,11 @@ def import_rate_color(rate):
state = "Chrg&nearr;"
state_color = "#3AEE85"
raw_state = "Chrg"
rate_kw = self.get_charge_rate_kw(charge_window_n, minute_start, minute_relative_start, pv_forecast_minute_step)
rate_kw, charge_low_power = self.get_charge_rate_kw(charge_window_n, minute_start, minute_relative_start, pv_forecast_minute_step)
reason_parts.append({"code": "charge_low_rate", "params": {"target_percent": limit_percent, "rate": rate_text_import, "rate_kw": "{:.2f}".format(rate_kw)}})
if charge_low_power:
# Snail symbol
state += "&#x1F40C;"
Comment thread
springfall2008 marked this conversation as resolved.

if self.charge_window_best[charge_window_n]["start"] in self.manual_charge_times:
state += " &#8526;"
Expand Down
67 changes: 67 additions & 0 deletions apps/predbat/tests/test_plan_why_reason.py
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,9 @@ def render(car_hold_minutes=None):
elif "Charging up to 80" not in _render(row, templates) or "{}kW".format(expected_charge_rate_kw) not in _render(row, templates):
print("ERROR: Chrg rendered text unexpected: {}".format(_render(row, templates)))
failed = True
elif "&#x1F40C;" in row["state_html"]:
print("ERROR: a full rate Chrg slot should not show the snail, got: {}".format(row["state_html"]))
failed = True

# --- Test 2: HoldChrg ---
print("Test HoldChrg reason")
Expand Down Expand Up @@ -451,6 +454,10 @@ def render(car_hold_minutes=None):
# dynamically via find_charge_rate() rather than a fixed fraction like export's snail encoding) ---
print("Test Chrg low power reason shows the throttled rate_kw, not the nameplate max")
low_power_window = [{"start": minutes_now, "end": minutes_now + 60, "average": 10.0}]
saved_predict_soc_best = my_predbat.predict_soc_best
saved_low_power_pv_threshold_w = my_predbat.low_power_pv_threshold_w
saved_inverter_hybrid = my_predbat.inverter_hybrid
saved_battery_rate_max_charge_dc = my_predbat.battery_rate_max_charge_dc
my_predbat.charge_window_best = low_power_window
my_predbat.charge_limit_best = [8.0] # small gap above the 7.9 current SoC - easily reached even throttled
my_predbat.predict_soc_best = _flat_soc(my_predbat, 7.9)
Expand All @@ -469,7 +476,67 @@ def render(car_hold_minutes=None):
elif "{:.2f}kW".format(rate_kw) not in _render(row, templates):
print("ERROR: Chrg low power rendered text missing the throttled rate: {}".format(_render(row, templates)))
failed = True
elif row["state_html"] != "Chrg&nearr;&#x1F40C;":
print("ERROR: Chrg low power state cell should show the snail like a slow export does (#5352), got: {}".format(row["state_html"]))
failed = True

# --- Test 4c: low power mode on but the target needs the full rate - not a slow charge, no snail ---
print("Test Chrg low power at the full rate shows no snail")
my_predbat.charge_limit_best = [10.0]
my_predbat.predict_soc_best = _flat_soc(my_predbat, 2.0) # too far from the target to throttle within the window
Comment thread
springfall2008 marked this conversation as resolved.
_, raw_plan = render()
row = _get_row(raw_plan, minutes_now)
if row is None or _codes(row) != ["charge_low_rate"]:
print("ERROR: Chrg low power full rate reasons unexpected: {}".format(row and _codes(row)))
failed = True
elif "&#x1F40C;" in row["state_html"]:
print("ERROR: Chrg low power at the full rate should not show the snail, got: {}".format(row["state_html"]))
failed = True

# --- Test 4d: PV that only arrives after the window's last whole step must not abandon low power,
# the prediction engine floors the window end to a step boundary and so never counts it ---
print("Test Chrg low power ignores PV past the floored window end")
my_predbat.charge_window_best = [{"start": minutes_now, "end": minutes_now + 62, "average": 10.0}]
my_predbat.charge_limit_best = [8.0]
my_predbat.predict_soc_best = _flat_soc(my_predbat, 7.9)
my_predbat.low_power_pv_threshold_w = 100
pv_step[60] = 0.25 # 3kW for the step starting at the floored window end
_, raw_plan = render()
row = _get_row(raw_plan, minutes_now)
if row is None or _codes(row) != ["charge_low_rate"]:
print("ERROR: Chrg low power floored window reasons unexpected: {}".format(row and _codes(row)))
failed = True
elif "&#x1F40C;" not in row["state_html"]:
print("ERROR: Chrg low power should stay throttled when the only PV is past the floored window end, got: {}".format(row["state_html"]))
failed = True
pv_step[60] = 0
my_predbat.low_power_pv_threshold_w = saved_low_power_pv_threshold_w
my_predbat.set_charge_low_power = False

# --- Test 4e: hybrid inverter with a DC charge rate above the AC one charges through bright PV at
# the combined rate, as the prediction engine does - that is the maximum, so no snail ---
print("Test Chrg on a hybrid inverter in bright PV shows the combined AC+DC rate")
my_predbat.charge_window_best = low_power_window
my_predbat.charge_limit_best = [10.0]
my_predbat.predict_soc_best = _flat_soc(my_predbat, 2.0)
my_predbat.inverter_hybrid = True
my_predbat.battery_rate_max_charge_dc = my_predbat.battery_rate_max_charge * 2
pv_step[0] = my_predbat.battery_rate_max_charge * 3 * 5 # PV at three times the AC charge rate
_, raw_plan = render()
row = _get_row(raw_plan, minutes_now)
if row is None or _codes(row) != ["charge_low_rate"]:
print("ERROR: hybrid Chrg reasons unexpected: {}".format(row and _codes(row)))
failed = True
elif row["reasons"][0]["params"]["rate_kw"] != "{:.2f}".format(max_rate_kw * 2):
print("ERROR: hybrid Chrg rate_kw should be the combined {}kW, got {}".format(max_rate_kw * 2, row["reasons"][0]["params"]["rate_kw"]))
failed = True
elif "&#x1F40C;" in row["state_html"]:
print("ERROR: hybrid Chrg at the combined rate should not show the snail, got: {}".format(row["state_html"]))
failed = True
pv_step[0] = 0
my_predbat.inverter_hybrid = saved_inverter_hybrid
my_predbat.battery_rate_max_charge_dc = saved_battery_rate_max_charge_dc
my_predbat.predict_soc_best = saved_predict_soc_best
my_predbat.charge_window_best = window
my_predbat.charge_limit_best = [8.0]

Expand Down
4 changes: 3 additions & 1 deletion docs/predbat-plan-card.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,9 @@ charging and discharging activity - if Predbat plans this, the state will show a

- **Limit %** - Alongside any battery activity (charging, discharging, etc) there will be a SoC limit. This limit is what the SoC is planned to be at the end of the slot.
e.g. 'Charge&nearr; 70%' is charge to 70% SoC, and 'Exp&searr; 4%' is force exporting the battery to the 4% reserve level.<BR>
If Predbat is planning a slow charge or slow export then the limit will be preceded by a snail symbol (&#x1F40C;) and will be shown as 'limit.tens_of_percentage_rate_reduction'. e.g.: 16.3 means limit of 16% and charge/discharge at 70% of normal rate.
If Predbat is planning a slow charge or slow export then the State will be followed by a snail symbol (&#x1F40C;).<BR>
For a slow export, with plan debug enabled, the limit will also be shown as 'limit.tens_of_percentage_rate_reduction'. e.g.: 16.3 means limit of 16% and discharge at 70% of normal rate.<BR>
For a slow charge ([low power charging mode](customisation.md#inverter-control-options)) the rate is worked out as the slot runs, so the planned rate is shown in the slot's description instead.

- **PV kWh** - The predicted solar forecast for the half-hour slot, estimated from the [Solcast Forecast](apps-yaml.md#solcast-solar-forecast).<BR>
If the PV forecast is above 0.2kWh for the slot it will be coloured Melon Red with a little sun symbol, above 0.1kWh it will be Yellow with a sun symbol,
Expand Down
Loading