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": {
"version": "0.2.0.dev70048701",
"version": "0.2.0.dev81043823",
"data_folder_path": null,
"data_output_subpath": "output",
"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) (°) |
| 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. |
| 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 -->
@@ -28,7 +28,7 @@
```json
{
"general": {
"version": "0.2.0.dev70048701",
"version": "0.2.0.dev81043823",
"data_folder_path": null,
"data_output_subpath": "output",
"latitude": 52.52,
@@ -46,7 +46,7 @@
```json
{
"general": {
"version": "0.2.0.dev70048701",
"version": "0.2.0.dev81043823",
"data_folder_path": null,
"data_output_subpath": "output",
"latitude": 52.52,

View File

@@ -1,6 +1,6 @@
# Akkudoktor-EOS
**Version**: `v0.2.0.dev70048701`
**Version**: `v0.2.0.dev81043823`
<!-- 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.

View File

@@ -3,7 +3,7 @@
"info": {
"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.",
"version": "v0.2.0.dev70048701"
"version": "v0.2.0.dev81043823"
},
"paths": {
"/v1/admin/cache/clear": {
@@ -2525,7 +2525,7 @@
"general": {
"$ref": "#/components/schemas/GeneralSettings-Output",
"default": {
"version": "0.2.0.dev70048701",
"version": "0.2.0.dev81043823",
"data_output_subpath": "output",
"latitude": 52.52,
"longitude": 13.405,
@@ -4272,7 +4272,7 @@
"type": "string",
"title": "Version",
"description": "Configuration file version. Used to check compatibility.",
"default": "0.2.0.dev70048701"
"default": "0.2.0.dev81043823"
},
"data_folder_path": {
"anyOf": [
@@ -4346,7 +4346,7 @@
"type": "string",
"title": "Version",
"description": "Configuration file version. Used to check compatibility.",
"default": "0.2.0.dev70048701"
"default": "0.2.0.dev81043823"
},
"data_folder_path": {
"anyOf": [

View File

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

View File

@@ -124,23 +124,23 @@ class LoadAkkudoktorAdjusted(LoadAkkudoktor):
compare_end = self.measurement.max_datetime
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,
end_datetime=compare_end,
interval=compare_interval,
)
compare_dt = compare_start
for i in range(len(load_total_array)):
load_total = load_total_array[i]
for i in range(len(load_total_kwh_array)):
load_total_wh = load_total_kwh_array[i] * 1000
# Extract mean (index 0) and standard deviation (index 1) for the given day and hour
# Day indexing starts at 0, -1 because of that
hourly_stats = data_year_energy[compare_dt.day_of_year - 1, :, compare_dt.hour]
weight = 1 / ((compare_end - compare_dt).days + 1)
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
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
compare_dt += compare_interval
# Calculate mean

View File

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

View File

@@ -217,6 +217,7 @@ class TestMeasurement:
@pytest.fixture
def measurement_eos(self, config_eos):
"""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"]
measurement = get_measurement()
record0 = MeasurementDataRecord(
@@ -365,35 +366,35 @@ class TestMeasurement:
with pytest.raises(ValueError, match="interval must be positive"):
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."""
start = datetime(2023, 1, 1, 0)
end = datetime(2023, 1, 1, 2)
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 = np.array([100, 100]) # Differences between consecutive meter readings
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."""
measurement_eos.records = []
start = datetime(2023, 1, 1, 0)
end = datetime(2023, 1, 1, 3)
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
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."""
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
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
np.testing.assert_array_equal(result, expected)