EOS/tests/test_pvforecastvrm.py
redmoon2711 8e69caba73
Some checks failed
docker-build / platform-excludes (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
feat(VRM forecast): add load and pv forecast by VRM API (#611)
Add support for fetching forecasts from the VRM API by Victron Energy. Retrieve forecasts for PV generation and total load of a Victron system from the internet. 

Tests for the new modules have been added, and the documentation has been updated accordingly.

Signed-off-by: redmoon2711 <redmoon2711@gmx.de>
2025-07-19 08:55:16 +02:00

117 lines
3.9 KiB
Python

import json
from unittest.mock import call, patch
import pendulum
import pytest
import requests
from akkudoktoreos.prediction.pvforecastvrm import (
PVForecastVrm,
VrmForecastRecords,
VrmForecastResponse,
)
@pytest.fixture
def pvforecast_instance(config_eos):
# Settings for PVForecastVrm
settings = {
"pvforecast": {
"provider": "PVForecastVrm",
"provider_settings": {
"pvforecast_vrm_token": "dummy-token",
"pvforecast_vrm_idsite": 12345
}
}
}
config_eos.merge_settings_from_dict(settings)
# start_datetime initialize
start_dt = pendulum.datetime(2025, 1, 1, tz='Europe/Berlin')
# create PVForecastVrm-instance with config and start_datetime
pv = PVForecastVrm(config=config_eos.load, start_datetime=start_dt)
return pv
def mock_forecast_response():
"""Return a fake VrmForecastResponse with sample data."""
return VrmForecastResponse(
success=True,
records=VrmForecastRecords(
vrm_consumption_fc=[],
solar_yield_forecast=[
(pendulum.datetime(2025, 1, 1, 0, 0, tz='Europe/Berlin').int_timestamp * 1000, 120.0),
(pendulum.datetime(2025, 1, 1, 1, 0, tz='Europe/Berlin').int_timestamp * 1000, 130.0)
]
),
totals={}
)
def test_update_data_updates_dc_and_ac_power(pvforecast_instance):
with patch.object(pvforecast_instance, "_request_forecast", return_value=mock_forecast_response()), \
patch.object(PVForecastVrm, "update_value") as mock_update:
pvforecast_instance._update_data()
# Check that update_value was called correctly
assert mock_update.call_count == 2
expected_calls = [
call(
pendulum.datetime(2025, 1, 1, 0, 0, tz='Europe/Berlin'),
{"pvforecast_dc_power": 120.0, "pvforecast_ac_power": 115.2}
),
call(
pendulum.datetime(2025, 1, 1, 1, 0, tz='Europe/Berlin'),
{"pvforecast_dc_power": 130.0, "pvforecast_ac_power": 124.8}
),
]
mock_update.assert_has_calls(expected_calls, any_order=False)
def test_validate_data_accepts_valid_json():
"""Test that _validate_data doesn't raise with valid input."""
response = mock_forecast_response()
json_data = response.model_dump_json()
validated = PVForecastVrm._validate_data(json_data)
assert validated.success
assert len(validated.records.solar_yield_forecast) == 2
def test_validate_data_invalid_json_raises():
"""Test that _validate_data raises with invalid input."""
invalid_json = json.dumps({"success": True}) # missing 'records'
with pytest.raises(ValueError) as exc_info:
PVForecastVrm._validate_data(invalid_json)
assert "Field:" in str(exc_info.value)
assert "records" in str(exc_info.value)
def test_request_forecast_raises_on_http_error(pvforecast_instance):
"""Ensure _request_forecast raises RuntimeError on HTTP failure."""
with patch("requests.get", side_effect=requests.Timeout("Request timed out")) as mock_get:
with pytest.raises(RuntimeError) as exc_info:
pvforecast_instance._request_forecast(0, 1)
assert "Failed to fetch pvforecast" in str(exc_info.value)
mock_get.assert_called_once()
def test_update_data_skips_on_empty_forecast(pvforecast_instance):
"""Ensure no update_value calls are made if no forecast data is present."""
empty_response = VrmForecastResponse(
success=True,
records=VrmForecastRecords(vrm_consumption_fc=[], solar_yield_forecast=[]),
totals={}
)
with patch.object(pvforecast_instance, "_request_forecast", return_value=empty_response), \
patch.object(PVForecastVrm, "update_value") as mock_update:
pvforecast_instance._update_data()
mock_update.assert_not_called()