2020makes that testable: it drives the load forecast directly instead of through the fixture's history.
2121"""
2222
23- from const import PREDICT_STEP
24- from utils import dp2
23+ from const import PREDICT_STEP , EXPORT_MODE_TARGET
24+ from utils import dp2 , export_mode_of
2525from tests .test_infra import reset_inverter , reset_rates , update_rates_import , update_rates_export
2626from prediction import Prediction
2727
@@ -461,6 +461,64 @@ def test_solar_battery_priority_level(my_predbat):
461461 return failed
462462
463463
464+ def test_away_moves_solar_earlier (my_predbat ):
465+ """Away time makes the battery-priority hold yield, so the car charges while it is still here.
466+
467+ The hold assumes the car can catch up later, which stops being true the moment the afternoon is
468+ marked away - and at a 100% priority level the car would otherwise never see a solar window at
469+ all. Reported from a live system: marking the afternoon away made the car charge from the grid
470+ at 30p instead of moving to the morning sun.
471+ """
472+ print (" - test_away_moves_solar_earlier" )
473+ failed = False
474+ setup_car (my_predbat , car_kwh = 30.0 , ready_ahead = 1200 , rate = 7.0 , house_kw = 1.0 )
475+ reset_rates (my_predbat , 30.0 , 5.0 )
476+ my_predbat .car_charging_solar = True
477+ my_predbat .soc_max = 27.0
478+ my_predbat .soc_kw = 0.0
479+ my_predbat .battery_rate_max_charge = 10.0 / 60.0
480+ my_predbat .battery_rate_max_scaling = 1.0
481+ my_predbat .car_charging_solar_battery_soc = 100
482+ set_pv (my_predbat , 8.0 , start_offset = 120 , length = 480 )
483+ low_rates = [{"start" : my_predbat .minutes_now + 30 * n , "end" : my_predbat .minutes_now + 30 * (n + 1 ), "average" : 30.0 } for n in range (40 )]
484+ update_rates_import (my_predbat , low_rates )
485+
486+ my_predbat .manual_car_away_times = []
487+ baseline = [slot for slot in my_predbat .plan_car_charging (0 , low_rates ) if slot .get ("solar" )]
488+ if not baseline :
489+ print ("ERROR: expected some solar charging with no away markers" )
490+ return True
491+ baseline_first = baseline [0 ]["start" ] - my_predbat .minutes_now
492+
493+ # Mark the afternoon away - the car's remaining chances are all in the morning
494+ my_predbat .manual_car_away_times = [my_predbat .minutes_now + m for m in range (300 , 600 , 30 )]
495+ plan = my_predbat .plan_car_charging (0 , low_rates )
496+ solar = [slot for slot in plan if slot .get ("solar" )]
497+
498+ if not solar :
499+ print ("ERROR: away time removed solar charging entirely instead of moving it earlier" )
500+ failed = True
501+ else :
502+ moved_to = solar [0 ]["start" ] - my_predbat .minutes_now
503+ if moved_to >= baseline_first :
504+ print ("ERROR: solar should start earlier than {} once the afternoon is away, got {}" .format (baseline_first , moved_to ))
505+ failed = True
506+
507+ for slot in plan :
508+ if my_predbat .car_slot_is_away (slot ["start" ], slot ["end" ]):
509+ print ("ERROR: planned a slot the car is away for: {}-{}" .format (slot ["start" ], slot ["end" ]))
510+ failed = True
511+
512+ # With no away time set the hold is untouched, which is the everyday case
513+ my_predbat .manual_car_away_times = []
514+ if my_predbat .car_solar_reserved_for_car (my_predbat .car_solar_load_forecast ()) != 0.0 :
515+ print ("ERROR: nothing should be reserved for the car when no away time is set" )
516+ failed = True
517+
518+ my_predbat .car_charging_solar_battery_soc = 0
519+ return failed
520+
521+
464522def test_solar_windows_ignore_ready_time (my_predbat ):
465523 """Solar windows run to the forecast horizon, so a morning ready time does not exclude daylight."""
466524 print (" - test_solar_windows_ignore_ready_time" )
@@ -531,7 +589,9 @@ def run_car_export_tradeoff(my_predbat, export_rate, from_battery, car_kwh=8.0):
531589 my_predbat .optimise_all_windows (result [0 ], result [8 ])
532590
533591 final = my_predbat .run_prediction (my_predbat .charge_limit_best , my_predbat .charge_window_best , my_predbat .export_window_best , my_predbat .export_limits_best , False , end_record = end_record , save = "best" )
534- exported = bool (my_predbat .export_limits_best ) and my_predbat .export_limits_best [0 ] < 100
592+ # Export limits are a packed structure since #5047, so ask the accessor rather than comparing the
593+ # raw value - a tuple is not less than 100, and reading it as one silently reports "did not export"
594+ exported = bool (my_predbat .export_limits_best ) and export_mode_of (my_predbat .export_limits_best [0 ]) == EXPORT_MODE_TARGET
535595 return exported , final [1 ] + final [2 ]
536596
537597
@@ -600,6 +660,7 @@ def run_car_solar_tests(my_predbat):
600660 "car_charging_now" ,
601661 "car_charging_solar" ,
602662 "car_charging_solar_excess" ,
663+ "manual_car_away_times" ,
603664 "car_charging_solar_battery_soc" ,
604665 "battery_rate_max_charge" ,
605666 "battery_rate_max_scaling" ,
@@ -622,6 +683,7 @@ def run_car_solar_tests(my_predbat):
622683 failed |= test_solar_slot_size_follows_surplus (my_predbat )
623684 failed |= test_solar_slot_capped_by_charger (my_predbat )
624685 failed |= test_solar_battery_priority_level (my_predbat )
686+ failed |= test_away_moves_solar_earlier (my_predbat )
625687 if failed :
626688 return failed
627689 failed |= test_car_export_tradeoff (my_predbat )
0 commit comments