from unittest.mock import Mock, patch import pytest from akkudoktoreos.devices.inverter import Inverter, InverterParameters @pytest.fixture def mock_battery() -> Mock: mock_battery = Mock() mock_battery.charge_energy = Mock(return_value=(0.0, 0.0)) mock_battery.discharge_energy = Mock(return_value=(0.0, 0.0)) mock_battery.device_id = "battery1" return mock_battery @pytest.fixture def inverter(mock_battery, devices_eos) -> Inverter: devices_eos.add_device(mock_battery) mock_self_consumption_predictor = Mock() mock_self_consumption_predictor.calculate_self_consumption.return_value = 1.0 with patch( "akkudoktoreos.devices.inverter.get_eos_load_interpolator", return_value=mock_self_consumption_predictor, ): iv = Inverter( InverterParameters( device_id="iv1", max_power_wh=500.0, battery_id=mock_battery.device_id ), ) devices_eos.add_device(iv) devices_eos.post_setup() return iv def test_process_energy_excess_generation(inverter, mock_battery): # Battery charges 100 Wh with 10 Wh loss mock_battery.charge_energy.return_value = (100.0, 10.0) generation = 600.0 consumption = 200.0 hour = 12 grid_export, grid_import, losses, self_consumption = inverter.process_energy( generation, consumption, hour ) assert grid_export == pytest.approx(290.0, rel=1e-2) # 290 Wh feed-in after battery charges assert grid_import == 0.0 # No grid draw assert losses == 10.0 # Battery charging losses assert self_consumption == 200.0 # All consumption is met mock_battery.charge_energy.assert_called_once_with(400.0, hour) mock_battery.discharge_energy.assert_not_called() inverter.self_consumption_predictor.calculate_self_consumption.assert_called_once_with( consumption, generation ) def test_process_energy_excess_generation_interpolator(inverter, mock_battery): # Battery charges 100 Wh with 10 Wh loss mock_battery.charge_energy.return_value = (100.0, 10.0) mock_battery.discharge_energy.return_value = (20.0, 2.0) inverter.self_consumption_predictor.calculate_self_consumption.return_value = 0.95 generation = 600.0 consumption = 200.0 hour = 12 grid_export, grid_import, losses, self_consumption = inverter.process_energy( generation, consumption, hour ) assert grid_export == pytest.approx( 270.0, rel=1e-2 ) # 290 Wh feed-in - 5% of generation-consumption self consumption after battery charges assert grid_import == pytest.approx(0.0, rel=1e-2) # No grid draw assert losses == 12.0 # Battery charging losses assert self_consumption == 220.0 # All consumption is met mock_battery.charge_energy.assert_called_once_with(pytest.approx(380.0, rel=1e-2), hour) mock_battery.discharge_energy.assert_called_once_with(pytest.approx(20.0, rel=1e-2), hour) inverter.self_consumption_predictor.calculate_self_consumption.assert_called_once_with( consumption, generation ) def test_process_energy_generation_equals_consumption(inverter, mock_battery): generation = 300.0 consumption = 300.0 hour = 12 grid_export, grid_import, losses, self_consumption = inverter.process_energy( generation, consumption, hour ) assert grid_export == 0.0 # No feed-in as generation equals consumption assert grid_import == 0.0 # No grid draw assert losses == 0.0 # No losses assert self_consumption == 300.0 # All consumption is met with generation mock_battery.charge_energy.assert_not_called() mock_battery.discharge_energy.assert_not_called() inverter.self_consumption_predictor.calculate_self_consumption.assert_called_once_with( consumption, generation ) def test_process_energy_battery_discharges(inverter, mock_battery): # Battery discharges 100 Wh with 10 Wh loss already accounted for in the discharge mock_battery.discharge_energy.return_value = (100.0, 10.0) generation = 100.0 consumption = 250.0 hour = 12 grid_export, grid_import, losses, self_consumption = inverter.process_energy( generation, consumption, hour ) assert grid_export == 0.0 # No feed-in as generation is insufficient assert grid_import == pytest.approx( 50.0, rel=1e-2 ) # Grid supplies remaining shortfall after battery discharge assert losses == 10.0 # Discharge losses assert self_consumption == 200.0 # Generation + battery discharge mock_battery.charge_energy.assert_not_called() mock_battery.discharge_energy.assert_called_once_with(150.0, hour) inverter.self_consumption_predictor.calculate_self_consumption.assert_not_called() def test_process_energy_battery_empty(inverter, mock_battery): # Battery is empty, so no energy can be discharged mock_battery.discharge_energy.return_value = (0.0, 0.0) generation = 100.0 consumption = 300.0 hour = 12 grid_export, grid_import, losses, self_consumption = inverter.process_energy( generation, consumption, hour ) assert grid_export == 0.0 # No feed-in as generation is insufficient assert grid_import == pytest.approx(200.0, rel=1e-2) # Grid has to cover the full shortfall assert losses == 0.0 # No losses as the battery didn't discharge assert self_consumption == 100.0 # Only generation is consumed mock_battery.charge_energy.assert_not_called() mock_battery.discharge_energy.assert_called_once_with(200.0, hour) inverter.self_consumption_predictor.calculate_self_consumption.assert_not_called() def test_process_energy_battery_full_at_start(inverter, mock_battery): # Battery is full, so no charging happens mock_battery.charge_energy.return_value = (0.0, 0.0) generation = 500.0 consumption = 200.0 hour = 12 grid_export, grid_import, losses, self_consumption = inverter.process_energy( generation, consumption, hour ) assert grid_export == pytest.approx( 300.0, rel=1e-2 ) # All excess energy should be fed into the grid assert grid_import == 0.0 # No grid draw assert losses == 0.0 # No losses assert self_consumption == 200.0 # Only consumption is met mock_battery.charge_energy.assert_called_once_with(300.0, hour) mock_battery.discharge_energy.assert_not_called() inverter.self_consumption_predictor.calculate_self_consumption.assert_called_once_with( consumption, generation ) def test_process_energy_insufficient_generation_no_battery(inverter, mock_battery): # Insufficient generation and no battery discharge mock_battery.discharge_energy.return_value = (0.0, 0.0) generation = 100.0 consumption = 500.0 hour = 12 grid_export, grid_import, losses, self_consumption = inverter.process_energy( generation, consumption, hour ) assert grid_export == 0.0 # No feed-in as generation is insufficient assert grid_import == pytest.approx(400.0, rel=1e-2) # Grid supplies the shortfall assert losses == 0.0 # No losses assert self_consumption == 100.0 # Only generation is consumed mock_battery.charge_energy.assert_not_called() mock_battery.discharge_energy.assert_called_once_with(400.0, hour) inverter.self_consumption_predictor.calculate_self_consumption.assert_not_called() def test_process_energy_insufficient_generation_battery_assists(inverter, mock_battery): # Battery assists with some discharge to cover the shortfall mock_battery.discharge_energy.return_value = ( 50.0, 5.0, ) # Battery discharges 50 Wh with 5 Wh loss generation = 200.0 consumption = 400.0 hour = 12 grid_export, grid_import, losses, self_consumption = inverter.process_energy( generation, consumption, hour ) assert grid_export == 0.0 # No feed-in as generation is insufficient assert grid_import == pytest.approx( 150.0, rel=1e-2 ) # Grid supplies the remaining shortfall after battery discharge assert losses == 5.0 # Discharge losses assert self_consumption == 250.0 # Generation + battery discharge mock_battery.charge_energy.assert_not_called() mock_battery.discharge_energy.assert_called_once_with(200.0, hour) inverter.self_consumption_predictor.calculate_self_consumption.assert_not_called() def test_process_energy_zero_generation(inverter, mock_battery): # Zero generation, full reliance on battery and grid mock_battery.discharge_energy.return_value = ( 100.0, 5.0, ) # Battery discharges 100 Wh with 5 Wh loss generation = 0.0 consumption = 300.0 hour = 12 grid_export, grid_import, losses, self_consumption = inverter.process_energy( generation, consumption, hour ) assert grid_export == 0.0 # No feed-in as there is zero generation assert grid_import == pytest.approx(200.0, rel=1e-2) # Grid supplies the remaining shortfall assert losses == 5.0 # Discharge losses assert self_consumption == 100.0 # Only battery discharge is consumed mock_battery.charge_energy.assert_not_called() mock_battery.discharge_energy.assert_called_once_with(300.0, hour) inverter.self_consumption_predictor.calculate_self_consumption.assert_not_called() def test_process_energy_zero_consumption(inverter, mock_battery): # Generation exceeds consumption, but consumption is zero mock_battery.charge_energy.return_value = (100.0, 10.0) generation = 500.0 consumption = 0.0 hour = 12 grid_export, grid_import, losses, self_consumption = inverter.process_energy( generation, consumption, hour ) assert grid_export == pytest.approx(390.0, rel=1e-2) # Excess energy after battery charges assert grid_import == 0.0 # No grid draw as no consumption assert losses == 10.0 # Charging losses assert self_consumption == 0.0 # Zero consumption mock_battery.charge_energy.assert_called_once_with(500.0, hour) mock_battery.discharge_energy.assert_not_called() inverter.self_consumption_predictor.calculate_self_consumption.assert_called_once_with( consumption, generation ) def test_process_energy_zero_generation_zero_consumption(inverter, mock_battery): generation = 0.0 consumption = 0.0 hour = 12 grid_export, grid_import, losses, self_consumption = inverter.process_energy( generation, consumption, hour ) assert grid_export == 0.0 # No feed-in assert grid_import == 0.0 # No grid draw assert losses == 0.0 # No losses assert self_consumption == 0.0 # No consumption mock_battery.charge_energy.assert_not_called() mock_battery.discharge_energy.assert_not_called() inverter.self_consumption_predictor.calculate_self_consumption.assert_called_once_with( consumption, generation ) def test_process_energy_partial_battery_discharge(inverter, mock_battery): mock_battery.discharge_energy.return_value = (50.0, 5.0) generation = 200.0 consumption = 400.0 hour = 12 grid_export, grid_import, losses, self_consumption = inverter.process_energy( generation, consumption, hour ) assert grid_export == 0.0 # No feed-in due to insufficient generation assert grid_import == pytest.approx( 150.0, rel=1e-2 ) # Grid supplies the shortfall after battery assist assert losses == 5.0 # Discharge losses assert self_consumption == 250.0 # Generation + battery discharge mock_battery.charge_energy.assert_not_called() mock_battery.discharge_energy.assert_called_once_with(200.0, 12) inverter.self_consumption_predictor.calculate_self_consumption.assert_not_called() def test_process_energy_consumption_exceeds_max_no_battery(inverter, mock_battery): # Battery is empty, and consumption is much higher than the inverter's max power mock_battery.discharge_energy.return_value = (0.0, 0.0) generation = 100.0 consumption = 1000.0 # Exceeds the inverter's max power hour = 12 grid_export, grid_import, losses, self_consumption = inverter.process_energy( generation, consumption, hour ) assert grid_export == 0.0 # No feed-in assert grid_import == pytest.approx(900.0, rel=1e-2) # Grid covers the remaining shortfall assert losses == 0.0 # No losses as the battery didn’t assist assert self_consumption == 100.0 # Only the generation is consumed, maxing out the inverter mock_battery.charge_energy.assert_not_called() mock_battery.discharge_energy.assert_called_once_with(400.0, hour) inverter.self_consumption_predictor.calculate_self_consumption.assert_not_called() def test_process_energy_zero_generation_full_battery_high_consumption(inverter, mock_battery): # Full battery, no generation, and high consumption mock_battery.discharge_energy.return_value = (500.0, 10.0) generation = 0.0 consumption = 600.0 hour = 12 grid_export, grid_import, losses, self_consumption = inverter.process_energy( generation, consumption, hour ) assert grid_export == 0.0 # No feed-in due to zero generation assert grid_import == pytest.approx( 100.0, rel=1e-2 ) # Grid covers remaining shortfall after battery discharge assert losses == 10.0 # Battery discharge losses assert self_consumption == 500.0 # Battery fully discharges to meet consumption mock_battery.charge_energy.assert_not_called() mock_battery.discharge_energy.assert_called_once_with(500.0, hour) inverter.self_consumption_predictor.calculate_self_consumption.assert_not_called()