Skip to content

Commit dde2714

Browse files
Merge pull request #5155 from springfall2008/fix/dawn-sensor-combine-charge-slots-gate
fix(fetch): compute dawn sensor regardless of combine_charge_slots
2 parents bc1d0de + 13b6069 commit dde2714

3 files changed

Lines changed: 34 additions & 58 deletions

File tree

‎apps/predbat/fetch.py‎

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -920,12 +920,13 @@ def fetch_pv_forecast_and_dawn(self):
920920
# Stored on self, not just local, so it can be used elsewhere rather than only by the
921921
# window split below.
922922
pv_light_dark = self.pv_light_dark = self.calc_pv_light_dark()
923-
# "on" past dawn, "off" before it or when unclassified (combine_charge_slots off, or no
924-
# PV forecast) - not the same as PV actually producing right now, see calc_dawn's docstring
923+
# "on" past dawn, "off" before it or when unclassified (no PV forecast) - based on the PV
924+
# forecast crossing low_power_pv_threshold_w, not the same as PV actually producing right
925+
# now, see calc_dawn's docstring. Independent of combine_charge_slots/set_charge_low_power.
925926
self.dashboard_item(
926927
"binary_sensor." + self.prefix + "_dawn",
927928
state="on" if pv_light_dark.get(self.minutes_now) == 1 else "off",
928-
attributes={"friendly_name": "Predbat is past dawn (light, not dark, in the low-power charge window split)", "icon": "mdi:weather-sunset-up"},
929+
attributes={"friendly_name": "Predbat is past dawn (light, not dark, by PV forecast)", "icon": "mdi:weather-sunset-up"},
929930
)
930931
return pv_light_dark
931932

@@ -1902,19 +1903,18 @@ def carbon_replicate(self, carbon_data):
19021903

19031904
def calc_pv_light_dark(self):
19041905
"""
1905-
Decide whether a dawn light/dark boundary is worth computing at all, and return it via
1906-
calc_dawn if so - otherwise an empty dict (no split).
1906+
Compute the dawn light/dark split via calc_dawn().
19071907
1908-
Only combine_charge_slots can merge a charge window across dawn in the first place - with it
1909-
off, find_charge_window already forces a break every charge_slot_split minutes (which equals
1910-
plan_interval_minutes, the same granularity calc_dawn buckets at), so the dawn boundary could
1911-
never be reached and computing it would be a pure no-op. This used to be gated on
1912-
set_charge_low_power instead, since that was the only feature that needed the split - but the
1913-
split also lets the plan optimizer charge just the dark portion of a combined window and skip
1914-
the daylight portion (where solar may cover the load) on its own merits, independent of low
1915-
power charging, so it now runs for any combine_charge_slots user.
1908+
Always computed, even when combine_charge_slots is off: with it off, find_charge_window
1909+
already forces a window break every charge_slot_split minutes (which equals
1910+
plan_interval_minutes, the same granularity calc_dawn buckets at), so the dawn boundary can
1911+
never actually be reached there and calc_dawn's result is a no-op for window splitting in
1912+
that case - but binary_sensor.predbat_dawn (published from this same result, see
1913+
fetch_pv_forecast_and_dawn) is a useful standalone signal regardless of combine_charge_slots,
1914+
and used to read permanently "off" for anyone with it disabled (the default) even in broad
1915+
daylight, which is what it is not meant to mean.
19161916
"""
1917-
return self.calc_dawn() if self.combine_charge_slots else {}
1917+
return self.calc_dawn()
19181918

19191919
def calc_dawn(self):
19201920
"""

‎apps/predbat/tests/test_find_charge_window.py‎

Lines changed: 19 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -503,17 +503,14 @@ def set_buckets(bucket_values):
503503

504504
def test_calc_pv_light_dark(my_predbat):
505505
"""
506-
Tests for calc_pv_light_dark: decides whether calc_dawn is worth computing at all.
507-
508-
Only combine_charge_slots can merge a charge window across dawn, so that's what gates the split -
509-
not set_charge_low_power. With combine_charge_slots off, find_charge_window already forces a break
510-
every charge_slot_split (=plan_interval_minutes) minutes regardless, so the dawn boundary could
511-
never be reached and computing it would be a no-op:
512-
513-
- combine_charge_slots=True, set_charge_low_power=False -> dawn split still runs (the new case -
514-
the optimizer can pick the dark portion of a combined window on its own merits)
515-
- combine_charge_slots=True, set_charge_low_power=True -> dawn split runs (unchanged behaviour)
516-
- combine_charge_slots=False, regardless of set_charge_low_power -> {} (moot, skipped)
506+
Tests for calc_pv_light_dark: always returns calc_dawn's result.
507+
508+
calc_dawn is computed regardless of combine_charge_slots or set_charge_low_power -
509+
binary_sensor.predbat_dawn (published from this same result) is a useful standalone light/dark
510+
signal even when neither feature is enabled, and used to read permanently "off" whenever
511+
combine_charge_slots was off (the default), which is not what the sensor is meant to mean. The
512+
combine_charge_slots gate on window-splitting itself lives in find_charge_window instead, where it
513+
is moot anyway (a break every charge_slot_split minutes already happens there regardless).
517514
"""
518515
failed = 0
519516
old_pv_forecast_minute = my_predbat.pv_forecast_minute
@@ -528,38 +525,17 @@ def test_calc_pv_light_dark(my_predbat):
528525
for m in range(30, 60, 5):
529526
my_predbat.pv_forecast_minute[m] = 1.0 # light, crosses the threshold
530527

531-
print("Test calc_pv_light_dark: combine_charge_slots=True, set_charge_low_power=False -> dawn split still runs")
532-
my_predbat.combine_charge_slots = True
533-
my_predbat.set_charge_low_power = False
534-
result = my_predbat.calc_pv_light_dark()
535-
if result != my_predbat.calc_dawn():
536-
print("ERROR: calc_pv_light_dark: expected calc_dawn's result when combine_charge_slots is True, got {}".format(result))
537-
failed = 1
538-
if result.get(0) != 0 or result.get(30) != 1:
539-
print("ERROR: calc_pv_light_dark: expected a dark->light split at minute 30, got {}".format({m: result.get(m) for m in (0, 30)}))
540-
failed = 1
541-
542-
print("Test calc_pv_light_dark: combine_charge_slots=True, set_charge_low_power=True -> dawn split runs")
543-
my_predbat.set_charge_low_power = True
544-
result = my_predbat.calc_pv_light_dark()
545-
if result != my_predbat.calc_dawn():
546-
print("ERROR: calc_pv_light_dark: expected calc_dawn's result when combine_charge_slots is True, got {}".format(result))
547-
failed = 1
548-
549-
print("Test calc_pv_light_dark: combine_charge_slots=False, set_charge_low_power=True -> {} (moot, skipped)")
550-
my_predbat.combine_charge_slots = False
551-
my_predbat.set_charge_low_power = True
552-
result = my_predbat.calc_pv_light_dark()
553-
if result != {}:
554-
print("ERROR: calc_pv_light_dark: expected {{}} when combine_charge_slots is False, got {}".format(result))
555-
failed = 1
556-
557-
print("Test calc_pv_light_dark: combine_charge_slots=False, set_charge_low_power=False -> {}")
558-
my_predbat.set_charge_low_power = False
559-
result = my_predbat.calc_pv_light_dark()
560-
if result != {}:
561-
print("ERROR: calc_pv_light_dark: expected {{}} when combine_charge_slots is False, got {}".format(result))
562-
failed = 1
528+
for combine_charge, low_power in ((True, False), (True, True), (False, True), (False, False)):
529+
print("Test calc_pv_light_dark: combine_charge_slots={}, set_charge_low_power={} -> dawn split always runs".format(combine_charge, low_power))
530+
my_predbat.combine_charge_slots = combine_charge
531+
my_predbat.set_charge_low_power = low_power
532+
result = my_predbat.calc_pv_light_dark()
533+
if result != my_predbat.calc_dawn():
534+
print("ERROR: calc_pv_light_dark: expected calc_dawn's result, got {}".format(result))
535+
failed = 1
536+
if result.get(0) != 0 or result.get(30) != 1:
537+
print("ERROR: calc_pv_light_dark: expected a dark->light split at minute 30, got {}".format({m: result.get(m) for m in (0, 30)}))
538+
failed = 1
563539

564540
my_predbat.pv_forecast_minute = old_pv_forecast_minute
565541
my_predbat.plan_interval_minutes = old_plan_interval

‎docs/output-data.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -703,7 +703,7 @@ The solar sensor attributes include:
703703
- remaining/remaining10/remaining90/remainingCL - forecast solar generation for the remainder of the day
704704
- detailedForecast - a half hourly breakdown of solar forecast for the day, with similar PV estimate, 10% estimate, 90% estimate and calibrated estimate values
705705

706-
- binary_sensor.predbat_dawn - Set to 'on' when the current time is past dawn (forecast solar power at or above [low_power_pv_threshold_w](customisation.md#inverter-control-options)), 'off' before dawn or when the underlying split (which requires **switch.predbat_combine_charge_slots**) isn't in use. This reflects the same dawn boundary Predbat uses to split a low-power charge window at sunrise, not whether solar is producing enough to be useful right now.
706+
- binary_sensor.predbat_dawn - Set to 'on' when the current time is past dawn (forecast solar power at or above [low_power_pv_threshold_w](customisation.md#inverter-control-options)), 'off' before dawn or when no PV forecast is available. This reflects the same dawn boundary Predbat uses to split a low-power charge window at sunrise, not whether solar is producing enough to be useful right now.
707707

708708
## Dummy inverter sensors
709709

0 commit comments

Comments
 (0)