88# Add the apps/predbat directory to the path
99sys .path .append (os .path .join (os .path .dirname (__file__ ), ".." , "apps" , "predbat" ))
1010
11+ from datetime import datetime , timezone
1112from fetch import Fetch
1213from utils import dp4
1314
@@ -20,6 +21,7 @@ def __init__(self):
2021 self .log_messages = []
2122 self .forecast_minutes = 24 * 60 # 24 hours
2223 self .plan_interval_minutes = 30
24+ self .now_utc = datetime .now (timezone .utc )
2325
2426 def log (self , message ):
2527 """Capture log messages"""
@@ -362,6 +364,7 @@ def run_all_tests(my_predbat=None):
362364 test_fill_load_from_power_zero_load ()
363365 test_fill_load_from_power_backwards_time ()
364366 test_fill_load_from_power_data_extends_beyond_load ()
367+ test_fill_load_from_power_distorted_power_inflation ()
365368
366369 print ("\n " + "=" * 60 )
367370 print ("✅ ALL TESTS PASSED" )
@@ -382,6 +385,88 @@ def run_all_tests(my_predbat=None):
382385 return 1 # Return 1 for error
383386
384387
388+ def test_fill_load_from_power_distorted_power_inflation ():
389+ """
390+ Regression test for issue #3692: ML load actual ~2x real consumption.
391+
392+ When load_power data is loaded via minute_data_load with clean_increment=True,
393+ clean_incrementing_reverse treats the fluctuating instantaneous power (W) values
394+ as a cumulative sensor and accumulates all positive increments into an ever-growing
395+ series. fill_load_from_power Phase 1 (zero-period filling) then integrates these
396+ inflated values, bumping all more-recent cumulative entries by the total inflated
397+ fill. Phase 2 preserves these bumped endpoints, so the overall energy
398+ (data[0] - data[max]) is inflated even though per-window redistribution is correct.
399+
400+ This test verifies that:
401+ 1. clean_incrementing_reverse grossly distorts non-cumulative power data
402+ 2. fill_load_from_power with distorted power inflates the total cumulative energy
403+ 3. fill_load_from_power with correct power produces a reasonable total
404+ """
405+ print ("\n === Test 8: Power data distortion causes load inflation (issue #3692) ===" )
406+
407+ from utils import clean_incrementing_reverse
408+
409+ fetch = TestFetch ()
410+
411+ # Simulate 240 minutes (4 hours) of load data with a 60-minute flat (zero) period
412+ # from minute 60 to minute 120. Backwards format: minute 0 is now (highest value).
413+ load_minutes = {}
414+ # Minute 0-59: smooth ramp (0.05 kWh/min consumed)
415+ for minute in range (0 , 60 ):
416+ load_minutes [minute ] = 15.0 - minute * 0.05
417+ # Minute 60-120: flat at 12.0 kWh (sensor didn't increment - zero period)
418+ for minute in range (60 , 121 ):
419+ load_minutes [minute ] = 12.0
420+ # Minute 121-240: smooth ramp
421+ for minute in range (121 , 241 ):
422+ load_minutes [minute ] = 12.0 - (minute - 120 ) * 0.05
423+
424+ total_original = load_minutes [0 ] - load_minutes [240 ]
425+
426+ # Realistic fluctuating power data (~600W, varying 300-900W) -
427+ # this is what minute_data_load should return when clean_increment=False
428+ correct_power_data = {}
429+ for minute in range (241 ):
430+ correct_power_data [minute ] = 600.0 + 100.0 * ((minute % 7 ) - 3 )
431+
432+ # Simulate the bug: clean_incrementing_reverse on non-cumulative power data
433+ distorted_power_data = clean_incrementing_reverse (correct_power_data )
434+
435+ # Verify distortion magnitude
436+ max_correct = max (correct_power_data .values ())
437+ max_distorted = max (distorted_power_data .values ())
438+ print (f" Max correct power: { max_correct :.0f} W, Max distorted: { max_distorted :.0f} W" )
439+ assert max_distorted > max_correct * 10 , f"Distorted power should be >>10x correct; got { max_distorted :.0f} vs { max_correct :.0f} "
440+
441+ # Run fill_load_from_power with CORRECT power data
442+ result_correct = fetch .fill_load_from_power (load_minutes .copy (), correct_power_data )
443+
444+ # Run fill_load_from_power with DISTORTED power data (the bug)
445+ result_distorted = fetch .fill_load_from_power (load_minutes .copy (), distorted_power_data )
446+
447+ # Check total energy (data[0] - data[max]): the Phase 1 bump inflates data[0]
448+ total_correct = result_correct [0 ] - result_correct .get (240 , result_correct .get (239 , 0 ))
449+ total_distorted = result_distorted [0 ] - result_distorted .get (240 , result_distorted .get (239 , 0 ))
450+
451+ print (f" Total energy original: { total_original :.2f} kWh" )
452+ print (f" Total energy (correct power): { total_correct :.2f} kWh" )
453+ print (f" Total energy (distorted power): { total_distorted :.2f} kWh" )
454+ print (f" Inflation ratio (distorted/orig): { total_distorted / total_original :.2f} x" )
455+
456+ # Correct power fill should add a reasonable amount (60 min at ~600W = ~0.6 kWh)
457+ correct_fill_excess = total_correct - total_original
458+ assert correct_fill_excess < 2.0 , f"Correct fill should add <2 kWh over original; added { correct_fill_excess :.2f} "
459+
460+ # Distorted power fill should cause significant inflation (the bug)
461+ distorted_fill_excess = total_distorted - total_original
462+ assert distorted_fill_excess > 2.0 , f"Distorted fill should inflate by >2 kWh; only added { distorted_fill_excess :.2f} . " "If this assertion fails, Phase 2 may be fully correcting Phase 1 and the bug " "mechanism is different than expected."
463+
464+ # The distorted total should be noticeably larger than the correct total
465+ assert total_distorted > total_correct * 1.2 , f"Distorted total ({ total_distorted :.2f} ) should be >1.2x correct ({ total_correct :.2f} )"
466+
467+ print (" PASS: Distorted power data causes measurable cumulative inflation" )
468+
469+
385470if __name__ == "__main__" :
386471 success = run_all_tests ()
387472 sys .exit (0 if success else 1 )
0 commit comments