fix: load prediction adjustment with measurement in kwh (#826)

Use load energy meter reading in kWh for load prediction adjustment.
Before the reading was falsely regarded to be in Wh.

Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
This commit is contained in:
Bobby Noelte
2026-01-01 12:26:29 +01:00
committed by GitHub
parent 4434b7109e
commit 39973bf836
8 changed files with 33 additions and 30 deletions

View File

@@ -136,7 +136,7 @@
} }
}, },
"general": { "general": {
"version": "0.2.0.dev70048701", "version": "0.2.0.dev81043823",
"data_folder_path": null, "data_folder_path": null,
"data_output_subpath": "output", "data_output_subpath": "output",
"latitude": 52.52, "latitude": 52.52,

View File

@@ -16,7 +16,7 @@
| latitude | `EOS_GENERAL__LATITUDE` | `Optional[float]` | `rw` | `52.52` | Latitude in decimal degrees between -90 and 90. North is positive (ISO 19115) (°) | | latitude | `EOS_GENERAL__LATITUDE` | `Optional[float]` | `rw` | `52.52` | Latitude in decimal degrees between -90 and 90. North is positive (ISO 19115) (°) |
| longitude | `EOS_GENERAL__LONGITUDE` | `Optional[float]` | `rw` | `13.405` | Longitude in decimal degrees within -180 to 180 (°) | | longitude | `EOS_GENERAL__LONGITUDE` | `Optional[float]` | `rw` | `13.405` | Longitude in decimal degrees within -180 to 180 (°) |
| timezone | | `Optional[str]` | `ro` | `N/A` | Computed timezone based on latitude and longitude. | | timezone | | `Optional[str]` | `ro` | `N/A` | Computed timezone based on latitude and longitude. |
| version | `EOS_GENERAL__VERSION` | `str` | `rw` | `0.2.0.dev70048701` | Configuration file version. Used to check compatibility. | | version | `EOS_GENERAL__VERSION` | `str` | `rw` | `0.2.0.dev81043823` | Configuration file version. Used to check compatibility. |
::: :::
<!-- pyml enable line-length --> <!-- pyml enable line-length -->
@@ -28,7 +28,7 @@
```json ```json
{ {
"general": { "general": {
"version": "0.2.0.dev70048701", "version": "0.2.0.dev81043823",
"data_folder_path": null, "data_folder_path": null,
"data_output_subpath": "output", "data_output_subpath": "output",
"latitude": 52.52, "latitude": 52.52,
@@ -46,7 +46,7 @@
```json ```json
{ {
"general": { "general": {
"version": "0.2.0.dev70048701", "version": "0.2.0.dev81043823",
"data_folder_path": null, "data_folder_path": null,
"data_output_subpath": "output", "data_output_subpath": "output",
"latitude": 52.52, "latitude": 52.52,

View File

@@ -1,6 +1,6 @@
# Akkudoktor-EOS # Akkudoktor-EOS
**Version**: `v0.2.0.dev70048701` **Version**: `v0.2.0.dev81043823`
<!-- pyml disable line-length --> <!-- pyml disable line-length -->
**Description**: This project provides a comprehensive solution for simulating and optimizing an energy system based on renewable energy sources. With a focus on photovoltaic (PV) systems, battery storage (batteries), load management (consumer requirements), heat pumps, electric vehicles, and consideration of electricity price data, this system enables forecasting and optimization of energy flow and costs over a specified period. **Description**: This project provides a comprehensive solution for simulating and optimizing an energy system based on renewable energy sources. With a focus on photovoltaic (PV) systems, battery storage (batteries), load management (consumer requirements), heat pumps, electric vehicles, and consideration of electricity price data, this system enables forecasting and optimization of energy flow and costs over a specified period.

View File

@@ -3,7 +3,7 @@
"info": { "info": {
"title": "Akkudoktor-EOS", "title": "Akkudoktor-EOS",
"description": "This project provides a comprehensive solution for simulating and optimizing an energy system based on renewable energy sources. With a focus on photovoltaic (PV) systems, battery storage (batteries), load management (consumer requirements), heat pumps, electric vehicles, and consideration of electricity price data, this system enables forecasting and optimization of energy flow and costs over a specified period.", "description": "This project provides a comprehensive solution for simulating and optimizing an energy system based on renewable energy sources. With a focus on photovoltaic (PV) systems, battery storage (batteries), load management (consumer requirements), heat pumps, electric vehicles, and consideration of electricity price data, this system enables forecasting and optimization of energy flow and costs over a specified period.",
"version": "v0.2.0.dev70048701" "version": "v0.2.0.dev81043823"
}, },
"paths": { "paths": {
"/v1/admin/cache/clear": { "/v1/admin/cache/clear": {
@@ -2525,7 +2525,7 @@
"general": { "general": {
"$ref": "#/components/schemas/GeneralSettings-Output", "$ref": "#/components/schemas/GeneralSettings-Output",
"default": { "default": {
"version": "0.2.0.dev70048701", "version": "0.2.0.dev81043823",
"data_output_subpath": "output", "data_output_subpath": "output",
"latitude": 52.52, "latitude": 52.52,
"longitude": 13.405, "longitude": 13.405,
@@ -4272,7 +4272,7 @@
"type": "string", "type": "string",
"title": "Version", "title": "Version",
"description": "Configuration file version. Used to check compatibility.", "description": "Configuration file version. Used to check compatibility.",
"default": "0.2.0.dev70048701" "default": "0.2.0.dev81043823"
}, },
"data_folder_path": { "data_folder_path": {
"anyOf": [ "anyOf": [
@@ -4346,7 +4346,7 @@
"type": "string", "type": "string",
"title": "Version", "title": "Version",
"description": "Configuration file version. Used to check compatibility.", "description": "Configuration file version. Used to check compatibility.",
"default": "0.2.0.dev70048701" "default": "0.2.0.dev81043823"
}, },
"data_folder_path": { "data_folder_path": {
"anyOf": [ "anyOf": [

View File

@@ -176,7 +176,7 @@ class Measurement(SingletonMixin, DataImportMixin, DataSequence):
logger.debug(debug_msg) logger.debug(debug_msg)
return energy_array return energy_array
def load_total( def load_total_kwh(
self, self,
start_datetime: Optional[DateTime] = None, start_datetime: Optional[DateTime] = None,
end_datetime: Optional[DateTime] = None, end_datetime: Optional[DateTime] = None,
@@ -207,7 +207,7 @@ class Measurement(SingletonMixin, DataImportMixin, DataSequence):
if end_datetime is None: if end_datetime is None:
end_datetime = self[-1].date_time end_datetime = self[-1].date_time
size = self._interval_count(start_datetime, end_datetime, interval) size = self._interval_count(start_datetime, end_datetime, interval)
load_total_array = np.zeros(size) load_total_kwh_array = np.zeros(size)
# Loop through all loads # Loop through all loads
if isinstance(self.config.measurement.load_emr_keys, list): if isinstance(self.config.measurement.load_emr_keys, list):
for key in self.config.measurement.load_emr_keys: for key in self.config.measurement.load_emr_keys:
@@ -219,11 +219,11 @@ class Measurement(SingletonMixin, DataImportMixin, DataSequence):
interval=interval, interval=interval,
) )
# Add calculated load to total load # Add calculated load to total load
load_total_array += load_array load_total_kwh_array += load_array
debug_msg = f"Total load '{key}' calculation: {load_total_array}" debug_msg = f"Total load '{key}' calculation: {load_total_kwh_array}"
logger.debug(debug_msg) logger.debug(debug_msg)
return load_total_array return load_total_kwh_array
def get_measurement() -> Measurement: def get_measurement() -> Measurement:

View File

@@ -124,23 +124,23 @@ class LoadAkkudoktorAdjusted(LoadAkkudoktor):
compare_end = self.measurement.max_datetime compare_end = self.measurement.max_datetime
compare_interval = to_duration("1 hour") compare_interval = to_duration("1 hour")
load_total_array = self.measurement.load_total( load_total_kwh_array = self.measurement.load_total_kwh(
start_datetime=compare_start, start_datetime=compare_start,
end_datetime=compare_end, end_datetime=compare_end,
interval=compare_interval, interval=compare_interval,
) )
compare_dt = compare_start compare_dt = compare_start
for i in range(len(load_total_array)): for i in range(len(load_total_kwh_array)):
load_total = load_total_array[i] load_total_wh = load_total_kwh_array[i] * 1000
# Extract mean (index 0) and standard deviation (index 1) for the given day and hour # Extract mean (index 0) and standard deviation (index 1) for the given day and hour
# Day indexing starts at 0, -1 because of that # Day indexing starts at 0, -1 because of that
hourly_stats = data_year_energy[compare_dt.day_of_year - 1, :, compare_dt.hour] hourly_stats = data_year_energy[compare_dt.day_of_year - 1, :, compare_dt.hour]
weight = 1 / ((compare_end - compare_dt).days + 1) weight = 1 / ((compare_end - compare_dt).days + 1)
if compare_dt.day_of_week < 5: if compare_dt.day_of_week < 5:
weekday_adjust[compare_dt.hour] += (load_total - hourly_stats[0]) * weight weekday_adjust[compare_dt.hour] += (load_total_wh - hourly_stats[0]) * weight
weekday_adjust_weight[compare_dt.hour] += weight weekday_adjust_weight[compare_dt.hour] += weight
else: else:
weekend_adjust[compare_dt.hour] += (load_total - hourly_stats[0]) * weight weekend_adjust[compare_dt.hour] += (load_total_wh - hourly_stats[0]) * weight
weekend_adjust_weight[compare_dt.hour] += weight weekend_adjust_weight[compare_dt.hour] += weight
compare_dt += compare_interval compare_dt += compare_interval
# Calculate mean # Calculate mean

View File

@@ -56,9 +56,10 @@ def loadakkudoktoradjusted(config_eos):
@pytest.fixture @pytest.fixture
def measurement_eos(): def measurement_eos():
"""Fixture to initialise the Measurement instance.""" """Fixture to initialise the Measurement instance."""
# Load meter readings are in kWh
measurement = get_measurement() measurement = get_measurement()
load0_mr = 500 load0_mr = 500.0
load1_mr = 500 load1_mr = 500.0
dt = to_datetime("2024-01-01T00:00:00") dt = to_datetime("2024-01-01T00:00:00")
interval = to_duration("1 hour") interval = to_duration("1 hour")
for i in range(25): for i in range(25):
@@ -70,8 +71,9 @@ def measurement_eos():
) )
) )
dt += interval dt += interval
load0_mr += 50 # 0.05 kWh = 50 Wh
load1_mr += 50 load0_mr += 0.05
load1_mr += 0.05
assert compare_datetimes(measurement.min_datetime, to_datetime("2024-01-01T00:00:00")).equal assert compare_datetimes(measurement.min_datetime, to_datetime("2024-01-01T00:00:00")).equal
assert compare_datetimes(measurement.max_datetime, to_datetime("2024-01-02T00:00:00")).equal assert compare_datetimes(measurement.max_datetime, to_datetime("2024-01-02T00:00:00")).equal
return measurement return measurement
@@ -187,7 +189,7 @@ def test_calculate_adjustment(loadakkudoktoradjusted, measurement_eos):
100.0, 100.0,
] ]
) )
np.testing.assert_array_equal(weekday_adjust, expected) np.testing.assert_allclose(weekday_adjust, expected)
assert weekend_adjust.shape == (24,) assert weekend_adjust.shape == (24,)
expected = np.array( expected = np.array(

View File

@@ -217,6 +217,7 @@ class TestMeasurement:
@pytest.fixture @pytest.fixture
def measurement_eos(self, config_eos): def measurement_eos(self, config_eos):
"""Fixture to create a Measurement instance.""" """Fixture to create a Measurement instance."""
# Load meter readings are in kWh
config_eos.measurement.load_emr_keys = ["load0_mr", "load1_mr", "load2_mr", "load3_mr"] config_eos.measurement.load_emr_keys = ["load0_mr", "load1_mr", "load2_mr", "load3_mr"]
measurement = get_measurement() measurement = get_measurement()
record0 = MeasurementDataRecord( record0 = MeasurementDataRecord(
@@ -365,35 +366,35 @@ class TestMeasurement:
with pytest.raises(ValueError, match="interval must be positive"): with pytest.raises(ValueError, match="interval must be positive"):
measurement_eos._energy_from_meter_readings(key, start_datetime, end_datetime, interval) measurement_eos._energy_from_meter_readings(key, start_datetime, end_datetime, interval)
def test_load_total(self, measurement_eos): def test_load_total_kwh(self, measurement_eos):
"""Test total load calculation.""" """Test total load calculation."""
start = datetime(2023, 1, 1, 0) start = datetime(2023, 1, 1, 0)
end = datetime(2023, 1, 1, 2) end = datetime(2023, 1, 1, 2)
interval = duration(hours=1) interval = duration(hours=1)
result = measurement_eos.load_total(start_datetime=start, end_datetime=end, interval=interval) result = measurement_eos.load_total_kwh(start_datetime=start, end_datetime=end, interval=interval)
# Expected total load per interval # Expected total load per interval
expected = np.array([100, 100]) # Differences between consecutive meter readings expected = np.array([100, 100]) # Differences between consecutive meter readings
np.testing.assert_array_equal(result, expected) np.testing.assert_array_equal(result, expected)
def test_load_total_no_data(self, measurement_eos): def test_load_total_kwh_no_data(self, measurement_eos):
"""Test total load calculation with no data.""" """Test total load calculation with no data."""
measurement_eos.records = [] measurement_eos.records = []
start = datetime(2023, 1, 1, 0) start = datetime(2023, 1, 1, 0)
end = datetime(2023, 1, 1, 3) end = datetime(2023, 1, 1, 3)
interval = duration(hours=1) interval = duration(hours=1)
result = measurement_eos.load_total(start_datetime=start, end_datetime=end, interval=interval) result = measurement_eos.load_total_kwh(start_datetime=start, end_datetime=end, interval=interval)
expected = np.zeros(3) # No data, so all intervals are zero expected = np.zeros(3) # No data, so all intervals are zero
np.testing.assert_array_equal(result, expected) np.testing.assert_array_equal(result, expected)
def test_load_total_partial_intervals(self, measurement_eos): def test_load_total_kwh_partial_intervals(self, measurement_eos):
"""Test total load calculation with partial intervals.""" """Test total load calculation with partial intervals."""
start = datetime(2023, 1, 1, 0, 30) # Start in the middle of an interval start = datetime(2023, 1, 1, 0, 30) # Start in the middle of an interval
end = datetime(2023, 1, 1, 1, 30) # End in the middle of another interval end = datetime(2023, 1, 1, 1, 30) # End in the middle of another interval
interval = duration(hours=1) interval = duration(hours=1)
result = measurement_eos.load_total(start_datetime=start, end_datetime=end, interval=interval) result = measurement_eos.load_total_kwh(start_datetime=start, end_datetime=end, interval=interval)
expected = np.array([100]) # Only one complete interval covered expected = np.array([100]) # Only one complete interval covered
np.testing.assert_array_equal(result, expected) np.testing.assert_array_equal(result, expected)