mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2026-07-12 21:08:13 +00:00
128 lines
4.7 KiB
Python
128 lines
4.7 KiB
Python
|
|
from unittest.mock import call, patch
|
||
|
|
|
||
|
|
import pendulum
|
||
|
|
import pytest
|
||
|
|
import requests
|
||
|
|
|
||
|
|
from akkudoktoreos.prediction.pvforecastforecastsolar import PVForecastForecastSolar
|
||
|
|
|
||
|
|
|
||
|
|
def _config(config_eos, planes=None, api_key=None):
|
||
|
|
settings = {
|
||
|
|
"general": {"latitude": 52.5, "longitude": 13.4},
|
||
|
|
"pvforecast": {
|
||
|
|
"provider": "PVForecastForecastSolar",
|
||
|
|
"planes": planes
|
||
|
|
if planes is not None
|
||
|
|
else [{"surface_tilt": 30.0, "surface_azimuth": 180.0, "peakpower": 5.0}],
|
||
|
|
"provider_settings": {"PVForecastForecastSolar": {"api_key": api_key}},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
config_eos.merge_settings_from_dict(settings)
|
||
|
|
return config_eos
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def pvforecast_instance(config_eos):
|
||
|
|
_config(config_eos)
|
||
|
|
start_dt = pendulum.datetime(2025, 1, 1, tz="Europe/Berlin")
|
||
|
|
return PVForecastForecastSolar(config=config_eos.load, start_datetime=start_dt)
|
||
|
|
|
||
|
|
|
||
|
|
def _http(watts, timezone="Europe/Berlin"):
|
||
|
|
return type(
|
||
|
|
"R",
|
||
|
|
(),
|
||
|
|
{
|
||
|
|
"raise_for_status": lambda self: None,
|
||
|
|
"json": lambda self: {
|
||
|
|
"result": {"watts": watts},
|
||
|
|
"message": {"info": {"timezone": timezone}},
|
||
|
|
},
|
||
|
|
},
|
||
|
|
)()
|
||
|
|
|
||
|
|
|
||
|
|
def test_provider_id(pvforecast_instance):
|
||
|
|
assert PVForecastForecastSolar.provider_id() == "PVForecastForecastSolar"
|
||
|
|
assert pvforecast_instance.enabled() is True
|
||
|
|
|
||
|
|
|
||
|
|
def test_update_data_resolves_tz_and_sets_power(pvforecast_instance):
|
||
|
|
body = {
|
||
|
|
"timezone": "Europe/Berlin",
|
||
|
|
"watts": {"2025-01-01 12:00:00": 1200.0, "2025-01-01 13:00:00": 1500.0},
|
||
|
|
}
|
||
|
|
with patch.object(pvforecast_instance, "_request_forecast", return_value=body), \
|
||
|
|
patch.object(PVForecastForecastSolar, "update_value") as mock_update:
|
||
|
|
pvforecast_instance._update_data()
|
||
|
|
|
||
|
|
assert mock_update.call_count == 2
|
||
|
|
expected = [
|
||
|
|
call(
|
||
|
|
pendulum.datetime(2025, 1, 1, 12, 0, tz="Europe/Berlin"),
|
||
|
|
{"pvforecast_ac_power": 1200.0, "pvforecast_dc_power": 1200.0},
|
||
|
|
),
|
||
|
|
call(
|
||
|
|
pendulum.datetime(2025, 1, 1, 13, 0, tz="Europe/Berlin"),
|
||
|
|
{"pvforecast_ac_power": 1500.0, "pvforecast_dc_power": 1500.0},
|
||
|
|
),
|
||
|
|
]
|
||
|
|
mock_update.assert_has_calls(expected, any_order=False)
|
||
|
|
# 12:00 Europe/Berlin (CET) is 11:00 UTC.
|
||
|
|
assert mock_update.call_args_list[0][0][0].in_timezone("UTC").hour == 11
|
||
|
|
|
||
|
|
|
||
|
|
def test_plane_url_converts_azimuth(config_eos):
|
||
|
|
"""EOS azimuth 270 (west) -> Forecast.Solar 90; the key + plane geometry land in the URL."""
|
||
|
|
_config(
|
||
|
|
config_eos,
|
||
|
|
planes=[{"surface_tilt": 25.0, "surface_azimuth": 270.0, "peakpower": 7.5}],
|
||
|
|
api_key="secret",
|
||
|
|
)
|
||
|
|
pv = PVForecastForecastSolar(
|
||
|
|
config=config_eos.load, start_datetime=pendulum.datetime(2025, 1, 1, tz="UTC")
|
||
|
|
)
|
||
|
|
with patch("requests.get", return_value=_http({})) as mock_get:
|
||
|
|
pv._request_forecast(force_update=True)
|
||
|
|
url = mock_get.call_args[0][0]
|
||
|
|
assert url == "https://api.forecast.solar/secret/estimate/52.5/13.4/25.0/90.0/7.5"
|
||
|
|
|
||
|
|
|
||
|
|
def test_request_forecast_sums_planes(config_eos):
|
||
|
|
"""Two planes -> two requests; instantaneous powers are summed per timestamp."""
|
||
|
|
_config(
|
||
|
|
config_eos,
|
||
|
|
planes=[
|
||
|
|
{"surface_tilt": 30.0, "surface_azimuth": 90.0, "peakpower": 3.0},
|
||
|
|
{"surface_tilt": 30.0, "surface_azimuth": 270.0, "peakpower": 3.0},
|
||
|
|
],
|
||
|
|
)
|
||
|
|
pv = PVForecastForecastSolar(
|
||
|
|
config=config_eos.load, start_datetime=pendulum.datetime(2025, 1, 1, tz="UTC")
|
||
|
|
)
|
||
|
|
responses = [
|
||
|
|
_http({"2025-01-01 12:00:00": 1000.0}),
|
||
|
|
_http({"2025-01-01 12:00:00": 800.0}),
|
||
|
|
]
|
||
|
|
with patch("requests.get", side_effect=responses) as mock_get:
|
||
|
|
body = pv._request_forecast(force_update=True)
|
||
|
|
assert mock_get.call_count == 2
|
||
|
|
assert body["watts"]["2025-01-01 12:00:00"] == 1800.0
|
||
|
|
|
||
|
|
|
||
|
|
def test_update_data_skips_when_disabled(pvforecast_instance, config_eos):
|
||
|
|
config_eos.merge_settings_from_dict({"pvforecast": {"provider": "PVForecastAkkudoktor"}})
|
||
|
|
with patch.object(pvforecast_instance, "_request_forecast") as mock_req, \
|
||
|
|
patch.object(PVForecastForecastSolar, "update_value") as mock_update:
|
||
|
|
pvforecast_instance._update_data()
|
||
|
|
mock_req.assert_not_called()
|
||
|
|
mock_update.assert_not_called()
|
||
|
|
|
||
|
|
|
||
|
|
def test_request_forecast_raises_on_http_error(pvforecast_instance):
|
||
|
|
with patch("requests.get", side_effect=requests.Timeout("timed out")):
|
||
|
|
with pytest.raises(RuntimeError) as exc_info:
|
||
|
|
pvforecast_instance._request_forecast(force_update=True)
|
||
|
|
assert "Failed to fetch pvforecast from Forecast.Solar" in str(exc_info.value)
|