@@ -154,7 +154,14 @@ def test_no_matching_sensor_fallback(self, mock_hass, mock_coordinator):
154154 assert "No suitable DC power sensor found" in warning_msg
155155
156156 def test_energy_calculation_with_selected_sensor (self , mock_hass , mock_coordinator ):
157- """Test that energy calculation works with auto-selected sensor."""
157+ """Test that energy calculation works with auto-selected sensor.
158+
159+ Uses controlled timestamps to verify the integration:
160+ - First update at t=0: uses fallback interval (3600s = 1h)
161+ - Second update at t=3600s: uses real elapsed time (1h)
162+ """
163+ from datetime import datetime , timedelta
164+
158165 mock_coordinator .data = [
159166 {"name" : "Inverter: Power DC Total (Huawei)" , "value" : "5000" , "timestamp" : "01/01/2024 12:00:00" },
160167 ]
@@ -164,18 +171,26 @@ def test_energy_calculation_with_selected_sensor(self, mock_hass, mock_coordinat
164171 )
165172 sensor ._value = 0.0
166173
167- # First update - selects sensor and calculates
168- sensor ._handle_coordinator_update ()
174+ # First update at t=0 — no previous timestamp, uses fallback interval (1h)
175+ t0 = datetime (2024 , 1 , 1 , 12 , 0 , 0 )
176+ with patch ("custom_components.enpal_webparser.sensor.datetime" ) as mock_dt :
177+ mock_dt .now .return_value = t0
178+ mock_dt .side_effect = lambda * a , ** kw : datetime (* a , ** kw )
179+ sensor ._handle_coordinator_update ()
169180
170181 # Should have calculated energy: 5000W * 1h / 1000 = 5 kWh
171182 assert sensor ._active_source_uid == "inverter_power_dc_total_huawei"
172183 assert sensor ._value == pytest .approx (5.0 , rel = 0.01 )
173184
174- # Second update with different power
185+ # Second update 1 hour later with different power
175186 mock_coordinator .data = [
176187 {"name" : "Inverter: Power DC Total (Huawei)" , "value" : "3000" , "timestamp" : "01/01/2024 13:00:00" },
177188 ]
178- sensor ._handle_coordinator_update ()
189+ t1 = t0 + timedelta (hours = 1 )
190+ with patch ("custom_components.enpal_webparser.sensor.datetime" ) as mock_dt :
191+ mock_dt .now .return_value = t1
192+ mock_dt .side_effect = lambda * a , ** kw : datetime (* a , ** kw )
193+ sensor ._handle_coordinator_update ()
179194
180195 # Should add: 5 + 3 = 8 kWh
181196 assert sensor ._value == pytest .approx (8.0 , rel = 0.01 )
@@ -318,6 +333,119 @@ def test_make_id_consistency(self, mock_hass, mock_coordinator):
318333 assert make_id ("Inverter: Power DC Total Calculated" ) == "inverter_power_dc_total_calculated"
319334 assert make_id ("Inverter: Power DC Total" ) == "inverter_power_dc_total"
320335 assert make_id ("Inverter: Power DC Total SMA" ) == "inverter_power_dc_total_sma"
336+
337+
338+ class TestCumulativeEnergySensorElapsedTime :
339+ """Test that the energy calculation uses real elapsed time, not the configured interval.
340+
341+ In WebSocket mode, updates arrive more frequently (e.g. every 15s) than the
342+ configured polling interval (e.g. 60s). Using the configured interval would
343+ over-count energy by a factor of (interval / real_elapsed).
344+ """
345+
346+ def test_first_update_uses_fallback_interval (self , mock_hass , mock_coordinator ):
347+ """On the very first update, use the configured interval as fallback."""
348+ mock_coordinator .data = [
349+ {"name" : "Inverter: Power DC Total (Huawei)" , "value" : "6000" , "timestamp" : "01/01/2024 12:00:00" },
350+ ]
351+ sensor = create_sensor_with_mocked_state (
352+ mock_hass , mock_coordinator , ["Inverter: Power DC Total (Huawei)" ], interval = 60
353+ )
354+ sensor ._value = 0.0
355+
356+ sensor ._handle_coordinator_update ()
357+
358+ # 6000 W * (60s / 3600) / 1000 = 0.1 kWh
359+ assert sensor ._value == pytest .approx (0.1 , abs = 1e-6 )
360+
361+ def test_elapsed_time_used_for_subsequent_updates (self , mock_hass , mock_coordinator ):
362+ """After the first update, real elapsed time must be used."""
363+ from datetime import datetime , timedelta
364+
365+ mock_coordinator .data = [
366+ {"name" : "Inverter: Power DC Total (Huawei)" , "value" : "6000" , "timestamp" : "01/01/2024 12:00:00" },
367+ ]
368+ sensor = create_sensor_with_mocked_state (
369+ mock_hass , mock_coordinator , ["Inverter: Power DC Total (Huawei)" ], interval = 60
370+ )
371+ sensor ._value = 0.0
372+
373+ # Simulate first update at a known time
374+ t0 = datetime (2024 , 6 , 1 , 12 , 0 , 0 )
375+ sensor ._last_update_time = t0
376+ sensor ._active_source_uid = "inverter_power_dc_total_huawei"
377+
378+ # Simulate second update 15 seconds later (WebSocket push)
379+ t1 = t0 + timedelta (seconds = 15 )
380+ with patch ("custom_components.enpal_webparser.sensor.datetime" ) as mock_dt :
381+ mock_dt .now .return_value = t1
382+ mock_dt .side_effect = lambda * a , ** kw : datetime (* a , ** kw )
383+ sensor ._handle_coordinator_update ()
384+
385+ # 6000 W * (15s / 3600) / 1000 = 0.025 kWh
386+ assert sensor ._value == pytest .approx (0.025 , abs = 1e-6 )
387+
388+ def test_rapid_updates_do_not_overcount (self , mock_hass , mock_coordinator ):
389+ """Simulate 4 rapid updates at 15s intervals — total must match 1 minute of production."""
390+ from datetime import datetime , timedelta
391+
392+ mock_coordinator .data = [
393+ {"name" : "Inverter: Power DC Total (Huawei)" , "value" : "3600" , "timestamp" : "01/01/2024 12:00:00" },
394+ ]
395+ sensor = create_sensor_with_mocked_state (
396+ mock_hass , mock_coordinator , ["Inverter: Power DC Total (Huawei)" ], interval = 60
397+ )
398+ sensor ._value = 0.0
399+ sensor ._active_source_uid = "inverter_power_dc_total_huawei"
400+
401+ base_time = datetime (2024 , 6 , 1 , 12 , 0 , 0 )
402+ sensor ._last_update_time = base_time
403+
404+ # 4 updates, each 15 seconds apart — totalling 60 seconds
405+ for i in range (1 , 5 ):
406+ t = base_time + timedelta (seconds = 15 * i )
407+ with patch ("custom_components.enpal_webparser.sensor.datetime" ) as mock_dt :
408+ mock_dt .now .return_value = t
409+ mock_dt .side_effect = lambda * a , ** kw : datetime (* a , ** kw )
410+ sensor ._handle_coordinator_update ()
411+
412+ # 3600 W * (60s total / 3600) / 1000 = 0.06 kWh
413+ # This is the same result as a single 60s poll — no overcounting.
414+ assert sensor ._value == pytest .approx (0.06 , abs = 1e-6 )
415+
416+ def test_old_interval_would_overcount (self , mock_hass , mock_coordinator ):
417+ """Demonstrate that using the fixed interval in the same scenario would overcount.
418+
419+ With 4 updates at 15s intervals but interval=60s the old code would have
420+ calculated 4 * (3600 * 60/3600 / 1000) = 4 * 0.06 = 0.24 kWh instead of
421+ the correct 0.06 kWh — a 4× overcounting.
422+
423+ This test verifies the fix by asserting the correct value (0.06 kWh).
424+ """
425+ from datetime import datetime , timedelta
426+
427+ mock_coordinator .data = [
428+ {"name" : "Inverter: Power DC Total (Huawei)" , "value" : "3600" , "timestamp" : "01/01/2024 12:00:00" },
429+ ]
430+ sensor = create_sensor_with_mocked_state (
431+ mock_hass , mock_coordinator , ["Inverter: Power DC Total (Huawei)" ], interval = 60
432+ )
433+ sensor ._value = 0.0
434+ sensor ._active_source_uid = "inverter_power_dc_total_huawei"
435+
436+ base_time = datetime (2024 , 6 , 1 , 12 , 0 , 0 )
437+ sensor ._last_update_time = base_time
438+
439+ for i in range (1 , 5 ):
440+ t = base_time + timedelta (seconds = 15 * i )
441+ with patch ("custom_components.enpal_webparser.sensor.datetime" ) as mock_dt :
442+ mock_dt .now .return_value = t
443+ mock_dt .side_effect = lambda * a , ** kw : datetime (* a , ** kw )
444+ sensor ._handle_coordinator_update ()
445+
446+ # Must NOT be 0.24 (the old bug) — must be 0.06
447+ assert sensor ._value != pytest .approx (0.24 , abs = 0.01 )
448+ assert sensor ._value == pytest .approx (0.06 , abs = 1e-6 )
321449
322450 # Verify sensor uses same transformation
323451 mock_coordinator .data = [
0 commit comments