From 8a9aec6d57eaa175200b312d528be39c0daea968 Mon Sep 17 00:00:00 2001 From: Bobby Noelte Date: Fri, 13 Mar 2026 12:23:21 +0100 Subject: [PATCH] feat: add openmeteo weather provider (#939) Add OpenMeteo to the selectable weather prediction providers. Also add tests and documentation. Signed-off-by: Bobby Noelte --- config.yaml | 2 +- docs/_generated/configweather.md | 1 + docs/_generated/openapi.md | 2 +- docs/akkudoktoreos/prediction.md | 30 +- openapi.json | 2 +- src/akkudoktoreos/prediction/prediction.py | 11 +- .../prediction/weatheropenmeteo.py | 357 +++ tests/test_prediction.py | 6 +- tests/test_weatheropenmeteo.py | 290 +++ .../testdata/weatherforecast_openmeteo_1.json | 1663 ++++++++++++++ .../testdata/weatherforecast_openmeteo_2.json | 1959 +++++++++++++++++ 11 files changed, 4316 insertions(+), 7 deletions(-) create mode 100644 src/akkudoktoreos/prediction/weatheropenmeteo.py create mode 100644 tests/test_weatheropenmeteo.py create mode 100644 tests/testdata/weatherforecast_openmeteo_1.json create mode 100644 tests/testdata/weatherforecast_openmeteo_2.json diff --git a/config.yaml b/config.yaml index 6574890..7fdfccf 100644 --- a/config.yaml +++ b/config.yaml @@ -6,7 +6,7 @@ # the root directory (no add-on folder as usual). name: "Akkudoktor-EOS" -version: "0.2.0.dev2603110720349451" +version: "0.2.0.dev2603130753300674" slug: "eos" description: "Akkudoktor-EOS add-on" url: "https://github.com/Akkudoktor-EOS/EOS" diff --git a/docs/_generated/configweather.md b/docs/_generated/configweather.md index 6d83c9f..dee9ad6 100644 --- a/docs/_generated/configweather.md +++ b/docs/_generated/configweather.md @@ -45,6 +45,7 @@ "providers": [ "BrightSky", "ClearOutside", + "OpenMeteo", "WeatherImport" ] } diff --git a/docs/_generated/openapi.md b/docs/_generated/openapi.md index 8181c1b..e2d6f2f 100644 --- a/docs/_generated/openapi.md +++ b/docs/_generated/openapi.md @@ -1,6 +1,6 @@ # Akkudoktor-EOS -**Version**: `v0.2.0.dev2603110720349451` +**Version**: `v0.2.0.dev2603130753300674` **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. diff --git a/docs/akkudoktoreos/prediction.md b/docs/akkudoktoreos/prediction.md index fe639dc..d1a5f68 100644 --- a/docs/akkudoktoreos/prediction.md +++ b/docs/akkudoktoreos/prediction.md @@ -571,6 +571,7 @@ Configuration options: - `BrightSky`: Retrieves from [BrightSky](https://api.brightsky.dev). - `ClearOutside`: Retrieves from [ClearOutside](https://clearoutside.com/forecast). + - `OpenMeteo`: Retrieves from [OpenMeteo](https://api.open-meteo.com/v1/forecast). - `LoadImport`: Imports from a file or JSON string. - `provider_settings.import_file_path`: Path to the file to import weatherforecast data from. @@ -578,7 +579,7 @@ Configuration options: ### BrightSky Provider -The `BrightSky` provider retrieves the PV power forecast data directly from +The `BrightSky` provider retrieves the weather forecast data directly from [**BrightSky**](https://api.brightsky.dev). The provider provides forecast data for the following prediction keys: @@ -597,7 +598,7 @@ The provider provides forecast data for the following prediction keys: ### ClearOutside Provider -The `ClearOutside` provider retrieves the PV power forecast data directly from +The `ClearOutside` provider retrieves the weather forecast data directly from [**ClearOutside**](https://clearoutside.com/forecast). The provider provides forecast data for the following prediction keys: @@ -625,6 +626,31 @@ The provider provides forecast data for the following prediction keys: - `weather_wind_direction`: Wind Direction (°) - `weather_wind_speed`: Wind Speed (kmph) +### OpenMeteo Provider + +The `OpenMeteo` provider retrieves the weather forecast data directly from +[**OpenMeteo**](https://api.open-meteo.com/v1/forecast). + +The provider provides forecast data for the following prediction keys: + +- `weather_dew_point`: Dew Point (°C) +- `weather_dhi`: Diffuse Horizontal Irradiance (W/m2) +- `weather_dni`: Direct Normal Irradiance (W/m2) +- `weather_feels_like`: Feels Like (°C) +- `weather_ghi`: Global Horizontal Irradiance (W/m2) +- `weather_high_clouds`: High Clouds (% Sky Obscured) +- `weather_low_clouds`: Low Clouds (% Sky Obscured) +- `weather_medium_clouds`: Medium Clouds (% Sky Obscured) +- `weather_precip_amt`: Precipitation Amount (mm) +- `weather_precip_prob`: Precipitation Probability (%) +- `weather_pressure`: Pressure (mb) +- `weather_relative_humidity`: Relative Humidity (%) +- `weather_temp_air`: Temperature (°C) +- `weather_total_clouds`: Total Clouds (% Sky Obscured) +- `weather_visibility`: Visibility (m) +- `weather_wind_direction`: Wind Direction (°) +- `weather_wind_speed`: Wind Speed (kmph) + ### WeatherImport Provider The `WeatherImport` provider is designed to import weather forecast data from a file or a JSON diff --git a/openapi.json b/openapi.json index 2d67c82..c83cd87 100644 --- a/openapi.json +++ b/openapi.json @@ -8,7 +8,7 @@ "name": "Apache 2.0", "url": "https://www.apache.org/licenses/LICENSE-2.0.html" }, - "version": "v0.2.0.dev2603110720349451" + "version": "v0.2.0.dev2603130753300674" }, "paths": { "/v1/admin/cache/clear": { diff --git a/src/akkudoktoreos/prediction/prediction.py b/src/akkudoktoreos/prediction/prediction.py index b415b7e..9fe6053 100644 --- a/src/akkudoktoreos/prediction/prediction.py +++ b/src/akkudoktoreos/prediction/prediction.py @@ -50,6 +50,7 @@ from akkudoktoreos.prediction.pvforecastvrm import PVForecastVrm from akkudoktoreos.prediction.weatherbrightsky import WeatherBrightSky from akkudoktoreos.prediction.weatherclearoutside import WeatherClearOutside from akkudoktoreos.prediction.weatherimport import WeatherImport +from akkudoktoreos.prediction.weatheropenmeteo import WeatherOpenMeteo class PredictionCommonSettings(SettingsBaseModel): @@ -86,6 +87,7 @@ pvforecast_vrm = PVForecastVrm() pvforecast_import = PVForecastImport() weather_brightsky = WeatherBrightSky() weather_clearoutside = WeatherClearOutside() +weather_openmeteo = WeatherOpenMeteo() weather_import = WeatherImport() @@ -106,10 +108,14 @@ def prediction_providers() -> list[ PVForecastImport, WeatherBrightSky, WeatherClearOutside, + WeatherOpenMeteo, WeatherImport, ] ]: - """Return list of prediction providers.""" + """Return list of prediction providers. + + Factory for prediction container. + """ global \ elecprice_akkudoktor, \ elecprice_energy_charts, \ @@ -126,6 +132,7 @@ def prediction_providers() -> list[ pvforecast_import, \ weather_brightsky, \ weather_clearoutside, \ + weather_openmeteo, \ weather_import # Care for provider sequence as providers may rely on others to be updated before. @@ -145,6 +152,7 @@ def prediction_providers() -> list[ pvforecast_import, weather_brightsky, weather_clearoutside, + weather_openmeteo, weather_import, ] @@ -169,6 +177,7 @@ class Prediction(PredictionContainer): PVForecastImport, WeatherBrightSky, WeatherClearOutside, + WeatherOpenMeteo, WeatherImport, ] ] = Field( diff --git a/src/akkudoktoreos/prediction/weatheropenmeteo.py b/src/akkudoktoreos/prediction/weatheropenmeteo.py new file mode 100644 index 0000000..e6957da --- /dev/null +++ b/src/akkudoktoreos/prediction/weatheropenmeteo.py @@ -0,0 +1,357 @@ +"""Retrieves and processes weather forecast data from Open-Meteo. + +This module provides classes and mappings to manage weather data obtained from the +Open-Meteo API, including support for various weather attributes such as temperature, +humidity, cloud cover, and solar irradiance. The data is mapped to the `WeatherDataRecord` +format, enabling consistent access to forecasted and historical weather attributes. +""" + +from typing import Dict, List, Optional, Tuple, Union + +import numpy as np +import pandas as pd +import pvlib +import requests +from loguru import logger + +from akkudoktoreos.core.cache import cache_in_file +from akkudoktoreos.prediction.weatherabc import WeatherDataRecord, WeatherProvider +from akkudoktoreos.utils.datetimeutil import to_datetime, to_duration + +WeatherDataOpenMeteoMapping: List[Tuple[str, Optional[str], Optional[Union[str, float]]]] = [ + # openmeteo_key, description, corr_factor + ("time", "DateTime", "to datetime in timezone"), + ("temperature_2m", "Temperature (°C)", 1), + ("relative_humidity_2m", "Relative Humidity (%)", 1), + ("precipitation", "Precipitation Amount (mm)", 1), + ("rain", None, None), + ("showers", None, None), + ("snowfall", None, None), + ("weather_code", None, None), + ("cloud_cover", "Total Clouds (% Sky Obscured)", 1), + ("cloud_cover_low", "Low Clouds (% Sky Obscured)", 1), + ("cloud_cover_mid", "Medium Clouds (% Sky Obscured)", 1), + ("cloud_cover_high", "High Clouds (% Sky Obscured)", 1), + ("pressure_msl", "Pressure (mb)", 0.01), # Pa to hPa + ("surface_pressure", None, None), + ("wind_speed_10m", "Wind Speed (kmph)", 3.6), # m/s to km/h + ("wind_direction_10m", "Wind Direction (°)", 1), + ("wind_gusts_10m", "Wind Gust Speed (kmph)", 3.6), # m/s to km/h + ("shortwave_radiation", "Global Horizontal Irradiance (W/m2)", 1), + ("direct_radiation", "Direct Normal Irradiance (W/m2)", 1), + ("diffuse_radiation", "Diffuse Horizontal Irradiance (W/m2)", 1), + ("direct_normal_irradiance", None, None), + ("global_tilted_irradiance", None, None), + ("terrestrial_radiation", None, None), + ("shortwave_radiation_instant", None, None), + ("direct_radiation_instant", None, None), + ("diffuse_radiation_instant", None, None), + ("direct_normal_irradiance_instant", None, None), + ("global_tilted_irradiance_instant", None, None), + ("terrestrial_radiation_instant", None, None), + ("dew_point_2m", "Dew Point (°C)", 1), + ("apparent_temperature", "Feels Like (°C)", 1), + ("precipitation_probability", "Precipitation Probability (%)", 1), + ("visibility", "Visibility (m)", 1), + ("cape", None, None), + ("evapotranspiration", None, None), + ("et0_fao_evapotranspiration", None, None), + ("vapour_pressure_deficit", None, None), + ("soil_temperature_0_to_7cm", None, None), + ("soil_temperature_7_to_28cm", None, None), + ("soil_temperature_28_to_100cm", None, None), + ("soil_temperature_100_to_255cm", None, None), + ("soil_moisture_0_to_7cm", None, None), + ("soil_moisture_7_to_28cm", None, None), + ("soil_moisture_28_to_100cm", None, None), + ("soil_moisture_100_to_255cm", None, None), + ("sunshine_duration", None, None), # seconds +] +"""Mapping of Open-Meteo weather data keys to WeatherDataRecord field descriptions. + +Each tuple represents a field in the Open-Meteo data, with: + +- The Open-Meteo field key, +- The corresponding `WeatherDataRecord` description, if applicable, +- A correction factor for unit or value scaling. + +Fields without descriptions or correction factors are mapped to `None`. +""" + + +class WeatherOpenMeteo(WeatherProvider): + """Fetch and process weather forecast data from Open-Meteo. + + WeatherOpenMeteo is a singleton-based class that retrieves weather forecast data + from the Open-Meteo API and maps it to `WeatherDataRecord` fields, applying + any necessary scaling or unit corrections. It manages the forecast over a range + of hours into the future and retains historical data. + + Attributes: + hours (int, optional): Number of hours in the future for the forecast. + historic_hours (int, optional): Number of past hours for retaining data. + latitude (float, optional): The latitude in degrees, validated to be between -90 and 90. + longitude (float, optional): The longitude in degrees, validated to be between -180 and 180. + start_datetime (datetime, optional): Start datetime for forecasts, defaults to the current + datetime. + end_datetime (datetime, computed): The forecast's end datetime, computed based on + `start_datetime` and `hours`. + keep_datetime (datetime, computed): The datetime to retain historical data, computed from + `start_datetime` and `historic_hours`. + + Methods: + provider_id(): Returns a unique identifier for the provider. + _request_forecast(): Fetches the forecast from the Open-Meteo API. + _update_data(): Processes and updates forecast data from Open-Meteo in WeatherDataRecord format. + """ + + @classmethod + def provider_id(cls) -> str: + """Return the unique identifier for the Open-Meteo provider.""" + return "OpenMeteo" + + @cache_in_file(with_ttl="1 hour") + def _request_forecast(self) -> dict: + """Fetch weather forecast data from Open-Meteo API. + + This method sends a request to Open-Meteo's API to retrieve forecast data + for a specified date range and location. The response data is parsed and + returned as JSON for further processing. + + Returns: + dict: The parsed JSON response from Open-Meteo API containing forecast data. + + Raises: + ValueError: If the API response does not include expected `hourly` data. + """ + source = "https://api.open-meteo.com/v1/forecast" + + # Parameters for Open-Meteo API + params = { + "latitude": self.config.general.latitude, + "longitude": self.config.general.longitude, + "hourly": [ + "temperature_2m", + "relative_humidity_2m", + "precipitation", + "rain", + "showers", + "snowfall", + "weather_code", + "cloud_cover", + "cloud_cover_low", + "cloud_cover_mid", + "cloud_cover_high", + "pressure_msl", + "surface_pressure", + "wind_speed_10m", + "wind_direction_10m", + "wind_gusts_10m", + "shortwave_radiation", # GHI + "direct_radiation", # DNI + "diffuse_radiation", # DHI + "dew_point_2m", + "apparent_temperature", + "precipitation_probability", + "visibility", + "sunshine_duration", + ], + "timezone": self.config.general.timezone, + } + + # Calculate the number of days between start and end + start_dt = to_datetime(self.ems_start_datetime) + end_dt = to_datetime(self.end_datetime) + days_diff = (end_dt - start_dt).days + 1 # +1 for inclusive range + + # Open-Meteo has a maximum of 16 days + forecast_days = min(days_diff, 16) + + # Decide whether we need forecast or historical data + now = to_datetime(in_timezone=self.config.general.timezone) + + if start_dt.date() >= now.date(): + # Future data - use forecast_days + params["forecast_days"] = forecast_days + else: + # Historical data - use start_date and end_date + params["start_date"] = start_dt.strftime("%Y-%m-%d") + params["end_date"] = end_dt.strftime("%Y-%m-%d") + # For historical data we must specify the forecast model + params["models"] = "best_match" # or specific e.g. "dwd", "icon", etc. + + logger.debug(f"Open-Meteo Request params: {params}") + + response = requests.get(source, params=params, timeout=10) + response.raise_for_status() # Raise an error for bad responses + logger.debug(f"Response from {source}: {response.status_code}") + + openmeteo_data = response.json() + + if "hourly" not in openmeteo_data: + error_msg = f"Open-Meteo schema change. `hourly` expected to be part of Open-Meteo data: {openmeteo_data}." + logger.error(error_msg) + raise ValueError(error_msg) + + # We are working with fresh data (no cache), report update time + self.update_datetime = to_datetime(in_timezone=self.config.general.timezone) + return openmeteo_data + + def _description_to_series(self, description: str) -> pd.Series: + """Retrieve a pandas Series corresponding to a weather data description. + + This method fetches the key associated with the provided description + and retrieves the data series mapped to that key. If the description + does not correspond to a valid key, a `ValueError` is raised. + + Args: + description (str): The description of the WeatherDataRecord to retrieve. + + Returns: + pd.Series: The data series corresponding to the description. + + Raises: + ValueError: If no key is found for the provided description. + """ + key = WeatherDataRecord.key_from_description(description) + if key is None: + error_msg = f"No WeatherDataRecord key for '{description}'" + logger.error(error_msg) + raise ValueError(error_msg) + series = self.key_to_series(key) + return series + + def _description_from_series(self, description: str, data: pd.Series) -> None: + """Update a weather data with a pandas Series based on its description. + + This method fetches the key associated with the provided description + and updates the weather data with the provided data series. If the description + does not correspond to a valid key, a `ValueError` is raised. + + Args: + description (str): The description of the weather data to update. + data (pd.Series): The pandas Series containing the data to update. + + Raises: + ValueError: If no key is found for the provided description. + """ + key = WeatherDataRecord.key_from_description(description) + if key is None: + error_msg = f"No WeatherDataRecord key for '{description}'" + logger.error(error_msg) + raise ValueError(error_msg) + self.key_from_series(key, data) + + def _update_data(self, force_update: Optional[bool] = False) -> None: + """Update forecast data in the WeatherDataRecord format. + + Retrieves data from Open-Meteo, maps each Open-Meteo field to the corresponding + `WeatherDataRecord` attribute using `WeatherDataOpenMeteoMapping`, and applies + any necessary scaling. Open-Meteo provides direct GHI, DNI, and DHI values which + are used directly without additional calculation. The final mapped and processed + data is inserted into the sequence as `WeatherDataRecord`. + """ + # Retrieve Open-Meteo weather data for the given coordinates + openmeteo_data = self._request_forecast(force_update=force_update) # type: ignore + + # Create key mapping from the description + openmeteo_key_mapping: Dict[str, Tuple[Optional[str], Optional[Union[str, float]]]] = {} + for openmeteo_key, description, corr_factor in WeatherDataOpenMeteoMapping: + if description is None: + openmeteo_key_mapping[openmeteo_key] = (None, None) + continue + weatherdata_key = WeatherDataRecord.key_from_description(description) + if weatherdata_key is None: + # Should not occur + error_msg = f"No WeatherDataRecord key for description '{description}'" + logger.error(error_msg) + raise ValueError(error_msg) + openmeteo_key_mapping[openmeteo_key] = (weatherdata_key, corr_factor) + + # Extract timestamps and values from Open-Meteo response + hourly_data = openmeteo_data["hourly"] + timestamps = hourly_data["time"] + + logger.info("Using direct radiation values from Open-Meteo (GHI, DNI, DHI)") + + # Process the data for each timestamp + for idx, timestamp in enumerate(timestamps): + weather_record = WeatherDataRecord() + + for openmeteo_key, item in openmeteo_key_mapping.items(): + key = item[0] + if key is None: + continue + + # Take value from hourly data, if available + if openmeteo_key in hourly_data: + value = hourly_data[openmeteo_key][idx] + else: + value = None + + corr_factor = item[1] + + if value is not None: + if corr_factor == "to datetime in timezone": + value = to_datetime(value, in_timezone=self.config.general.timezone) + elif isinstance(corr_factor, (int, float)): + value = value * corr_factor + + setattr(weather_record, key, value) + + self.insert_by_datetime(weather_record) + + # Check whether radiation values exist (for logging) + description_ghi = "Global Horizontal Irradiance (W/m2)" + ghi_series = self._description_to_series(description_ghi) + + if ghi_series.isnull().all(): + logger.warning("No GHI data received from Open-Meteo") + else: + logger.debug( + f"GHI data successfully loaded from Open-Meteo. Range: {ghi_series.min():.1f} - {ghi_series.max():.1f} W/m²" + ) + + # Add Precipitable Water (PWAT) using PVLib method + key = WeatherDataRecord.key_from_description("Temperature (°C)") + assert key # noqa: S101 + temperature = self.key_to_array( + key=key, + start_datetime=self.ems_start_datetime, + end_datetime=self.end_datetime, + interval=to_duration("1 hour"), + ) + if any(x is None or isinstance(x, float) and np.isnan(x) for x in temperature): + # PWAT cannot be calculated + debug_msg = f"Invalid temperature '{temperature}'" + logger.debug(debug_msg) + return + + key = WeatherDataRecord.key_from_description("Relative Humidity (%)") + assert key # noqa: S101 + humidity = self.key_to_array( + key=key, + start_datetime=self.ems_start_datetime, + end_datetime=self.end_datetime, + interval=to_duration("1 hour"), + ) + if any(x is None or isinstance(x, float) and np.isnan(x) for x in humidity): + # PWAT cannot be calculated + debug_msg = f"Invalid humidity '{humidity}'" + logger.debug(debug_msg) + return + + data = pvlib.atmosphere.gueymard94_pw(temperature, humidity) + pwat = pd.Series( + data=data, + index=pd.DatetimeIndex( + pd.date_range( + start=self.ems_start_datetime, + end=self.end_datetime, + freq="1h", + inclusive="left", + ) + ), + ) + description = "Precipitable Water (cm)" + self._description_from_series(description, pwat) diff --git a/tests/test_prediction.py b/tests/test_prediction.py index d7a805c..b973017 100644 --- a/tests/test_prediction.py +++ b/tests/test_prediction.py @@ -24,6 +24,7 @@ from akkudoktoreos.prediction.pvforecastvrm import PVForecastVrm from akkudoktoreos.prediction.weatherbrightsky import WeatherBrightSky from akkudoktoreos.prediction.weatherclearoutside import WeatherClearOutside from akkudoktoreos.prediction.weatherimport import WeatherImport +from akkudoktoreos.prediction.weatheropenmeteo import WeatherOpenMeteo @pytest.fixture @@ -51,6 +52,7 @@ def forecast_providers(): PVForecastImport(), WeatherBrightSky(), WeatherClearOutside(), + WeatherOpenMeteo(), WeatherImport(), ] @@ -99,7 +101,8 @@ def test_provider_sequence(prediction): assert isinstance(prediction.providers[12], PVForecastImport) assert isinstance(prediction.providers[13], WeatherBrightSky) assert isinstance(prediction.providers[14], WeatherClearOutside) - assert isinstance(prediction.providers[15], WeatherImport) + assert isinstance(prediction.providers[15], WeatherOpenMeteo) + assert isinstance(prediction.providers[16], WeatherImport) def test_provider_by_id(prediction, forecast_providers): @@ -126,6 +129,7 @@ def test_prediction_repr(prediction): assert "PVForecastImport" in result assert "WeatherBrightSky" in result assert "WeatherClearOutside" in result + assert "WeatherOpenMeteo" in result assert "WeatherImport" in result diff --git a/tests/test_weatheropenmeteo.py b/tests/test_weatheropenmeteo.py new file mode 100644 index 0000000..8aab298 --- /dev/null +++ b/tests/test_weatheropenmeteo.py @@ -0,0 +1,290 @@ +import json +from pathlib import Path +from unittest.mock import Mock, patch + +import pandas as pd +import pytest +from loguru import logger + +from akkudoktoreos.core.cache import CacheFileStore +from akkudoktoreos.core.coreabc import get_ems +from akkudoktoreos.prediction.weatheropenmeteo import WeatherOpenMeteo +from akkudoktoreos.utils.datetimeutil import to_datetime + +DIR_TESTDATA = Path(__file__).absolute().parent.joinpath("testdata") + +FILE_TESTDATA_WEATHEROPENMETEO_1_JSON = DIR_TESTDATA.joinpath("weatherforecast_openmeteo_1.json") +FILE_TESTDATA_WEATHEROPENMETEO_2_JSON = DIR_TESTDATA.joinpath("weatherforecast_openmeteo_2.json") + + +@pytest.fixture +def provider(monkeypatch): + """Fixture to create a WeatherProvider instance.""" + monkeypatch.setenv("EOS_WEATHER__WEATHER_PROVIDER", "OpenMeteo") + monkeypatch.setenv("EOS_GENERAL__LATITUDE", "50.0") + monkeypatch.setenv("EOS_GENERAL__LONGITUDE", "10.0") + return WeatherOpenMeteo() + + +@pytest.fixture +def sample_openmeteo_1_json(): + """Fixture that returns sample forecast data report from Open-Meteo.""" + with FILE_TESTDATA_WEATHEROPENMETEO_1_JSON.open("r", encoding="utf-8", newline=None) as f_res: + input_data = json.load(f_res) + return input_data + + +@pytest.fixture +def sample_openmeteo_2_json(): + """Fixture that returns sample processed forecast data from Open-Meteo.""" + with FILE_TESTDATA_WEATHEROPENMETEO_2_JSON.open("r", encoding="utf-8", newline=None) as f_res: + input_data = json.load(f_res) + return input_data + + +@pytest.fixture +def cache_store(): + """A pytest fixture that creates a new CacheFileStore instance for testing.""" + return CacheFileStore() + + +# ------------------------------------------------ +# General forecast +# ------------------------------------------------ + + +def test_singleton_instance(provider): + """Test that WeatherForecast behaves as a singleton.""" + another_instance = WeatherOpenMeteo() + assert provider is another_instance + + +def test_invalid_provider(provider, monkeypatch): + """Test requesting an unsupported provider.""" + monkeypatch.setenv("EOS_WEATHER__WEATHER_PROVIDER", "") + provider.config.reset_settings() + assert not provider.enabled() + + +def test_invalid_coordinates(provider, monkeypatch): + """Test invalid coordinates raise ValueError.""" + monkeypatch.setenv("EOS_GENERAL__LATITUDE", "1000") + monkeypatch.setenv("EOS_GENERAL__LONGITUDE", "1000") + with pytest.raises( + ValueError, # match="Latitude '1000' and/ or longitude `1000` out of valid range." + ): + provider.config.reset_settings() + + +# ------------------------------------------------ +# Irradiance calculation +# ------------------------------------------------ + + +def test_irridiance_estimate_from_cloud_cover(provider): + """Test cloud cover to irradiance estimation (fallback method).""" + cloud_cover_data = pd.Series( + data=[20, 50, 80], index=pd.date_range("2023-10-22", periods=3, freq="h") + ) + + ghi, dni, dhi = provider.estimate_irradiance_from_cloud_cover(50.0, 10.0, cloud_cover_data) + + # This is just the fallback method - Open-Meteo normally provides direct values + assert len(ghi) == 3 + assert len(dni) == 3 + assert len(dhi) == 3 + # Values should be floats (actual values depend on the algorithm) + assert all(isinstance(x, float) for x in ghi) + assert all(isinstance(x, float) for x in dni) + assert all(isinstance(x, float) for x in dhi) + + +# ------------------------------------------------ +# Open-Meteo +# ------------------------------------------------ + + +@patch("requests.get") +def test_request_forecast(mock_get, provider, sample_openmeteo_1_json): + """Test requesting forecast from Open-Meteo.""" + # Mock response object + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = sample_openmeteo_1_json + mock_response.content = json.dumps(sample_openmeteo_1_json) + mock_get.return_value = mock_response + + # Test function + openmeteo_data = provider._request_forecast() + + # Verify API was called with correct parameters + mock_get.assert_called_once() + call_args = mock_get.call_args + assert call_args[0][0] == "https://api.open-meteo.com/v1/forecast" + assert "latitude" in call_args[1]["params"] + assert "longitude" in call_args[1]["params"] + assert "hourly" in call_args[1]["params"] + + # Verify returned data structure + assert isinstance(openmeteo_data, dict) + assert "hourly" in openmeteo_data + assert "time" in openmeteo_data["hourly"] + assert "temperature_2m" in openmeteo_data["hourly"] + assert "shortwave_radiation" in openmeteo_data["hourly"] # GHI + assert "direct_radiation" in openmeteo_data["hourly"] # DNI + assert "diffuse_radiation" in openmeteo_data["hourly"] # DHI + + +@patch("requests.get") +def test_update_data(mock_get, provider, sample_openmeteo_1_json, cache_store): + """Test fetching and processing forecast from Open-Meteo.""" + # Mock response object + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = sample_openmeteo_1_json + mock_response.content = json.dumps(sample_openmeteo_1_json) + mock_get.return_value = mock_response + + cache_store.clear(clear_all=True) + + # Call the method + ems_eos = get_ems() + start_datetime = to_datetime("2026-03-02 09:00:00+01:00", in_timezone="Europe/Berlin") + ems_eos.set_start_datetime(start_datetime) + provider.update_data(force_enable=True, force_update=True) + + # Assert: Verify the result is as expected + mock_get.assert_called_once() + assert len(provider) > 0 + + # Verify that direct radiation values were properly mapped + # Get the first record and check for irradiance values + value_datetime = to_datetime("2026-03-04 09:00:00+01:00", in_timezone="Europe/Berlin") + assert provider.key_to_value("weather_ghi", target_datetime=start_datetime) == 21.8 + assert provider.key_to_value("weather_dni", target_datetime=start_datetime) == 1.2 + assert provider.key_to_value("weather_dhi", target_datetime=start_datetime) == 20.5 + + +# ------------------------------------------------ +# Test specific Open-Meteo features +# ------------------------------------------------ + + +def test_openmeteo_radiation_mapping(provider): + """Test that radiation values are correctly mapped from Open-Meteo keys.""" + # Verify mapping contains the radiation fields + from akkudoktoreos.prediction.weatheropenmeteo import WeatherDataOpenMeteoMapping + + radiation_keys = [item[0] for item in WeatherDataOpenMeteoMapping + if item[0] in ['shortwave_radiation', 'direct_radiation', 'diffuse_radiation']] + + assert 'shortwave_radiation' in radiation_keys + assert 'direct_radiation' in radiation_keys + assert 'diffuse_radiation' in radiation_keys + + # Verify they map to correct descriptions + for key, desc, _ in WeatherDataOpenMeteoMapping: + if key == 'shortwave_radiation': + assert desc == "Global Horizontal Irradiance (W/m2)" + elif key == 'direct_radiation': + assert desc == "Direct Normal Irradiance (W/m2)" + elif key == 'diffuse_radiation': + assert desc == "Diffuse Horizontal Irradiance (W/m2)" + + +def test_openmeteo_unit_conversions(provider): + """Test that unit conversions are correctly applied.""" + from akkudoktoreos.prediction.weatheropenmeteo import WeatherDataOpenMeteoMapping + + # Check wind speed conversion (m/s to km/h) + wind_speed_mapping = next(item for item in WeatherDataOpenMeteoMapping + if item[0] == 'wind_speed_10m') + assert wind_speed_mapping[2] == 3.6 # Conversion factor + + # Check pressure conversion (Pa to hPa) + pressure_mapping = next(item for item in WeatherDataOpenMeteoMapping + if item[0] == 'pressure_msl') + assert pressure_mapping[2] == 0.01 # Conversion factor + + +@patch("requests.get") +def test_forecast_days_calculation(mock_get, provider, sample_openmeteo_1_json): + """Test that forecast_days is correctly calculated.""" + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = sample_openmeteo_1_json + mock_response.content = json.dumps(sample_openmeteo_1_json) + mock_get.return_value = mock_response + + ems_eos = get_ems() + + # Test with 3 days forecast + start = to_datetime(in_timezone="Europe/Berlin") + ems_eos.set_start_datetime(start) + + provider._request_forecast() + + # Check that forecast_days was set correctly + call_args = mock_get.call_args + params = call_args[1]["params"] + assert params["forecast_days"] + + +# ------------------------------------------------ +# Development Open-Meteo +# ------------------------------------------------ + + +def test_openmeteo_development_forecast_data(provider, config_eos, is_system_test): + """Fetch data from real Open-Meteo server for development purposes.""" + if not is_system_test: + return + + # Us actual date for forecast (not historic data) + now = to_datetime(in_timezone="Europe/Berlin") + start_date = now.replace(hour=0, minute=0, second=0, microsecond=0) + end_date = start_date + pd.Timedelta(days=3) # 3 Tage Vorhersage + + ems_eos = get_ems() + ems_eos.set_start_datetime(start_date) + + config_eos.general.latitude = 50.0 + config_eos.general.longitude = 10.0 + + # Fetch raw data from Open-Meteo + try: + openmeteo_data = provider._request_forecast() + + # Save raw API response + with FILE_TESTDATA_WEATHEROPENMETEO_1_JSON.open("w", encoding="utf-8", newline="\n") as f_out: + json.dump(openmeteo_data, f_out, indent=4) + + # Update and process data + provider.update_data(force_enable=True, force_update=True) + + # Save processed data + with FILE_TESTDATA_WEATHEROPENMETEO_2_JSON.open("w", encoding="utf-8", newline="\n") as f_out: + f_out.write(provider.model_dump_json(indent=4)) + + # Verify radiation values + if len(provider) > 0: + records = list(provider.data_records.values()) + + # Check fo radiation values available + has_ghi = any(hasattr(r, 'ghi') and r.ghi is not None for r in records) + has_dni = any(hasattr(r, 'dni') and r.dni is not None for r in records) + has_dhi = any(hasattr(r, 'dhi') and r.dhi is not None for r in records) + + logger.info(f"Open-Meteo data verification: GHI={has_ghi}, DNI={has_dni}, DHI={has_dhi}") + + # Optional: Check for positive values (at day time) + daytime_values = [getattr(r, 'ghi', 0) for r in records[:24] + if hasattr(r, 'ghi') and r.ghi is not None and r.ghi > 10] + if daytime_values: + logger.info(f"Found {len(daytime_values)} positive GHI values") + + except Exception as e: + logger.error(f"Error fetching Open-Meteo data: {e}") + # Debug-Ausgabe + logger.error(f"Request would have been: https://api.open-meteo.com/v1/forecast?latitude=50.0&longitude=10.0&hourly=temperature_2m,relative_humidity_2m,shortwave_radiation&timezone=Europe/Berlin&forecast_days=3") + raise diff --git a/tests/testdata/weatherforecast_openmeteo_1.json b/tests/testdata/weatherforecast_openmeteo_1.json new file mode 100644 index 0000000..648fa30 --- /dev/null +++ b/tests/testdata/weatherforecast_openmeteo_1.json @@ -0,0 +1,1663 @@ +{ + "latitude": 50.0, + "longitude": 10.0, + "generationtime_ms": 0.5924701690673828, + "utc_offset_seconds": 3600, + "timezone": "Europe/Berlin", + "timezone_abbreviation": "GMT+1", + "elevation": 291.0, + "hourly_units": { + "time": "iso8601", + "temperature_2m": "\u00b0C", + "relative_humidity_2m": "%", + "precipitation": "mm", + "rain": "mm", + "showers": "mm", + "snowfall": "cm", + "weather_code": "wmo code", + "cloud_cover": "%", + "pressure_msl": "hPa", + "surface_pressure": "hPa", + "wind_speed_10m": "km/h", + "wind_direction_10m": "\u00b0", + "wind_gusts_10m": "km/h", + "shortwave_radiation": "W/m\u00b2", + "direct_radiation": "W/m\u00b2", + "diffuse_radiation": "W/m\u00b2", + "dew_point_2m": "\u00b0C", + "apparent_temperature": "\u00b0C", + "precipitation_probability": "%", + "visibility": "m", + "sunshine_duration": "s" + }, + "hourly": { + "time": [ + "2026-03-02T00:00", + "2026-03-02T01:00", + "2026-03-02T02:00", + "2026-03-02T03:00", + "2026-03-02T04:00", + "2026-03-02T05:00", + "2026-03-02T06:00", + "2026-03-02T07:00", + "2026-03-02T08:00", + "2026-03-02T09:00", + "2026-03-02T10:00", + "2026-03-02T11:00", + "2026-03-02T12:00", + "2026-03-02T13:00", + "2026-03-02T14:00", + "2026-03-02T15:00", + "2026-03-02T16:00", + "2026-03-02T17:00", + "2026-03-02T18:00", + "2026-03-02T19:00", + "2026-03-02T20:00", + "2026-03-02T21:00", + "2026-03-02T22:00", + "2026-03-02T23:00", + "2026-03-03T00:00", + "2026-03-03T01:00", + "2026-03-03T02:00", + "2026-03-03T03:00", + "2026-03-03T04:00", + "2026-03-03T05:00", + "2026-03-03T06:00", + "2026-03-03T07:00", + "2026-03-03T08:00", + "2026-03-03T09:00", + "2026-03-03T10:00", + "2026-03-03T11:00", + "2026-03-03T12:00", + "2026-03-03T13:00", + "2026-03-03T14:00", + "2026-03-03T15:00", + "2026-03-03T16:00", + "2026-03-03T17:00", + "2026-03-03T18:00", + "2026-03-03T19:00", + "2026-03-03T20:00", + "2026-03-03T21:00", + "2026-03-03T22:00", + "2026-03-03T23:00", + "2026-03-04T00:00", + "2026-03-04T01:00", + "2026-03-04T02:00", + "2026-03-04T03:00", + "2026-03-04T04:00", + "2026-03-04T05:00", + "2026-03-04T06:00", + "2026-03-04T07:00", + "2026-03-04T08:00", + "2026-03-04T09:00", + "2026-03-04T10:00", + "2026-03-04T11:00", + "2026-03-04T12:00", + "2026-03-04T13:00", + "2026-03-04T14:00", + "2026-03-04T15:00", + "2026-03-04T16:00", + "2026-03-04T17:00", + "2026-03-04T18:00", + "2026-03-04T19:00", + "2026-03-04T20:00", + "2026-03-04T21:00", + "2026-03-04T22:00", + "2026-03-04T23:00" + ], + "temperature_2m": [ + 3.2, + 2.3, + 1.9, + 1.0, + 0.3, + 0.2, + -0.2, + -0.6, + -0.7, + 0.9, + 4.9, + 7.3, + 9.4, + 11.3, + 12.7, + 13.0, + 13.1, + 12.4, + 10.2, + 8.0, + 6.9, + 5.9, + 5.1, + 4.2, + 3.4, + 2.5, + 1.8, + 1.5, + 1.1, + 1.1, + 0.6, + 0.2, + -1.1, + 0.5, + 1.3, + 6.0, + 9.8, + 11.2, + 12.2, + 12.8, + 12.8, + 12.1, + 10.0, + 8.5, + 7.6, + 6.7, + 6.2, + 5.0, + 4.7, + 4.1, + 3.3, + 2.6, + 1.1, + -0.5, + -1.4, + -1.6, + -1.0, + 0.2, + 3.1, + 7.3, + 10.6, + 12.3, + 13.2, + 13.6, + 13.5, + 12.9, + 11.2, + 9.9, + 8.8, + 7.9, + 7.0, + 6.0 + ], + "relative_humidity_2m": [ + 89, + 93, + 93, + 96, + 100, + 100, + 100, + 100, + 100, + 100, + 86, + 75, + 67, + 60, + 53, + 52, + 52, + 55, + 67, + 75, + 78, + 81, + 84, + 87, + 90, + 94, + 97, + 98, + 100, + 100, + 100, + 100, + 99, + 100, + 100, + 86, + 70, + 64, + 61, + 57, + 56, + 58, + 71, + 74, + 75, + 78, + 79, + 83, + 85, + 87, + 91, + 96, + 100, + 100, + 99, + 99, + 99, + 100, + 92, + 79, + 70, + 63, + 56, + 52, + 52, + 54, + 63, + 64, + 66, + 69, + 70, + 73 + ], + "precipitation": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "rain": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "showers": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "snowfall": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "weather_code": [ + 0, + 0, + 1, + 45, + 45, + 45, + 45, + 45, + 45, + 45, + 2, + 0, + 0, + 0, + 2, + 1, + 1, + 1, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 45, + 45, + 45, + 45, + 45, + 45, + 45, + 45, + 45, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 2, + 1, + 1, + 1, + 1, + 3, + 3, + 3, + 3, + 45, + 45, + 45, + 45, + 45, + 45, + 45, + 45, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "cloud_cover": [ + 0, + 0, + 0, + 24, + 76, + 76, + 100, + 100, + 100, + 100, + 0, + 0, + 0, + 0, + 93, + 16, + 26, + 21, + 0, + 12, + 0, + 0, + 0, + 0, + 0, + 0, + 23, + 48, + 72, + 76, + 100, + 100, + 100, + 100, + 100, + 2, + 0, + 0, + 0, + 0, + 0, + 10, + 68, + 27, + 19, + 43, + 3, + 100, + 100, + 81, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 100, + 64, + 4, + 6, + 2, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "pressure_msl": [ + 1025.9, + 1026.0, + 1026.2, + 1026.3, + 1026.2, + 1025.6, + 1026.1, + 1026.0, + 1026.5, + 1026.9, + 1026.2, + 1025.7, + 1025.2, + 1024.7, + 1023.5, + 1023.0, + 1022.8, + 1022.7, + 1023.0, + 1023.8, + 1024.5, + 1025.1, + 1025.3, + 1025.4, + 1025.7, + 1026.1, + 1026.4, + 1026.5, + 1026.4, + 1026.5, + 1027.0, + 1027.8, + 1028.7, + 1028.8, + 1028.6, + 1027.9, + 1027.2, + 1026.8, + 1026.5, + 1026.1, + 1025.7, + 1025.7, + 1026.0, + 1026.4, + 1026.8, + 1027.2, + 1027.4, + 1027.6, + 1027.7, + 1027.8, + 1028.0, + 1028.0, + 1028.3, + 1028.5, + 1028.7, + 1029.0, + 1029.3, + 1029.5, + 1029.4, + 1028.8, + 1027.8, + 1026.8, + 1026.3, + 1026.1, + 1025.8, + 1025.5, + 1025.5, + 1025.6, + 1025.8, + 1026.0, + 1026.0, + 1025.9 + ], + "surface_pressure": [ + 989.8, + 989.7, + 989.9, + 989.9, + 989.7, + 989.1, + 989.5, + 989.4, + 989.8, + 990.4, + 990.3, + 990.1, + 989.9, + 989.6, + 988.6, + 988.2, + 988.0, + 987.8, + 987.8, + 988.3, + 988.9, + 989.3, + 989.4, + 989.4, + 989.6, + 989.9, + 990.1, + 990.1, + 990.0, + 990.1, + 990.5, + 991.2, + 991.9, + 992.2, + 992.1, + 992.1, + 991.8, + 991.6, + 991.5, + 991.1, + 990.8, + 990.7, + 990.7, + 990.9, + 991.2, + 991.5, + 991.6, + 991.6, + 991.7, + 991.7, + 991.8, + 991.7, + 991.8, + 991.8, + 991.9, + 992.1, + 992.5, + 992.8, + 993.1, + 993.1, + 992.5, + 991.8, + 991.4, + 991.3, + 990.9, + 990.6, + 990.4, + 990.3, + 990.4, + 990.5, + 990.4, + 990.1 + ], + "wind_speed_10m": [ + 4.3, + 4.6, + 4.4, + 3.6, + 4.0, + 4.2, + 4.4, + 3.3, + 3.0, + 2.5, + 5.6, + 9.2, + 8.9, + 10.0, + 14.3, + 9.9, + 10.1, + 6.1, + 5.1, + 5.2, + 5.4, + 4.7, + 3.6, + 3.6, + 3.7, + 3.2, + 2.3, + 2.8, + 2.1, + 2.5, + 0.8, + 3.0, + 0.4, + 0.7, + 1.5, + 1.1, + 2.5, + 3.3, + 2.6, + 3.4, + 5.1, + 5.6, + 2.8, + 2.3, + 1.8, + 0.8, + 1.6, + 1.9, + 1.8, + 1.1, + 1.5, + 0.5, + 0.0, + 1.5, + 1.6, + 0.8, + 1.1, + 0.4, + 0.0, + 1.2, + 1.8, + 2.1, + 1.5, + 2.1, + 2.9, + 3.2, + 2.0, + 1.1, + 0.4, + 1.1, + 2.0, + 2.8 + ], + "wind_direction_10m": [ + 95, + 108, + 99, + 90, + 80, + 110, + 125, + 96, + 76, + 98, + 117, + 121, + 122, + 120, + 146, + 136, + 125, + 118, + 86, + 74, + 90, + 99, + 96, + 90, + 61, + 63, + 72, + 40, + 31, + 45, + 153, + 104, + 270, + 90, + 256, + 198, + 135, + 139, + 146, + 122, + 129, + 135, + 130, + 51, + 37, + 63, + 27, + 22, + 360, + 72, + 14, + 45, + 270, + 14, + 27, + 63, + 72, + 360, + 270, + 176, + 133, + 124, + 135, + 121, + 120, + 117, + 135, + 162, + 360, + 18, + 45, + 50 + ], + "wind_gusts_10m": [ + 8.3, + 6.8, + 7.2, + 6.5, + 6.5, + 7.2, + 6.5, + 6.1, + 5.4, + 5.0, + 5.4, + 15.8, + 16.2, + 16.9, + 23.8, + 23.0, + 17.6, + 17.3, + 9.4, + 7.9, + 8.3, + 8.3, + 7.2, + 7.2, + 6.1, + 5.4, + 4.7, + 3.6, + 3.6, + 3.2, + 4.0, + 4.3, + 4.3, + 2.5, + 4.3, + 4.0, + 6.5, + 8.3, + 7.9, + 7.6, + 8.6, + 10.4, + 8.3, + 3.2, + 2.9, + 1.8, + 1.8, + 2.9, + 2.2, + 2.2, + 1.8, + 1.4, + 0.7, + 1.8, + 2.5, + 2.2, + 2.5, + 2.5, + 1.4, + 4.1, + 5.9, + 6.9, + 5.8, + 6.1, + 6.5, + 6.8, + 5.8, + 2.9, + 1.8, + 1.8, + 2.5, + 4.0 + ], + "shortwave_radiation": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 21.8, + 101.2, + 207.0, + 407.5, + 487.2, + 519.5, + 450.8, + 367.2, + 315.0, + 174.2, + 42.5, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 23.5, + 90.0, + 177.0, + 352.0, + 496.0, + 530.5, + 509.0, + 437.2, + 321.2, + 179.0, + 44.2, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 24.2, + 98.0, + 218.8, + 380.7, + 496.1, + 528.0, + 508.0, + 435.0, + 320.0, + 180.0, + 45.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "direct_radiation": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.2, + 13.8, + 61.5, + 304.2, + 382.5, + 416.0, + 302.0, + 199.8, + 228.5, + 107.5, + 17.8, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.8, + 10.0, + 27.5, + 181.5, + 387.2, + 425.0, + 408.8, + 344.5, + 240.8, + 118.5, + 19.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.5, + 12.5, + 74.6, + 228.8, + 391.0, + 425.8, + 412.0, + 345.0, + 241.0, + 120.0, + 20.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "diffuse_radiation": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 20.5, + 87.5, + 145.5, + 103.2, + 104.8, + 103.5, + 148.8, + 167.5, + 86.5, + 66.8, + 24.8, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 22.8, + 80.0, + 149.5, + 170.5, + 108.8, + 105.5, + 100.2, + 92.8, + 80.5, + 60.5, + 25.2, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 23.8, + 85.5, + 144.2, + 151.8, + 105.1, + 102.2, + 96.0, + 90.0, + 79.0, + 60.0, + 25.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "dew_point_2m": [ + 1.6, + 1.2, + 0.9, + 0.4, + 0.3, + 0.2, + -0.2, + -0.6, + -0.7, + 0.9, + 2.8, + 3.1, + 3.5, + 3.8, + 3.4, + 3.4, + 3.4, + 3.6, + 4.3, + 3.8, + 3.3, + 2.9, + 2.6, + 2.2, + 1.9, + 1.6, + 1.4, + 1.2, + 1.1, + 1.1, + 0.6, + 0.2, + -1.2, + 0.5, + 1.3, + 3.8, + 4.5, + 4.6, + 4.9, + 4.4, + 4.2, + 4.1, + 5.0, + 4.1, + 3.5, + 3.1, + 2.8, + 2.4, + 2.3, + 2.1, + 2.0, + 2.0, + 1.1, + -0.5, + -1.5, + -1.8, + -1.2, + 0.2, + 1.9, + 4.0, + 5.3, + 5.4, + 4.6, + 3.9, + 3.9, + 3.8, + 4.4, + 3.4, + 2.8, + 2.5, + 1.9, + 1.5 + ], + "apparent_temperature": [ + 0.7, + -0.3, + -0.7, + -1.6, + -2.4, + -2.5, + -3.0, + -3.3, + -3.4, + -1.5, + 2.4, + 4.3, + 6.5, + 8.4, + 9.1, + 10.0, + 10.0, + 10.0, + 8.1, + 5.8, + 4.6, + 3.6, + 2.9, + 1.9, + 1.0, + 0.1, + -0.4, + -0.9, + -1.2, + -1.3, + -1.6, + -2.4, + -3.5, + -1.7, + -0.9, + 4.3, + 8.1, + 9.4, + 10.6, + 10.9, + 10.7, + 9.9, + 8.4, + 6.8, + 5.8, + 5.0, + 4.3, + 3.0, + 2.7, + 2.1, + 1.3, + 0.7, + -0.9, + -3.0, + -4.0, + -4.2, + -3.5, + -2.0, + 1.2, + 5.7, + 9.2, + 10.8, + 11.7, + 11.9, + 11.6, + 11.0, + 9.5, + 8.2, + 7.1, + 6.0, + 4.9, + 3.7 + ], + "precipitation_probability": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "visibility": [ + 23940.0, + 18000.0, + 16520.0, + 9960.0, + 660.0, + 800.0, + 140.0, + 140.0, + 60.0, + 80.0, + 23180.0, + 33060.0, + 37200.0, + 39560.0, + 40880.0, + 41140.0, + 41300.0, + 41040.0, + 39300.0, + 36620.0, + 35180.0, + 32420.0, + 30060.0, + 26180.0, + 21780.0, + 15640.0, + 9620.0, + 5300.0, + 1020.0, + 580.0, + 100.0, + 60.0, + 20.0, + 40.0, + 60.0, + 19480.0, + 35320.0, + 38300.0, + 39360.0, + 40440.0, + 40680.0, + 40620.0, + 38520.0, + 37180.0, + 36500.0, + 34540.0, + 34160.0, + 31560.0, + 29140.0, + 26940.0, + 21680.0, + 11620.0, + 80.0, + 40.0, + 40.0, + 20.0, + 20.0, + 60.0, + 960.0, + 27015.0, + 36050.0, + 38705.0, + 40520.0, + 41140.0, + 41380.0, + 41380.0, + 40540.0, + 40180.0, + 39700.0, + 39060.0, + 38560.0, + 37780.0 + ], + "sunshine_duration": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 96.51, + 3465.72, + 3600.0, + 3600.0, + 3600.0, + 3600.0, + 3600.0, + 3600.0, + 3600.0, + 1501.58, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 516.76, + 3600.0, + 3600.0, + 3600.0, + 3600.0, + 3600.0, + 3600.0, + 3600.0, + 1708.51, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 3600.0, + 3600.0, + 3600.0, + 3600.0, + 3600.0, + 3600.0, + 3600.0, + 3600.0, + 1886.03, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + } +} \ No newline at end of file diff --git a/tests/testdata/weatherforecast_openmeteo_2.json b/tests/testdata/weatherforecast_openmeteo_2.json new file mode 100644 index 0000000..0b2b7ee --- /dev/null +++ b/tests/testdata/weatherforecast_openmeteo_2.json @@ -0,0 +1,1959 @@ +{ + "records": [ + { + "date_time": "2026-03-02 00:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": null, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": null, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": null, + "weather_precip_amt": null, + "weather_preciptable_water": 1.2042659110128489, + "weather_wind_speed": null, + "weather_wind_direction": null, + "weather_frost_chance": null, + "weather_temp_air": null, + "weather_feels_like": null, + "weather_dew_point": null, + "weather_relative_humidity": null, + "weather_pressure": null, + "weather_ozone": null, + "weather_ghi": null, + "weather_dni": null, + "weather_dhi": null + }, + { + "date_time": "2026-03-02 01:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 0.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 23940.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": 1.2042659110128489, + "weather_wind_speed": 29.880000000000003, + "weather_wind_direction": 95.0, + "weather_frost_chance": null, + "weather_temp_air": 3.2, + "weather_feels_like": null, + "weather_dew_point": 1.6, + "weather_relative_humidity": 89.0, + "weather_pressure": 10.259, + "weather_ozone": null, + "weather_ghi": 0.0, + "weather_dni": 0.0, + "weather_dhi": 0.0 + }, + { + "date_time": "2026-03-02 02:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 0.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 18000.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": 1.1934849311102316, + "weather_wind_speed": 24.48, + "weather_wind_direction": 108.0, + "weather_frost_chance": null, + "weather_temp_air": 2.3, + "weather_feels_like": null, + "weather_dew_point": 1.2, + "weather_relative_humidity": 93.0, + "weather_pressure": 10.26, + "weather_ozone": null, + "weather_ghi": 0.0, + "weather_dni": 0.0, + "weather_dhi": 0.0 + }, + { + "date_time": "2026-03-02 03:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 0.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 16520.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": 1.1658932361170335, + "weather_wind_speed": 25.92, + "weather_wind_direction": 99.0, + "weather_frost_chance": null, + "weather_temp_air": 1.9, + "weather_feels_like": null, + "weather_dew_point": 0.9, + "weather_relative_humidity": 93.0, + "weather_pressure": 10.262, + "weather_ozone": null, + "weather_ghi": 0.0, + "weather_dni": 0.0, + "weather_dhi": 0.0 + }, + { + "date_time": "2026-03-02 04:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 24.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 9960.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": 1.1422156465678917, + "weather_wind_speed": 23.400000000000002, + "weather_wind_direction": 90.0, + "weather_frost_chance": null, + "weather_temp_air": 1.0, + "weather_feels_like": null, + "weather_dew_point": 0.4, + "weather_relative_humidity": 96.0, + "weather_pressure": 10.263, + "weather_ozone": null, + "weather_ghi": 0.0, + "weather_dni": 0.0, + "weather_dhi": 0.0 + }, + { + "date_time": "2026-03-02 05:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 76.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 660.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": 1.1428386578193466, + "weather_wind_speed": 23.400000000000002, + "weather_wind_direction": 80.0, + "weather_frost_chance": null, + "weather_temp_air": 0.3, + "weather_feels_like": null, + "weather_dew_point": 0.3, + "weather_relative_humidity": 100.0, + "weather_pressure": 10.262, + "weather_ozone": null, + "weather_ghi": 0.0, + "weather_dni": 0.0, + "weather_dhi": 0.0 + }, + { + "date_time": "2026-03-02 06:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 76.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 800.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": 1.1363142933431636, + "weather_wind_speed": 25.92, + "weather_wind_direction": 110.0, + "weather_frost_chance": null, + "weather_temp_air": 0.2, + "weather_feels_like": null, + "weather_dew_point": 0.2, + "weather_relative_humidity": 100.0, + "weather_pressure": 10.255999999999998, + "weather_ozone": null, + "weather_ghi": 0.0, + "weather_dni": 0.0, + "weather_dhi": 0.0 + }, + { + "date_time": "2026-03-02 07:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 100.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 140.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": 1.1106699134332962, + "weather_wind_speed": 23.400000000000002, + "weather_wind_direction": 125.0, + "weather_frost_chance": null, + "weather_temp_air": -0.2, + "weather_feels_like": null, + "weather_dew_point": -0.2, + "weather_relative_humidity": 100.0, + "weather_pressure": 10.261, + "weather_ozone": null, + "weather_ghi": 0.0, + "weather_dni": 0.0, + "weather_dhi": 0.0 + }, + { + "date_time": "2026-03-02 08:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 100.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 140.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": 1.0857377296688624, + "weather_wind_speed": 21.96, + "weather_wind_direction": 96.0, + "weather_frost_chance": null, + "weather_temp_air": -0.6, + "weather_feels_like": null, + "weather_dew_point": -0.6, + "weather_relative_humidity": 100.0, + "weather_pressure": 10.26, + "weather_ozone": null, + "weather_ghi": 0.0, + "weather_dni": 0.0, + "weather_dhi": 0.0 + }, + { + "date_time": "2026-03-02 09:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 100.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 60.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": 1.0796139936651077, + "weather_wind_speed": 19.44, + "weather_wind_direction": 76.0, + "weather_frost_chance": null, + "weather_temp_air": -0.7, + "weather_feels_like": null, + "weather_dew_point": -0.7, + "weather_relative_humidity": 100.0, + "weather_pressure": 10.265, + "weather_ozone": null, + "weather_ghi": 21.8, + "weather_dni": 1.2, + "weather_dhi": 20.5 + }, + { + "date_time": "2026-03-02 10:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 100.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 80.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": 1.1829570091378367, + "weather_wind_speed": 18.0, + "weather_wind_direction": 98.0, + "weather_frost_chance": null, + "weather_temp_air": 0.9, + "weather_feels_like": null, + "weather_dew_point": 0.9, + "weather_relative_humidity": 100.0, + "weather_pressure": 10.269000000000002, + "weather_ozone": null, + "weather_ghi": 101.2, + "weather_dni": 13.8, + "weather_dhi": 87.5 + }, + { + "date_time": "2026-03-02 11:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 0.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 23180.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": 1.2874739363131709, + "weather_wind_speed": 19.44, + "weather_wind_direction": 117.0, + "weather_frost_chance": null, + "weather_temp_air": 4.9, + "weather_feels_like": null, + "weather_dew_point": 2.8, + "weather_relative_humidity": 86.0, + "weather_pressure": 10.262, + "weather_ozone": null, + "weather_ghi": 207.0, + "weather_dni": 61.5, + "weather_dhi": 145.5 + }, + { + "date_time": "2026-03-02 12:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 0.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 33060.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": 1.297388847386016, + "weather_wind_speed": 56.88, + "weather_wind_direction": 121.0, + "weather_frost_chance": null, + "weather_temp_air": 7.3, + "weather_feels_like": null, + "weather_dew_point": 3.1, + "weather_relative_humidity": 75.0, + "weather_pressure": 10.257000000000001, + "weather_ozone": null, + "weather_ghi": 407.5, + "weather_dni": 304.2, + "weather_dhi": 103.2 + }, + { + "date_time": "2026-03-02 13:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 0.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 37200.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": 1.316667162739719, + "weather_wind_speed": 58.32, + "weather_wind_direction": 122.0, + "weather_frost_chance": null, + "weather_temp_air": 9.4, + "weather_feels_like": null, + "weather_dew_point": 3.5, + "weather_relative_humidity": 67.0, + "weather_pressure": 10.252, + "weather_ozone": null, + "weather_ghi": 487.2, + "weather_dni": 382.5, + "weather_dhi": 104.8 + }, + { + "date_time": "2026-03-02 14:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 0.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 39560.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": 1.3238525905229552, + "weather_wind_speed": 60.839999999999996, + "weather_wind_direction": 120.0, + "weather_frost_chance": null, + "weather_temp_air": 11.3, + "weather_feels_like": null, + "weather_dew_point": 3.8, + "weather_relative_humidity": 60.0, + "weather_pressure": 10.247, + "weather_ozone": null, + "weather_ghi": 519.5, + "weather_dni": 416.0, + "weather_dhi": 103.5 + }, + { + "date_time": "2026-03-02 15:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 93.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 40880.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": 1.2735879652622393, + "weather_wind_speed": 85.68, + "weather_wind_direction": 146.0, + "weather_frost_chance": null, + "weather_temp_air": 12.7, + "weather_feels_like": null, + "weather_dew_point": 3.4, + "weather_relative_humidity": 53.0, + "weather_pressure": 10.235, + "weather_ozone": null, + "weather_ghi": 450.8, + "weather_dni": 302.0, + "weather_dhi": 148.8 + }, + { + "date_time": "2026-03-02 16:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 16.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 41140.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": 1.272606097109109, + "weather_wind_speed": 82.8, + "weather_wind_direction": 136.0, + "weather_frost_chance": null, + "weather_temp_air": 13.0, + "weather_feels_like": null, + "weather_dew_point": 3.4, + "weather_relative_humidity": 52.0, + "weather_pressure": 10.23, + "weather_ozone": null, + "weather_ghi": 367.2, + "weather_dni": 199.8, + "weather_dhi": 167.5 + }, + { + "date_time": "2026-03-02 17:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 26.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 41300.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": 1.2803811122002409, + "weather_wind_speed": 63.36000000000001, + "weather_wind_direction": 125.0, + "weather_frost_chance": null, + "weather_temp_air": 13.1, + "weather_feels_like": null, + "weather_dew_point": 3.4, + "weather_relative_humidity": 52.0, + "weather_pressure": 10.228, + "weather_ozone": null, + "weather_ghi": 315.0, + "weather_dni": 228.5, + "weather_dhi": 86.5 + }, + { + "date_time": "2026-03-02 18:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 21.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 41040.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": 1.2977046486987933, + "weather_wind_speed": 62.28, + "weather_wind_direction": 118.0, + "weather_frost_chance": null, + "weather_temp_air": 12.4, + "weather_feels_like": null, + "weather_dew_point": 3.6, + "weather_relative_humidity": 55.0, + "weather_pressure": 10.227, + "weather_ozone": null, + "weather_ghi": 174.2, + "weather_dni": 107.5, + "weather_dhi": 66.8 + }, + { + "date_time": "2026-03-02 19:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 0.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 39300.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": 1.382419189119901, + "weather_wind_speed": 33.84, + "weather_wind_direction": 86.0, + "weather_frost_chance": null, + "weather_temp_air": 10.2, + "weather_feels_like": null, + "weather_dew_point": 4.3, + "weather_relative_humidity": 67.0, + "weather_pressure": 10.23, + "weather_ozone": null, + "weather_ghi": 42.5, + "weather_dni": 17.8, + "weather_dhi": 24.8 + }, + { + "date_time": "2026-03-02 20:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 12.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 36620.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": 1.3536276460290044, + "weather_wind_speed": 28.44, + "weather_wind_direction": 74.0, + "weather_frost_chance": null, + "weather_temp_air": 8.0, + "weather_feels_like": null, + "weather_dew_point": 3.8, + "weather_relative_humidity": 75.0, + "weather_pressure": 10.238, + "weather_ozone": null, + "weather_ghi": 0.0, + "weather_dni": 0.0, + "weather_dhi": 0.0 + }, + { + "date_time": "2026-03-02 21:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 0.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 35180.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": 1.3170223273598753, + "weather_wind_speed": 29.880000000000003, + "weather_wind_direction": 90.0, + "weather_frost_chance": null, + "weather_temp_air": 6.9, + "weather_feels_like": null, + "weather_dew_point": 3.3, + "weather_relative_humidity": 78.0, + "weather_pressure": 10.245000000000001, + "weather_ozone": null, + "weather_ghi": 0.0, + "weather_dni": 0.0, + "weather_dhi": 0.0 + }, + { + "date_time": "2026-03-02 22:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 0.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 32420.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": 1.2876163144781736, + "weather_wind_speed": 29.880000000000003, + "weather_wind_direction": 99.0, + "weather_frost_chance": null, + "weather_temp_air": 5.9, + "weather_feels_like": null, + "weather_dew_point": 2.9, + "weather_relative_humidity": 81.0, + "weather_pressure": 10.251, + "weather_ozone": null, + "weather_ghi": 0.0, + "weather_dni": 0.0, + "weather_dhi": 0.0 + }, + { + "date_time": "2026-03-02 23:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 0.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 30060.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": 1.2726802406239508, + "weather_wind_speed": 25.92, + "weather_wind_direction": 96.0, + "weather_frost_chance": null, + "weather_temp_air": 5.1, + "weather_feels_like": null, + "weather_dew_point": 2.6, + "weather_relative_humidity": 84.0, + "weather_pressure": 10.253, + "weather_ozone": null, + "weather_ghi": 0.0, + "weather_dni": 0.0, + "weather_dhi": 0.0 + }, + { + "date_time": "2026-03-03 00:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 0.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 26180.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": 1.2491452555476052, + "weather_wind_speed": 25.92, + "weather_wind_direction": 90.0, + "weather_frost_chance": null, + "weather_temp_air": 4.2, + "weather_feels_like": null, + "weather_dew_point": 2.2, + "weather_relative_humidity": 87.0, + "weather_pressure": 10.254000000000001, + "weather_ozone": null, + "weather_ghi": 0.0, + "weather_dni": 0.0, + "weather_dhi": 0.0 + }, + { + "date_time": "2026-03-03 01:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 0.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 21780.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": 1.232283504828222, + "weather_wind_speed": 21.96, + "weather_wind_direction": 61.0, + "weather_frost_chance": null, + "weather_temp_air": 3.4, + "weather_feels_like": null, + "weather_dew_point": 1.9, + "weather_relative_humidity": 90.0, + "weather_pressure": 10.257000000000001, + "weather_ozone": null, + "weather_ghi": 0.0, + "weather_dni": 0.0, + "weather_dhi": 0.0 + }, + { + "date_time": "2026-03-03 02:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 0.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 15640.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": 1.2205512177914701, + "weather_wind_speed": 19.44, + "weather_wind_direction": 63.0, + "weather_frost_chance": null, + "weather_temp_air": 2.5, + "weather_feels_like": null, + "weather_dew_point": 1.6, + "weather_relative_humidity": 94.0, + "weather_pressure": 10.261, + "weather_ozone": null, + "weather_ghi": 0.0, + "weather_dni": 0.0, + "weather_dhi": 0.0 + }, + { + "date_time": "2026-03-03 03:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 23.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 9620.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": 1.2089671756817106, + "weather_wind_speed": 16.92, + "weather_wind_direction": 72.0, + "weather_frost_chance": null, + "weather_temp_air": 1.8, + "weather_feels_like": null, + "weather_dew_point": 1.4, + "weather_relative_humidity": 97.0, + "weather_pressure": 10.264000000000001, + "weather_ozone": null, + "weather_ghi": 0.0, + "weather_dni": 0.0, + "weather_dhi": 0.0 + }, + { + "date_time": "2026-03-03 04:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 48.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 5300.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": 1.2002888313052433, + "weather_wind_speed": 12.96, + "weather_wind_direction": 40.0, + "weather_frost_chance": null, + "weather_temp_air": 1.5, + "weather_feels_like": null, + "weather_dew_point": 1.2, + "weather_relative_humidity": 98.0, + "weather_pressure": 10.265, + "weather_ozone": null, + "weather_ghi": 0.0, + "weather_dni": 0.0, + "weather_dhi": 0.0 + }, + { + "date_time": "2026-03-03 05:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 72.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 1020.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": 1.196706670274171, + "weather_wind_speed": 12.96, + "weather_wind_direction": 31.0, + "weather_frost_chance": null, + "weather_temp_air": 1.1, + "weather_feels_like": null, + "weather_dew_point": 1.1, + "weather_relative_humidity": 100.0, + "weather_pressure": 10.264000000000001, + "weather_ozone": null, + "weather_ghi": 0.0, + "weather_dni": 0.0, + "weather_dhi": 0.0 + }, + { + "date_time": "2026-03-03 06:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 76.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 580.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": 1.196706670274171, + "weather_wind_speed": 11.520000000000001, + "weather_wind_direction": 45.0, + "weather_frost_chance": null, + "weather_temp_air": 1.1, + "weather_feels_like": null, + "weather_dew_point": 1.1, + "weather_relative_humidity": 100.0, + "weather_pressure": 10.265, + "weather_ozone": null, + "weather_ghi": 0.0, + "weather_dni": 0.0, + "weather_dhi": 0.0 + }, + { + "date_time": "2026-03-03 07:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 100.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 100.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": 1.1626878876946607, + "weather_wind_speed": 14.4, + "weather_wind_direction": 153.0, + "weather_frost_chance": null, + "weather_temp_air": 0.6, + "weather_feels_like": null, + "weather_dew_point": 0.6, + "weather_relative_humidity": 100.0, + "weather_pressure": 10.27, + "weather_ozone": null, + "weather_ghi": 0.0, + "weather_dni": 0.0, + "weather_dhi": 0.0 + }, + { + "date_time": "2026-03-03 08:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 100.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 60.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": 1.1363142933431636, + "weather_wind_speed": 15.48, + "weather_wind_direction": 104.0, + "weather_frost_chance": null, + "weather_temp_air": 0.2, + "weather_feels_like": null, + "weather_dew_point": 0.2, + "weather_relative_humidity": 100.0, + "weather_pressure": 10.278, + "weather_ozone": null, + "weather_ghi": 0.0, + "weather_dni": 0.0, + "weather_dhi": 0.0 + }, + { + "date_time": "2026-03-03 09:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 100.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 20.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": 1.0449930151874698, + "weather_wind_speed": 15.48, + "weather_wind_direction": 270.0, + "weather_frost_chance": null, + "weather_temp_air": -1.1, + "weather_feels_like": null, + "weather_dew_point": -1.2, + "weather_relative_humidity": 99.0, + "weather_pressure": 10.287, + "weather_ozone": null, + "weather_ghi": 23.5, + "weather_dni": 0.8, + "weather_dhi": 22.8 + }, + { + "date_time": "2026-03-03 10:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 100.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 40.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": 1.1560251851766363, + "weather_wind_speed": 9.0, + "weather_wind_direction": 90.0, + "weather_frost_chance": null, + "weather_temp_air": 0.5, + "weather_feels_like": null, + "weather_dew_point": 0.5, + "weather_relative_humidity": 100.0, + "weather_pressure": 10.288, + "weather_ozone": null, + "weather_ghi": 90.0, + "weather_dni": 10.0, + "weather_dhi": 80.0 + }, + { + "date_time": "2026-03-03 11:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 100.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 60.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": 1.2106484337390766, + "weather_wind_speed": 15.48, + "weather_wind_direction": 256.0, + "weather_frost_chance": null, + "weather_temp_air": 1.3, + "weather_feels_like": null, + "weather_dew_point": 1.3, + "weather_relative_humidity": 100.0, + "weather_pressure": 10.286, + "weather_ozone": null, + "weather_ghi": 177.0, + "weather_dni": 27.5, + "weather_dhi": 149.5 + }, + { + "date_time": "2026-03-03 12:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 2.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 19480.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": 1.375352404829511, + "weather_wind_speed": 14.4, + "weather_wind_direction": 198.0, + "weather_frost_chance": null, + "weather_temp_air": 6.0, + "weather_feels_like": null, + "weather_dew_point": 3.8, + "weather_relative_humidity": 86.0, + "weather_pressure": 10.279000000000002, + "weather_ozone": null, + "weather_ghi": 352.0, + "weather_dni": 181.5, + "weather_dhi": 170.5 + }, + { + "date_time": "2026-03-03 13:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 0.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 35320.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": 1.4095433740059495, + "weather_wind_speed": 23.400000000000002, + "weather_wind_direction": 135.0, + "weather_frost_chance": null, + "weather_temp_air": 9.8, + "weather_feels_like": null, + "weather_dew_point": 4.5, + "weather_relative_humidity": 70.0, + "weather_pressure": 10.272, + "weather_ozone": null, + "weather_ghi": 496.0, + "weather_dni": 387.2, + "weather_dhi": 108.8 + }, + { + "date_time": "2026-03-03 14:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 0.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 38300.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": 1.403525491545411, + "weather_wind_speed": 29.880000000000003, + "weather_wind_direction": 139.0, + "weather_frost_chance": null, + "weather_temp_air": 11.2, + "weather_feels_like": null, + "weather_dew_point": 4.6, + "weather_relative_humidity": 64.0, + "weather_pressure": 10.267999999999999, + "weather_ozone": null, + "weather_ghi": 530.5, + "weather_dni": 425.0, + "weather_dhi": 105.5 + }, + { + "date_time": "2026-03-03 15:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 0.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 39360.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": 1.4218333123351257, + "weather_wind_speed": 28.44, + "weather_wind_direction": 146.0, + "weather_frost_chance": null, + "weather_temp_air": 12.2, + "weather_feels_like": null, + "weather_dew_point": 4.9, + "weather_relative_humidity": 61.0, + "weather_pressure": 10.265, + "weather_ozone": null, + "weather_ghi": 509.0, + "weather_dni": 408.8, + "weather_dhi": 100.2 + }, + { + "date_time": "2026-03-03 16:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 0.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 40440.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": 1.3780788733557598, + "weather_wind_speed": 27.36, + "weather_wind_direction": 122.0, + "weather_frost_chance": null, + "weather_temp_air": 12.8, + "weather_feels_like": null, + "weather_dew_point": 4.4, + "weather_relative_humidity": 57.0, + "weather_pressure": 10.261, + "weather_ozone": null, + "weather_ghi": 437.2, + "weather_dni": 344.5, + "weather_dhi": 92.8 + }, + { + "date_time": "2026-03-03 17:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 0.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 40680.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": 1.3539020510161852, + "weather_wind_speed": 30.96, + "weather_wind_direction": 129.0, + "weather_frost_chance": null, + "weather_temp_air": 12.8, + "weather_feels_like": null, + "weather_dew_point": 4.2, + "weather_relative_humidity": 56.0, + "weather_pressure": 10.257000000000001, + "weather_ozone": null, + "weather_ghi": 321.2, + "weather_dni": 240.8, + "weather_dhi": 80.5 + }, + { + "date_time": "2026-03-03 18:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 10.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 40620.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": 1.343691048801143, + "weather_wind_speed": 37.440000000000005, + "weather_wind_direction": 135.0, + "weather_frost_chance": null, + "weather_temp_air": 12.1, + "weather_feels_like": null, + "weather_dew_point": 4.1, + "weather_relative_humidity": 58.0, + "weather_pressure": 10.257000000000001, + "weather_ozone": null, + "weather_ghi": 179.0, + "weather_dni": 118.5, + "weather_dhi": 60.5 + }, + { + "date_time": "2026-03-03 19:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 68.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 38520.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": 1.4472063171924825, + "weather_wind_speed": 29.880000000000003, + "weather_wind_direction": 130.0, + "weather_frost_chance": null, + "weather_temp_air": 10.0, + "weather_feels_like": null, + "weather_dew_point": 5.0, + "weather_relative_humidity": 71.0, + "weather_pressure": 10.26, + "weather_ozone": null, + "weather_ghi": 44.2, + "weather_dni": 19.0, + "weather_dhi": 25.2 + }, + { + "date_time": "2026-03-03 20:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 27.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 37180.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": 1.3767561712466725, + "weather_wind_speed": 11.520000000000001, + "weather_wind_direction": 51.0, + "weather_frost_chance": null, + "weather_temp_air": 8.5, + "weather_feels_like": null, + "weather_dew_point": 4.1, + "weather_relative_humidity": 74.0, + "weather_pressure": 10.264000000000001, + "weather_ozone": null, + "weather_ghi": 0.0, + "weather_dni": 0.0, + "weather_dhi": 0.0 + }, + { + "date_time": "2026-03-03 21:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 19.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 36500.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": 1.3211833900681151, + "weather_wind_speed": 10.44, + "weather_wind_direction": 37.0, + "weather_frost_chance": null, + "weather_temp_air": 7.6, + "weather_feels_like": null, + "weather_dew_point": 3.5, + "weather_relative_humidity": 75.0, + "weather_pressure": 10.267999999999999, + "weather_ozone": null, + "weather_ghi": 0.0, + "weather_dni": 0.0, + "weather_dhi": 0.0 + }, + { + "date_time": "2026-03-03 22:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 43.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 34540.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": 1.30120062522716, + "weather_wind_speed": 6.48, + "weather_wind_direction": 63.0, + "weather_frost_chance": null, + "weather_temp_air": 6.7, + "weather_feels_like": null, + "weather_dew_point": 3.1, + "weather_relative_humidity": 78.0, + "weather_pressure": 10.272, + "weather_ozone": null, + "weather_ghi": 0.0, + "weather_dni": 0.0, + "weather_dhi": 0.0 + }, + { + "date_time": "2026-03-03 23:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 3.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 34160.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": 1.2787179326425673, + "weather_wind_speed": 6.48, + "weather_wind_direction": 27.0, + "weather_frost_chance": null, + "weather_temp_air": 6.2, + "weather_feels_like": null, + "weather_dew_point": 2.8, + "weather_relative_humidity": 79.0, + "weather_pressure": 10.274000000000001, + "weather_ozone": null, + "weather_ghi": 0.0, + "weather_dni": 0.0, + "weather_dhi": 0.0 + }, + { + "date_time": "2026-03-04 00:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 100.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 31560.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": null, + "weather_wind_speed": 10.44, + "weather_wind_direction": 22.0, + "weather_frost_chance": null, + "weather_temp_air": 5.0, + "weather_feels_like": null, + "weather_dew_point": 2.4, + "weather_relative_humidity": 83.0, + "weather_pressure": 10.276, + "weather_ozone": null, + "weather_ghi": 0.0, + "weather_dni": 0.0, + "weather_dhi": 0.0 + }, + { + "date_time": "2026-03-04 01:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 100.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 29140.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": null, + "weather_wind_speed": 7.920000000000001, + "weather_wind_direction": 360.0, + "weather_frost_chance": null, + "weather_temp_air": 4.7, + "weather_feels_like": null, + "weather_dew_point": 2.3, + "weather_relative_humidity": 85.0, + "weather_pressure": 10.277000000000001, + "weather_ozone": null, + "weather_ghi": 0.0, + "weather_dni": 0.0, + "weather_dhi": 0.0 + }, + { + "date_time": "2026-03-04 02:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 81.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 26940.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": null, + "weather_wind_speed": 7.920000000000001, + "weather_wind_direction": 72.0, + "weather_frost_chance": null, + "weather_temp_air": 4.1, + "weather_feels_like": null, + "weather_dew_point": 2.1, + "weather_relative_humidity": 87.0, + "weather_pressure": 10.278, + "weather_ozone": null, + "weather_ghi": 0.0, + "weather_dni": 0.0, + "weather_dhi": 0.0 + }, + { + "date_time": "2026-03-04 03:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 100.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 21680.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": null, + "weather_wind_speed": 6.48, + "weather_wind_direction": 14.0, + "weather_frost_chance": null, + "weather_temp_air": 3.3, + "weather_feels_like": null, + "weather_dew_point": 2.0, + "weather_relative_humidity": 91.0, + "weather_pressure": 10.28, + "weather_ozone": null, + "weather_ghi": 0.0, + "weather_dni": 0.0, + "weather_dhi": 0.0 + }, + { + "date_time": "2026-03-04 04:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 100.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 11620.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": null, + "weather_wind_speed": 5.04, + "weather_wind_direction": 45.0, + "weather_frost_chance": null, + "weather_temp_air": 2.6, + "weather_feels_like": null, + "weather_dew_point": 2.0, + "weather_relative_humidity": 96.0, + "weather_pressure": 10.28, + "weather_ozone": null, + "weather_ghi": 0.0, + "weather_dni": 0.0, + "weather_dhi": 0.0 + }, + { + "date_time": "2026-03-04 05:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 100.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 80.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": null, + "weather_wind_speed": 2.52, + "weather_wind_direction": 270.0, + "weather_frost_chance": null, + "weather_temp_air": 1.1, + "weather_feels_like": null, + "weather_dew_point": 1.1, + "weather_relative_humidity": 100.0, + "weather_pressure": 10.283, + "weather_ozone": null, + "weather_ghi": 0.0, + "weather_dni": 0.0, + "weather_dhi": 0.0 + }, + { + "date_time": "2026-03-04 06:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 100.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 40.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": null, + "weather_wind_speed": 6.48, + "weather_wind_direction": 14.0, + "weather_frost_chance": null, + "weather_temp_air": -0.5, + "weather_feels_like": null, + "weather_dew_point": -0.5, + "weather_relative_humidity": 100.0, + "weather_pressure": 10.285, + "weather_ozone": null, + "weather_ghi": 0.0, + "weather_dni": 0.0, + "weather_dhi": 0.0 + }, + { + "date_time": "2026-03-04 07:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 100.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 40.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": null, + "weather_wind_speed": 9.0, + "weather_wind_direction": 27.0, + "weather_frost_chance": null, + "weather_temp_air": -1.4, + "weather_feels_like": null, + "weather_dew_point": -1.5, + "weather_relative_humidity": 99.0, + "weather_pressure": 10.287, + "weather_ozone": null, + "weather_ghi": 0.0, + "weather_dni": 0.0, + "weather_dhi": 0.0 + }, + { + "date_time": "2026-03-04 08:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 100.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 20.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": null, + "weather_wind_speed": 7.920000000000001, + "weather_wind_direction": 63.0, + "weather_frost_chance": null, + "weather_temp_air": -1.6, + "weather_feels_like": null, + "weather_dew_point": -1.8, + "weather_relative_humidity": 99.0, + "weather_pressure": 10.290000000000001, + "weather_ozone": null, + "weather_ghi": 0.0, + "weather_dni": 0.0, + "weather_dhi": 0.0 + }, + { + "date_time": "2026-03-04 09:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 100.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 20.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": null, + "weather_wind_speed": 9.0, + "weather_wind_direction": 72.0, + "weather_frost_chance": null, + "weather_temp_air": -1.0, + "weather_feels_like": null, + "weather_dew_point": -1.2, + "weather_relative_humidity": 99.0, + "weather_pressure": 10.293, + "weather_ozone": null, + "weather_ghi": 24.2, + "weather_dni": 0.5, + "weather_dhi": 23.8 + }, + { + "date_time": "2026-03-04 10:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 100.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 60.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": null, + "weather_wind_speed": 9.0, + "weather_wind_direction": 360.0, + "weather_frost_chance": null, + "weather_temp_air": 0.2, + "weather_feels_like": null, + "weather_dew_point": 0.2, + "weather_relative_humidity": 100.0, + "weather_pressure": 10.295, + "weather_ozone": null, + "weather_ghi": 98.0, + "weather_dni": 12.5, + "weather_dhi": 85.5 + }, + { + "date_time": "2026-03-04 11:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 64.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 960.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": null, + "weather_wind_speed": 5.04, + "weather_wind_direction": 270.0, + "weather_frost_chance": null, + "weather_temp_air": 3.1, + "weather_feels_like": null, + "weather_dew_point": 1.9, + "weather_relative_humidity": 92.0, + "weather_pressure": 10.294, + "weather_ozone": null, + "weather_ghi": 218.8, + "weather_dni": 74.6, + "weather_dhi": 144.2 + }, + { + "date_time": "2026-03-04 12:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 4.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 27015.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": null, + "weather_wind_speed": 14.76, + "weather_wind_direction": 176.0, + "weather_frost_chance": null, + "weather_temp_air": 7.3, + "weather_feels_like": null, + "weather_dew_point": 4.0, + "weather_relative_humidity": 79.0, + "weather_pressure": 10.288, + "weather_ozone": null, + "weather_ghi": 380.7, + "weather_dni": 228.8, + "weather_dhi": 151.8 + }, + { + "date_time": "2026-03-04 13:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 6.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 36050.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": null, + "weather_wind_speed": 21.240000000000002, + "weather_wind_direction": 133.0, + "weather_frost_chance": null, + "weather_temp_air": 10.6, + "weather_feels_like": null, + "weather_dew_point": 5.3, + "weather_relative_humidity": 70.0, + "weather_pressure": 10.278, + "weather_ozone": null, + "weather_ghi": 496.1, + "weather_dni": 391.0, + "weather_dhi": 105.1 + }, + { + "date_time": "2026-03-04 14:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 2.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 38705.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": null, + "weather_wind_speed": 24.840000000000003, + "weather_wind_direction": 124.0, + "weather_frost_chance": null, + "weather_temp_air": 12.3, + "weather_feels_like": null, + "weather_dew_point": 5.4, + "weather_relative_humidity": 63.0, + "weather_pressure": 10.267999999999999, + "weather_ozone": null, + "weather_ghi": 528.0, + "weather_dni": 425.8, + "weather_dhi": 102.2 + }, + { + "date_time": "2026-03-04 15:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 0.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 40520.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": null, + "weather_wind_speed": 20.88, + "weather_wind_direction": 135.0, + "weather_frost_chance": null, + "weather_temp_air": 13.2, + "weather_feels_like": null, + "weather_dew_point": 4.6, + "weather_relative_humidity": 56.0, + "weather_pressure": 10.263, + "weather_ozone": null, + "weather_ghi": 508.0, + "weather_dni": 412.0, + "weather_dhi": 96.0 + }, + { + "date_time": "2026-03-04 16:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 0.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 41140.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": null, + "weather_wind_speed": 21.96, + "weather_wind_direction": 121.0, + "weather_frost_chance": null, + "weather_temp_air": 13.6, + "weather_feels_like": null, + "weather_dew_point": 3.9, + "weather_relative_humidity": 52.0, + "weather_pressure": 10.261, + "weather_ozone": null, + "weather_ghi": 435.0, + "weather_dni": 345.0, + "weather_dhi": 90.0 + }, + { + "date_time": "2026-03-04 17:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 0.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 41380.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": null, + "weather_wind_speed": 23.400000000000002, + "weather_wind_direction": 120.0, + "weather_frost_chance": null, + "weather_temp_air": 13.5, + "weather_feels_like": null, + "weather_dew_point": 3.9, + "weather_relative_humidity": 52.0, + "weather_pressure": 10.258, + "weather_ozone": null, + "weather_ghi": 320.0, + "weather_dni": 241.0, + "weather_dhi": 79.0 + }, + { + "date_time": "2026-03-04 18:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 0.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 41380.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": null, + "weather_wind_speed": 24.48, + "weather_wind_direction": 117.0, + "weather_frost_chance": null, + "weather_temp_air": 12.9, + "weather_feels_like": null, + "weather_dew_point": 3.8, + "weather_relative_humidity": 54.0, + "weather_pressure": 10.255, + "weather_ozone": null, + "weather_ghi": 180.0, + "weather_dni": 120.0, + "weather_dhi": 60.0 + }, + { + "date_time": "2026-03-04 19:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 0.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 40540.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": null, + "weather_wind_speed": 20.88, + "weather_wind_direction": 135.0, + "weather_frost_chance": null, + "weather_temp_air": 11.2, + "weather_feels_like": null, + "weather_dew_point": 4.4, + "weather_relative_humidity": 63.0, + "weather_pressure": 10.255, + "weather_ozone": null, + "weather_ghi": 45.0, + "weather_dni": 20.0, + "weather_dhi": 25.0 + }, + { + "date_time": "2026-03-04 20:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 0.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 40180.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": null, + "weather_wind_speed": 10.44, + "weather_wind_direction": 162.0, + "weather_frost_chance": null, + "weather_temp_air": 9.9, + "weather_feels_like": null, + "weather_dew_point": 3.4, + "weather_relative_humidity": 64.0, + "weather_pressure": 10.255999999999998, + "weather_ozone": null, + "weather_ghi": 0.0, + "weather_dni": 0.0, + "weather_dhi": 0.0 + }, + { + "date_time": "2026-03-04 21:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 0.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 39700.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": null, + "weather_wind_speed": 6.48, + "weather_wind_direction": 360.0, + "weather_frost_chance": null, + "weather_temp_air": 8.8, + "weather_feels_like": null, + "weather_dew_point": 2.8, + "weather_relative_humidity": 66.0, + "weather_pressure": 10.258, + "weather_ozone": null, + "weather_ghi": 0.0, + "weather_dni": 0.0, + "weather_dhi": 0.0 + }, + { + "date_time": "2026-03-04 22:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 0.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 39060.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": null, + "weather_wind_speed": 6.48, + "weather_wind_direction": 18.0, + "weather_frost_chance": null, + "weather_temp_air": 7.9, + "weather_feels_like": null, + "weather_dew_point": 2.5, + "weather_relative_humidity": 69.0, + "weather_pressure": 10.26, + "weather_ozone": null, + "weather_ghi": 0.0, + "weather_dni": 0.0, + "weather_dhi": 0.0 + }, + { + "date_time": "2026-03-04 23:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 0.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 38560.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": null, + "weather_wind_speed": 9.0, + "weather_wind_direction": 45.0, + "weather_frost_chance": null, + "weather_temp_air": 7.0, + "weather_feels_like": null, + "weather_dew_point": 1.9, + "weather_relative_humidity": 70.0, + "weather_pressure": 10.26, + "weather_ozone": null, + "weather_ghi": 0.0, + "weather_dni": 0.0, + "weather_dhi": 0.0 + }, + { + "date_time": "2026-03-05 00:00:00+01:00", + "configured_data": {}, + "weather_total_clouds": 0.0, + "weather_low_clouds": null, + "weather_medium_clouds": null, + "weather_high_clouds": null, + "weather_visibility": 37780.0, + "weather_fog": null, + "weather_precip_type": null, + "weather_precip_prob": 0.0, + "weather_precip_amt": 0.0, + "weather_preciptable_water": null, + "weather_wind_speed": 14.4, + "weather_wind_direction": 50.0, + "weather_frost_chance": null, + "weather_temp_air": 6.0, + "weather_feels_like": null, + "weather_dew_point": 1.5, + "weather_relative_humidity": 73.0, + "weather_pressure": 10.259, + "weather_ozone": null, + "weather_ghi": 0.0, + "weather_dni": 0.0, + "weather_dhi": 0.0 + } + ], + "update_datetime": "2026-03-02 15:28:46.627533+01:00", + "min_datetime": "2026-03-02 00:00:00+01:00", + "max_datetime": "2026-03-05 00:00:00+01:00", + "record_keys": [ + "date_time", + "weather_total_clouds", + "weather_low_clouds", + "weather_medium_clouds", + "weather_high_clouds", + "weather_visibility", + "weather_fog", + "weather_precip_type", + "weather_precip_prob", + "weather_precip_amt", + "weather_preciptable_water", + "weather_wind_speed", + "weather_wind_direction", + "weather_frost_chance", + "weather_temp_air", + "weather_feels_like", + "weather_dew_point", + "weather_relative_humidity", + "weather_pressure", + "weather_ozone", + "weather_ghi", + "weather_dni", + "weather_dhi" + ], + "record_keys_writable": [ + "date_time", + "weather_total_clouds", + "weather_low_clouds", + "weather_medium_clouds", + "weather_high_clouds", + "weather_visibility", + "weather_fog", + "weather_precip_type", + "weather_precip_prob", + "weather_precip_amt", + "weather_preciptable_water", + "weather_wind_speed", + "weather_wind_direction", + "weather_frost_chance", + "weather_temp_air", + "weather_feels_like", + "weather_dew_point", + "weather_relative_humidity", + "weather_pressure", + "weather_ozone", + "weather_ghi", + "weather_dni", + "weather_dhi" + ], + "end_datetime": "2026-03-04 00:00:00+01:00", + "keep_datetime": "2026-02-28 00:00:00+01:00", + "total_hours": 48, + "keep_hours": 48 +} \ No newline at end of file