mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2025-06-28 00:46:53 +00:00
Inverter v2 (#245)
* inverter class rewritten second try * cleanup * inverter section of decives.py translation * open api fix * fix openapi v2 * renamed the class itself * ruff fix * Update genetic.py * cleanup * reverted indent
This commit is contained in:
parent
763926d8e8
commit
810cc17c0b
@ -2275,15 +2275,7 @@
|
|||||||
],
|
],
|
||||||
"title": "Optimization Ev Available Charge Rates Percent",
|
"title": "Optimization Ev Available Charge Rates Percent",
|
||||||
"description": "Charge rates available for the EV in percent of maximum charge.",
|
"description": "Charge rates available for the EV in percent of maximum charge.",
|
||||||
"default": [
|
"default": [0.0, 0.375, 0.5, 0.625, 0.75, 0.875, 1.0]
|
||||||
0.0,
|
|
||||||
0.375,
|
|
||||||
0.5,
|
|
||||||
0.625,
|
|
||||||
0.75,
|
|
||||||
0.875,
|
|
||||||
1.0
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"battery_provider": {
|
"battery_provider": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
@ -2780,9 +2772,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": ["kapazitaet_wh"],
|
||||||
"kapazitaet_wh"
|
|
||||||
],
|
|
||||||
"title": "EAutoParameters"
|
"title": "EAutoParameters"
|
||||||
},
|
},
|
||||||
"EAutoResult": {
|
"EAutoResult": {
|
||||||
@ -2929,10 +2919,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": ["temperature", "pvpower"],
|
||||||
"temperature",
|
|
||||||
"pvpower"
|
|
||||||
],
|
|
||||||
"title": "ForecastResponse"
|
"title": "ForecastResponse"
|
||||||
},
|
},
|
||||||
"GesamtlastRequest": {
|
"GesamtlastRequest": {
|
||||||
@ -2954,11 +2941,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": ["year_energy", "measured_data", "hours"],
|
||||||
"year_energy",
|
|
||||||
"measured_data",
|
|
||||||
"hours"
|
|
||||||
],
|
|
||||||
"title": "GesamtlastRequest"
|
"title": "GesamtlastRequest"
|
||||||
},
|
},
|
||||||
"HTTPValidationError": {
|
"HTTPValidationError": {
|
||||||
@ -2990,12 +2973,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": ["consumption_wh", "duration_h"],
|
||||||
"consumption_wh",
|
|
||||||
"duration_h"
|
|
||||||
],
|
|
||||||
"title": "HomeApplianceParameters"
|
"title": "HomeApplianceParameters"
|
||||||
},
|
},
|
||||||
|
"InverterParameters": {
|
||||||
|
"properties": {
|
||||||
|
"max_power_wh": {
|
||||||
|
"type": "number",
|
||||||
|
"exclusiveMinimum": 0.0,
|
||||||
|
"title": "Max Power Wh",
|
||||||
|
"default": 10000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"title": "InverterParameters"
|
||||||
|
},
|
||||||
"OptimizationParameters": {
|
"OptimizationParameters": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"ems": {
|
"ems": {
|
||||||
@ -3004,10 +2996,10 @@
|
|||||||
"pv_akku": {
|
"pv_akku": {
|
||||||
"$ref": "#/components/schemas/PVAkkuParameters"
|
"$ref": "#/components/schemas/PVAkkuParameters"
|
||||||
},
|
},
|
||||||
"wechselrichter": {
|
"inverter": {
|
||||||
"$ref": "#/components/schemas/WechselrichterParameters",
|
"$ref": "#/components/schemas/InverterParameters",
|
||||||
"default": {
|
"default": {
|
||||||
"max_leistung_wh": 10000.0
|
"max_power_wh": 10000.0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"eauto": {
|
"eauto": {
|
||||||
@ -3062,11 +3054,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": ["ems", "pv_akku", "eauto"],
|
||||||
"ems",
|
|
||||||
"pv_akku",
|
|
||||||
"eauto"
|
|
||||||
],
|
|
||||||
"title": "OptimizationParameters"
|
"title": "OptimizationParameters"
|
||||||
},
|
},
|
||||||
"OptimizeResponse": {
|
"OptimizeResponse": {
|
||||||
@ -3225,9 +3213,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": ["kapazitaet_wh"],
|
||||||
"kapazitaet_wh"
|
|
||||||
],
|
|
||||||
"title": "PVAkkuParameters"
|
"title": "PVAkkuParameters"
|
||||||
},
|
},
|
||||||
"SettingsEOS": {
|
"SettingsEOS": {
|
||||||
@ -4994,15 +4980,7 @@
|
|||||||
],
|
],
|
||||||
"title": "Optimization Ev Available Charge Rates Percent",
|
"title": "Optimization Ev Available Charge Rates Percent",
|
||||||
"description": "Charge rates available for the EV in percent of maximum charge.",
|
"description": "Charge rates available for the EV in percent of maximum charge.",
|
||||||
"default": [
|
"default": [0.0, 0.375, 0.5, 0.625, 0.75, 0.875, 1.0]
|
||||||
0.0,
|
|
||||||
0.375,
|
|
||||||
0.5,
|
|
||||||
0.625,
|
|
||||||
0.75,
|
|
||||||
0.875,
|
|
||||||
1.0
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"battery_provider": {
|
"battery_provider": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
@ -5493,25 +5471,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": ["loc", "msg", "type"],
|
||||||
"loc",
|
|
||||||
"msg",
|
|
||||||
"type"
|
|
||||||
],
|
|
||||||
"title": "ValidationError"
|
"title": "ValidationError"
|
||||||
},
|
|
||||||
"WechselrichterParameters": {
|
|
||||||
"properties": {
|
|
||||||
"max_leistung_wh": {
|
|
||||||
"type": "number",
|
|
||||||
"exclusiveMinimum": 0.0,
|
|
||||||
"title": "Max Leistung Wh",
|
|
||||||
"default": 10000
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "object",
|
|
||||||
"title": "WechselrichterParameters"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ from akkudoktoreos.core.coreabc import ConfigMixin, PredictionMixin, SingletonMi
|
|||||||
from akkudoktoreos.core.pydantic import PydanticBaseModel
|
from akkudoktoreos.core.pydantic import PydanticBaseModel
|
||||||
from akkudoktoreos.devices.battery import PVAkku
|
from akkudoktoreos.devices.battery import PVAkku
|
||||||
from akkudoktoreos.devices.generic import HomeAppliance
|
from akkudoktoreos.devices.generic import HomeAppliance
|
||||||
from akkudoktoreos.devices.inverter import Wechselrichter
|
from akkudoktoreos.devices.inverter import Inverter
|
||||||
from akkudoktoreos.utils.datetimeutil import to_datetime
|
from akkudoktoreos.utils.datetimeutil import to_datetime
|
||||||
from akkudoktoreos.utils.logutil import get_logger
|
from akkudoktoreos.utils.logutil import get_logger
|
||||||
from akkudoktoreos.utils.utils import NumpyEncoder
|
from akkudoktoreos.utils.utils import NumpyEncoder
|
||||||
@ -155,7 +155,7 @@ class EnergieManagementSystem(SingletonMixin, ConfigMixin, PredictionMixin, Pyda
|
|||||||
akku: Optional[PVAkku] = Field(default=None, description="TBD.")
|
akku: Optional[PVAkku] = Field(default=None, description="TBD.")
|
||||||
eauto: Optional[PVAkku] = Field(default=None, description="TBD.")
|
eauto: Optional[PVAkku] = Field(default=None, description="TBD.")
|
||||||
home_appliance: Optional[HomeAppliance] = Field(default=None, description="TBD.")
|
home_appliance: Optional[HomeAppliance] = Field(default=None, description="TBD.")
|
||||||
wechselrichter: Optional[Wechselrichter] = Field(default=None, description="TBD.")
|
inverter: Optional[Inverter] = Field(default=None, description="TBD.")
|
||||||
|
|
||||||
# -------------------------
|
# -------------------------
|
||||||
# TODO: Move to devices
|
# TODO: Move to devices
|
||||||
@ -170,7 +170,7 @@ class EnergieManagementSystem(SingletonMixin, ConfigMixin, PredictionMixin, Pyda
|
|||||||
parameters: EnergieManagementSystemParameters,
|
parameters: EnergieManagementSystemParameters,
|
||||||
eauto: Optional[PVAkku] = None,
|
eauto: Optional[PVAkku] = None,
|
||||||
home_appliance: Optional[HomeAppliance] = None,
|
home_appliance: Optional[HomeAppliance] = None,
|
||||||
wechselrichter: Optional[Wechselrichter] = None,
|
inverter: Optional[Inverter] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.gesamtlast = np.array(parameters.gesamtlast, float)
|
self.gesamtlast = np.array(parameters.gesamtlast, float)
|
||||||
self.pv_prognose_wh = np.array(parameters.pv_prognose_wh, float)
|
self.pv_prognose_wh = np.array(parameters.pv_prognose_wh, float)
|
||||||
@ -180,13 +180,13 @@ class EnergieManagementSystem(SingletonMixin, ConfigMixin, PredictionMixin, Pyda
|
|||||||
if isinstance(parameters.einspeiseverguetung_euro_pro_wh, list)
|
if isinstance(parameters.einspeiseverguetung_euro_pro_wh, list)
|
||||||
else np.full(len(self.gesamtlast), parameters.einspeiseverguetung_euro_pro_wh, float)
|
else np.full(len(self.gesamtlast), parameters.einspeiseverguetung_euro_pro_wh, float)
|
||||||
)
|
)
|
||||||
if wechselrichter is not None:
|
if inverter is not None:
|
||||||
self.akku = wechselrichter.akku
|
self.akku = inverter.akku
|
||||||
else:
|
else:
|
||||||
self.akku = None
|
self.akku = None
|
||||||
self.eauto = eauto
|
self.eauto = eauto
|
||||||
self.home_appliance = home_appliance
|
self.home_appliance = home_appliance
|
||||||
self.wechselrichter = wechselrichter
|
self.inverter = inverter
|
||||||
self.ac_charge_hours = np.full(self.config.prediction_hours, 0.0)
|
self.ac_charge_hours = np.full(self.config.prediction_hours, 0.0)
|
||||||
self.dc_charge_hours = np.full(self.config.prediction_hours, 1.0)
|
self.dc_charge_hours = np.full(self.config.prediction_hours, 1.0)
|
||||||
self.ev_charge_hours = np.full(self.config.prediction_hours, 0.0)
|
self.ev_charge_hours = np.full(self.config.prediction_hours, 0.0)
|
||||||
@ -354,10 +354,10 @@ class EnergieManagementSystem(SingletonMixin, ConfigMixin, PredictionMixin, Pyda
|
|||||||
netzeinspeisung, netzbezug, verluste, eigenverbrauch = (0.0, 0.0, 0.0, 0.0)
|
netzeinspeisung, netzbezug, verluste, eigenverbrauch = (0.0, 0.0, 0.0, 0.0)
|
||||||
if self.akku:
|
if self.akku:
|
||||||
self.akku.set_charge_allowed_for_hour(self.dc_charge_hours[stunde], stunde)
|
self.akku.set_charge_allowed_for_hour(self.dc_charge_hours[stunde], stunde)
|
||||||
if self.wechselrichter:
|
if self.inverter:
|
||||||
erzeugung = self.pv_prognose_wh[stunde]
|
erzeugung = self.pv_prognose_wh[stunde]
|
||||||
netzeinspeisung, netzbezug, verluste, eigenverbrauch = (
|
netzeinspeisung, netzbezug, verluste, eigenverbrauch = self.inverter.process_energy(
|
||||||
self.wechselrichter.energie_verarbeiten(erzeugung, verbrauch, stunde)
|
erzeugung, verbrauch, stunde
|
||||||
)
|
)
|
||||||
|
|
||||||
# AC PV Battery Charge
|
# AC PV Battery Charge
|
||||||
|
@ -9,7 +9,7 @@ from akkudoktoreos.core.coreabc import SingletonMixin
|
|||||||
from akkudoktoreos.devices.battery import PVAkku
|
from akkudoktoreos.devices.battery import PVAkku
|
||||||
from akkudoktoreos.devices.devicesabc import DevicesBase
|
from akkudoktoreos.devices.devicesabc import DevicesBase
|
||||||
from akkudoktoreos.devices.generic import HomeAppliance
|
from akkudoktoreos.devices.generic import HomeAppliance
|
||||||
from akkudoktoreos.devices.inverter import Wechselrichter
|
from akkudoktoreos.devices.inverter import Inverter
|
||||||
from akkudoktoreos.utils.datetimeutil import to_duration
|
from akkudoktoreos.utils.datetimeutil import to_duration
|
||||||
from akkudoktoreos.utils.logutil import get_logger
|
from akkudoktoreos.utils.logutil import get_logger
|
||||||
|
|
||||||
@ -111,10 +111,10 @@ class Devices(SingletonMixin, DevicesBase):
|
|||||||
kosten_euro_pro_stunde: Optional[NDArray[Shape["*"], float]] = Field(
|
kosten_euro_pro_stunde: Optional[NDArray[Shape["*"], float]] = Field(
|
||||||
default=None, description="The costs in euros per hour."
|
default=None, description="The costs in euros per hour."
|
||||||
)
|
)
|
||||||
netzbezug_wh_pro_stunde: Optional[NDArray[Shape["*"], float]] = Field(
|
grid_import_wh_pro_stunde: Optional[NDArray[Shape["*"], float]] = Field(
|
||||||
default=None, description="The grid energy drawn in watt-hours per hour."
|
default=None, description="The grid energy drawn in watt-hours per hour."
|
||||||
)
|
)
|
||||||
netzeinspeisung_wh_pro_stunde: Optional[NDArray[Shape["*"], float]] = Field(
|
grid_export_wh_pro_stunde: Optional[NDArray[Shape["*"], float]] = Field(
|
||||||
default=None, description="The energy fed into the grid in watt-hours per hour."
|
default=None, description="The energy fed into the grid in watt-hours per hour."
|
||||||
)
|
)
|
||||||
verluste_wh_pro_stunde: Optional[NDArray[Shape["*"], float]] = Field(
|
verluste_wh_pro_stunde: Optional[NDArray[Shape["*"], float]] = Field(
|
||||||
@ -162,9 +162,7 @@ class Devices(SingletonMixin, DevicesBase):
|
|||||||
akku: ClassVar[PVAkku] = PVAkku(provider_id="GenericBattery")
|
akku: ClassVar[PVAkku] = PVAkku(provider_id="GenericBattery")
|
||||||
eauto: ClassVar[PVAkku] = PVAkku(provider_id="GenericBEV")
|
eauto: ClassVar[PVAkku] = PVAkku(provider_id="GenericBEV")
|
||||||
home_appliance: ClassVar[HomeAppliance] = HomeAppliance(provider_id="GenericDishWasher")
|
home_appliance: ClassVar[HomeAppliance] = HomeAppliance(provider_id="GenericDishWasher")
|
||||||
wechselrichter: ClassVar[Wechselrichter] = Wechselrichter(
|
inverter: ClassVar[Inverter] = Inverter(akku=akku, provider_id="GenericInverter")
|
||||||
akku=akku, provider_id="GenericInverter"
|
|
||||||
)
|
|
||||||
|
|
||||||
def update_data(self) -> None:
|
def update_data(self) -> None:
|
||||||
"""Update device simulation data."""
|
"""Update device simulation data."""
|
||||||
@ -172,12 +170,12 @@ class Devices(SingletonMixin, DevicesBase):
|
|||||||
self.akku.setup()
|
self.akku.setup()
|
||||||
self.eauto.setup()
|
self.eauto.setup()
|
||||||
self.home_appliance.setup()
|
self.home_appliance.setup()
|
||||||
self.wechselrichter.setup()
|
self.inverter.setup()
|
||||||
|
|
||||||
# Pre-allocate arrays for the results, optimized for speed
|
# Pre-allocate arrays for the results, optimized for speed
|
||||||
self.last_wh_pro_stunde = np.full((self.total_hours), np.nan)
|
self.last_wh_pro_stunde = np.full((self.total_hours), np.nan)
|
||||||
self.netzeinspeisung_wh_pro_stunde = np.full((self.total_hours), np.nan)
|
self.grid_export_wh_pro_stunde = np.full((self.total_hours), np.nan)
|
||||||
self.netzbezug_wh_pro_stunde = np.full((self.total_hours), np.nan)
|
self.grid_import_wh_pro_stunde = np.full((self.total_hours), np.nan)
|
||||||
self.kosten_euro_pro_stunde = np.full((self.total_hours), np.nan)
|
self.kosten_euro_pro_stunde = np.full((self.total_hours), np.nan)
|
||||||
self.einnahmen_euro_pro_stunde = np.full((self.total_hours), np.nan)
|
self.einnahmen_euro_pro_stunde = np.full((self.total_hours), np.nan)
|
||||||
self.akku_soc_pro_stunde = np.full((self.total_hours), np.nan)
|
self.akku_soc_pro_stunde = np.full((self.total_hours), np.nan)
|
||||||
@ -219,60 +217,60 @@ class Devices(SingletonMixin, DevicesBase):
|
|||||||
einspeiseverguetung_euro_pro_wh_arr = np.full((self.total_hours), 0.078)
|
einspeiseverguetung_euro_pro_wh_arr = np.full((self.total_hours), 0.078)
|
||||||
|
|
||||||
for stunde_since_now in range(0, self.total_hours):
|
for stunde_since_now in range(0, self.total_hours):
|
||||||
stunde = self.start_datetime.hour + stunde_since_now
|
hour = self.start_datetime.hour + stunde_since_now
|
||||||
|
|
||||||
# Accumulate loads and PV generation
|
# Accumulate loads and PV generation
|
||||||
verbrauch = load_total_mean[stunde_since_now]
|
consumption = load_total_mean[stunde_since_now]
|
||||||
self.verluste_wh_pro_stunde[stunde_since_now] = 0.0
|
self.verluste_wh_pro_stunde[stunde_since_now] = 0.0
|
||||||
|
|
||||||
# Home appliances
|
# Home appliances
|
||||||
if self.home_appliance:
|
if self.home_appliance:
|
||||||
ha_load = self.home_appliance.get_load_for_hour(stunde)
|
ha_load = self.home_appliance.get_load_for_hour(hour)
|
||||||
verbrauch += ha_load
|
consumption += ha_load
|
||||||
self.home_appliance_wh_per_hour[stunde_since_now] = ha_load
|
self.home_appliance_wh_per_hour[stunde_since_now] = ha_load
|
||||||
|
|
||||||
# E-Auto handling
|
# E-Auto handling
|
||||||
if self.eauto:
|
if self.eauto:
|
||||||
if self.ev_charge_hours[stunde] > 0:
|
if self.ev_charge_hours[hour] > 0:
|
||||||
geladene_menge_eauto, verluste_eauto = self.eauto.energie_laden(
|
geladene_menge_eauto, verluste_eauto = self.eauto.energie_laden(
|
||||||
None, stunde, relative_power=self.ev_charge_hours[stunde]
|
None, hour, relative_power=self.ev_charge_hours[hour]
|
||||||
)
|
)
|
||||||
verbrauch += geladene_menge_eauto
|
consumption += geladene_menge_eauto
|
||||||
self.verluste_wh_pro_stunde[stunde_since_now] += verluste_eauto
|
self.verluste_wh_pro_stunde[stunde_since_now] += verluste_eauto
|
||||||
self.eauto_soc_pro_stunde[stunde_since_now] = self.eauto.ladezustand_in_prozent()
|
self.eauto_soc_pro_stunde[stunde_since_now] = self.eauto.ladezustand_in_prozent()
|
||||||
|
|
||||||
# Process inverter logic
|
# Process inverter logic
|
||||||
netzeinspeisung, netzbezug, verluste, eigenverbrauch = (0.0, 0.0, 0.0, 0.0)
|
grid_export, grid_import, losses, self_consumption = (0.0, 0.0, 0.0, 0.0)
|
||||||
if self.akku:
|
if self.akku:
|
||||||
self.akku.set_charge_allowed_for_hour(self.dc_charge_hours[stunde], stunde)
|
self.akku.set_charge_allowed_for_hour(self.dc_charge_hours[hour], hour)
|
||||||
if self.wechselrichter:
|
if self.inverter:
|
||||||
erzeugung = pvforecast_ac_power[stunde]
|
generation = pvforecast_ac_power[hour]
|
||||||
netzeinspeisung, netzbezug, verluste, eigenverbrauch = (
|
grid_export, grid_import, losses, self_consumption = self.inverter.process_energy(
|
||||||
self.wechselrichter.energie_verarbeiten(erzeugung, verbrauch, stunde)
|
generation, consumption, hour
|
||||||
)
|
)
|
||||||
|
|
||||||
# AC PV Battery Charge
|
# AC PV Battery Charge
|
||||||
if self.akku and self.ac_charge_hours[stunde] > 0.0:
|
if self.akku and self.ac_charge_hours[hour] > 0.0:
|
||||||
self.akku.set_charge_allowed_for_hour(1, stunde)
|
self.akku.set_charge_allowed_for_hour(1, hour)
|
||||||
geladene_menge, verluste_wh = self.akku.energie_laden(
|
geladene_menge, verluste_wh = self.akku.energie_laden(
|
||||||
None, stunde, relative_power=self.ac_charge_hours[stunde]
|
None, hour, relative_power=self.ac_charge_hours[hour]
|
||||||
)
|
)
|
||||||
# print(stunde, " ", geladene_menge, " ",self.ac_charge_hours[stunde]," ",self.akku.ladezustand_in_prozent())
|
# print(stunde, " ", geladene_menge, " ",self.ac_charge_hours[stunde]," ",self.akku.ladezustand_in_prozent())
|
||||||
verbrauch += geladene_menge
|
consumption += geladene_menge
|
||||||
netzbezug += geladene_menge
|
grid_import += geladene_menge
|
||||||
self.verluste_wh_pro_stunde[stunde_since_now] += verluste_wh
|
self.verluste_wh_pro_stunde[stunde_since_now] += verluste_wh
|
||||||
|
|
||||||
self.netzeinspeisung_wh_pro_stunde[stunde_since_now] = netzeinspeisung
|
self.grid_export_wh_pro_stunde[stunde_since_now] = grid_export
|
||||||
self.netzbezug_wh_pro_stunde[stunde_since_now] = netzbezug
|
self.grid_import_wh_pro_stunde[stunde_since_now] = grid_import
|
||||||
self.verluste_wh_pro_stunde[stunde_since_now] += verluste
|
self.verluste_wh_pro_stunde[stunde_since_now] += losses
|
||||||
self.last_wh_pro_stunde[stunde_since_now] = verbrauch
|
self.last_wh_pro_stunde[stunde_since_now] = consumption
|
||||||
|
|
||||||
# Financial calculations
|
# Financial calculations
|
||||||
self.kosten_euro_pro_stunde[stunde_since_now] = (
|
self.kosten_euro_pro_stunde[stunde_since_now] = (
|
||||||
netzbezug * self.strompreis_euro_pro_wh[stunde]
|
grid_import * self.strompreis_euro_pro_wh[hour]
|
||||||
)
|
)
|
||||||
self.einnahmen_euro_pro_stunde[stunde_since_now] = (
|
self.einnahmen_euro_pro_stunde[stunde_since_now] = (
|
||||||
netzeinspeisung * self.einspeiseverguetung_euro_pro_wh_arr[stunde]
|
grid_export * self.einspeiseverguetung_euro_pro_wh_arr[hour]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Akku SOC tracking
|
# Akku SOC tracking
|
||||||
@ -285,8 +283,8 @@ class Devices(SingletonMixin, DevicesBase):
|
|||||||
"""Provides devices simulation output as a dictionary."""
|
"""Provides devices simulation output as a dictionary."""
|
||||||
out: Dict[str, Optional[Union[np.ndarray, float]]] = {
|
out: Dict[str, Optional[Union[np.ndarray, float]]] = {
|
||||||
"Last_Wh_pro_Stunde": self.last_wh_pro_stunde,
|
"Last_Wh_pro_Stunde": self.last_wh_pro_stunde,
|
||||||
"Netzeinspeisung_Wh_pro_Stunde": self.netzeinspeisung_wh_pro_stunde,
|
"grid_export_Wh_pro_Stunde": self.grid_export_wh_pro_stunde,
|
||||||
"Netzbezug_Wh_pro_Stunde": self.netzbezug_wh_pro_stunde,
|
"grid_import_Wh_pro_Stunde": self.grid_import_wh_pro_stunde,
|
||||||
"Kosten_Euro_pro_Stunde": self.kosten_euro_pro_stunde,
|
"Kosten_Euro_pro_Stunde": self.kosten_euro_pro_stunde,
|
||||||
"akku_soc_pro_stunde": self.akku_soc_pro_stunde,
|
"akku_soc_pro_stunde": self.akku_soc_pro_stunde,
|
||||||
"Einnahmen_Euro_pro_Stunde": self.einnahmen_euro_pro_stunde,
|
"Einnahmen_Euro_pro_Stunde": self.einnahmen_euro_pro_stunde,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from typing import Optional
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
@ -9,14 +9,14 @@ from akkudoktoreos.utils.logutil import get_logger
|
|||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class WechselrichterParameters(BaseModel):
|
class InverterParameters(BaseModel):
|
||||||
max_leistung_wh: float = Field(default=10000, gt=0)
|
max_power_wh: float = Field(default=10000, gt=0)
|
||||||
|
|
||||||
|
|
||||||
class Wechselrichter(DeviceBase):
|
class Inverter(DeviceBase):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
parameters: Optional[WechselrichterParameters] = None,
|
parameters: Optional[InverterParameters] = None,
|
||||||
akku: Optional[PVAkku] = None,
|
akku: Optional[PVAkku] = None,
|
||||||
provider_id: Optional[str] = None,
|
provider_id: Optional[str] = None,
|
||||||
):
|
):
|
||||||
@ -45,69 +45,55 @@ class Wechselrichter(DeviceBase):
|
|||||||
return
|
return
|
||||||
if self.provider_id is not None:
|
if self.provider_id is not None:
|
||||||
# Setup by configuration
|
# Setup by configuration
|
||||||
self.max_leistung_wh = getattr(self.config, f"{self.prefix}_power_max")
|
self.max_power_wh = getattr(self.config, f"{self.prefix}_power_max")
|
||||||
elif self.parameters is not None:
|
elif self.parameters is not None:
|
||||||
# Setup by parameters
|
# Setup by parameters
|
||||||
self.max_leistung_wh = (
|
self.max_power_wh = (
|
||||||
self.parameters.max_leistung_wh # Maximum power that the inverter can handle
|
self.parameters.max_power_wh # Maximum power that the inverter can handle
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
error_msg = "Parameters and provider ID missing. Can't instantiate."
|
error_msg = "Parameters and provider ID missing. Can't instantiate."
|
||||||
logger.error(error_msg)
|
logger.error(error_msg)
|
||||||
raise ValueError(error_msg)
|
raise ValueError(error_msg)
|
||||||
|
|
||||||
def energie_verarbeiten(
|
def process_energy(
|
||||||
self, erzeugung: float, verbrauch: float, hour: int
|
self, generation: float, consumption: float, hour: int
|
||||||
) -> tuple[float, float, float, float]:
|
) -> Tuple[float, float, float, float]:
|
||||||
verluste = 0.0 # Losses during processing
|
losses = 0.0
|
||||||
netzeinspeisung = 0.0 # Grid feed-in
|
grid_export = 0.0
|
||||||
netzbezug = 0.0 # Grid draw
|
grid_import = 0.0
|
||||||
eigenverbrauch = 0.0 # Self-consumption
|
self_consumption = 0.0
|
||||||
|
|
||||||
if erzeugung >= verbrauch:
|
if generation >= consumption:
|
||||||
if verbrauch > self.max_leistung_wh:
|
# Case 1: Sufficient or excess generation
|
||||||
# If consumption exceeds maximum inverter power
|
actual_consumption = min(consumption, self.max_power_wh)
|
||||||
verluste += erzeugung - self.max_leistung_wh
|
remaining_energy = generation - actual_consumption
|
||||||
restleistung_nach_verbrauch = self.max_leistung_wh - verbrauch
|
|
||||||
netzbezug = -restleistung_nach_verbrauch # Negative indicates feeding into the grid
|
|
||||||
eigenverbrauch = self.max_leistung_wh
|
|
||||||
else:
|
|
||||||
# Remaining power after consumption
|
|
||||||
restleistung_nach_verbrauch = erzeugung - verbrauch
|
|
||||||
|
|
||||||
# Load battery with excess energy
|
# Charge battery with excess energy
|
||||||
geladene_energie, verluste_laden_akku = self.akku.energie_laden(
|
charged_energy, charging_losses = self.akku.energie_laden(remaining_energy, hour)
|
||||||
restleistung_nach_verbrauch, hour
|
losses += charging_losses
|
||||||
)
|
|
||||||
rest_überschuss = restleistung_nach_verbrauch - (
|
|
||||||
geladene_energie + verluste_laden_akku
|
|
||||||
)
|
|
||||||
|
|
||||||
# Feed-in to the grid based on remaining capacity
|
# Calculate remaining surplus after battery charge
|
||||||
if rest_überschuss > self.max_leistung_wh - verbrauch:
|
remaining_surplus = remaining_energy - (charged_energy + charging_losses)
|
||||||
netzeinspeisung = self.max_leistung_wh - verbrauch
|
grid_export = min(remaining_surplus, self.max_power_wh - actual_consumption)
|
||||||
verluste += rest_überschuss - netzeinspeisung
|
|
||||||
else:
|
|
||||||
netzeinspeisung = rest_überschuss
|
|
||||||
|
|
||||||
verluste += verluste_laden_akku
|
# If any remaining surplus can't be fed to the grid, count as losses
|
||||||
eigenverbrauch = verbrauch # Self-consumption is equal to the load
|
losses += max(remaining_surplus - grid_export, 0)
|
||||||
|
self_consumption = actual_consumption
|
||||||
|
|
||||||
else:
|
else:
|
||||||
benötigte_energie = verbrauch - erzeugung # Energy needed from external sources
|
# Case 2: Insufficient generation, cover shortfall
|
||||||
max_akku_leistung = self.akku.max_ladeleistung_w # Maximum battery discharge power
|
shortfall = consumption - generation
|
||||||
|
available_ac_power = max(self.max_power_wh - generation, 0)
|
||||||
|
|
||||||
# Calculate remaining AC power available
|
# Discharge battery to cover shortfall, if possible
|
||||||
rest_ac_leistung = max(self.max_leistung_wh - erzeugung, 0)
|
battery_discharge, discharge_losses = self.akku.energie_abgeben(
|
||||||
|
min(shortfall, available_ac_power), hour
|
||||||
|
)
|
||||||
|
losses += discharge_losses
|
||||||
|
|
||||||
# Discharge energy from the battery based on need
|
# Draw remaining required power from the grid (discharge_losses are already substraved in the battery)
|
||||||
if benötigte_energie < rest_ac_leistung:
|
grid_import = shortfall - battery_discharge
|
||||||
aus_akku, akku_entladeverluste = self.akku.energie_abgeben(benötigte_energie, hour)
|
self_consumption = generation + battery_discharge
|
||||||
else:
|
|
||||||
aus_akku, akku_entladeverluste = self.akku.energie_abgeben(rest_ac_leistung, hour)
|
|
||||||
|
|
||||||
verluste += akku_entladeverluste # Include losses from battery discharge
|
return grid_export, grid_import, losses, self_consumption
|
||||||
netzbezug = benötigte_energie - aus_akku # Energy drawn from the grid
|
|
||||||
eigenverbrauch = erzeugung + aus_akku # Total self-consumption
|
|
||||||
|
|
||||||
return netzeinspeisung, netzbezug, verluste, eigenverbrauch
|
|
||||||
|
@ -19,7 +19,7 @@ from akkudoktoreos.devices.battery import (
|
|||||||
PVAkkuParameters,
|
PVAkkuParameters,
|
||||||
)
|
)
|
||||||
from akkudoktoreos.devices.generic import HomeAppliance, HomeApplianceParameters
|
from akkudoktoreos.devices.generic import HomeAppliance, HomeApplianceParameters
|
||||||
from akkudoktoreos.devices.inverter import Wechselrichter, WechselrichterParameters
|
from akkudoktoreos.devices.inverter import Inverter, InverterParameters
|
||||||
from akkudoktoreos.utils.utils import NumpyEncoder
|
from akkudoktoreos.utils.utils import NumpyEncoder
|
||||||
from akkudoktoreos.visualize import visualisiere_ergebnisse
|
from akkudoktoreos.visualize import visualisiere_ergebnisse
|
||||||
|
|
||||||
@ -27,7 +27,7 @@ from akkudoktoreos.visualize import visualisiere_ergebnisse
|
|||||||
class OptimizationParameters(BaseModel):
|
class OptimizationParameters(BaseModel):
|
||||||
ems: EnergieManagementSystemParameters
|
ems: EnergieManagementSystemParameters
|
||||||
pv_akku: PVAkkuParameters
|
pv_akku: PVAkkuParameters
|
||||||
wechselrichter: WechselrichterParameters = WechselrichterParameters()
|
inverter: InverterParameters = InverterParameters()
|
||||||
eauto: Optional[EAutoParameters]
|
eauto: Optional[EAutoParameters]
|
||||||
dishwasher: Optional[HomeApplianceParameters] = None
|
dishwasher: Optional[HomeApplianceParameters] = None
|
||||||
temperature_forecast: Optional[list[float]] = Field(
|
temperature_forecast: Optional[list[float]] = Field(
|
||||||
@ -488,10 +488,9 @@ class optimization_problem(ConfigMixin, DevicesMixin, EnergyManagementSystemMixi
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Initialize the inverter and energy management system
|
# Initialize the inverter and energy management system
|
||||||
wr = Wechselrichter(parameters.wechselrichter, akku)
|
|
||||||
self.ems.set_parameters(
|
self.ems.set_parameters(
|
||||||
parameters.ems,
|
parameters.ems,
|
||||||
wechselrichter=wr,
|
inverter=Inverter(parameters.inverter, akku),
|
||||||
eauto=eauto,
|
eauto=eauto,
|
||||||
home_appliance=dishwasher,
|
home_appliance=dishwasher,
|
||||||
)
|
)
|
||||||
|
@ -10,7 +10,7 @@ from akkudoktoreos.core.ems import (
|
|||||||
)
|
)
|
||||||
from akkudoktoreos.devices.battery import EAutoParameters, PVAkku, PVAkkuParameters
|
from akkudoktoreos.devices.battery import EAutoParameters, PVAkku, PVAkkuParameters
|
||||||
from akkudoktoreos.devices.generic import HomeAppliance, HomeApplianceParameters
|
from akkudoktoreos.devices.generic import HomeAppliance, HomeApplianceParameters
|
||||||
from akkudoktoreos.devices.inverter import Wechselrichter, WechselrichterParameters
|
from akkudoktoreos.devices.inverter import Inverter, InverterParameters
|
||||||
|
|
||||||
start_hour = 1
|
start_hour = 1
|
||||||
|
|
||||||
@ -30,7 +30,7 @@ def create_ems_instance() -> EnergieManagementSystem:
|
|||||||
hours=config_eos.prediction_hours,
|
hours=config_eos.prediction_hours,
|
||||||
)
|
)
|
||||||
akku.reset()
|
akku.reset()
|
||||||
wechselrichter = Wechselrichter(WechselrichterParameters(max_leistung_wh=10000), akku)
|
inverter = Inverter(InverterParameters(max_power_wh=10000), akku)
|
||||||
|
|
||||||
# Household device (currently not used, set to None)
|
# Household device (currently not used, set to None)
|
||||||
home_appliance = HomeAppliance(
|
home_appliance = HomeAppliance(
|
||||||
@ -216,7 +216,7 @@ def create_ems_instance() -> EnergieManagementSystem:
|
|||||||
preis_euro_pro_wh_akku=preis_euro_pro_wh_akku,
|
preis_euro_pro_wh_akku=preis_euro_pro_wh_akku,
|
||||||
gesamtlast=gesamtlast,
|
gesamtlast=gesamtlast,
|
||||||
),
|
),
|
||||||
wechselrichter=wechselrichter,
|
inverter=inverter,
|
||||||
eauto=eauto,
|
eauto=eauto,
|
||||||
home_appliance=home_appliance,
|
home_appliance=home_appliance,
|
||||||
)
|
)
|
||||||
|
@ -9,7 +9,7 @@ from akkudoktoreos.core.ems import (
|
|||||||
)
|
)
|
||||||
from akkudoktoreos.devices.battery import EAutoParameters, PVAkku, PVAkkuParameters
|
from akkudoktoreos.devices.battery import EAutoParameters, PVAkku, PVAkkuParameters
|
||||||
from akkudoktoreos.devices.generic import HomeAppliance, HomeApplianceParameters
|
from akkudoktoreos.devices.generic import HomeAppliance, HomeApplianceParameters
|
||||||
from akkudoktoreos.devices.inverter import Wechselrichter, WechselrichterParameters
|
from akkudoktoreos.devices.inverter import Inverter, InverterParameters
|
||||||
|
|
||||||
start_hour = 0
|
start_hour = 0
|
||||||
|
|
||||||
@ -29,7 +29,7 @@ def create_ems_instance() -> EnergieManagementSystem:
|
|||||||
hours=config_eos.prediction_hours,
|
hours=config_eos.prediction_hours,
|
||||||
)
|
)
|
||||||
akku.reset()
|
akku.reset()
|
||||||
wechselrichter = Wechselrichter(WechselrichterParameters(max_leistung_wh=10000), akku)
|
inverter = Inverter(InverterParameters(max_power_wh=10000), akku)
|
||||||
|
|
||||||
# Household device (currently not used, set to None)
|
# Household device (currently not used, set to None)
|
||||||
home_appliance = HomeAppliance(
|
home_appliance = HomeAppliance(
|
||||||
@ -121,7 +121,7 @@ def create_ems_instance() -> EnergieManagementSystem:
|
|||||||
preis_euro_pro_wh_akku=preis_euro_pro_wh_akku,
|
preis_euro_pro_wh_akku=preis_euro_pro_wh_akku,
|
||||||
gesamtlast=gesamtlast,
|
gesamtlast=gesamtlast,
|
||||||
),
|
),
|
||||||
wechselrichter=wechselrichter,
|
inverter=inverter,
|
||||||
eauto=eauto,
|
eauto=eauto,
|
||||||
home_appliance=home_appliance,
|
home_appliance=home_appliance,
|
||||||
)
|
)
|
||||||
|
262
tests/test_inverter.py
Normal file
262
tests/test_inverter.py
Normal file
@ -0,0 +1,262 @@
|
|||||||
|
from unittest.mock import Mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from akkudoktoreos.devices.inverter import Inverter, InverterParameters
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_battery():
|
||||||
|
mock_battery = Mock()
|
||||||
|
mock_battery.energie_laden = Mock(return_value=(0.0, 0.0))
|
||||||
|
mock_battery.energie_abgeben = Mock(return_value=(0.0, 0.0))
|
||||||
|
return mock_battery
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def inverter(mock_battery):
|
||||||
|
return Inverter(InverterParameters(max_power_wh=500.0), akku=mock_battery)
|
||||||
|
|
||||||
|
|
||||||
|
def test_process_energy_excess_generation(inverter, mock_battery):
|
||||||
|
# Battery charges 100 Wh with 10 Wh loss
|
||||||
|
mock_battery.energie_laden.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.energie_laden.assert_called_once_with(400.0, hour)
|
||||||
|
|
||||||
|
|
||||||
|
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.energie_laden.assert_called_once_with(0.0, hour)
|
||||||
|
|
||||||
|
|
||||||
|
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.energie_abgeben.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.energie_abgeben.assert_called_once_with(150.0, hour)
|
||||||
|
|
||||||
|
|
||||||
|
def test_process_energy_battery_empty(inverter, mock_battery):
|
||||||
|
# Battery is empty, so no energy can be discharged
|
||||||
|
mock_battery.energie_abgeben.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.energie_abgeben.assert_called_once_with(200.0, hour)
|
||||||
|
|
||||||
|
|
||||||
|
def test_process_energy_battery_full_at_start(inverter, mock_battery):
|
||||||
|
# Battery is full, so no charging happens
|
||||||
|
mock_battery.energie_laden.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.energie_laden.assert_called_once_with(300.0, hour)
|
||||||
|
|
||||||
|
|
||||||
|
def test_process_energy_insufficient_generation_no_battery(inverter, mock_battery):
|
||||||
|
# Insufficient generation and no battery discharge
|
||||||
|
mock_battery.energie_abgeben.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.energie_abgeben.assert_called_once_with(400.0, hour)
|
||||||
|
|
||||||
|
|
||||||
|
def test_process_energy_insufficient_generation_battery_assists(inverter, mock_battery):
|
||||||
|
# Battery assists with some discharge to cover the shortfall
|
||||||
|
mock_battery.energie_abgeben.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.energie_abgeben.assert_called_once_with(200.0, hour)
|
||||||
|
|
||||||
|
|
||||||
|
def test_process_energy_zero_generation(inverter, mock_battery):
|
||||||
|
# Zero generation, full reliance on battery and grid
|
||||||
|
mock_battery.energie_abgeben.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.energie_abgeben.assert_called_once_with(300.0, hour)
|
||||||
|
|
||||||
|
|
||||||
|
def test_process_energy_zero_consumption(inverter, mock_battery):
|
||||||
|
# Generation exceeds consumption, but consumption is zero
|
||||||
|
mock_battery.energie_laden.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.energie_laden.assert_called_once_with(500.0, hour)
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def test_process_energy_partial_battery_discharge(inverter, mock_battery):
|
||||||
|
mock_battery.energie_abgeben.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
|
||||||
|
|
||||||
|
|
||||||
|
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.energie_abgeben.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.energie_abgeben.assert_called_once_with(400.0, hour)
|
||||||
|
|
||||||
|
|
||||||
|
def test_process_energy_zero_generation_full_battery_high_consumption(inverter, mock_battery):
|
||||||
|
# Full battery, no generation, and high consumption
|
||||||
|
mock_battery.energie_abgeben.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.energie_abgeben.assert_called_once_with(500.0, hour)
|
6
tests/testdata/optimize_input_1.json
vendored
6
tests/testdata/optimize_input_1.json
vendored
@ -31,8 +31,8 @@
|
|||||||
"start_soc_prozent": 80,
|
"start_soc_prozent": 80,
|
||||||
"min_soc_prozent": 15
|
"min_soc_prozent": 15
|
||||||
},
|
},
|
||||||
"wechselrichter": {
|
"inverter": {
|
||||||
"max_leistung_wh": 10000
|
"max_power_wh": 10000
|
||||||
},
|
},
|
||||||
"eauto": {
|
"eauto": {
|
||||||
"kapazitaet_wh": 60000,
|
"kapazitaet_wh": 60000,
|
||||||
@ -55,4 +55,4 @@
|
|||||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user