2024-12-15 14:40:03 +01:00
|
|
|
import sys
|
|
|
|
from pathlib import Path
|
|
|
|
from unittest.mock import Mock, patch
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
from akkudoktoreos.core.ems import get_ems
|
2025-01-26 22:42:54 +01:00
|
|
|
from akkudoktoreos.core.logging import get_logger
|
2024-12-15 14:40:03 +01:00
|
|
|
from akkudoktoreos.prediction.prediction import get_prediction
|
|
|
|
from akkudoktoreos.prediction.pvforecastakkudoktor import (
|
|
|
|
AkkudoktorForecastHorizon,
|
|
|
|
AkkudoktorForecastMeta,
|
|
|
|
AkkudoktorForecastValue,
|
|
|
|
PVForecastAkkudoktor,
|
|
|
|
PVForecastAkkudoktorDataRecord,
|
|
|
|
)
|
|
|
|
from akkudoktoreos.utils.datetimeutil import compare_datetimes, to_datetime, to_duration
|
|
|
|
|
|
|
|
DIR_TESTDATA = Path(__file__).absolute().parent.joinpath("testdata")
|
|
|
|
|
|
|
|
FILE_TESTDATA_PV_FORECAST_INPUT_1 = DIR_TESTDATA.joinpath("pv_forecast_input_1.json")
|
|
|
|
FILE_TESTDATA_PV_FORECAST_RESULT_1 = DIR_TESTDATA.joinpath("pv_forecast_result_1.txt")
|
|
|
|
|
2025-01-26 22:42:54 +01:00
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
2024-12-15 14:40:03 +01:00
|
|
|
|
|
|
|
@pytest.fixture
|
2024-12-30 13:41:39 +01:00
|
|
|
def sample_settings(config_eos):
|
2024-12-15 14:40:03 +01:00
|
|
|
"""Fixture that adds settings data to the global config."""
|
|
|
|
settings = {
|
2025-01-20 22:58:59 +01:00
|
|
|
"general": {
|
|
|
|
"latitude": 52.52,
|
|
|
|
"longitude": 13.405,
|
|
|
|
},
|
2025-01-12 05:19:37 +01:00
|
|
|
"prediction": {
|
2025-01-18 14:26:34 +01:00
|
|
|
"hours": 48,
|
|
|
|
"historic_hours": 24,
|
2025-01-12 05:19:37 +01:00
|
|
|
},
|
|
|
|
"pvforecast": {
|
2025-01-18 14:26:34 +01:00
|
|
|
"provider": "PVForecastAkkudoktor",
|
2025-01-19 18:12:50 +01:00
|
|
|
"planes": [
|
|
|
|
{
|
|
|
|
"peakpower": 5.0,
|
|
|
|
"surface_azimuth": -10,
|
|
|
|
"surface_tilt": 7,
|
|
|
|
"userhorizon": [20, 27, 22, 20],
|
|
|
|
"inverter_paco": 10000,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"peakpower": 4.8,
|
|
|
|
"surface_azimuth": -90,
|
|
|
|
"surface_tilt": 7,
|
|
|
|
"userhorizon": [30, 30, 30, 50],
|
|
|
|
"inverter_paco": 10000,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"peakpower": 1.4,
|
|
|
|
"surface_azimuth": -40,
|
|
|
|
"surface_tilt": 60,
|
|
|
|
"userhorizon": [60, 30, 0, 30],
|
|
|
|
"inverter_paco": 2000,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"peakpower": 1.6,
|
|
|
|
"surface_azimuth": 5,
|
|
|
|
"surface_tilt": 45,
|
|
|
|
"userhorizon": [45, 25, 30, 60],
|
|
|
|
"inverter_paco": 1400,
|
|
|
|
},
|
|
|
|
],
|
2025-01-12 05:19:37 +01:00
|
|
|
},
|
2024-12-15 14:40:03 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
# Merge settings to config
|
|
|
|
config_eos.merge_settings_from_dict(settings)
|
2025-01-18 14:26:34 +01:00
|
|
|
assert config_eos.pvforecast.provider == "PVForecastAkkudoktor"
|
2024-12-15 14:40:03 +01:00
|
|
|
return config_eos
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
def sample_forecast_data():
|
|
|
|
"""Fixture that returns sample forecast data converted to pydantic model."""
|
2025-02-12 21:35:51 +01:00
|
|
|
with FILE_TESTDATA_PV_FORECAST_INPUT_1.open("r", encoding="utf-8", newline=None) as f_in:
|
2024-12-15 14:40:03 +01:00
|
|
|
input_data = f_in.read()
|
|
|
|
return PVForecastAkkudoktor._validate_data(input_data)
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
def sample_forecast_data_raw():
|
|
|
|
"""Fixture that returns raw sample forecast data."""
|
2025-02-12 21:35:51 +01:00
|
|
|
with FILE_TESTDATA_PV_FORECAST_INPUT_1.open("r", encoding="utf-8", newline=None) as f_in:
|
2024-12-15 14:40:03 +01:00
|
|
|
input_data = f_in.read()
|
|
|
|
return input_data
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
def sample_forecast_report():
|
|
|
|
"""Fixture that returns sample forecast data report."""
|
2025-02-12 21:35:51 +01:00
|
|
|
with FILE_TESTDATA_PV_FORECAST_RESULT_1.open("r", encoding="utf-8", newline=None) as f_res:
|
2024-12-15 14:40:03 +01:00
|
|
|
input_data = f_res.read()
|
|
|
|
return input_data
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
def sample_forecast_start(sample_forecast_data):
|
|
|
|
"""Fixture that returns the start date of the sample forecast data."""
|
|
|
|
forecast_start = to_datetime(sample_forecast_data.values[0][0].datetime)
|
|
|
|
expected_datetime = to_datetime("2024-10-06T00:00:00.000+02:00")
|
|
|
|
assert compare_datetimes(to_datetime(forecast_start), expected_datetime).equal
|
|
|
|
|
|
|
|
timezone_name = sample_forecast_data.meta.timezone
|
|
|
|
assert timezone_name == "Europe/Berlin"
|
|
|
|
return forecast_start
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
def provider():
|
|
|
|
"""Fixture that returns the PVForecastAkkudoktor instance from the prediction."""
|
|
|
|
prediction = get_prediction()
|
|
|
|
provider = prediction.provider_by_id("PVForecastAkkudoktor")
|
|
|
|
assert isinstance(provider, PVForecastAkkudoktor)
|
|
|
|
return provider
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
def provider_empty_instance():
|
|
|
|
"""Fixture that returns an empty instance of PVForecast."""
|
|
|
|
empty_instance = PVForecastAkkudoktor()
|
|
|
|
empty_instance.clear()
|
|
|
|
assert len(empty_instance) == 0
|
|
|
|
return empty_instance
|
|
|
|
|
|
|
|
|
|
|
|
# Sample data for testing
|
|
|
|
sample_horizon = AkkudoktorForecastHorizon(altitude=30, azimuthFrom=90, azimuthTo=180)
|
|
|
|
sample_meta = AkkudoktorForecastMeta(
|
|
|
|
lat=52.52,
|
|
|
|
lon=13.405,
|
|
|
|
power=[5000],
|
|
|
|
azimuth=[180],
|
|
|
|
tilt=[30],
|
|
|
|
timezone="Europe/Berlin",
|
|
|
|
albedo=0.25,
|
|
|
|
past_days=5,
|
|
|
|
inverterEfficiency=0.8,
|
|
|
|
powerInverter=[10000],
|
|
|
|
cellCoEff=-0.36,
|
|
|
|
range=True,
|
|
|
|
horizont=[[sample_horizon]],
|
|
|
|
horizontString=["sample_horizon"],
|
|
|
|
)
|
|
|
|
sample_value = AkkudoktorForecastValue(
|
|
|
|
datetime="2024-11-09T12:00:00",
|
|
|
|
dcPower=500.0,
|
|
|
|
power=480.0,
|
|
|
|
sunTilt=30.0,
|
|
|
|
sunAzimuth=180.0,
|
|
|
|
temperature=15.0,
|
|
|
|
relativehumidity_2m=50.0,
|
|
|
|
windspeed_10m=10.0,
|
|
|
|
)
|
|
|
|
sample_config_data = {
|
2025-01-20 22:58:59 +01:00
|
|
|
"general": {
|
|
|
|
"latitude": 52.52,
|
|
|
|
"longitude": 13.405,
|
|
|
|
},
|
2025-01-12 05:19:37 +01:00
|
|
|
"prediction": {
|
2025-01-18 14:26:34 +01:00
|
|
|
"hours": 48,
|
|
|
|
"historic_hours": 24,
|
2025-01-12 05:19:37 +01:00
|
|
|
},
|
|
|
|
"pvforecast": {
|
2025-01-18 14:26:34 +01:00
|
|
|
"provider": "PVForecastAkkudoktor",
|
2025-01-19 21:47:21 +01:00
|
|
|
"planes": [
|
|
|
|
{
|
|
|
|
"peakpower": 5.0,
|
|
|
|
"surface_azimuth": 180,
|
|
|
|
"surface_tilt": 30,
|
|
|
|
"inverter_paco": 10000,
|
|
|
|
}
|
|
|
|
],
|
2025-01-12 05:19:37 +01:00
|
|
|
},
|
2024-12-15 14:40:03 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# Tests for AkkudoktorForecastHorizon
|
|
|
|
def test_akkudoktor_forecast_horizon():
|
|
|
|
horizon = AkkudoktorForecastHorizon(altitude=30, azimuthFrom=90, azimuthTo=180)
|
|
|
|
assert horizon.altitude == 30
|
|
|
|
assert horizon.azimuthFrom == 90
|
|
|
|
assert horizon.azimuthTo == 180
|
|
|
|
|
|
|
|
|
|
|
|
# Tests for AkkudoktorForecastMeta
|
|
|
|
def test_akkudoktor_forecast_meta():
|
|
|
|
meta = sample_meta
|
|
|
|
assert meta.lat == 52.52
|
2025-01-02 18:34:55 +01:00
|
|
|
assert meta.lon == 13.405
|
2024-12-15 14:40:03 +01:00
|
|
|
assert meta.power == [5000]
|
|
|
|
assert meta.tilt == [30]
|
|
|
|
assert meta.timezone == "Europe/Berlin"
|
|
|
|
|
|
|
|
|
|
|
|
# Tests for AkkudoktorForecastValue
|
|
|
|
def test_akkudoktor_forecast_value():
|
|
|
|
value = sample_value
|
|
|
|
assert value.dcPower == 500.0
|
|
|
|
assert value.power == 480.0
|
|
|
|
assert value.temperature == 15.0
|
|
|
|
assert value.windspeed_10m == 10.0
|
|
|
|
|
|
|
|
|
|
|
|
# Tests for PVForecastAkkudoktorDataRecord
|
|
|
|
def test_pvforecast_akkudoktor_data_record():
|
|
|
|
record = PVForecastAkkudoktorDataRecord(
|
|
|
|
pvforecastakkudoktor_ac_power_measured=1000.0,
|
|
|
|
pvforecastakkudoktor_wind_speed_10m=10.0,
|
|
|
|
pvforecastakkudoktor_temp_air=15.0,
|
|
|
|
)
|
|
|
|
assert record.pvforecastakkudoktor_ac_power_measured == 1000.0
|
|
|
|
assert record.pvforecastakkudoktor_wind_speed_10m == 10.0
|
|
|
|
assert record.pvforecastakkudoktor_temp_air == 15.0
|
|
|
|
assert (
|
|
|
|
record.pvforecastakkudoktor_ac_power_any == 1000.0
|
|
|
|
) # Assuming AC power measured is preferred
|
|
|
|
|
|
|
|
|
|
|
|
def test_pvforecast_akkudoktor_validate_data(provider_empty_instance, sample_forecast_data_raw):
|
|
|
|
"""Test validation of PV forecast data on sample data."""
|
2025-01-26 22:42:54 +01:00
|
|
|
logger.info("The following errors are intentional and part of the test.")
|
2024-12-15 14:40:03 +01:00
|
|
|
with pytest.raises(
|
|
|
|
ValueError,
|
|
|
|
match="Field: meta\nError: Field required\nType: missing\nField: values\nError: Field required\nType: missing\n",
|
|
|
|
):
|
|
|
|
ret = provider_empty_instance._validate_data("{}")
|
|
|
|
data = provider_empty_instance._validate_data(sample_forecast_data_raw)
|
|
|
|
# everything worked
|
|
|
|
|
|
|
|
|
|
|
|
@patch("requests.get")
|
|
|
|
def test_pvforecast_akkudoktor_update_with_sample_forecast(
|
|
|
|
mock_get, sample_settings, sample_forecast_data_raw, sample_forecast_start, provider
|
|
|
|
):
|
|
|
|
"""Test data processing using sample forecast data."""
|
|
|
|
# Mock response object
|
|
|
|
mock_response = Mock()
|
|
|
|
mock_response.status_code = 200
|
|
|
|
mock_response.content = sample_forecast_data_raw
|
|
|
|
mock_get.return_value = mock_response
|
|
|
|
|
|
|
|
# Test that update properly inserts data records
|
2024-12-30 13:41:39 +01:00
|
|
|
ems_eos = get_ems()
|
2024-12-15 14:40:03 +01:00
|
|
|
ems_eos.set_start_datetime(sample_forecast_start)
|
|
|
|
provider.update_data(force_enable=True, force_update=True)
|
|
|
|
assert compare_datetimes(provider.start_datetime, sample_forecast_start).equal
|
|
|
|
assert compare_datetimes(provider[0].date_time, to_datetime(sample_forecast_start)).equal
|
|
|
|
|
|
|
|
|
|
|
|
# Report Generation Test
|
2024-12-30 13:41:39 +01:00
|
|
|
def test_report_ac_power_and_measurement(provider, config_eos):
|
2024-12-15 14:40:03 +01:00
|
|
|
# Set the configuration
|
2024-12-30 13:41:39 +01:00
|
|
|
config_eos.merge_settings_from_dict(sample_config_data)
|
2024-12-15 14:40:03 +01:00
|
|
|
|
|
|
|
record = PVForecastAkkudoktorDataRecord(
|
|
|
|
pvforecastakkudoktor_ac_power_measured=900.0,
|
|
|
|
pvforecast_dc_power=450.0,
|
|
|
|
pvforecast_ac_power=400.0,
|
|
|
|
)
|
|
|
|
provider.append(record)
|
|
|
|
|
|
|
|
report = provider.report_ac_power_and_measurement()
|
|
|
|
assert "DC: 450.0" in report
|
|
|
|
assert "AC: 400.0" in report
|
|
|
|
assert "AC sampled: 900.0" in report
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
|
|
sys.platform.startswith("win"), reason="'other_timezone' fixture not supported on Windows"
|
|
|
|
)
|
|
|
|
@patch("requests.get")
|
|
|
|
def test_timezone_behaviour(
|
|
|
|
mock_get,
|
|
|
|
sample_settings,
|
|
|
|
sample_forecast_data_raw,
|
|
|
|
sample_forecast_start,
|
|
|
|
provider,
|
|
|
|
set_other_timezone,
|
|
|
|
):
|
|
|
|
"""Test PVForecast in another timezone."""
|
|
|
|
mock_response = Mock()
|
|
|
|
mock_response.status_code = 200
|
|
|
|
mock_response.content = sample_forecast_data_raw
|
|
|
|
mock_get.return_value = mock_response
|
|
|
|
|
|
|
|
# sample forecast start in other timezone
|
|
|
|
other_timezone = set_other_timezone()
|
|
|
|
other_start_datetime = to_datetime(sample_forecast_start, in_timezone=other_timezone)
|
|
|
|
assert compare_datetimes(other_start_datetime, sample_forecast_start).equal
|
|
|
|
expected_datetime = to_datetime("2024-10-06T00:00:00+0200", in_timezone=other_timezone)
|
|
|
|
assert compare_datetimes(other_start_datetime, expected_datetime).equal
|
|
|
|
|
|
|
|
provider.clear()
|
|
|
|
assert len(provider) == 0
|
2024-12-30 13:41:39 +01:00
|
|
|
ems_eos = get_ems()
|
2024-12-15 14:40:03 +01:00
|
|
|
ems_eos.set_start_datetime(other_start_datetime)
|
|
|
|
provider.update_data(force_update=True)
|
|
|
|
assert compare_datetimes(provider.start_datetime, other_start_datetime).equal
|
|
|
|
# Check wether first record starts at requested sample start time
|
|
|
|
assert compare_datetimes(provider[0].date_time, sample_forecast_start).equal
|
|
|
|
|
|
|
|
# Test updating AC power measurement for a specific date.
|
|
|
|
provider.update_value(sample_forecast_start, "pvforecastakkudoktor_ac_power_measured", 1000)
|
|
|
|
# Check wether first record was filled with ac power measurement
|
|
|
|
assert provider[0].pvforecastakkudoktor_ac_power_measured == 1000
|
|
|
|
|
|
|
|
# Test fetching temperature forecast for a specific date.
|
|
|
|
other_end_datetime = other_start_datetime + to_duration("24 hours")
|
|
|
|
expected_end_datetime = to_datetime("2024-10-07T00:00:00+0200", in_timezone=other_timezone)
|
|
|
|
assert compare_datetimes(other_end_datetime, expected_end_datetime).equal
|
|
|
|
forecast_temps = provider.key_to_series(
|
|
|
|
"pvforecastakkudoktor_temp_air", other_start_datetime, other_end_datetime
|
|
|
|
)
|
2024-12-29 16:43:37 +01:00
|
|
|
assert len(forecast_temps) == 23 # 24-1, first temperature is null
|
|
|
|
assert forecast_temps.iloc[0] == 6.5
|
|
|
|
assert forecast_temps.iloc[1] == 6.0
|
2024-12-15 14:40:03 +01:00
|
|
|
|
|
|
|
# Test fetching AC power forecast
|
|
|
|
other_end_datetime = other_start_datetime + to_duration("48 hours")
|
|
|
|
forecast_measured = provider.key_to_series(
|
|
|
|
"pvforecastakkudoktor_ac_power_measured", other_start_datetime, other_end_datetime
|
|
|
|
)
|
2024-12-29 18:42:49 +01:00
|
|
|
assert len(forecast_measured) == 1
|
2024-12-15 14:40:03 +01:00
|
|
|
assert forecast_measured.iloc[0] == 1000.0 # changed before
|