mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2025-09-13 07:21:16 +00:00
Improve Configuration and Prediction Usability (#220)
* Update utilities in utils submodule. * Add base configuration modules. * Add server base configuration modules. * Add devices base configuration modules. * Add optimization base configuration modules. * Add utils base configuration modules. * Add prediction abstract and base classes plus tests. * Add PV forecast to prediction submodule. The PV forecast modules are adapted from the class_pvforecast module and replace it. * Add weather forecast to prediction submodule. The modules provide classes and methods to retrieve, manage, and process weather forecast data from various sources. Includes are structured representations of weather data and utilities for fetching forecasts for specific locations and time ranges. BrightSky and ClearOutside are currently supported. * Add electricity price forecast to prediction submodule. * Adapt fastapi server to base config and add fasthtml server. * Add ems to core submodule. * Adapt genetic to config. * Adapt visualize to config. * Adapt common test fixtures to config. * Add load forecast to prediction submodule. * Add core abstract and base classes. * Adapt single test optimization to config. * Adapt devices to config. Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
This commit is contained in:
@@ -1,31 +1,41 @@
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import pendulum
|
||||
import platformdirs
|
||||
import pytest
|
||||
from xprocess import ProcessStarter
|
||||
|
||||
from akkudoktoreos.config import EOS_DIR, AppConfig, load_config
|
||||
from akkudoktoreos.config.config import get_config
|
||||
from akkudoktoreos.utils.logutil import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@pytest.fixture(name="tmp_config")
|
||||
def load_config_tmp(tmp_path: Path) -> AppConfig:
|
||||
"""Creates an AppConfig from default.config.json with a tmp output directory."""
|
||||
config = load_config(tmp_path)
|
||||
config.directories.output = str(tmp_path)
|
||||
return config
|
||||
@pytest.fixture()
|
||||
def disable_debug_logging(scope="session", autouse=True):
|
||||
"""Automatically disable debug logging for all tests."""
|
||||
original_levels = {}
|
||||
root_logger = logging.getLogger()
|
||||
|
||||
original_levels[root_logger] = root_logger.level
|
||||
root_logger.setLevel(logging.INFO)
|
||||
|
||||
for logger_name, logger in logging.root.manager.loggerDict.items():
|
||||
if isinstance(logger, logging.Logger):
|
||||
original_levels[logger] = logger.level
|
||||
if logger.level <= logging.DEBUG:
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def disable_debug_logging():
|
||||
# Temporarily set logging level higher than DEBUG
|
||||
logging.disable(logging.DEBUG)
|
||||
yield
|
||||
# Re-enable logging back to its original state after the test
|
||||
logging.disable(logging.NOTSET)
|
||||
|
||||
for logger, level in original_levels.items():
|
||||
logger.setLevel(level)
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
@@ -39,6 +49,84 @@ def is_full_run(request):
|
||||
yield bool(request.config.getoption("--full-run"))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def reset_config(disable_debug_logging):
|
||||
"""Fixture to reset EOS config to default values."""
|
||||
config_eos = get_config()
|
||||
config_eos.reset_settings()
|
||||
config_eos.reset_to_defaults()
|
||||
return config_eos
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config_default_dirs():
|
||||
"""Fixture that provides a list of directories to be used as config dir."""
|
||||
config_eos = get_config()
|
||||
# Default config directory from platform user config directory
|
||||
config_default_dir_user = Path(platformdirs.user_config_dir(config_eos.APP_NAME))
|
||||
# Default config directory from current working directory
|
||||
config_default_dir_cwd = Path.cwd()
|
||||
# Default config directory from default config file
|
||||
config_default_dir_default = Path(__file__).parent.parent.joinpath("src/akkudoktoreos/data")
|
||||
return config_default_dir_user, config_default_dir_cwd, config_default_dir_default
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def stash_config_file(config_default_dirs):
|
||||
"""Fixture to temporarily stash away an existing config file during a test.
|
||||
|
||||
If the specified config file exists, it moves the file to a temporary directory.
|
||||
The file is restored to its original location after the test.
|
||||
|
||||
Keep right most in fixture parameter list to assure application at last.
|
||||
|
||||
Returns:
|
||||
Path: Path to the stashed config file.
|
||||
"""
|
||||
config_eos = get_config()
|
||||
config_default_dir_user, config_default_dir_cwd, _ = config_default_dirs
|
||||
|
||||
config_file_path_user = config_default_dir_user.joinpath(config_eos.CONFIG_FILE_NAME)
|
||||
config_file_path_cwd = config_default_dir_cwd.joinpath(config_eos.CONFIG_FILE_NAME)
|
||||
|
||||
original_config_file_user = None
|
||||
original_config_file_cwd = None
|
||||
if config_file_path_user.exists():
|
||||
original_config_file_user = config_file_path_user
|
||||
if config_file_path_cwd.exists():
|
||||
original_config_file_cwd = config_file_path_cwd
|
||||
|
||||
temp_dir = tempfile.TemporaryDirectory()
|
||||
temp_file_user = None
|
||||
temp_file_cwd = None
|
||||
|
||||
# If the file exists, move it to the temporary directory
|
||||
if original_config_file_user:
|
||||
temp_file_user = Path(temp_dir.name) / f"user.{original_config_file_user.name}"
|
||||
shutil.move(original_config_file_user, temp_file_user)
|
||||
assert not original_config_file_user.exists()
|
||||
logger.debug(f"Stashed: '{original_config_file_user}'")
|
||||
if original_config_file_cwd:
|
||||
temp_file_cwd = Path(temp_dir.name) / f"cwd.{original_config_file_cwd.name}"
|
||||
shutil.move(original_config_file_cwd, temp_file_cwd)
|
||||
assert not original_config_file_cwd.exists()
|
||||
logger.debug(f"Stashed: '{original_config_file_cwd}'")
|
||||
|
||||
# Yield the temporary file path to the test
|
||||
yield temp_file_user, temp_file_cwd
|
||||
|
||||
# Cleanup after the test
|
||||
if temp_file_user:
|
||||
# Restore the file to its original location
|
||||
shutil.move(temp_file_user, original_config_file_user)
|
||||
assert original_config_file_user.exists()
|
||||
if temp_file_cwd:
|
||||
# Restore the file to its original location
|
||||
shutil.move(temp_file_cwd, original_config_file_cwd)
|
||||
assert original_config_file_cwd.exists()
|
||||
temp_dir.cleanup()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def server(xprocess, tmp_path: Path):
|
||||
"""Fixture to start the server.
|
||||
@@ -50,13 +138,13 @@ def server(xprocess, tmp_path: Path):
|
||||
# assure server to be installed
|
||||
try:
|
||||
subprocess.run(
|
||||
[sys.executable, "-c", "import akkudoktoreos.server"],
|
||||
[sys.executable, "-c", "import akkudoktoreos.server.fastapi_server"],
|
||||
check=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
project_dir = Path(__file__).parent.parent
|
||||
project_dir = Path(__file__).parent.parent.parent
|
||||
subprocess.run(
|
||||
[sys.executable, "-m", "pip", "install", "-e", project_dir],
|
||||
check=True,
|
||||
@@ -66,11 +154,15 @@ def server(xprocess, tmp_path: Path):
|
||||
|
||||
# command to start server process
|
||||
args = [sys.executable, "-m", "akkudoktoreos.server.fastapi_server"]
|
||||
env = {EOS_DIR: f"{tmp_path}", **os.environ.copy()}
|
||||
config_eos = get_config()
|
||||
settings = {
|
||||
"data_folder_path": tmp_path,
|
||||
}
|
||||
config_eos.merge_settings_from_dict(settings)
|
||||
|
||||
# startup pattern
|
||||
pattern = "Application startup complete."
|
||||
# search the first 30 lines for the startup pattern, if not found
|
||||
# search this number of lines for the startup pattern, if not found
|
||||
# a RuntimeError will be raised informing the user
|
||||
max_read_lines = 30
|
||||
|
||||
@@ -81,7 +173,7 @@ def server(xprocess, tmp_path: Path):
|
||||
terminate_on_interrupt = True
|
||||
|
||||
# ensure process is running and return its logfile
|
||||
logfile = xprocess.ensure("eos", Starter)
|
||||
pid, logfile = xprocess.ensure("eos", Starter)
|
||||
|
||||
# create url/port info to the server
|
||||
url = "http://127.0.0.1:8503"
|
||||
@@ -92,26 +184,26 @@ def server(xprocess, tmp_path: Path):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def other_timezone():
|
||||
"""Fixture to temporarily change the timezone.
|
||||
def set_other_timezone():
|
||||
"""Temporarily sets a timezone for Pendulum during a test.
|
||||
|
||||
Restores the original timezone after the test.
|
||||
Resets to the original timezone after the test completes.
|
||||
"""
|
||||
original_tz = os.environ.get("TZ", None)
|
||||
original_timezone = pendulum.local_timezone()
|
||||
|
||||
other_tz = "Atlantic/Canary"
|
||||
if original_tz == other_tz:
|
||||
other_tz = "Asia/Singapore"
|
||||
default_other_timezone = "Atlantic/Canary"
|
||||
if default_other_timezone == original_timezone:
|
||||
default_other_timezone = "Asia/Singapore"
|
||||
|
||||
# Change the timezone to another
|
||||
os.environ["TZ"] = other_tz
|
||||
time.tzset() # For Unix/Linux to apply the timezone change
|
||||
def _set_timezone(other_timezone: Optional[str] = None) -> str:
|
||||
if other_timezone is None:
|
||||
other_timezone = default_other_timezone
|
||||
pendulum.set_local_timezone(other_timezone)
|
||||
assert pendulum.local_timezone() == other_timezone
|
||||
return other_timezone
|
||||
|
||||
yield os.environ["TZ"] # Yield control back to the test case
|
||||
yield _set_timezone
|
||||
|
||||
# Restore the original timezone after the test
|
||||
if original_tz:
|
||||
os.environ["TZ"] = original_tz
|
||||
else:
|
||||
del os.environ["TZ"]
|
||||
time.tzset() # Re-apply the original timezone
|
||||
# Restore the original timezone
|
||||
pendulum.set_local_timezone(original_timezone)
|
||||
assert pendulum.local_timezone() == original_timezone
|
||||
|
@@ -7,7 +7,7 @@ from time import sleep
|
||||
|
||||
import pytest
|
||||
|
||||
from akkudoktoreos.utils.cachefilestore import CacheFileStore, cache_in_file
|
||||
from akkudoktoreos.utils.cacheutil import CacheFileStore, cache_in_file
|
||||
from akkudoktoreos.utils.datetimeutil import to_datetime
|
||||
|
||||
# -----------------------------
|
||||
@@ -268,7 +268,7 @@ def test_cache_in_file_decorator_uses_cache(cache_store):
|
||||
assert result == result2
|
||||
|
||||
|
||||
def test_cache_in_file_decorator_forces_update(cache_store):
|
||||
def test_cache_in_file_decorator_forces_update_data(cache_store):
|
||||
"""Test that the cache_in_file decorator reuses cached file on subsequent calls."""
|
||||
# Clear store to assure it is empty
|
||||
cache_store.clear(clear_all=True)
|
@@ -1,29 +1,33 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from akkudoktoreos.config import AppConfig
|
||||
from akkudoktoreos.devices.battery import EAutoParameters, PVAkku, PVAkkuParameters
|
||||
from akkudoktoreos.devices.generic import HomeAppliance, HomeApplianceParameters
|
||||
from akkudoktoreos.devices.inverter import Wechselrichter, WechselrichterParameters
|
||||
from akkudoktoreos.prediction.ems import (
|
||||
from akkudoktoreos.config.config import get_config
|
||||
from akkudoktoreos.core.ems import (
|
||||
EnergieManagementSystem,
|
||||
EnergieManagementSystemParameters,
|
||||
SimulationResult,
|
||||
get_ems,
|
||||
)
|
||||
from akkudoktoreos.devices.battery import EAutoParameters, PVAkku, PVAkkuParameters
|
||||
from akkudoktoreos.devices.generic import HomeAppliance, HomeApplianceParameters
|
||||
from akkudoktoreos.devices.inverter import Wechselrichter, WechselrichterParameters
|
||||
|
||||
prediction_hours = 48
|
||||
optimization_hours = 24
|
||||
start_hour = 1
|
||||
|
||||
|
||||
# Example initialization of necessary components
|
||||
@pytest.fixture
|
||||
def create_ems_instance(tmp_config: AppConfig) -> EnergieManagementSystem:
|
||||
def create_ems_instance() -> EnergieManagementSystem:
|
||||
"""Fixture to create an EnergieManagementSystem instance with given test parameters."""
|
||||
# Assure configuration holds the correct values
|
||||
config_eos = get_config()
|
||||
config_eos.merge_settings_from_dict({"prediction_hours": 48, "optimization_hours": 24})
|
||||
assert config_eos.prediction_hours is not None
|
||||
|
||||
# Initialize the battery and the inverter
|
||||
akku = PVAkku(
|
||||
PVAkkuParameters(kapazitaet_wh=5000, start_soc_prozent=80, min_soc_prozent=10),
|
||||
hours=prediction_hours,
|
||||
hours=config_eos.prediction_hours,
|
||||
)
|
||||
akku.reset()
|
||||
wechselrichter = Wechselrichter(WechselrichterParameters(max_leistung_wh=10000), akku)
|
||||
@@ -34,16 +38,16 @@ def create_ems_instance(tmp_config: AppConfig) -> EnergieManagementSystem:
|
||||
consumption_wh=2000,
|
||||
duration_h=2,
|
||||
),
|
||||
hours=prediction_hours,
|
||||
hours=config_eos.prediction_hours,
|
||||
)
|
||||
home_appliance.set_starting_time(2)
|
||||
|
||||
# Example initialization of electric car battery
|
||||
eauto = PVAkku(
|
||||
EAutoParameters(kapazitaet_wh=26400, start_soc_prozent=10, min_soc_prozent=10),
|
||||
hours=prediction_hours,
|
||||
hours=config_eos.prediction_hours,
|
||||
)
|
||||
eauto.set_charge_per_hour(np.full(prediction_hours, 1))
|
||||
eauto.set_charge_per_hour(np.full(config_eos.prediction_hours, 1))
|
||||
|
||||
# Parameters based on previous example data
|
||||
pv_prognose_wh = [
|
||||
@@ -203,8 +207,8 @@ def create_ems_instance(tmp_config: AppConfig) -> EnergieManagementSystem:
|
||||
]
|
||||
|
||||
# Initialize the energy management system with the respective parameters
|
||||
ems = EnergieManagementSystem(
|
||||
tmp_config.eos,
|
||||
ems = get_ems()
|
||||
ems.set_parameters(
|
||||
EnergieManagementSystemParameters(
|
||||
pv_prognose_wh=pv_prognose_wh,
|
||||
strompreis_euro_pro_wh=strompreis_euro_pro_wh,
|
||||
|
@@ -1,28 +1,32 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from akkudoktoreos.config import AppConfig
|
||||
from akkudoktoreos.config.config import get_config
|
||||
from akkudoktoreos.core.ems import (
|
||||
EnergieManagementSystem,
|
||||
EnergieManagementSystemParameters,
|
||||
get_ems,
|
||||
)
|
||||
from akkudoktoreos.devices.battery import EAutoParameters, PVAkku, PVAkkuParameters
|
||||
from akkudoktoreos.devices.generic import HomeAppliance, HomeApplianceParameters
|
||||
from akkudoktoreos.devices.inverter import Wechselrichter, WechselrichterParameters
|
||||
from akkudoktoreos.prediction.ems import (
|
||||
EnergieManagementSystem,
|
||||
EnergieManagementSystemParameters,
|
||||
)
|
||||
|
||||
prediction_hours = 48
|
||||
optimization_hours = 24
|
||||
start_hour = 0
|
||||
|
||||
|
||||
# Example initialization of necessary components
|
||||
@pytest.fixture
|
||||
def create_ems_instance(tmp_config: AppConfig) -> EnergieManagementSystem:
|
||||
def create_ems_instance() -> EnergieManagementSystem:
|
||||
"""Fixture to create an EnergieManagementSystem instance with given test parameters."""
|
||||
# Assure configuration holds the correct values
|
||||
config_eos = get_config()
|
||||
config_eos.merge_settings_from_dict({"prediction_hours": 48, "optimization_hours": 24})
|
||||
assert config_eos.prediction_hours is not None
|
||||
|
||||
# Initialize the battery and the inverter
|
||||
akku = PVAkku(
|
||||
PVAkkuParameters(kapazitaet_wh=5000, start_soc_prozent=80, min_soc_prozent=10),
|
||||
hours=prediction_hours,
|
||||
hours=config_eos.prediction_hours,
|
||||
)
|
||||
akku.reset()
|
||||
wechselrichter = Wechselrichter(WechselrichterParameters(max_leistung_wh=10000), akku)
|
||||
@@ -33,27 +37,28 @@ def create_ems_instance(tmp_config: AppConfig) -> EnergieManagementSystem:
|
||||
consumption_wh=2000,
|
||||
duration_h=2,
|
||||
),
|
||||
hours=prediction_hours,
|
||||
hours=config_eos.prediction_hours,
|
||||
)
|
||||
home_appliance.set_starting_time(2)
|
||||
|
||||
# Example initialization of electric car battery
|
||||
eauto = PVAkku(
|
||||
EAutoParameters(kapazitaet_wh=26400, start_soc_prozent=100, min_soc_prozent=100),
|
||||
hours=prediction_hours,
|
||||
hours=config_eos.prediction_hours,
|
||||
)
|
||||
|
||||
# Parameters based on previous example data
|
||||
pv_prognose_wh = [0.0] * prediction_hours
|
||||
pv_prognose_wh = [0.0] * config_eos.prediction_hours
|
||||
pv_prognose_wh[10] = 5000.0
|
||||
pv_prognose_wh[11] = 5000.0
|
||||
|
||||
strompreis_euro_pro_wh = [0.001] * prediction_hours
|
||||
strompreis_euro_pro_wh = [0.001] * config_eos.prediction_hours
|
||||
strompreis_euro_pro_wh[0:10] = [0.00001] * 10
|
||||
strompreis_euro_pro_wh[11:15] = [0.00005] * 4
|
||||
strompreis_euro_pro_wh[20] = 0.00001
|
||||
|
||||
einspeiseverguetung_euro_pro_wh = [0.00007] * len(strompreis_euro_pro_wh)
|
||||
preis_euro_pro_wh_akku = 0.0001
|
||||
|
||||
gesamtlast = [
|
||||
676.71,
|
||||
@@ -107,13 +112,13 @@ def create_ems_instance(tmp_config: AppConfig) -> EnergieManagementSystem:
|
||||
]
|
||||
|
||||
# Initialize the energy management system with the respective parameters
|
||||
ems = EnergieManagementSystem(
|
||||
tmp_config.eos,
|
||||
ems = get_ems()
|
||||
ems.set_parameters(
|
||||
EnergieManagementSystemParameters(
|
||||
pv_prognose_wh=pv_prognose_wh,
|
||||
strompreis_euro_pro_wh=strompreis_euro_pro_wh,
|
||||
einspeiseverguetung_euro_pro_wh=einspeiseverguetung_euro_pro_wh,
|
||||
preis_euro_pro_wh_akku=0,
|
||||
preis_euro_pro_wh_akku=preis_euro_pro_wh_akku,
|
||||
gesamtlast=gesamtlast,
|
||||
),
|
||||
wechselrichter=wechselrichter,
|
||||
@@ -121,10 +126,10 @@ def create_ems_instance(tmp_config: AppConfig) -> EnergieManagementSystem:
|
||||
home_appliance=home_appliance,
|
||||
)
|
||||
|
||||
ac = np.full(prediction_hours, 0)
|
||||
ac = np.full(config_eos.prediction_hours, 0.0)
|
||||
ac[20] = 1
|
||||
ems.set_akku_ac_charge_hours(ac)
|
||||
dc = np.full(prediction_hours, 0)
|
||||
dc = np.full(config_eos.prediction_hours, 0.0)
|
||||
dc[11] = 1
|
||||
ems.set_akku_dc_charge_hours(dc)
|
||||
|
||||
|
@@ -5,7 +5,7 @@ from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from akkudoktoreos.config import AppConfig
|
||||
from akkudoktoreos.config.config import get_config
|
||||
from akkudoktoreos.optimization.genetic import (
|
||||
OptimizationParameters,
|
||||
OptimizeResponse,
|
||||
@@ -39,14 +39,13 @@ def compare_dict(actual: dict[str, Any], expected: dict[str, Any]):
|
||||
)
|
||||
@patch("akkudoktoreos.optimization.genetic.visualisiere_ergebnisse")
|
||||
def test_optimize(
|
||||
visualisiere_ergebnisse_patch,
|
||||
fn_in: str,
|
||||
fn_out: str,
|
||||
ngen: int,
|
||||
is_full_run: bool,
|
||||
tmp_config: AppConfig,
|
||||
visualisiere_ergebnisse_patch, fn_in: str, fn_out: str, ngen: int, is_full_run: bool
|
||||
):
|
||||
"""Test optimierung_ems."""
|
||||
# Assure configuration holds the correct values
|
||||
config_eos = get_config()
|
||||
config_eos.merge_settings_from_dict({"prediction_hours": 48, "optimization_hours": 24})
|
||||
|
||||
# Load input and output data
|
||||
file = DIR_TESTDATA / fn_in
|
||||
with file.open("r") as f_in:
|
||||
@@ -56,7 +55,7 @@ def test_optimize(
|
||||
with file.open("r") as f_out:
|
||||
expected_result = OptimizeResponse(**json.load(f_out))
|
||||
|
||||
opt_class = optimization_problem(tmp_config, fixed_seed=42)
|
||||
opt_class = optimization_problem(fixed_seed=42)
|
||||
start_hour = 10
|
||||
|
||||
if ngen > 10 and not is_full_run:
|
||||
|
@@ -1,71 +1,112 @@
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from akkudoktoreos.config import (
|
||||
CONFIG_FILE_NAME,
|
||||
DEFAULT_CONFIG_FILE,
|
||||
get_config_file,
|
||||
load_config,
|
||||
)
|
||||
from akkudoktoreos.config.config import ConfigEOS, get_config
|
||||
from akkudoktoreos.utils.logutil import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
config_eos = get_config()
|
||||
|
||||
DIR_TESTDATA = Path(__file__).absolute().parent.joinpath("testdata")
|
||||
|
||||
FILE_TESTDATA_CONFIGEOS_1_JSON = DIR_TESTDATA.joinpath(config_eos.CONFIG_FILE_NAME)
|
||||
FILE_TESTDATA_CONFIGEOS_1_DIR = FILE_TESTDATA_CONFIGEOS_1_JSON.parent
|
||||
|
||||
|
||||
def test_config() -> None:
|
||||
"""Test the default config file."""
|
||||
try:
|
||||
load_config(Path.cwd())
|
||||
except ValidationError as exc:
|
||||
pytest.fail(f"Default configuration is not valid: {exc}")
|
||||
@pytest.fixture
|
||||
def reset_config_singleton():
|
||||
"""Fixture to reset the ConfigEOS singleton instance before a test."""
|
||||
ConfigEOS.reset_instance()
|
||||
yield
|
||||
ConfigEOS.reset_instance()
|
||||
|
||||
|
||||
def test_config_copy(tmp_path: Path) -> None:
|
||||
"""Test if the config is copied to the provided path."""
|
||||
assert DEFAULT_CONFIG_FILE == get_config_file(Path("does", "not", "exist"), False)
|
||||
def test_fixture_stash_config_file(stash_config_file, config_default_dirs):
|
||||
"""Assure fixture stash_config_file is working."""
|
||||
config_default_dir_user, config_default_dir_cwd, _ = config_default_dirs
|
||||
|
||||
load_config(tmp_path, True)
|
||||
expected_config = tmp_path.joinpath(CONFIG_FILE_NAME)
|
||||
config_file_path_user = config_default_dir_user.joinpath(config_eos.CONFIG_FILE_NAME)
|
||||
config_file_path_cwd = config_default_dir_cwd.joinpath(config_eos.CONFIG_FILE_NAME)
|
||||
|
||||
assert expected_config == get_config_file(tmp_path, False)
|
||||
assert expected_config.is_file()
|
||||
assert not config_file_path_user.exists()
|
||||
assert not config_file_path_cwd.exists()
|
||||
|
||||
|
||||
def test_config_merge(tmp_path: Path) -> None:
|
||||
"""Test if config is merged and updated correctly."""
|
||||
config_file = tmp_path.joinpath(CONFIG_FILE_NAME)
|
||||
custom_config = {
|
||||
"eos": {
|
||||
"optimization_hours": 30,
|
||||
"penalty": 21,
|
||||
"does_not_exist": "nope",
|
||||
"available_charging_rates_in_percentage": "False entry",
|
||||
def test_config_constants():
|
||||
"""Test config constants are the way expected by the tests."""
|
||||
assert config_eos.APP_NAME == "net.akkudoktor.eos"
|
||||
assert config_eos.APP_AUTHOR == "akkudoktor"
|
||||
assert config_eos.EOS_DIR == "EOS_DIR"
|
||||
assert config_eos.ENCODING == "UTF-8"
|
||||
assert config_eos.CONFIG_FILE_NAME == "EOS.config.json"
|
||||
|
||||
|
||||
def test_computed_paths(reset_config):
|
||||
"""Test computed paths for output and cache."""
|
||||
config_eos.merge_settings_from_dict(
|
||||
{
|
||||
"data_folder_path": "/base/data",
|
||||
"data_output_subpath": "output",
|
||||
"data_cache_subpath": "cache",
|
||||
}
|
||||
}
|
||||
with config_file.open("w") as f_out:
|
||||
json.dump(custom_config, f_out)
|
||||
|
||||
assert config_file.exists()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
# custom configuration is broken but not updated.
|
||||
load_config(tmp_path, True, False)
|
||||
|
||||
with config_file.open("r") as f_in:
|
||||
# custom configuration is not changed.
|
||||
assert json.load(f_in) == custom_config
|
||||
|
||||
config = load_config(tmp_path)
|
||||
|
||||
assert config.eos.optimization_hours == 30
|
||||
assert config.eos.penalty == 21
|
||||
)
|
||||
assert config_eos.data_output_path == Path("/base/data/output")
|
||||
assert config_eos.data_cache_path == Path("/base/data/cache")
|
||||
|
||||
|
||||
def test_setup(tmp_path: Path) -> None:
|
||||
"""Test setup."""
|
||||
config = load_config(tmp_path, True)
|
||||
config.run_setup()
|
||||
def test_singleton_behavior(reset_config_singleton):
|
||||
"""Test that ConfigEOS behaves as a singleton."""
|
||||
instance1 = ConfigEOS()
|
||||
instance2 = ConfigEOS()
|
||||
assert instance1 is instance2
|
||||
|
||||
assert tmp_path.joinpath(CONFIG_FILE_NAME).is_file()
|
||||
assert tmp_path.joinpath(config.directories.cache).is_dir()
|
||||
assert tmp_path.joinpath(config.directories.output).is_dir()
|
||||
|
||||
def test_default_config_path(reset_config, config_default_dirs, stash_config_file):
|
||||
"""Test that the default config file path is computed correctly."""
|
||||
_, _, config_default_dir_default = config_default_dirs
|
||||
|
||||
expected_path = config_default_dir_default.joinpath("default.config.json")
|
||||
assert config_eos.config_default_file_path == expected_path
|
||||
assert config_eos.config_default_file_path.is_file()
|
||||
|
||||
|
||||
def test_config_folder_path(reset_config, config_default_dirs, stash_config_file, monkeypatch):
|
||||
"""Test that _config_folder_path identifies the correct config directory or None."""
|
||||
config_default_dir_user, _, _ = config_default_dirs
|
||||
|
||||
# All config files are stashed away, no config folder path
|
||||
assert config_eos._config_folder_path() is None
|
||||
|
||||
config_file_user = config_default_dir_user.joinpath(config_eos.CONFIG_FILE_NAME)
|
||||
shutil.copy2(config_eos.config_default_file_path, config_file_user)
|
||||
assert config_eos._config_folder_path() == config_default_dir_user
|
||||
|
||||
monkeypatch.setenv("EOS_DIR", str(FILE_TESTDATA_CONFIGEOS_1_DIR))
|
||||
assert config_eos._config_folder_path() == FILE_TESTDATA_CONFIGEOS_1_DIR
|
||||
|
||||
# Cleanup after the test
|
||||
os.remove(config_file_user)
|
||||
|
||||
|
||||
def test_config_copy(reset_config, stash_config_file, monkeypatch):
|
||||
"""Test if the config is copied to the provided path."""
|
||||
temp_dir = tempfile.TemporaryDirectory()
|
||||
temp_folder_path = Path(temp_dir.name)
|
||||
temp_config_file_path = temp_folder_path.joinpath(config_eos.CONFIG_FILE_NAME).resolve()
|
||||
monkeypatch.setenv(config_eos.EOS_DIR, str(temp_folder_path))
|
||||
if temp_config_file_path.exists():
|
||||
temp_config_file_path.unlink()
|
||||
assert not temp_config_file_path.exists()
|
||||
assert config_eos._config_folder_path() is None
|
||||
assert config_eos._config_file_path() == temp_config_file_path
|
||||
|
||||
config_eos.from_config_file()
|
||||
assert temp_config_file_path.exists()
|
||||
|
||||
# Cleanup after the test
|
||||
temp_dir.cleanup()
|
||||
|
94
tests/test_configabc.py
Normal file
94
tests/test_configabc.py
Normal file
@@ -0,0 +1,94 @@
|
||||
from typing import List, Literal, Optional, no_type_check
|
||||
|
||||
import pytest
|
||||
from pydantic import Field, ValidationError
|
||||
|
||||
from akkudoktoreos.config.configabc import SettingsBaseModel
|
||||
|
||||
|
||||
class SettingsModel(SettingsBaseModel):
|
||||
name: str = "Default Name"
|
||||
age: int = 18
|
||||
tags: List[str] = Field(default_factory=list)
|
||||
readonly_field: Literal["ReadOnly"] = "ReadOnly" # Use Literal instead of const
|
||||
|
||||
|
||||
def test_reset_to_defaults():
|
||||
"""Test resetting to default values."""
|
||||
instance = SettingsModel(name="Custom Name", age=25, tags=["tag1", "tag2"])
|
||||
|
||||
# Modify the instance
|
||||
instance.name = "Modified Name"
|
||||
instance.age = 30
|
||||
instance.tags.append("tag3")
|
||||
|
||||
# Ensure the instance is modified
|
||||
assert instance.name == "Modified Name"
|
||||
assert instance.age == 30
|
||||
assert instance.tags == ["tag1", "tag2", "tag3"]
|
||||
|
||||
# Reset to defaults
|
||||
instance.reset_to_defaults()
|
||||
|
||||
# Verify default values
|
||||
assert instance.name == "Default Name"
|
||||
assert instance.age == 18
|
||||
assert instance.tags == []
|
||||
assert instance.readonly_field == "ReadOnly"
|
||||
|
||||
|
||||
@no_type_check
|
||||
def test_reset_to_defaults_readonly_field():
|
||||
"""Ensure read-only fields remain unchanged."""
|
||||
instance = SettingsModel()
|
||||
|
||||
# Attempt to modify readonly_field (should raise an error)
|
||||
with pytest.raises(ValidationError):
|
||||
instance.readonly_field = "New Value"
|
||||
|
||||
# Reset to defaults
|
||||
instance.reset_to_defaults()
|
||||
|
||||
# Ensure readonly_field is still at its default value
|
||||
assert instance.readonly_field == "ReadOnly"
|
||||
|
||||
|
||||
def test_reset_to_defaults_with_default_factory():
|
||||
"""Test reset with fields having default_factory."""
|
||||
|
||||
class FactoryModel(SettingsBaseModel):
|
||||
items: List[int] = Field(default_factory=lambda: [1, 2, 3])
|
||||
value: Optional[int] = None
|
||||
|
||||
instance = FactoryModel(items=[4, 5, 6], value=10)
|
||||
|
||||
# Ensure instance has custom values
|
||||
assert instance.items == [4, 5, 6]
|
||||
assert instance.value == 10
|
||||
|
||||
# Reset to defaults
|
||||
instance.reset_to_defaults()
|
||||
|
||||
# Verify reset values
|
||||
assert instance.items == [1, 2, 3]
|
||||
assert instance.value is None
|
||||
|
||||
|
||||
@no_type_check
|
||||
def test_reset_to_defaults_error_handling():
|
||||
"""Ensure reset_to_defaults skips fields that cannot be set."""
|
||||
|
||||
class ReadOnlyModel(SettingsBaseModel):
|
||||
readonly_field: Literal["ReadOnly"] = "ReadOnly"
|
||||
|
||||
instance = ReadOnlyModel()
|
||||
|
||||
# Attempt to modify readonly_field (should raise an error)
|
||||
with pytest.raises(ValidationError):
|
||||
instance.readonly_field = "New Value"
|
||||
|
||||
# Reset to defaults
|
||||
instance.reset_to_defaults()
|
||||
|
||||
# Ensure readonly_field is unaffected
|
||||
assert instance.readonly_field == "ReadOnly"
|
667
tests/test_dataabc.py
Normal file
667
tests/test_dataabc.py
Normal file
@@ -0,0 +1,667 @@
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, ClassVar, List, Optional, Union
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import pendulum
|
||||
import pytest
|
||||
from pydantic import Field, ValidationError
|
||||
|
||||
from akkudoktoreos.config.configabc import SettingsBaseModel
|
||||
from akkudoktoreos.core.dataabc import (
|
||||
DataBase,
|
||||
DataContainer,
|
||||
DataImportProvider,
|
||||
DataProvider,
|
||||
DataRecord,
|
||||
DataSequence,
|
||||
)
|
||||
from akkudoktoreos.core.ems import get_ems
|
||||
from akkudoktoreos.utils.datetimeutil import compare_datetimes, to_datetime, to_duration
|
||||
|
||||
# Derived classes for testing
|
||||
# ---------------------------
|
||||
|
||||
|
||||
class DerivedConfig(SettingsBaseModel):
|
||||
env_var: Optional[int] = Field(default=None, description="Test config by environment var")
|
||||
instance_field: Optional[str] = Field(default=None, description="Test config by instance field")
|
||||
class_constant: Optional[int] = Field(default=None, description="Test config by class constant")
|
||||
|
||||
|
||||
class DerivedBase(DataBase):
|
||||
instance_field: Optional[str] = Field(default=None, description="Field Value")
|
||||
class_constant: ClassVar[int] = 30
|
||||
|
||||
|
||||
class DerivedRecord(DataRecord):
|
||||
data_value: Optional[float] = Field(default=None, description="Data Value")
|
||||
|
||||
|
||||
class DerivedSequence(DataSequence):
|
||||
# overload
|
||||
records: List[DerivedRecord] = Field(
|
||||
default_factory=list, description="List of DerivedRecord records"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def record_class(cls) -> Any:
|
||||
return DerivedRecord
|
||||
|
||||
|
||||
class DerivedDataProvider(DataProvider):
|
||||
"""A concrete subclass of DataProvider for testing purposes."""
|
||||
|
||||
# overload
|
||||
records: List[DerivedRecord] = Field(
|
||||
default_factory=list, description="List of DerivedRecord records"
|
||||
)
|
||||
provider_enabled: ClassVar[bool] = False
|
||||
provider_updated: ClassVar[bool] = False
|
||||
|
||||
@classmethod
|
||||
def record_class(cls) -> Any:
|
||||
return DerivedRecord
|
||||
|
||||
# Implement abstract methods for test purposes
|
||||
def provider_id(self) -> str:
|
||||
return "DerivedDataProvider"
|
||||
|
||||
def enabled(self) -> bool:
|
||||
return self.provider_enabled
|
||||
|
||||
def _update_data(self, force_update: Optional[bool] = False) -> None:
|
||||
# Simulate update logic
|
||||
DerivedDataProvider.provider_updated = True
|
||||
|
||||
|
||||
class DerivedDataImportProvider(DataImportProvider):
|
||||
"""A concrete subclass of DataImportProvider for testing purposes."""
|
||||
|
||||
# overload
|
||||
records: List[DerivedRecord] = Field(
|
||||
default_factory=list, description="List of DerivedRecord records"
|
||||
)
|
||||
provider_enabled: ClassVar[bool] = False
|
||||
provider_updated: ClassVar[bool] = False
|
||||
|
||||
@classmethod
|
||||
def record_class(cls) -> Any:
|
||||
return DerivedRecord
|
||||
|
||||
# Implement abstract methods for test purposes
|
||||
def provider_id(self) -> str:
|
||||
return "DerivedDataImportProvider"
|
||||
|
||||
def enabled(self) -> bool:
|
||||
return self.provider_enabled
|
||||
|
||||
def _update_data(self, force_update: Optional[bool] = False) -> None:
|
||||
# Simulate update logic
|
||||
DerivedDataImportProvider.provider_updated = True
|
||||
|
||||
|
||||
class DerivedDataContainer(DataContainer):
|
||||
providers: List[Union[DerivedDataProvider, DataProvider]] = Field(
|
||||
default_factory=list, description="List of data providers"
|
||||
)
|
||||
|
||||
|
||||
# Tests
|
||||
# ----------
|
||||
|
||||
|
||||
class TestDataBase:
|
||||
@pytest.fixture
|
||||
def base(self, reset_config, monkeypatch):
|
||||
# Provide default values for configuration
|
||||
derived = DerivedBase()
|
||||
derived.config.update()
|
||||
return derived
|
||||
|
||||
def test_get_config_value_key_error(self, base):
|
||||
with pytest.raises(AttributeError):
|
||||
base.config.non_existent_key
|
||||
|
||||
|
||||
class TestDataRecord:
|
||||
def create_test_record(self, date, value):
|
||||
"""Helper function to create a test DataRecord."""
|
||||
return DerivedRecord(date_time=date, data_value=value)
|
||||
|
||||
def test_getitem(self):
|
||||
record = self.create_test_record(datetime(2024, 1, 3, tzinfo=timezone.utc), 10.0)
|
||||
assert record["data_value"] == 10.0
|
||||
|
||||
def test_setitem(self):
|
||||
record = self.create_test_record(datetime(2024, 1, 3, tzinfo=timezone.utc), 10.0)
|
||||
record["data_value"] = 20.0
|
||||
assert record.data_value == 20.0
|
||||
|
||||
def test_delitem(self):
|
||||
record = self.create_test_record(datetime(2024, 1, 3, tzinfo=timezone.utc), 10.0)
|
||||
record.data_value = 20.0
|
||||
del record["data_value"]
|
||||
assert record.data_value is None
|
||||
|
||||
def test_len(self):
|
||||
record = self.create_test_record(datetime(2024, 1, 3, tzinfo=timezone.utc), 10.0)
|
||||
record.date_time = None
|
||||
record.data_value = 20.0
|
||||
assert len(record) == 2
|
||||
|
||||
def test_to_dict(self):
|
||||
record = self.create_test_record(datetime(2024, 1, 3, tzinfo=timezone.utc), 10.0)
|
||||
record.data_value = 20.0
|
||||
record_dict = record.to_dict()
|
||||
assert "data_value" in record_dict
|
||||
assert record_dict["data_value"] == 20.0
|
||||
record2 = DerivedRecord.from_dict(record_dict)
|
||||
assert record2 == record
|
||||
|
||||
def test_to_json(self):
|
||||
record = self.create_test_record(datetime(2024, 1, 3, tzinfo=timezone.utc), 10.0)
|
||||
record.data_value = 20.0
|
||||
json_str = record.to_json()
|
||||
assert "data_value" in json_str
|
||||
assert "20.0" in json_str
|
||||
record2 = DerivedRecord.from_json(json_str)
|
||||
assert record2 == record
|
||||
|
||||
|
||||
class TestDataSequence:
|
||||
@pytest.fixture
|
||||
def sequence(self):
|
||||
sequence0 = DerivedSequence()
|
||||
assert len(sequence0) == 0
|
||||
return sequence0
|
||||
|
||||
@pytest.fixture
|
||||
def sequence2(self):
|
||||
sequence = DerivedSequence()
|
||||
record1 = self.create_test_record(datetime(1970, 1, 1), 1970)
|
||||
record2 = self.create_test_record(datetime(1971, 1, 1), 1971)
|
||||
sequence.append(record1)
|
||||
sequence.append(record2)
|
||||
assert len(sequence) == 2
|
||||
return sequence
|
||||
|
||||
def create_test_record(self, date, value):
|
||||
"""Helper function to create a test DataRecord."""
|
||||
return DerivedRecord(date_time=date, data_value=value)
|
||||
|
||||
# Test cases
|
||||
def test_getitem(self, sequence):
|
||||
assert len(sequence) == 0
|
||||
record = self.create_test_record("2024-01-01 00:00:00", 0)
|
||||
sequence.insert_by_datetime(record)
|
||||
assert isinstance(sequence[0], DerivedRecord)
|
||||
|
||||
def test_setitem(self, sequence2):
|
||||
new_record = self.create_test_record(datetime(2024, 1, 3, tzinfo=timezone.utc), 1)
|
||||
sequence2[0] = new_record
|
||||
assert sequence2[0].date_time == datetime(2024, 1, 3, tzinfo=timezone.utc)
|
||||
|
||||
def test_set_record_at_index(self, sequence2):
|
||||
record1 = self.create_test_record(datetime(2024, 1, 3, tzinfo=timezone.utc), 1)
|
||||
record2 = self.create_test_record(datetime(2023, 11, 5), 0.8)
|
||||
sequence2[1] = record1
|
||||
assert sequence2[1].date_time == datetime(2024, 1, 3, tzinfo=timezone.utc)
|
||||
sequence2[0] = record2
|
||||
assert len(sequence2) == 2
|
||||
assert sequence2[0] == record2
|
||||
|
||||
def test_insert_duplicate_date_record(self, sequence):
|
||||
record1 = self.create_test_record(datetime(2023, 11, 5), 0.8)
|
||||
record2 = self.create_test_record(datetime(2023, 11, 5), 0.9) # Duplicate date
|
||||
sequence.insert_by_datetime(record1)
|
||||
sequence.insert_by_datetime(record2)
|
||||
assert len(sequence) == 1
|
||||
assert sequence[0].data_value == 0.9 # Record should have merged with new value
|
||||
|
||||
def test_sort_by_datetime_ascending(self, sequence):
|
||||
"""Test sorting records in ascending order by date_time."""
|
||||
records = [
|
||||
self.create_test_record(pendulum.datetime(2024, 11, 1), 0.7),
|
||||
self.create_test_record(pendulum.datetime(2024, 10, 1), 0.8),
|
||||
self.create_test_record(pendulum.datetime(2024, 12, 1), 0.9),
|
||||
]
|
||||
for i, record in enumerate(records):
|
||||
sequence.insert(i, record)
|
||||
sequence.sort_by_datetime()
|
||||
sorted_dates = [record.date_time for record in sequence.records]
|
||||
for i, expected_date in enumerate(
|
||||
[
|
||||
pendulum.datetime(2024, 10, 1),
|
||||
pendulum.datetime(2024, 11, 1),
|
||||
pendulum.datetime(2024, 12, 1),
|
||||
]
|
||||
):
|
||||
assert compare_datetimes(sorted_dates[i], expected_date).equal
|
||||
|
||||
def test_sort_by_datetime_descending(self, sequence):
|
||||
"""Test sorting records in descending order by date_time."""
|
||||
records = [
|
||||
self.create_test_record(pendulum.datetime(2024, 11, 1), 0.7),
|
||||
self.create_test_record(pendulum.datetime(2024, 10, 1), 0.8),
|
||||
self.create_test_record(pendulum.datetime(2024, 12, 1), 0.9),
|
||||
]
|
||||
for i, record in enumerate(records):
|
||||
sequence.insert(i, record)
|
||||
sequence.sort_by_datetime(reverse=True)
|
||||
sorted_dates = [record.date_time for record in sequence.records]
|
||||
for i, expected_date in enumerate(
|
||||
[
|
||||
pendulum.datetime(2024, 12, 1),
|
||||
pendulum.datetime(2024, 11, 1),
|
||||
pendulum.datetime(2024, 10, 1),
|
||||
]
|
||||
):
|
||||
assert compare_datetimes(sorted_dates[i], expected_date).equal
|
||||
|
||||
def test_sort_by_datetime_with_none(self, sequence):
|
||||
"""Test sorting records when some date_time values are None."""
|
||||
records = [
|
||||
self.create_test_record(pendulum.datetime(2024, 11, 1), 0.7),
|
||||
self.create_test_record(pendulum.datetime(2024, 10, 1), 0.8),
|
||||
self.create_test_record(pendulum.datetime(2024, 12, 1), 0.9),
|
||||
]
|
||||
for i, record in enumerate(records):
|
||||
sequence.insert(i, record)
|
||||
sequence.records[2].date_time = None
|
||||
assert sequence.records[2].date_time is None
|
||||
sequence.sort_by_datetime()
|
||||
sorted_dates = [record.date_time for record in sequence.records]
|
||||
for i, expected_date in enumerate(
|
||||
[
|
||||
None, # None values should come first
|
||||
pendulum.datetime(2024, 10, 1),
|
||||
pendulum.datetime(2024, 11, 1),
|
||||
]
|
||||
):
|
||||
if expected_date is None:
|
||||
assert sorted_dates[i] is None
|
||||
else:
|
||||
assert compare_datetimes(sorted_dates[i], expected_date).equal
|
||||
|
||||
def test_sort_by_datetime_error_on_uncomparable(self, sequence):
|
||||
"""Test error is raised when date_time contains uncomparable values."""
|
||||
records = [
|
||||
self.create_test_record(pendulum.datetime(2024, 11, 1), 0.7),
|
||||
self.create_test_record(pendulum.datetime(2024, 12, 1), 0.9),
|
||||
self.create_test_record(pendulum.datetime(2024, 10, 1), 0.8),
|
||||
]
|
||||
for i, record in enumerate(records):
|
||||
sequence.insert(i, record)
|
||||
with pytest.raises(
|
||||
ValidationError, match="Date string not_a_datetime does not match any known formats."
|
||||
):
|
||||
sequence.records[2].date_time = "not_a_datetime" # Invalid date_time
|
||||
sequence.sort_by_datetime()
|
||||
|
||||
def test_key_to_series(self, sequence):
|
||||
record = self.create_test_record(datetime(2023, 11, 6), 0.8)
|
||||
sequence.append(record)
|
||||
series = sequence.key_to_series("data_value")
|
||||
assert isinstance(series, pd.Series)
|
||||
assert series[to_datetime(datetime(2023, 11, 6))] == 0.8
|
||||
|
||||
def test_key_from_series(self, sequence):
|
||||
series = pd.Series(
|
||||
data=[0.8, 0.9], index=pd.to_datetime([datetime(2023, 11, 5), datetime(2023, 11, 6)])
|
||||
)
|
||||
sequence.key_from_series("data_value", series)
|
||||
assert len(sequence) == 2
|
||||
assert sequence[0].data_value == 0.8
|
||||
assert sequence[1].data_value == 0.9
|
||||
|
||||
def test_key_to_array(self, sequence):
|
||||
interval = to_duration("1 day")
|
||||
start_datetime = to_datetime("2023-11-6")
|
||||
last_datetime = to_datetime("2023-11-8")
|
||||
end_datetime = to_datetime("2023-11-9")
|
||||
record = self.create_test_record(start_datetime, float(start_datetime.day))
|
||||
sequence.insert_by_datetime(record)
|
||||
record = self.create_test_record(last_datetime, float(last_datetime.day))
|
||||
sequence.insert_by_datetime(record)
|
||||
assert sequence[0].data_value == 6.0
|
||||
assert sequence[1].data_value == 8.0
|
||||
|
||||
series = sequence.key_to_series(
|
||||
key="data_value", start_datetime=start_datetime, end_datetime=end_datetime
|
||||
)
|
||||
assert len(series) == 2
|
||||
assert series[to_datetime("2023-11-6")] == 6
|
||||
assert series[to_datetime("2023-11-8")] == 8
|
||||
|
||||
array = sequence.key_to_array(
|
||||
key="data_value",
|
||||
start_datetime=start_datetime,
|
||||
end_datetime=end_datetime,
|
||||
interval=interval,
|
||||
)
|
||||
assert isinstance(array, np.ndarray)
|
||||
assert len(array) == 3
|
||||
assert array[0] == start_datetime.day
|
||||
assert array[1] == 7
|
||||
assert array[2] == last_datetime.day
|
||||
|
||||
def test_to_datetimeindex(self, sequence2):
|
||||
record1 = self.create_test_record(datetime(2023, 11, 5), 0.8)
|
||||
record2 = self.create_test_record(datetime(2023, 11, 6), 0.9)
|
||||
sequence2.insert(0, record1)
|
||||
sequence2.insert(1, record2)
|
||||
dt_index = sequence2.to_datetimeindex()
|
||||
assert isinstance(dt_index, pd.DatetimeIndex)
|
||||
assert dt_index[0] == to_datetime(datetime(2023, 11, 5))
|
||||
assert dt_index[1] == to_datetime(datetime(2023, 11, 6))
|
||||
|
||||
def test_delete_by_datetime_range(self, sequence):
|
||||
record1 = self.create_test_record(datetime(2023, 11, 5), 0.8)
|
||||
record2 = self.create_test_record(datetime(2023, 11, 6), 0.9)
|
||||
record3 = self.create_test_record(datetime(2023, 11, 7), 1.0)
|
||||
sequence.append(record1)
|
||||
sequence.append(record2)
|
||||
sequence.append(record3)
|
||||
assert len(sequence) == 3
|
||||
sequence.delete_by_datetime(
|
||||
start_datetime=datetime(2023, 11, 6), end_datetime=datetime(2023, 11, 7)
|
||||
)
|
||||
assert len(sequence) == 2
|
||||
assert sequence[0].date_time == to_datetime(datetime(2023, 11, 5))
|
||||
assert sequence[1].date_time == to_datetime(datetime(2023, 11, 7))
|
||||
|
||||
def test_delete_by_datetime_start(self, sequence):
|
||||
record1 = self.create_test_record(datetime(2023, 11, 5), 0.8)
|
||||
record2 = self.create_test_record(datetime(2023, 11, 6), 0.9)
|
||||
sequence.append(record1)
|
||||
sequence.append(record2)
|
||||
assert len(sequence) == 2
|
||||
sequence.delete_by_datetime(start_datetime=datetime(2023, 11, 6))
|
||||
assert len(sequence) == 1
|
||||
assert sequence[0].date_time == to_datetime(datetime(2023, 11, 5))
|
||||
|
||||
def test_delete_by_datetime_end(self, sequence):
|
||||
record1 = self.create_test_record(datetime(2023, 11, 5), 0.8)
|
||||
record2 = self.create_test_record(datetime(2023, 11, 6), 0.9)
|
||||
sequence.append(record1)
|
||||
sequence.append(record2)
|
||||
assert len(sequence) == 2
|
||||
sequence.delete_by_datetime(end_datetime=datetime(2023, 11, 6))
|
||||
assert len(sequence) == 1
|
||||
assert sequence[0].date_time == to_datetime(datetime(2023, 11, 6))
|
||||
|
||||
def test_filter_by_datetime(self, sequence):
|
||||
record1 = self.create_test_record(datetime(2023, 11, 5), 0.8)
|
||||
record2 = self.create_test_record(datetime(2023, 11, 6), 0.9)
|
||||
sequence.append(record1)
|
||||
sequence.append(record2)
|
||||
filtered_sequence = sequence.filter_by_datetime(start_datetime=datetime(2023, 11, 6))
|
||||
assert len(filtered_sequence) == 1
|
||||
assert filtered_sequence[0].date_time == to_datetime(datetime(2023, 11, 6))
|
||||
|
||||
def test_to_dict(self, sequence):
|
||||
record = self.create_test_record(datetime(2023, 11, 6), 0.8)
|
||||
sequence.append(record)
|
||||
data_dict = sequence.to_dict()
|
||||
assert isinstance(data_dict, dict)
|
||||
sequence_other = sequence.from_dict(data_dict)
|
||||
assert sequence_other == sequence
|
||||
|
||||
def test_to_json(self, sequence):
|
||||
record = self.create_test_record(datetime(2023, 11, 6), 0.8)
|
||||
sequence.append(record)
|
||||
json_str = sequence.to_json()
|
||||
assert isinstance(json_str, str)
|
||||
assert "2023-11-06" in json_str
|
||||
assert ":0.8" in json_str
|
||||
|
||||
def test_from_json(self, sequence, sequence2):
|
||||
json_str = sequence2.to_json()
|
||||
sequence = sequence.from_json(json_str)
|
||||
assert len(sequence) == len(sequence2)
|
||||
assert sequence[0].date_time == sequence2[0].date_time
|
||||
assert sequence[0].data_value == sequence2[0].data_value
|
||||
|
||||
def test_key_to_dict(self, sequence):
|
||||
record1 = self.create_test_record(datetime(2023, 11, 5), 0.8)
|
||||
record2 = self.create_test_record(datetime(2023, 11, 6), 0.9)
|
||||
sequence.append(record1)
|
||||
sequence.append(record2)
|
||||
data_dict = sequence.key_to_dict("data_value")
|
||||
assert isinstance(data_dict, dict)
|
||||
assert data_dict[to_datetime(datetime(2023, 11, 5), as_string=True)] == 0.8
|
||||
assert data_dict[to_datetime(datetime(2023, 11, 6), as_string=True)] == 0.9
|
||||
|
||||
def test_key_to_lists(self, sequence):
|
||||
record1 = self.create_test_record(datetime(2023, 11, 5), 0.8)
|
||||
record2 = self.create_test_record(datetime(2023, 11, 6), 0.9)
|
||||
sequence.append(record1)
|
||||
sequence.append(record2)
|
||||
dates, values = sequence.key_to_lists("data_value")
|
||||
assert dates == [to_datetime(datetime(2023, 11, 5)), to_datetime(datetime(2023, 11, 6))]
|
||||
assert values == [0.8, 0.9]
|
||||
|
||||
|
||||
class TestDataProvider:
|
||||
# Fixtures and helper functions
|
||||
@pytest.fixture
|
||||
def provider(self):
|
||||
"""Fixture to provide an instance of TestDataProvider for testing."""
|
||||
DerivedDataProvider.provider_enabled = True
|
||||
DerivedDataProvider.provider_updated = False
|
||||
return DerivedDataProvider()
|
||||
|
||||
@pytest.fixture
|
||||
def sample_start_datetime(self):
|
||||
"""Fixture for a sample start datetime."""
|
||||
return to_datetime(datetime(2024, 11, 1, 12, 0))
|
||||
|
||||
def create_test_record(self, date, value):
|
||||
"""Helper function to create a test DataRecord."""
|
||||
return DerivedRecord(date_time=date, data_value=value)
|
||||
|
||||
# Tests
|
||||
|
||||
def test_singleton_behavior(self, provider):
|
||||
"""Test that DataProvider enforces singleton behavior."""
|
||||
instance1 = provider
|
||||
instance2 = DerivedDataProvider()
|
||||
assert (
|
||||
instance1 is instance2
|
||||
), "Singleton pattern is not enforced; instances are not the same."
|
||||
|
||||
def test_update_method_with_defaults(self, provider, sample_start_datetime, monkeypatch):
|
||||
"""Test the `update` method with default parameters."""
|
||||
ems_eos = get_ems()
|
||||
|
||||
ems_eos.set_start_datetime(sample_start_datetime)
|
||||
provider.update_data()
|
||||
|
||||
assert provider.start_datetime == sample_start_datetime
|
||||
|
||||
def test_update_method_force_enable(self, provider, monkeypatch):
|
||||
"""Test that `update` executes when `force_enable` is True, even if `enabled` is False."""
|
||||
# Override enabled to return False for this test
|
||||
DerivedDataProvider.provider_enabled = False
|
||||
DerivedDataProvider.provider_updated = False
|
||||
provider.update_data(force_enable=True)
|
||||
assert provider.enabled() is False, "Provider should be disabled, but enabled() is True."
|
||||
assert (
|
||||
DerivedDataProvider.provider_updated is True
|
||||
), "Provider should have been executed, but was not."
|
||||
|
||||
def test_delete_by_datetime(self, provider, sample_start_datetime):
|
||||
"""Test `delete_by_datetime` method for removing records by datetime range."""
|
||||
# Add records to the provider for deletion testing
|
||||
provider.records = [
|
||||
self.create_test_record(sample_start_datetime - to_duration("3 hours"), 1),
|
||||
self.create_test_record(sample_start_datetime - to_duration("1 hour"), 2),
|
||||
self.create_test_record(sample_start_datetime + to_duration("1 hour"), 3),
|
||||
]
|
||||
|
||||
provider.delete_by_datetime(
|
||||
start_datetime=sample_start_datetime - to_duration("2 hours"),
|
||||
end_datetime=sample_start_datetime + to_duration("2 hours"),
|
||||
)
|
||||
assert (
|
||||
len(provider.records) == 1
|
||||
), "Only one record should remain after deletion by datetime."
|
||||
assert provider.records[0].date_time == sample_start_datetime - to_duration(
|
||||
"3 hours"
|
||||
), "Unexpected record remains."
|
||||
|
||||
|
||||
class TestDataImportProvider:
|
||||
# Fixtures and helper functions
|
||||
@pytest.fixture
|
||||
def provider(self):
|
||||
"""Fixture to provide an instance of DerivedDataImportProvider for testing."""
|
||||
DerivedDataImportProvider.provider_enabled = True
|
||||
DerivedDataImportProvider.provider_updated = False
|
||||
return DerivedDataImportProvider()
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"start_datetime, value_count, expected_mapping_count",
|
||||
[
|
||||
("2024-11-10 00:00:00", 24, 24), # No DST in Germany
|
||||
("2024-08-10 00:00:00", 24, 24), # DST in Germany
|
||||
("2024-03-31 00:00:00", 24, 23), # DST change in Germany (23 hours/ day)
|
||||
("2024-10-27 00:00:00", 24, 25), # DST change in Germany (25 hours/ day)
|
||||
],
|
||||
)
|
||||
def test_import_datetimes(self, provider, start_datetime, value_count, expected_mapping_count):
|
||||
ems_eos = get_ems()
|
||||
ems_eos.set_start_datetime(to_datetime(start_datetime, in_timezone="Europe/Berlin"))
|
||||
|
||||
value_datetime_mapping = provider.import_datetimes(value_count)
|
||||
|
||||
assert len(value_datetime_mapping) == expected_mapping_count
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"start_datetime, value_count, expected_mapping_count",
|
||||
[
|
||||
("2024-11-10 00:00:00", 24, 24), # No DST in Germany
|
||||
("2024-08-10 00:00:00", 24, 24), # DST in Germany
|
||||
("2024-03-31 00:00:00", 24, 23), # DST change in Germany (23 hours/ day)
|
||||
("2024-10-27 00:00:00", 24, 25), # DST change in Germany (25 hours/ day)
|
||||
],
|
||||
)
|
||||
def test_import_datetimes_utc(
|
||||
self, set_other_timezone, provider, start_datetime, value_count, expected_mapping_count
|
||||
):
|
||||
original_tz = set_other_timezone("Etc/UTC")
|
||||
ems_eos = get_ems()
|
||||
ems_eos.set_start_datetime(to_datetime(start_datetime, in_timezone="Europe/Berlin"))
|
||||
assert ems_eos.start_datetime.timezone.name == "Europe/Berlin"
|
||||
|
||||
value_datetime_mapping = provider.import_datetimes(value_count)
|
||||
|
||||
assert len(value_datetime_mapping) == expected_mapping_count
|
||||
|
||||
|
||||
class TestDataContainer:
|
||||
# Fixture and helpers
|
||||
@pytest.fixture
|
||||
def container(self):
|
||||
container = DerivedDataContainer()
|
||||
return container
|
||||
|
||||
@pytest.fixture
|
||||
def container_with_providers(self):
|
||||
record1 = self.create_test_record(datetime(2023, 11, 5), 1)
|
||||
record2 = self.create_test_record(datetime(2023, 11, 6), 2)
|
||||
record3 = self.create_test_record(datetime(2023, 11, 7), 3)
|
||||
provider = DerivedDataProvider()
|
||||
provider.clear()
|
||||
assert len(provider) == 0
|
||||
provider.append(record1)
|
||||
provider.append(record2)
|
||||
provider.append(record3)
|
||||
assert len(provider) == 3
|
||||
container = DerivedDataContainer()
|
||||
container.providers.clear()
|
||||
assert len(container.providers) == 0
|
||||
container.providers.append(provider)
|
||||
assert len(container.providers) == 1
|
||||
return container
|
||||
|
||||
def create_test_record(self, date, value):
|
||||
"""Helper function to create a test DataRecord."""
|
||||
return DerivedRecord(date_time=date, data_value=value)
|
||||
|
||||
def test_append_provider(self, container):
|
||||
assert len(container.providers) == 0
|
||||
container.providers.append(DerivedDataProvider())
|
||||
assert len(container.providers) == 1
|
||||
assert isinstance(container.providers[0], DerivedDataProvider)
|
||||
|
||||
@pytest.mark.skip(reason="type check not implemented")
|
||||
def test_append_provider_invalid_type(self, container):
|
||||
with pytest.raises(ValueError, match="must be an instance of DataProvider"):
|
||||
container.providers.append("not_a_provider")
|
||||
|
||||
def test_getitem_existing_key(self, container_with_providers):
|
||||
assert len(container_with_providers.providers) == 1
|
||||
# check all keys are available (don't care for position)
|
||||
for key in ["data_value", "date_time"]:
|
||||
assert key in list(container_with_providers.keys())
|
||||
series = container_with_providers["data_value"]
|
||||
assert isinstance(series, pd.Series)
|
||||
assert series.name == "data_value"
|
||||
assert series.tolist() == [1.0, 2.0, 3.0]
|
||||
|
||||
def test_getitem_non_existing_key(self, container_with_providers):
|
||||
with pytest.raises(KeyError, match="No data found for key 'non_existent_key'"):
|
||||
container_with_providers["non_existent_key"]
|
||||
|
||||
def test_setitem_existing_key(self, container_with_providers):
|
||||
new_series = container_with_providers["data_value"]
|
||||
new_series[:] = [4, 5, 6]
|
||||
container_with_providers["data_value"] = new_series
|
||||
series = container_with_providers["data_value"]
|
||||
assert series.name == "data_value"
|
||||
assert series.tolist() == [4, 5, 6]
|
||||
|
||||
def test_setitem_invalid_value(self, container_with_providers):
|
||||
with pytest.raises(ValueError, match="Value must be an instance of pd.Series"):
|
||||
container_with_providers["test_key"] = "not_a_series"
|
||||
|
||||
def test_setitem_non_existing_key(self, container_with_providers):
|
||||
new_series = pd.Series([4, 5, 6], name="non_existent_key")
|
||||
with pytest.raises(KeyError, match="Key 'non_existent_key' not found"):
|
||||
container_with_providers["non_existent_key"] = new_series
|
||||
|
||||
def test_delitem_existing_key(self, container_with_providers):
|
||||
del container_with_providers["data_value"]
|
||||
series = container_with_providers["data_value"]
|
||||
assert series.name == "data_value"
|
||||
assert series.tolist() == [None, None, None]
|
||||
|
||||
def test_delitem_non_existing_key(self, container_with_providers):
|
||||
with pytest.raises(KeyError, match="Key 'non_existent_key' not found"):
|
||||
del container_with_providers["non_existent_key"]
|
||||
|
||||
def test_len(self, container_with_providers):
|
||||
assert len(container_with_providers) == 3
|
||||
|
||||
def test_repr(self, container_with_providers):
|
||||
representation = repr(container_with_providers)
|
||||
assert representation.startswith("DerivedDataContainer(")
|
||||
assert "DerivedDataProvider" in representation
|
||||
|
||||
def test_to_json(self, container_with_providers):
|
||||
json_str = container_with_providers.to_json()
|
||||
container_other = DerivedDataContainer.from_json(json_str)
|
||||
assert container_other == container_with_providers
|
||||
|
||||
def test_from_json(self, container_with_providers):
|
||||
json_str = container_with_providers.to_json()
|
||||
container = DerivedDataContainer.from_json(json_str)
|
||||
assert isinstance(container, DerivedDataContainer)
|
||||
assert len(container.providers) == 1
|
||||
assert container.providers[0] == container_with_providers.providers[0]
|
||||
|
||||
def test_provider_by_id(self, container_with_providers):
|
||||
provider = container_with_providers.provider_by_id("DerivedDataProvider")
|
||||
assert isinstance(provider, DerivedDataProvider)
|
@@ -1,95 +1,379 @@
|
||||
"""Test Module for datetimeutil Module."""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from zoneinfo import ZoneInfo
|
||||
"""Test Module for pendulum.datetimeutil Module."""
|
||||
|
||||
import pendulum
|
||||
import pytest
|
||||
from pendulum.tz.timezone import Timezone
|
||||
|
||||
from akkudoktoreos.utils.datetimeutil import to_datetime, to_timedelta, to_timezone
|
||||
from akkudoktoreos.utils.datetimeutil import (
|
||||
compare_datetimes,
|
||||
hours_in_day,
|
||||
to_datetime,
|
||||
to_duration,
|
||||
to_timezone,
|
||||
)
|
||||
|
||||
# -----------------------------
|
||||
# to_datetime
|
||||
# -----------------------------
|
||||
|
||||
|
||||
# Test cases for valid timedelta inputs
|
||||
# Test cases for valid pendulum.duration inputs
|
||||
@pytest.mark.parametrize(
|
||||
"date_input, as_string, to_timezone, to_naiv, to_maxtime, expected_output",
|
||||
"test_case, local_timezone, date_input, as_string, in_timezone, to_naiv, to_maxtime, expected_output",
|
||||
[
|
||||
# as datetime object
|
||||
# ---------------------------------------
|
||||
# from string to pendulum.datetime object
|
||||
# ---------------------------------------
|
||||
# - no timezone
|
||||
(
|
||||
"2024-10-07T10:20:30.000+02:00",
|
||||
"TC001",
|
||||
"Etc/UTC",
|
||||
"2024-01-01",
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
False,
|
||||
pendulum.datetime(2024, 1, 1, 0, 0, 0, tz="Etc/UTC"),
|
||||
),
|
||||
(
|
||||
"TC002",
|
||||
"Europe/Berlin",
|
||||
"2024-01-01",
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
False,
|
||||
pendulum.datetime(2024, 1, 1, 0, 0, 0, tz="Europe/Berlin"),
|
||||
),
|
||||
(
|
||||
"TC003",
|
||||
"Europe/Berlin",
|
||||
"2024-01-01",
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
False,
|
||||
pendulum.datetime(2023, 12, 31, 23, 0, 0, tz="Etc/UTC"),
|
||||
),
|
||||
(
|
||||
"TC004",
|
||||
"Europe/Paris",
|
||||
"2024-01-01 00:00:00",
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
False,
|
||||
pendulum.datetime(2024, 1, 1, 0, 0, 0, tz="Europe/Paris"),
|
||||
),
|
||||
(
|
||||
"TC005",
|
||||
"Etc/UTC",
|
||||
"2024-01-01 00:00:00",
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
False,
|
||||
pendulum.datetime(2024, 1, 1, 1, 0, 0, tz="Europe/Berlin"),
|
||||
),
|
||||
(
|
||||
"TC006",
|
||||
"Europe/Berlin",
|
||||
"2024-01-01 00:00:00",
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
False,
|
||||
pendulum.datetime(2023, 12, 31, 23, 0, 0, tz="Etc/UTC"),
|
||||
),
|
||||
(
|
||||
"TC007",
|
||||
"Atlantic/Canary",
|
||||
"2024-01-01 12:00:00",
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
False,
|
||||
pendulum.datetime(
|
||||
2024,
|
||||
1,
|
||||
1,
|
||||
12,
|
||||
0,
|
||||
0,
|
||||
tz="Atlantic/Canary",
|
||||
),
|
||||
),
|
||||
(
|
||||
"TC008",
|
||||
"Etc/UTC",
|
||||
"2024-01-01 12:00:00",
|
||||
None,
|
||||
None, # force local timezone
|
||||
None,
|
||||
False,
|
||||
pendulum.datetime(2024, 1, 1, 13, 0, 0, tz="Europe/Berlin"),
|
||||
),
|
||||
(
|
||||
"TC009",
|
||||
"Europe/Berlin",
|
||||
"2024-01-01 12:00:00",
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
False,
|
||||
pendulum.datetime(2024, 1, 1, 11, 0, 0, tz="Etc/UTC"),
|
||||
),
|
||||
# - with timezone
|
||||
(
|
||||
"TC010",
|
||||
"Etc/UTC",
|
||||
"02/02/24",
|
||||
None,
|
||||
"Europe/Berlin",
|
||||
None,
|
||||
False,
|
||||
pendulum.datetime(2024, 2, 2, 0, 0, 0, tz="Europe/Berlin"),
|
||||
),
|
||||
(
|
||||
"TC011",
|
||||
"Etc/UTC",
|
||||
"2024-03-03T10:20:30.000+01:00", # No dalight saving time at this date
|
||||
None,
|
||||
"Europe/Berlin",
|
||||
None,
|
||||
None,
|
||||
datetime(2024, 10, 7, 10, 20, 30, 0, tzinfo=ZoneInfo("Europe/Berlin")),
|
||||
pendulum.datetime(2024, 3, 3, 10, 20, 30, 0, tz="Europe/Berlin"),
|
||||
),
|
||||
(
|
||||
"2024-10-07T10:20:30.000+02:00",
|
||||
"TC012",
|
||||
"Etc/UTC",
|
||||
"2024-04-04T10:20:30.000+02:00",
|
||||
None,
|
||||
"Europe/Berlin",
|
||||
False,
|
||||
None,
|
||||
datetime(2024, 10, 7, 10, 20, 30, 0, tzinfo=ZoneInfo("Europe/Berlin")),
|
||||
pendulum.datetime(2024, 4, 4, 10, 20, 30, 0, tz="Europe/Berlin"),
|
||||
),
|
||||
(
|
||||
"2024-10-07T10:20:30.000+02:00",
|
||||
"TC013",
|
||||
"Etc/UTC",
|
||||
"2024-05-05T10:20:30.000+02:00",
|
||||
None,
|
||||
"Europe/Berlin",
|
||||
True,
|
||||
None,
|
||||
datetime(2024, 10, 7, 10, 20, 30, 0),
|
||||
pendulum.naive(2024, 5, 5, 10, 20, 30, 0),
|
||||
),
|
||||
# - without local timezone as UTC
|
||||
(
|
||||
"TC014",
|
||||
"Atlantic/Canary",
|
||||
"02/02/24",
|
||||
None,
|
||||
"UTC",
|
||||
None,
|
||||
False,
|
||||
pendulum.datetime(2024, 2, 2, 0, 0, 0, tz="UTC"),
|
||||
),
|
||||
(
|
||||
"TC015",
|
||||
"Atlantic/Canary",
|
||||
"2024-03-03T10:20:30.000Z", # No dalight saving time at this date
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
pendulum.datetime(2024, 3, 3, 10, 20, 30, 0, tz="UTC"),
|
||||
),
|
||||
# ---------------------------------------
|
||||
# from pendulum.datetime to pendulum.datetime object
|
||||
# ---------------------------------------
|
||||
(
|
||||
"TC016",
|
||||
"Atlantic/Canary",
|
||||
pendulum.datetime(2024, 4, 4, 0, 0, 0),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
False,
|
||||
pendulum.datetime(2024, 4, 4, 0, 0, 0, tz="Etc/UTC"),
|
||||
),
|
||||
(
|
||||
"TC017",
|
||||
"Atlantic/Canary",
|
||||
pendulum.datetime(2024, 4, 4, 1, 0, 0),
|
||||
None,
|
||||
"Europe/Berlin",
|
||||
None,
|
||||
False,
|
||||
pendulum.datetime(2024, 4, 4, 3, 0, 0, tz="Europe/Berlin"),
|
||||
),
|
||||
(
|
||||
"TC018",
|
||||
"Atlantic/Canary",
|
||||
pendulum.datetime(2024, 4, 4, 1, 0, 0, tz="Etc/UTC"),
|
||||
None,
|
||||
"Europe/Berlin",
|
||||
None,
|
||||
False,
|
||||
pendulum.datetime(2024, 4, 4, 3, 0, 0, tz="Europe/Berlin"),
|
||||
),
|
||||
(
|
||||
"TC019",
|
||||
"Atlantic/Canary",
|
||||
pendulum.datetime(2024, 4, 4, 2, 0, 0, tz="Europe/Berlin"),
|
||||
None,
|
||||
"Etc/UTC",
|
||||
None,
|
||||
False,
|
||||
pendulum.datetime(2024, 4, 4, 0, 0, 0, tz="Etc/UTC"),
|
||||
),
|
||||
# ---------------------------------------
|
||||
# from string to UTC string
|
||||
# ---------------------------------------
|
||||
# - no timezone
|
||||
# local timezone UTC
|
||||
(
|
||||
"TC020",
|
||||
"Etc/UTC",
|
||||
"2023-11-06T00:00:00",
|
||||
"UTC",
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
"2023-11-06T00:00:00Z",
|
||||
),
|
||||
# local timezone "Europe/Berlin"
|
||||
(
|
||||
"TC021",
|
||||
"Europe/Berlin",
|
||||
"2023-11-06T00:00:00",
|
||||
"UTC",
|
||||
"Europe/Berlin",
|
||||
None,
|
||||
None,
|
||||
"2023-11-05T23:00:00Z",
|
||||
),
|
||||
# - no microseconds
|
||||
(
|
||||
"TC022",
|
||||
"Atlantic/Canary",
|
||||
"2024-10-30T00:00:00+01:00",
|
||||
"UTC",
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
"2024-10-29T23:00:00Z",
|
||||
),
|
||||
(
|
||||
"TC023",
|
||||
"Atlantic/Canary",
|
||||
"2024-10-30T01:00:00+01:00",
|
||||
"utc",
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
"2024-10-30T00:00:00Z",
|
||||
),
|
||||
# - with microseconds
|
||||
(
|
||||
"TC024",
|
||||
"Atlantic/Canary",
|
||||
"2024-10-07T10:20:30.000+02:00",
|
||||
"UTC",
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
"2024-10-07T08:20:30Z",
|
||||
),
|
||||
# as string
|
||||
("2024-10-07T10:20:30.000+02:00", "UTC", None, None, None, "2024-10-07T08:20:30+00:00"),
|
||||
("2024-10-07T10:20:30.000+02:00", "utc", None, None, None, "2024-10-07T08:20:30+00:00"),
|
||||
],
|
||||
)
|
||||
def test_to_datetime(date_input, as_string, to_timezone, to_naiv, to_maxtime, expected_output):
|
||||
"""Test datetime conversion with valid inputs."""
|
||||
assert (
|
||||
to_datetime(
|
||||
date_input,
|
||||
as_string=as_string,
|
||||
to_timezone=to_timezone,
|
||||
to_naiv=to_naiv,
|
||||
to_maxtime=to_maxtime,
|
||||
)
|
||||
== expected_output
|
||||
def test_to_datetime(
|
||||
set_other_timezone,
|
||||
test_case,
|
||||
local_timezone,
|
||||
date_input,
|
||||
as_string,
|
||||
in_timezone,
|
||||
to_naiv,
|
||||
to_maxtime,
|
||||
expected_output,
|
||||
):
|
||||
"""Test pendulum.datetime conversion with valid inputs."""
|
||||
set_other_timezone(local_timezone)
|
||||
result = to_datetime(
|
||||
date_input,
|
||||
as_string=as_string,
|
||||
in_timezone=in_timezone,
|
||||
to_naiv=to_naiv,
|
||||
to_maxtime=to_maxtime,
|
||||
)
|
||||
# if isinstance(date_input, str):
|
||||
# print(f"Input: {date_input}")
|
||||
# else:
|
||||
# print(f"Input: {date_input} tz={date_input.timezone}")
|
||||
if isinstance(expected_output, str):
|
||||
# print(f"Expected: {expected_output}")
|
||||
# print(f"Result: {result}")
|
||||
assert result == expected_output
|
||||
elif expected_output.timezone is None:
|
||||
# We expect an exception
|
||||
with pytest.raises(TypeError):
|
||||
assert compare_datetimes(result, expected_output).equal
|
||||
else:
|
||||
compare = compare_datetimes(result, expected_output)
|
||||
# print(f"---- Testcase: {test_case} ----")
|
||||
# print(f"Expected: {expected_output} tz={expected_output.timezone}")
|
||||
# print(f"Result: {result} tz={result.timezone}")
|
||||
# print(f"Compare: {compare}")
|
||||
assert compare.equal == True
|
||||
|
||||
|
||||
# -----------------------------
|
||||
# to_timedelta
|
||||
# to_duration
|
||||
# -----------------------------
|
||||
|
||||
|
||||
# Test cases for valid timedelta inputs
|
||||
# Test cases for valid duration inputs
|
||||
@pytest.mark.parametrize(
|
||||
"input_value, expected_output",
|
||||
[
|
||||
# timedelta input
|
||||
(timedelta(days=1), timedelta(days=1)),
|
||||
# duration input
|
||||
(pendulum.duration(days=1), pendulum.duration(days=1)),
|
||||
# String input
|
||||
("2 days", timedelta(days=2)),
|
||||
("5 hours", timedelta(hours=5)),
|
||||
("30 minutes", timedelta(minutes=30)),
|
||||
("45 seconds", timedelta(seconds=45)),
|
||||
("1 day 2 hours 30 minutes 15 seconds", timedelta(days=1, hours=2, minutes=30, seconds=15)),
|
||||
("3 days 4 hours", timedelta(days=3, hours=4)),
|
||||
("2 days", pendulum.duration(days=2)),
|
||||
("5 hours", pendulum.duration(hours=5)),
|
||||
("47 hours", pendulum.duration(hours=47)),
|
||||
("48 hours", pendulum.duration(seconds=48 * 3600)),
|
||||
("30 minutes", pendulum.duration(minutes=30)),
|
||||
("45 seconds", pendulum.duration(seconds=45)),
|
||||
(
|
||||
"1 day 2 hours 30 minutes 15 seconds",
|
||||
pendulum.duration(days=1, hours=2, minutes=30, seconds=15),
|
||||
),
|
||||
("3 days 4 hours", pendulum.duration(days=3, hours=4)),
|
||||
# Integer/Float input
|
||||
(3600, timedelta(seconds=3600)), # 1 hour
|
||||
(86400, timedelta(days=1)), # 1 day
|
||||
(1800.5, timedelta(seconds=1800.5)), # 30 minutes and 0.5 seconds
|
||||
(3600, pendulum.duration(seconds=3600)), # 1 hour
|
||||
(86400, pendulum.duration(days=1)), # 1 day
|
||||
(1800.5, pendulum.duration(seconds=1800.5)), # 30 minutes and 0.5 seconds
|
||||
# Tuple/List input
|
||||
((1, 2, 30, 15), timedelta(days=1, hours=2, minutes=30, seconds=15)),
|
||||
([0, 10, 0, 0], timedelta(hours=10)),
|
||||
((1, 2, 30, 15), pendulum.duration(days=1, hours=2, minutes=30, seconds=15)),
|
||||
([0, 10, 0, 0], pendulum.duration(hours=10)),
|
||||
],
|
||||
)
|
||||
def test_to_timedelta_valid(input_value, expected_output):
|
||||
"""Test to_timedelta with valid inputs."""
|
||||
assert to_timedelta(input_value) == expected_output
|
||||
def test_to_duration_valid(input_value, expected_output):
|
||||
"""Test to_duration with valid inputs."""
|
||||
assert to_duration(input_value) == expected_output
|
||||
|
||||
|
||||
def test_to_duration_summation():
|
||||
start_datetime = to_datetime("2028-01-11 00:00:00")
|
||||
index_datetime = start_datetime
|
||||
for i in range(48):
|
||||
expected_datetime = start_datetime + to_duration(f"{i} hours")
|
||||
assert index_datetime == expected_datetime
|
||||
index_datetime += to_duration("1 hour")
|
||||
assert index_datetime == to_datetime("2028-01-13 00:00:00")
|
||||
|
||||
|
||||
# -----------------------------
|
||||
@@ -99,21 +383,196 @@ def test_to_timedelta_valid(input_value, expected_output):
|
||||
|
||||
def test_to_timezone_string():
|
||||
"""Test to_timezone function returns correct timezone as a string."""
|
||||
lat, lon = 40.7128, -74.0060 # New York City coordinates
|
||||
result = to_timezone(lat, lon, as_string=True)
|
||||
location = (40.7128, -74.0060) # New York City coordinates
|
||||
result = to_timezone(location=location, as_string=True)
|
||||
assert result == "America/New_York", "Expected timezone string 'America/New_York'"
|
||||
|
||||
|
||||
def test_to_timezone_zoneinfo():
|
||||
"""Test to_timezone function returns correct timezone as a ZoneInfo object."""
|
||||
lat, lon = 40.7128, -74.0060 # New York City coordinates
|
||||
result = to_timezone(lat, lon)
|
||||
assert isinstance(result, ZoneInfo), "Expected a ZoneInfo object"
|
||||
assert result.key == "America/New_York", "Expected ZoneInfo key 'America/New_York'"
|
||||
def test_to_timezone_timezone():
|
||||
"""Test to_timezone function returns correct timezone as a Timezone object."""
|
||||
location = (40.7128, -74.0060) # New York City coordinates
|
||||
result = to_timezone(location=location)
|
||||
assert isinstance(result, Timezone), "Expected a Timezone object"
|
||||
assert result.name == "America/New_York", "Expected Timezone name 'America/New_York'"
|
||||
|
||||
|
||||
def test_to_timezone_invalid_coordinates():
|
||||
"""Test to_timezone function handles invalid coordinates gracefully."""
|
||||
lat, lon = 100.0, 200.0 # Invalid coordinates outside Earth range
|
||||
with pytest.raises(ValueError, match="Invalid location"):
|
||||
to_timezone(lat, lon, as_string=True)
|
||||
location = (100.0, 200.0) # Invalid coordinates outside Earth range
|
||||
with pytest.raises(ValueError, match="Invalid latitude/longitude"):
|
||||
to_timezone(location=location, as_string=True)
|
||||
|
||||
|
||||
# -----------------------------
|
||||
# hours_in_day
|
||||
# -----------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"local_timezone, date, in_timezone, expected_hours",
|
||||
[
|
||||
("Etc/UTC", "2024-11-10 00:00:00", "Europe/Berlin", 24), # No DST in Germany
|
||||
("Etc/UTC", "2024-08-10 00:00:00", "Europe/Berlin", 24), # DST in Germany
|
||||
("Etc/UTC", "2024-03-31 00:00:00", "Europe/Berlin", 23), # DST change (23 hours/ day)
|
||||
("Etc/UTC", "2024-10-27 00:00:00", "Europe/Berlin", 25), # DST change (25 hours/ day)
|
||||
("Europe/Berlin", "2024-11-10 00:00:00", "Europe/Berlin", 24), # No DST in Germany
|
||||
("Europe/Berlin", "2024-08-10 00:00:00", "Europe/Berlin", 24), # DST in Germany
|
||||
("Europe/Berlin", "2024-03-31 00:00:00", "Europe/Berlin", 23), # DST change (23 hours/ day)
|
||||
("Europe/Berlin", "2024-10-27 00:00:00", "Europe/Berlin", 25), # DST change (25 hours/ day)
|
||||
],
|
||||
)
|
||||
def test_hours_in_day(set_other_timezone, local_timezone, date, in_timezone, expected_hours):
|
||||
"""Test the `test_hours_in_day` function."""
|
||||
set_other_timezone(local_timezone)
|
||||
date_input = to_datetime(date, in_timezone=in_timezone)
|
||||
assert date_input.timezone.name == in_timezone
|
||||
assert hours_in_day(date_input) == expected_hours
|
||||
|
||||
|
||||
# -----------------------------
|
||||
# compare_datetimes
|
||||
# -----------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"dt1, dt2, equal, ge, gt, le, lt",
|
||||
[
|
||||
# Same time in the same timezone
|
||||
(
|
||||
pendulum.datetime(2024, 3, 15, 12, 0, 0, tz="UTC"),
|
||||
pendulum.datetime(2024, 3, 15, 12, 0, 0, tz="UTC"),
|
||||
True,
|
||||
True,
|
||||
False,
|
||||
True,
|
||||
False,
|
||||
),
|
||||
(
|
||||
pendulum.datetime(2024, 4, 4, 0, 0, 0, tz="Europe/Berlin"),
|
||||
pendulum.datetime(2024, 4, 4, 0, 0, 0, tz="Europe/Berlin"),
|
||||
True,
|
||||
True,
|
||||
False,
|
||||
True,
|
||||
False,
|
||||
),
|
||||
# Same instant in different timezones (converted to UTC)
|
||||
(
|
||||
pendulum.datetime(2024, 3, 15, 8, 0, 0, tz="Europe/Berlin"),
|
||||
pendulum.datetime(2024, 3, 15, 7, 0, 0, tz="UTC"),
|
||||
True,
|
||||
True,
|
||||
False,
|
||||
True,
|
||||
False,
|
||||
),
|
||||
# Different times across timezones (converted to UTC)
|
||||
(
|
||||
pendulum.datetime(2024, 3, 15, 8, 0, 0, tz="America/New_York"),
|
||||
pendulum.datetime(2024, 3, 15, 12, 0, 0, tz="UTC"),
|
||||
True,
|
||||
True,
|
||||
False,
|
||||
True,
|
||||
False,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_compare_datetimes_equal(dt1, dt2, equal, ge, gt, le, lt):
|
||||
# requal = compare_datetimes(dt1, dt2).equal
|
||||
# rgt = compare_datetimes(dt1, dt2).gt
|
||||
# rge = compare_datetimes(dt1, dt2).ge
|
||||
# rlt = compare_datetimes(dt1, dt2).lt
|
||||
# rle = compare_datetimes(dt1, dt2).le
|
||||
# print(f"{dt1} vs. {dt2}: expected equal={equal}, ge={ge}, gt={gt}, le={le}, lt={lt}")
|
||||
# print(f"{dt1} vs. {dt2}: result equal={requal}, ge={rge}, gt={rgt}, le={rle}, lt={rlt}")
|
||||
assert compare_datetimes(dt1, dt2).equal == equal
|
||||
assert compare_datetimes(dt1, dt2).ge == ge
|
||||
assert compare_datetimes(dt1, dt2).gt == gt
|
||||
assert compare_datetimes(dt1, dt2).le == le
|
||||
assert compare_datetimes(dt1, dt2).lt == lt
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"dt1, dt2, equal, ge, gt, le, lt",
|
||||
[
|
||||
# Different times in the same timezone
|
||||
(
|
||||
pendulum.datetime(2024, 3, 15, 11, 0, 0, tz="UTC"),
|
||||
pendulum.datetime(2024, 3, 15, 12, 0, 0, tz="UTC"),
|
||||
False,
|
||||
False,
|
||||
False,
|
||||
True,
|
||||
True,
|
||||
),
|
||||
# Different times across timezones (converted to UTC)
|
||||
(
|
||||
pendulum.datetime(2024, 3, 15, 6, 0, 0, tz="America/New_York"),
|
||||
pendulum.datetime(2024, 3, 15, 12, 0, 0, tz="UTC"),
|
||||
False,
|
||||
False,
|
||||
False,
|
||||
True,
|
||||
True,
|
||||
),
|
||||
# DST changes: spring forward
|
||||
(
|
||||
pendulum.datetime(2024, 3, 10, 1, 59, 0, tz="America/New_York"),
|
||||
pendulum.datetime(2024, 3, 10, 3, 0, 0, tz="America/New_York"),
|
||||
False,
|
||||
False,
|
||||
False,
|
||||
True,
|
||||
True,
|
||||
),
|
||||
# DST changes: fall back
|
||||
(
|
||||
pendulum.datetime(2024, 11, 3, 1, 0, 0, tz="America/New_York"),
|
||||
pendulum.datetime(2024, 11, 3, 1, 30, 0, tz="America/New_York"),
|
||||
False,
|
||||
False,
|
||||
False,
|
||||
True,
|
||||
True,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_compare_datetimes_lt(dt1, dt2, equal, ge, gt, le, lt):
|
||||
# requal = compare_datetimes(dt1, dt2).equal
|
||||
# rgt = compare_datetimes(dt1, dt2).gt
|
||||
# rge = compare_datetimes(dt1, dt2).ge
|
||||
# rlt = compare_datetimes(dt1, dt2).lt
|
||||
# rle = compare_datetimes(dt1, dt2).le
|
||||
# print(f"{dt1} vs. {dt2}: expected equal={equal}, ge={ge}, gt={gt}, le={le}, lt={lt}")
|
||||
# print(f"{dt1} vs. {dt2}: result equal={requal}, ge={rge}, gt={rgt}, le={rle}, lt={rlt}")
|
||||
assert compare_datetimes(dt1, dt2).equal == equal
|
||||
assert compare_datetimes(dt1, dt2).ge == ge
|
||||
assert compare_datetimes(dt1, dt2).gt == gt
|
||||
assert compare_datetimes(dt1, dt2).le == le
|
||||
assert compare_datetimes(dt1, dt2).lt == lt
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"dt1, dt2",
|
||||
[
|
||||
# Different times in the same timezone
|
||||
(
|
||||
pendulum.datetime(2024, 3, 15, 13, 0, 0, tz="UTC"),
|
||||
pendulum.datetime(2024, 3, 15, 12, 0, 0, tz="UTC"),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_compare_datetimes_gt(dt1, dt2):
|
||||
# requal = compare_datetimes(dt1, dt2).equal
|
||||
# rgt = compare_datetimes(dt1, dt2).gt
|
||||
# rge = compare_datetimes(dt1, dt2).ge
|
||||
# rlt = compare_datetimes(dt1, dt2).lt
|
||||
# rle = compare_datetimes(dt1, dt2).le
|
||||
# print(f"{dt1} vs. {dt2}: expected equal={equal}, ge={ge}, gt={gt}, le={le}, lt={lt}")
|
||||
# print(f"{dt1} vs. {dt2}: result equal={requal}, ge={rge}, gt={rgt}, le={rle}, lt={rlt}")
|
||||
assert compare_datetimes(dt1, dt2).equal == False
|
||||
assert compare_datetimes(dt1, dt2).ge
|
||||
assert compare_datetimes(dt1, dt2).gt
|
||||
assert compare_datetimes(dt1, dt2).le == False
|
||||
assert compare_datetimes(dt1, dt2).lt == False
|
||||
|
141
tests/test_elecpriceakkudoktor.py
Normal file
141
tests/test_elecpriceakkudoktor.py
Normal file
@@ -0,0 +1,141 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from akkudoktoreos.core.ems import get_ems
|
||||
from akkudoktoreos.prediction.elecpriceakkudoktor import (
|
||||
AkkudoktorElecPrice,
|
||||
AkkudoktorElecPriceValue,
|
||||
ElecPriceAkkudoktor,
|
||||
)
|
||||
from akkudoktoreos.utils.cacheutil import CacheFileStore
|
||||
from akkudoktoreos.utils.datetimeutil import to_datetime
|
||||
|
||||
DIR_TESTDATA = Path(__file__).absolute().parent.joinpath("testdata")
|
||||
|
||||
FILE_TESTDATA_ELECPRICEAKKUDOKTOR_1_JSON = DIR_TESTDATA.joinpath(
|
||||
"elecpriceforecast_akkudoktor_1.json"
|
||||
)
|
||||
|
||||
ems_eos = get_ems()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def elecprice_provider(monkeypatch):
|
||||
"""Fixture to create a ElecPriceProvider instance."""
|
||||
monkeypatch.setenv("elecprice_provider", "Akkudoktor")
|
||||
return ElecPriceAkkudoktor()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_akkudoktor_1_json():
|
||||
"""Fixture that returns sample forecast data report."""
|
||||
with open(FILE_TESTDATA_ELECPRICEAKKUDOKTOR_1_JSON, "r") 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(elecprice_provider):
|
||||
"""Test that ElecPriceForecast behaves as a singleton."""
|
||||
another_instance = ElecPriceAkkudoktor()
|
||||
assert elecprice_provider is another_instance
|
||||
|
||||
|
||||
def test_invalid_provider(elecprice_provider, monkeypatch):
|
||||
"""Test requesting an unsupported elecprice_provider."""
|
||||
monkeypatch.setenv("elecprice_provider", "<invalid>")
|
||||
elecprice_provider.config.update()
|
||||
assert elecprice_provider.enabled() == False
|
||||
|
||||
|
||||
# ------------------------------------------------
|
||||
# Akkudoktor
|
||||
# ------------------------------------------------
|
||||
|
||||
|
||||
@patch("requests.get")
|
||||
def test_request_forecast(mock_get, elecprice_provider, sample_akkudoktor_1_json):
|
||||
"""Test requesting forecast from Akkudoktor."""
|
||||
# Mock response object
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.content = json.dumps(sample_akkudoktor_1_json)
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
# Preset, as this is usually done by update()
|
||||
elecprice_provider.config.update()
|
||||
|
||||
# Test function
|
||||
akkudoktor_data = elecprice_provider._request_forecast()
|
||||
|
||||
assert isinstance(akkudoktor_data, AkkudoktorElecPrice)
|
||||
assert akkudoktor_data.values[0] == AkkudoktorElecPriceValue(
|
||||
start_timestamp=1733871600000,
|
||||
end_timestamp=1733875200000,
|
||||
start="2024-12-10T23:00:00.000Z",
|
||||
end="2024-12-11T00:00:00.000Z",
|
||||
marketprice=115.94,
|
||||
unit="Eur/MWh",
|
||||
marketpriceEurocentPerKWh=11.59,
|
||||
)
|
||||
|
||||
|
||||
@patch("requests.get")
|
||||
def test_update_data(mock_get, elecprice_provider, sample_akkudoktor_1_json, cache_store):
|
||||
"""Test fetching forecast from Akkudoktor."""
|
||||
# Mock response object
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.content = json.dumps(sample_akkudoktor_1_json)
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
cache_store.clear(clear_all=True)
|
||||
|
||||
# Call the method
|
||||
ems_eos.set_start_datetime(to_datetime("2024-12-11 00:00:00", in_timezone="Europe/Berlin"))
|
||||
elecprice_provider.update_data(force_enable=True, force_update=True)
|
||||
|
||||
# Assert: Verify the result is as expected
|
||||
mock_get.assert_called_once()
|
||||
assert len(elecprice_provider) == 25
|
||||
|
||||
# Assert we get prediction_hours prioce values by resampling
|
||||
np_price_array = elecprice_provider.key_to_array(
|
||||
key="elecprice_marketprice",
|
||||
start_datetime=elecprice_provider.start_datetime,
|
||||
end_datetime=elecprice_provider.end_datetime,
|
||||
)
|
||||
assert len(np_price_array) == elecprice_provider.total_hours
|
||||
|
||||
# with open(FILE_TESTDATA_ELECPRICEAKKUDOKTOR_2_JSON, "w") as f_out:
|
||||
# f_out.write(elecprice_provider.to_json())
|
||||
|
||||
|
||||
# ------------------------------------------------
|
||||
# Development Akkudoktor
|
||||
# ------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="For development only")
|
||||
def test_akkudoktor_development_forecast_data(elecprice_provider):
|
||||
"""Fetch data from real Akkudoktor server."""
|
||||
# Preset, as this is usually done by update_data()
|
||||
elecprice_provider.start_datetime = to_datetime("2024-10-26 00:00:00")
|
||||
|
||||
akkudoktor_data = elecprice_provider._request_forecast()
|
||||
|
||||
with open(FILE_TESTDATA_ELECPRICEAKKUDOKTOR_1_JSON, "w") as f_out:
|
||||
json.dump(akkudoktor_data, f_out, indent=4)
|
110
tests/test_elecpriceimport.py
Normal file
110
tests/test_elecpriceimport.py
Normal file
@@ -0,0 +1,110 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from akkudoktoreos.config.config import get_config
|
||||
from akkudoktoreos.core.ems import get_ems
|
||||
from akkudoktoreos.prediction.elecpriceimport import ElecPriceImport
|
||||
from akkudoktoreos.utils.datetimeutil import compare_datetimes, to_datetime
|
||||
|
||||
DIR_TESTDATA = Path(__file__).absolute().parent.joinpath("testdata")
|
||||
|
||||
FILE_TESTDATA_ELECPRICEIMPORT_1_JSON = DIR_TESTDATA.joinpath("import_input_1.json")
|
||||
|
||||
config_eos = get_config()
|
||||
ems_eos = get_ems()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def elecprice_provider(reset_config, sample_import_1_json):
|
||||
"""Fixture to create a ElecPriceProvider instance."""
|
||||
settings = {
|
||||
"elecprice_provider": "ElecPriceImport",
|
||||
"elecpriceimport_file_path": str(FILE_TESTDATA_ELECPRICEIMPORT_1_JSON),
|
||||
"elecpriceimport_json": json.dumps(sample_import_1_json),
|
||||
}
|
||||
config_eos.merge_settings_from_dict(settings)
|
||||
provider = ElecPriceImport()
|
||||
assert provider.enabled() == True
|
||||
return provider
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_import_1_json():
|
||||
"""Fixture that returns sample forecast data report."""
|
||||
with open(FILE_TESTDATA_ELECPRICEIMPORT_1_JSON, "r") as f_res:
|
||||
input_data = json.load(f_res)
|
||||
return input_data
|
||||
|
||||
|
||||
# ------------------------------------------------
|
||||
# General forecast
|
||||
# ------------------------------------------------
|
||||
|
||||
|
||||
def test_singleton_instance(elecprice_provider):
|
||||
"""Test that ElecPriceForecast behaves as a singleton."""
|
||||
another_instance = ElecPriceImport()
|
||||
assert elecprice_provider is another_instance
|
||||
|
||||
|
||||
def test_invalid_provider(elecprice_provider):
|
||||
"""Test requesting an unsupported elecprice_provider."""
|
||||
settings = {
|
||||
"elecprice_provider": "<invalid>",
|
||||
"elecpriceimport_file_path": str(FILE_TESTDATA_ELECPRICEIMPORT_1_JSON),
|
||||
}
|
||||
config_eos.merge_settings_from_dict(settings)
|
||||
assert elecprice_provider.enabled() == False
|
||||
|
||||
|
||||
# ------------------------------------------------
|
||||
# Import
|
||||
# ------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"start_datetime, from_file",
|
||||
[
|
||||
("2024-11-10 00:00:00", True), # No DST in Germany
|
||||
("2024-08-10 00:00:00", True), # DST in Germany
|
||||
("2024-03-31 00:00:00", True), # DST change in Germany (23 hours/ day)
|
||||
("2024-10-27 00:00:00", True), # DST change in Germany (25 hours/ day)
|
||||
("2024-11-10 00:00:00", False), # No DST in Germany
|
||||
("2024-08-10 00:00:00", False), # DST in Germany
|
||||
("2024-03-31 00:00:00", False), # DST change in Germany (23 hours/ day)
|
||||
("2024-10-27 00:00:00", False), # DST change in Germany (25 hours/ day)
|
||||
],
|
||||
)
|
||||
def test_import(elecprice_provider, sample_import_1_json, start_datetime, from_file):
|
||||
"""Test fetching forecast from Import."""
|
||||
ems_eos.set_start_datetime(to_datetime(start_datetime, in_timezone="Europe/Berlin"))
|
||||
if from_file:
|
||||
config_eos.elecpriceimport_json = None
|
||||
assert config_eos.elecpriceimport_json is None
|
||||
else:
|
||||
config_eos.elecpriceimport_file_path = None
|
||||
assert config_eos.elecpriceimport_file_path is None
|
||||
elecprice_provider.clear()
|
||||
|
||||
# Call the method
|
||||
elecprice_provider.update_data()
|
||||
|
||||
# Assert: Verify the result is as expected
|
||||
assert elecprice_provider.start_datetime is not None
|
||||
assert elecprice_provider.total_hours is not None
|
||||
assert compare_datetimes(elecprice_provider.start_datetime, ems_eos.start_datetime).equal
|
||||
values = sample_import_1_json["elecprice_marketprice"]
|
||||
value_datetime_mapping = elecprice_provider.import_datetimes(len(values))
|
||||
for i, mapping in enumerate(value_datetime_mapping):
|
||||
assert i < len(elecprice_provider.records)
|
||||
expected_datetime, expected_value_index = mapping
|
||||
expected_value = values[expected_value_index]
|
||||
result_datetime = elecprice_provider.records[i].date_time
|
||||
result_value = elecprice_provider.records[i]["elecprice_marketprice"]
|
||||
|
||||
# print(f"{i}: Expected: {expected_datetime}:{expected_value}")
|
||||
# print(f"{i}: Result: {result_datetime}:{result_value}")
|
||||
assert compare_datetimes(result_datetime, expected_datetime).equal
|
||||
assert result_value == expected_value
|
99
tests/test_loadakkudoktor.py
Normal file
99
tests/test_loadakkudoktor.py
Normal file
@@ -0,0 +1,99 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
import numpy as np
|
||||
import pendulum
|
||||
import pytest
|
||||
|
||||
from akkudoktoreos.config.config import get_config
|
||||
from akkudoktoreos.core.ems import get_ems
|
||||
from akkudoktoreos.prediction.loadakkudoktor import (
|
||||
LoadAkkudoktor,
|
||||
LoadAkkudoktorCommonSettings,
|
||||
)
|
||||
|
||||
config_eos = get_config()
|
||||
ems_eos = get_ems()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def load_provider(monkeypatch):
|
||||
"""Fixture to create a LoadAkkudoktor instance."""
|
||||
settings = {
|
||||
"load0_provider": "LoadAkkudoktor",
|
||||
"load0_name": "Akkudoktor Profile",
|
||||
"loadakkudoktor_year_energy": "1000",
|
||||
}
|
||||
config_eos.merge_settings_from_dict(settings)
|
||||
return LoadAkkudoktor()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_load_profiles_file(tmp_path):
|
||||
"""Fixture to create a mock load profiles file."""
|
||||
load_profiles_path = tmp_path / "load_profiles.npz"
|
||||
np.savez(
|
||||
load_profiles_path,
|
||||
yearly_profiles=np.random.rand(365, 24), # Random load profiles
|
||||
yearly_profiles_std=np.random.rand(365, 24), # Random standard deviation
|
||||
)
|
||||
return load_profiles_path
|
||||
|
||||
|
||||
def test_loadakkudoktor_settings_validator():
|
||||
"""Test the field validator for `loadakkudoktor_year_energy`."""
|
||||
settings = LoadAkkudoktorCommonSettings(loadakkudoktor_year_energy=1234)
|
||||
assert isinstance(settings.loadakkudoktor_year_energy, float)
|
||||
assert settings.loadakkudoktor_year_energy == 1234.0
|
||||
|
||||
settings = LoadAkkudoktorCommonSettings(loadakkudoktor_year_energy=1234.56)
|
||||
assert isinstance(settings.loadakkudoktor_year_energy, float)
|
||||
assert settings.loadakkudoktor_year_energy == 1234.56
|
||||
|
||||
|
||||
def test_loadakkudoktor_provider_id(load_provider):
|
||||
"""Test the `provider_id` class method."""
|
||||
assert load_provider.provider_id() == "LoadAkkudoktor"
|
||||
|
||||
|
||||
@patch("akkudoktoreos.prediction.loadakkudoktor.Path")
|
||||
@patch("akkudoktoreos.prediction.loadakkudoktor.np.load")
|
||||
def test_load_data_from_mock(mock_np_load, mock_path, mock_load_profiles_file, load_provider):
|
||||
"""Test the `load_data` method."""
|
||||
# Mock path behavior to return the test file
|
||||
mock_path.return_value.parent.parent.joinpath.return_value = mock_load_profiles_file
|
||||
|
||||
# Mock numpy load to return data similar to what would be in the file
|
||||
mock_np_load.return_value = {
|
||||
"yearly_profiles": np.ones((365, 24)),
|
||||
"yearly_profiles_std": np.zeros((365, 24)),
|
||||
}
|
||||
|
||||
# Test data loading
|
||||
data_year_energy = load_provider.load_data()
|
||||
assert data_year_energy is not None
|
||||
assert data_year_energy.shape == (365, 2, 24)
|
||||
|
||||
|
||||
def test_load_data_from_file(load_provider):
|
||||
"""Test `load_data` loads data from the profiles file."""
|
||||
data_year_energy = load_provider.load_data()
|
||||
assert data_year_energy is not None
|
||||
|
||||
|
||||
@patch("akkudoktoreos.prediction.loadakkudoktor.LoadAkkudoktor.load_data")
|
||||
def test_update_data(mock_load_data, load_provider):
|
||||
"""Test the `_update` method."""
|
||||
mock_load_data.return_value = np.random.rand(365, 2, 24)
|
||||
|
||||
# Mock methods for updating values
|
||||
ems_eos.set_start_datetime(pendulum.datetime(2024, 1, 1))
|
||||
|
||||
# Assure there are no prediction records
|
||||
load_provider.clear()
|
||||
assert len(load_provider) == 0
|
||||
|
||||
# Execute the method
|
||||
load_provider._update_data()
|
||||
|
||||
# Validate that update_value is called
|
||||
assert len(load_provider) > 0
|
@@ -16,4 +16,9 @@ def test_openapi_spec_current():
|
||||
new_spec = json.load(f_new)
|
||||
with open(old_spec_path) as f_old:
|
||||
old_spec = json.load(f_old)
|
||||
|
||||
# Serialize to ensure comparison is consistent
|
||||
new_spec = json.dumps(new_spec, indent=4, sort_keys=True)
|
||||
old_spec = json.dumps(old_spec, indent=4, sort_keys=True)
|
||||
|
||||
assert new_spec == old_spec
|
||||
|
226
tests/test_prediction.py
Normal file
226
tests/test_prediction.py
Normal file
@@ -0,0 +1,226 @@
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from akkudoktoreos.config.config import get_config
|
||||
from akkudoktoreos.core.ems import get_ems
|
||||
from akkudoktoreos.prediction.elecpriceakkudoktor import ElecPriceAkkudoktor
|
||||
from akkudoktoreos.prediction.elecpriceimport import ElecPriceImport
|
||||
from akkudoktoreos.prediction.loadakkudoktor import LoadAkkudoktor
|
||||
from akkudoktoreos.prediction.loadimport import LoadImport
|
||||
from akkudoktoreos.prediction.prediction import (
|
||||
Prediction,
|
||||
PredictionCommonSettings,
|
||||
get_prediction,
|
||||
)
|
||||
from akkudoktoreos.prediction.pvforecastakkudoktor import PVForecastAkkudoktor
|
||||
from akkudoktoreos.prediction.pvforecastimport import PVForecastImport
|
||||
from akkudoktoreos.prediction.weatherbrightsky import WeatherBrightSky
|
||||
from akkudoktoreos.prediction.weatherclearoutside import WeatherClearOutside
|
||||
from akkudoktoreos.prediction.weatherimport import WeatherImport
|
||||
from akkudoktoreos.utils.datetimeutil import compare_datetimes, to_datetime
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_settings(reset_config):
|
||||
"""Fixture that adds settings data to the global config."""
|
||||
settings = {
|
||||
"prediction_hours": 48,
|
||||
"prediction_historic_hours": 24,
|
||||
"latitude": 52.52,
|
||||
"longitude": 13.405,
|
||||
"pvforecast_provider": "PVForecastAkkudoktor",
|
||||
"pvforecast0_peakpower": 5.0,
|
||||
"pvforecast0_surface_azimuth": -10,
|
||||
"pvforecast0_surface_tilt": 7,
|
||||
"pvforecast0_userhorizon": [20, 27, 22, 20],
|
||||
"pvforecast0_inverter_paco": 10000,
|
||||
"pvforecast1_peakpower": 4.8,
|
||||
"pvforecast1_surface_azimuth": -90,
|
||||
"pvforecast1_surface_tilt": 7,
|
||||
"pvforecast1_userhorizon": [30, 30, 30, 50],
|
||||
"pvforecast1_inverter_paco": 10000,
|
||||
"pvforecast2_peakpower": 1.4,
|
||||
"pvforecast2_surface_azimuth": -40,
|
||||
"pvforecast2_surface_tilt": 60,
|
||||
"pvforecast2_userhorizon": [60, 30, 0, 30],
|
||||
"pvforecast2_inverter_paco": 2000,
|
||||
"pvforecast3_peakpower": 1.6,
|
||||
"pvforecast3_surface_azimuth": 5,
|
||||
"pvforecast3_surface_tilt": 45,
|
||||
"pvforecast3_userhorizon": [45, 25, 30, 60],
|
||||
"pvforecast3_inverter_paco": 1400,
|
||||
"pvforecast4_peakpower": None,
|
||||
}
|
||||
|
||||
# Merge settings to config
|
||||
config = get_config()
|
||||
config.merge_settings_from_dict(settings)
|
||||
return config
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def prediction():
|
||||
"""All EOS predictions."""
|
||||
return get_prediction()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def forecast_providers():
|
||||
"""Fixture for singleton forecast provider instances."""
|
||||
return [
|
||||
ElecPriceAkkudoktor(),
|
||||
ElecPriceImport(),
|
||||
LoadAkkudoktor(),
|
||||
LoadImport(),
|
||||
PVForecastAkkudoktor(),
|
||||
PVForecastImport(),
|
||||
WeatherBrightSky(),
|
||||
WeatherClearOutside(),
|
||||
WeatherImport(),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"prediction_hours, prediction_historic_hours, latitude, longitude, expected_timezone",
|
||||
[
|
||||
(48, 24, 40.7128, -74.0060, "America/New_York"), # Valid latitude/longitude
|
||||
(0, 0, None, None, None), # No location
|
||||
(100, 50, 51.5074, -0.1278, "Europe/London"), # Another valid location
|
||||
],
|
||||
)
|
||||
def test_prediction_common_settings_valid(
|
||||
prediction_hours, prediction_historic_hours, latitude, longitude, expected_timezone
|
||||
):
|
||||
"""Test valid settings for PredictionCommonSettings."""
|
||||
settings = PredictionCommonSettings(
|
||||
prediction_hours=prediction_hours,
|
||||
prediction_historic_hours=prediction_historic_hours,
|
||||
latitude=latitude,
|
||||
longitude=longitude,
|
||||
)
|
||||
assert settings.prediction_hours == prediction_hours
|
||||
assert settings.prediction_historic_hours == prediction_historic_hours
|
||||
assert settings.latitude == latitude
|
||||
assert settings.longitude == longitude
|
||||
assert settings.timezone == expected_timezone
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"field_name, invalid_value, expected_error",
|
||||
[
|
||||
("prediction_hours", -1, "Input should be greater than or equal to 0"),
|
||||
("prediction_historic_hours", -5, "Input should be greater than or equal to 0"),
|
||||
("latitude", -91.0, "Input should be greater than or equal to -90"),
|
||||
("latitude", 91.0, "Input should be less than or equal to 90"),
|
||||
("longitude", -181.0, "Input should be greater than or equal to -180"),
|
||||
("longitude", 181.0, "Input should be less than or equal to 180"),
|
||||
],
|
||||
)
|
||||
def test_prediction_common_settings_invalid(field_name, invalid_value, expected_error):
|
||||
"""Test invalid settings for PredictionCommonSettings."""
|
||||
valid_data = {
|
||||
"prediction_hours": 48,
|
||||
"prediction_historic_hours": 24,
|
||||
"latitude": 40.7128,
|
||||
"longitude": -74.0060,
|
||||
}
|
||||
valid_data[field_name] = invalid_value
|
||||
|
||||
with pytest.raises(ValidationError, match=expected_error):
|
||||
PredictionCommonSettings(**valid_data)
|
||||
|
||||
|
||||
def test_prediction_common_settings_no_location():
|
||||
"""Test that timezone is None when latitude and longitude are not provided."""
|
||||
settings = PredictionCommonSettings(
|
||||
prediction_hours=48, prediction_historic_hours=24, latitude=None, longitude=None
|
||||
)
|
||||
assert settings.timezone is None
|
||||
|
||||
|
||||
def test_prediction_common_settings_with_location():
|
||||
"""Test that timezone is correctly computed when latitude and longitude are provided."""
|
||||
settings = PredictionCommonSettings(
|
||||
prediction_hours=48, prediction_historic_hours=24, latitude=34.0522, longitude=-118.2437
|
||||
)
|
||||
assert settings.timezone == "America/Los_Angeles"
|
||||
|
||||
|
||||
def test_prediction_common_settings_timezone_none_when_coordinates_missing():
|
||||
"""Test that timezone is None when latitude or longitude is missing."""
|
||||
config_no_latitude = PredictionCommonSettings(longitude=-74.0060)
|
||||
config_no_longitude = PredictionCommonSettings(latitude=40.7128)
|
||||
config_no_coords = PredictionCommonSettings()
|
||||
|
||||
assert config_no_latitude.timezone is None
|
||||
assert config_no_longitude.timezone is None
|
||||
assert config_no_coords.timezone is None
|
||||
|
||||
|
||||
def test_initialization(prediction, forecast_providers):
|
||||
"""Test that Prediction is initialized with the correct providers in sequence."""
|
||||
assert isinstance(prediction, Prediction)
|
||||
assert prediction.providers == forecast_providers
|
||||
|
||||
|
||||
def test_provider_sequence(prediction):
|
||||
"""Test the provider sequence is maintained in the Prediction instance."""
|
||||
assert isinstance(prediction.providers[0], ElecPriceAkkudoktor)
|
||||
assert isinstance(prediction.providers[1], ElecPriceImport)
|
||||
assert isinstance(prediction.providers[2], LoadAkkudoktor)
|
||||
assert isinstance(prediction.providers[3], LoadImport)
|
||||
assert isinstance(prediction.providers[4], PVForecastAkkudoktor)
|
||||
assert isinstance(prediction.providers[5], PVForecastImport)
|
||||
assert isinstance(prediction.providers[6], WeatherBrightSky)
|
||||
assert isinstance(prediction.providers[7], WeatherClearOutside)
|
||||
assert isinstance(prediction.providers[8], WeatherImport)
|
||||
|
||||
|
||||
def test_update_calls_providers(sample_settings, prediction):
|
||||
"""Test that the update method calls the update method for each provider in sequence."""
|
||||
# Mark the `update_datetime` method for each provider
|
||||
old_datetime = to_datetime("1970-01-01 00:00:00")
|
||||
for provider in prediction.providers:
|
||||
provider.update_datetime = old_datetime
|
||||
|
||||
ems_eos = get_ems()
|
||||
ems_eos.set_start_datetime(to_datetime())
|
||||
prediction.update_data()
|
||||
|
||||
# Verify each provider's `update` method was called
|
||||
for provider in prediction.providers:
|
||||
if provider.enabled():
|
||||
assert compare_datetimes(provider.update_datetime, old_datetime).gt
|
||||
|
||||
|
||||
def test_provider_by_id(prediction, forecast_providers):
|
||||
"""Test that provider_by_id method returns the correct provider."""
|
||||
for provider in forecast_providers:
|
||||
assert prediction.provider_by_id(provider.provider_id()) == provider
|
||||
|
||||
|
||||
def test_prediction_repr(prediction):
|
||||
"""Test that the Prediction instance's representation is correct."""
|
||||
result = repr(prediction)
|
||||
assert "Prediction([" in result
|
||||
assert "ElecPriceAkkudoktor" in result
|
||||
assert "ElecPriceImport" in result
|
||||
assert "LoadAkkudoktor" in result
|
||||
assert "LoadImport" in result
|
||||
assert "PVForecastAkkudoktor" in result
|
||||
assert "PVForecastImport" in result
|
||||
assert "WeatherBrightSky" in result
|
||||
assert "WeatherClearOutside" in result
|
||||
assert "WeatherImport" in result
|
||||
|
||||
|
||||
def test_empty_providers(prediction, forecast_providers):
|
||||
"""Test behavior when Prediction does not have providers."""
|
||||
# Clear all prediction providers from prediction
|
||||
providers_bkup = prediction.providers.copy()
|
||||
prediction.providers.clear()
|
||||
assert prediction.providers == []
|
||||
prediction.update_data() # Should not raise an error even with no providers
|
||||
|
||||
# Cleanup after Test
|
||||
prediction.providers = providers_bkup
|
437
tests/test_predictionabc.py
Normal file
437
tests/test_predictionabc.py
Normal file
@@ -0,0 +1,437 @@
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import Any, ClassVar, List, Optional, Union
|
||||
|
||||
import pandas as pd
|
||||
import pendulum
|
||||
import pytest
|
||||
from pydantic import Field
|
||||
|
||||
from akkudoktoreos.config.config import get_config
|
||||
from akkudoktoreos.core.ems import get_ems
|
||||
from akkudoktoreos.prediction.prediction import PredictionCommonSettings
|
||||
from akkudoktoreos.prediction.predictionabc import (
|
||||
PredictionBase,
|
||||
PredictionContainer,
|
||||
PredictionProvider,
|
||||
PredictionRecord,
|
||||
PredictionSequence,
|
||||
)
|
||||
from akkudoktoreos.utils.datetimeutil import compare_datetimes, to_datetime, to_duration
|
||||
|
||||
# Derived classes for testing
|
||||
# ---------------------------
|
||||
|
||||
|
||||
class DerivedConfig(PredictionCommonSettings):
|
||||
env_var: Optional[int] = Field(default=None, description="Test config by environment var")
|
||||
instance_field: Optional[str] = Field(default=None, description="Test config by instance field")
|
||||
class_constant: Optional[int] = Field(default=None, description="Test config by class constant")
|
||||
|
||||
|
||||
class DerivedBase(PredictionBase):
|
||||
instance_field: Optional[str] = Field(default=None, description="Field Value")
|
||||
class_constant: ClassVar[int] = 30
|
||||
|
||||
|
||||
class DerivedRecord(PredictionRecord):
|
||||
prediction_value: Optional[float] = Field(default=None, description="Prediction Value")
|
||||
|
||||
|
||||
class DerivedSequence(PredictionSequence):
|
||||
# overload
|
||||
records: List[DerivedRecord] = Field(
|
||||
default_factory=list, description="List of DerivedRecord records"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def record_class(cls) -> Any:
|
||||
return DerivedRecord
|
||||
|
||||
|
||||
class DerivedPredictionProvider(PredictionProvider):
|
||||
"""A concrete subclass of PredictionProvider for testing purposes."""
|
||||
|
||||
# overload
|
||||
records: List[DerivedRecord] = Field(
|
||||
default_factory=list, description="List of DerivedRecord records"
|
||||
)
|
||||
provider_enabled: ClassVar[bool] = False
|
||||
provider_updated: ClassVar[bool] = False
|
||||
|
||||
@classmethod
|
||||
def record_class(cls) -> Any:
|
||||
return DerivedRecord
|
||||
|
||||
# Implement abstract methods for test purposes
|
||||
def provider_id(self) -> str:
|
||||
return "DerivedPredictionProvider"
|
||||
|
||||
def enabled(self) -> bool:
|
||||
return self.provider_enabled
|
||||
|
||||
def _update_data(self, force_update: Optional[bool] = False) -> None:
|
||||
# Simulate update logic
|
||||
DerivedPredictionProvider.provider_updated = True
|
||||
|
||||
|
||||
class DerivedPredictionContainer(PredictionContainer):
|
||||
providers: List[Union[DerivedPredictionProvider, PredictionProvider]] = Field(
|
||||
default_factory=list, description="List of prediction providers"
|
||||
)
|
||||
|
||||
|
||||
# Tests
|
||||
# ----------
|
||||
|
||||
|
||||
class TestPredictionBase:
|
||||
@pytest.fixture
|
||||
def base(self, reset_config, monkeypatch):
|
||||
# Provide default values for configuration
|
||||
monkeypatch.setenv("latitude", "50.0")
|
||||
monkeypatch.setenv("longitude", "10.0")
|
||||
derived = DerivedBase()
|
||||
derived.config.update()
|
||||
return derived
|
||||
|
||||
def test_config_value_from_env_variable(self, base, monkeypatch):
|
||||
# From Prediction Config
|
||||
monkeypatch.setenv("latitude", "2.5")
|
||||
base.config.update()
|
||||
assert base.config.latitude == 2.5
|
||||
|
||||
def test_config_value_from_field_default(self, base, monkeypatch):
|
||||
assert base.config.model_fields["prediction_hours"].default == 48
|
||||
assert base.config.prediction_hours == 48
|
||||
monkeypatch.setenv("prediction_hours", "128")
|
||||
base.config.update()
|
||||
assert base.config.prediction_hours == 128
|
||||
monkeypatch.delenv("prediction_hours")
|
||||
base.config.update()
|
||||
assert base.config.prediction_hours == 48
|
||||
|
||||
def test_get_config_value_key_error(self, base):
|
||||
with pytest.raises(AttributeError):
|
||||
base.config.non_existent_key
|
||||
|
||||
|
||||
# TestPredictionRecord fully covered by TestDataRecord
|
||||
# ----------------------------------------------------
|
||||
|
||||
|
||||
# TestPredictionSequence fully covered by TestDataSequence
|
||||
# --------------------------------------------------------
|
||||
|
||||
|
||||
# TestPredictionStartEndKeepMixin fully covered by TestPredictionContainer
|
||||
# --------------------------------------------------------
|
||||
|
||||
|
||||
class TestPredictionProvider:
|
||||
# Fixtures and helper functions
|
||||
@pytest.fixture
|
||||
def provider(self):
|
||||
"""Fixture to provide an instance of TestPredictionProvider for testing."""
|
||||
DerivedPredictionProvider.provider_enabled = True
|
||||
DerivedPredictionProvider.provider_updated = False
|
||||
return DerivedPredictionProvider()
|
||||
|
||||
@pytest.fixture
|
||||
def sample_start_datetime(self):
|
||||
"""Fixture for a sample start datetime."""
|
||||
return to_datetime(datetime(2024, 11, 1, 12, 0))
|
||||
|
||||
def create_test_record(self, date, value):
|
||||
"""Helper function to create a test PredictionRecord."""
|
||||
return DerivedRecord(date_time=date, prediction_value=value)
|
||||
|
||||
# Tests
|
||||
|
||||
def test_singleton_behavior(self, provider):
|
||||
"""Test that PredictionProvider enforces singleton behavior."""
|
||||
instance1 = provider
|
||||
instance2 = DerivedPredictionProvider()
|
||||
assert (
|
||||
instance1 is instance2
|
||||
), "Singleton pattern is not enforced; instances are not the same."
|
||||
|
||||
def test_update_computed_fields(self, provider, sample_start_datetime):
|
||||
"""Test that computed fields `end_datetime` and `keep_datetime` are correctly calculated."""
|
||||
ems_eos = get_ems()
|
||||
ems_eos.set_start_datetime(sample_start_datetime)
|
||||
provider.config.prediction_hours = 24 # 24 hours into the future
|
||||
provider.config.prediction_historic_hours = 48 # 48 hours into the past
|
||||
|
||||
expected_end_datetime = sample_start_datetime + to_duration(
|
||||
provider.config.prediction_hours * 3600
|
||||
)
|
||||
expected_keep_datetime = sample_start_datetime - to_duration(
|
||||
provider.config.prediction_historic_hours * 3600
|
||||
)
|
||||
|
||||
assert (
|
||||
provider.end_datetime == expected_end_datetime
|
||||
), "End datetime is not calculated correctly."
|
||||
assert (
|
||||
provider.keep_datetime == expected_keep_datetime
|
||||
), "Keep datetime is not calculated correctly."
|
||||
|
||||
def test_update_method_with_defaults(self, provider, sample_start_datetime, monkeypatch):
|
||||
"""Test the `update` method with default parameters."""
|
||||
# EOS config supersedes
|
||||
config_eos = get_config()
|
||||
ems_eos = get_ems()
|
||||
# The following values are currently not set in EOS config, we can override
|
||||
monkeypatch.setenv("prediction_historic_hours", "2")
|
||||
assert os.getenv("prediction_historic_hours") == "2"
|
||||
monkeypatch.setenv("latitude", "37.7749")
|
||||
assert os.getenv("latitude") == "37.7749"
|
||||
monkeypatch.setenv("longitude", "-122.4194")
|
||||
assert os.getenv("longitude") == "-122.4194"
|
||||
|
||||
ems_eos.set_start_datetime(sample_start_datetime)
|
||||
provider.update_data()
|
||||
|
||||
assert provider.config.prediction_hours == config_eos.prediction_hours
|
||||
assert provider.config.prediction_historic_hours == 2
|
||||
assert provider.config.latitude == 37.7749
|
||||
assert provider.config.longitude == -122.4194
|
||||
assert provider.start_datetime == sample_start_datetime
|
||||
assert provider.end_datetime == sample_start_datetime + to_duration(
|
||||
f"{provider.config.prediction_hours} hours"
|
||||
)
|
||||
assert provider.keep_datetime == sample_start_datetime - to_duration("2 hours")
|
||||
|
||||
def test_update_method_force_enable(self, provider, monkeypatch):
|
||||
"""Test that `update` executes when `force_enable` is True, even if `enabled` is False."""
|
||||
# Preset values that are needed by update
|
||||
monkeypatch.setenv("latitude", "37.7749")
|
||||
monkeypatch.setenv("longitude", "-122.4194")
|
||||
|
||||
# Override enabled to return False for this test
|
||||
DerivedPredictionProvider.provider_enabled = False
|
||||
DerivedPredictionProvider.provider_updated = False
|
||||
provider.update_data(force_enable=True)
|
||||
assert provider.enabled() is False, "Provider should be disabled, but enabled() is True."
|
||||
assert (
|
||||
DerivedPredictionProvider.provider_updated is True
|
||||
), "Provider should have been executed, but was not."
|
||||
|
||||
def test_delete_by_datetime(self, provider, sample_start_datetime):
|
||||
"""Test `delete_by_datetime` method for removing records by datetime range."""
|
||||
# Add records to the provider for deletion testing
|
||||
provider.records = [
|
||||
self.create_test_record(sample_start_datetime - to_duration("3 hours"), 1),
|
||||
self.create_test_record(sample_start_datetime - to_duration("1 hour"), 2),
|
||||
self.create_test_record(sample_start_datetime + to_duration("1 hour"), 3),
|
||||
]
|
||||
|
||||
provider.delete_by_datetime(
|
||||
start_datetime=sample_start_datetime - to_duration("2 hours"),
|
||||
end_datetime=sample_start_datetime + to_duration("2 hours"),
|
||||
)
|
||||
assert (
|
||||
len(provider.records) == 1
|
||||
), "Only one record should remain after deletion by datetime."
|
||||
assert provider.records[0].date_time == sample_start_datetime - to_duration(
|
||||
"3 hours"
|
||||
), "Unexpected record remains."
|
||||
|
||||
|
||||
class TestPredictionContainer:
|
||||
# Fixture and helpers
|
||||
@pytest.fixture
|
||||
def container(self):
|
||||
container = DerivedPredictionContainer()
|
||||
return container
|
||||
|
||||
@pytest.fixture
|
||||
def container_with_providers(self):
|
||||
record1 = self.create_test_record(datetime(2023, 11, 5), 1)
|
||||
record2 = self.create_test_record(datetime(2023, 11, 6), 2)
|
||||
record3 = self.create_test_record(datetime(2023, 11, 7), 3)
|
||||
provider = DerivedPredictionProvider()
|
||||
provider.clear()
|
||||
assert len(provider) == 0
|
||||
provider.append(record1)
|
||||
provider.append(record2)
|
||||
provider.append(record3)
|
||||
assert len(provider) == 3
|
||||
container = DerivedPredictionContainer()
|
||||
container.providers.clear()
|
||||
assert len(container.providers) == 0
|
||||
container.providers.append(provider)
|
||||
assert len(container.providers) == 1
|
||||
return container
|
||||
|
||||
def create_test_record(self, date, value):
|
||||
"""Helper function to create a test PredictionRecord."""
|
||||
return DerivedRecord(date_time=date, prediction_value=value)
|
||||
|
||||
# Tests
|
||||
@pytest.mark.parametrize(
|
||||
"start, hours, end",
|
||||
[
|
||||
("2024-11-10 00:00:00", 24, "2024-11-11 00:00:00"), # No DST in Germany
|
||||
("2024-08-10 00:00:00", 24, "2024-08-11 00:00:00"), # DST in Germany
|
||||
("2024-03-31 00:00:00", 24, "2024-04-01 00:00:00"), # DST change (23 hours/ day)
|
||||
("2024-10-27 00:00:00", 24, "2024-10-28 00:00:00"), # DST change (25 hours/ day)
|
||||
("2024-11-10 00:00:00", 48, "2024-11-12 00:00:00"), # No DST in Germany
|
||||
("2024-08-10 00:00:00", 48, "2024-08-12 00:00:00"), # DST in Germany
|
||||
("2024-03-31 00:00:00", 48, "2024-04-02 00:00:00"), # DST change (47 hours/ day)
|
||||
("2024-10-27 00:00:00", 48, "2024-10-29 00:00:00"), # DST change (49 hours/ day)
|
||||
],
|
||||
)
|
||||
def test_end_datetime(self, container, start, hours, end):
|
||||
"""Test end datetime calculation from start datetime."""
|
||||
ems_eos = get_ems()
|
||||
ems_eos.set_start_datetime(to_datetime(start, in_timezone="Europe/Berlin"))
|
||||
settings = {
|
||||
"prediction_hours": hours,
|
||||
}
|
||||
container.config.merge_settings_from_dict(settings)
|
||||
expected = to_datetime(end, in_timezone="Europe/Berlin")
|
||||
assert compare_datetimes(container.end_datetime, expected).equal
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"start, historic_hours, expected_keep",
|
||||
[
|
||||
# Standard case
|
||||
(
|
||||
pendulum.datetime(2024, 8, 10, 0, 0, tz="Europe/Berlin"),
|
||||
24,
|
||||
pendulum.datetime(2024, 8, 9, 0, 0, tz="Europe/Berlin"),
|
||||
),
|
||||
# With DST, but should not affect historical data
|
||||
(
|
||||
pendulum.datetime(2024, 4, 1, 0, 0, tz="Europe/Berlin"),
|
||||
24,
|
||||
pendulum.datetime(2024, 3, 30, 23, 0, tz="Europe/Berlin"),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_keep_datetime(self, container, start, historic_hours, expected_keep):
|
||||
"""Test the `keep_datetime` property."""
|
||||
ems_eos = get_ems()
|
||||
ems_eos.set_start_datetime(to_datetime(start, in_timezone="Europe/Berlin"))
|
||||
settings = {
|
||||
"prediction_historic_hours": historic_hours,
|
||||
}
|
||||
container.config.merge_settings_from_dict(settings)
|
||||
expected = to_datetime(expected_keep, in_timezone="Europe/Berlin")
|
||||
assert compare_datetimes(container.keep_datetime, expected).equal
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"start, prediction_hours, expected_hours",
|
||||
[
|
||||
("2024-11-10 00:00:00", 24, 24), # No DST in Germany
|
||||
("2024-08-10 00:00:00", 24, 24), # DST in Germany
|
||||
("2024-03-31 00:00:00", 24, 23), # DST change in Germany (23 hours/ day)
|
||||
("2024-10-27 00:00:00", 24, 25), # DST change in Germany (25 hours/ day)
|
||||
],
|
||||
)
|
||||
def test_total_hours(self, container, start, prediction_hours, expected_hours):
|
||||
"""Test the `total_hours` property."""
|
||||
ems_eos = get_ems()
|
||||
ems_eos.set_start_datetime(to_datetime(start, in_timezone="Europe/Berlin"))
|
||||
settings = {
|
||||
"prediction_hours": prediction_hours,
|
||||
}
|
||||
container.config.merge_settings_from_dict(settings)
|
||||
assert container.total_hours == expected_hours
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"start, historic_hours, expected_hours",
|
||||
[
|
||||
("2024-11-10 00:00:00", 24, 24), # No DST in Germany
|
||||
("2024-08-10 00:00:00", 24, 24), # DST in Germany
|
||||
("2024-04-01 00:00:00", 24, 24), # DST change on 2024-03-31 in Germany (23 hours/ day)
|
||||
("2024-10-28 00:00:00", 24, 24), # DST change on 2024-10-27 in Germany (25 hours/ day)
|
||||
],
|
||||
)
|
||||
def test_keep_hours(self, container, start, historic_hours, expected_hours):
|
||||
"""Test the `keep_hours` property."""
|
||||
ems_eos = get_ems()
|
||||
ems_eos.set_start_datetime(to_datetime(start, in_timezone="Europe/Berlin"))
|
||||
settings = {
|
||||
"prediction_historic_hours": historic_hours,
|
||||
}
|
||||
container.config.merge_settings_from_dict(settings)
|
||||
assert container.keep_hours == expected_hours
|
||||
|
||||
def test_append_provider(self, container):
|
||||
assert len(container.providers) == 0
|
||||
container.providers.append(DerivedPredictionProvider())
|
||||
assert len(container.providers) == 1
|
||||
assert isinstance(container.providers[0], DerivedPredictionProvider)
|
||||
|
||||
@pytest.mark.skip(reason="type check not implemented")
|
||||
def test_append_provider_invalid_type(self, container):
|
||||
with pytest.raises(ValueError, match="must be an instance of PredictionProvider"):
|
||||
container.providers.append("not_a_provider")
|
||||
|
||||
def test_getitem_existing_key(self, container_with_providers):
|
||||
assert len(container_with_providers.providers) == 1
|
||||
# check all keys are available (don't care for position)
|
||||
for key in ["prediction_value", "date_time"]:
|
||||
assert key in list(container_with_providers.keys())
|
||||
series = container_with_providers["prediction_value"]
|
||||
assert isinstance(series, pd.Series)
|
||||
assert series.name == "prediction_value"
|
||||
assert series.tolist() == [1.0, 2.0, 3.0]
|
||||
|
||||
def test_getitem_non_existing_key(self, container_with_providers):
|
||||
with pytest.raises(KeyError, match="No data found for key 'non_existent_key'"):
|
||||
container_with_providers["non_existent_key"]
|
||||
|
||||
def test_setitem_existing_key(self, container_with_providers):
|
||||
new_series = container_with_providers["prediction_value"]
|
||||
new_series[:] = [4, 5, 6]
|
||||
container_with_providers["prediction_value"] = new_series
|
||||
series = container_with_providers["prediction_value"]
|
||||
assert series.name == "prediction_value"
|
||||
assert series.tolist() == [4, 5, 6]
|
||||
|
||||
def test_setitem_invalid_value(self, container_with_providers):
|
||||
with pytest.raises(ValueError, match="Value must be an instance of pd.Series"):
|
||||
container_with_providers["test_key"] = "not_a_series"
|
||||
|
||||
def test_setitem_non_existing_key(self, container_with_providers):
|
||||
new_series = pd.Series([4, 5, 6], name="non_existent_key")
|
||||
with pytest.raises(KeyError, match="Key 'non_existent_key' not found"):
|
||||
container_with_providers["non_existent_key"] = new_series
|
||||
|
||||
def test_delitem_existing_key(self, container_with_providers):
|
||||
del container_with_providers["prediction_value"]
|
||||
series = container_with_providers["prediction_value"]
|
||||
assert series.name == "prediction_value"
|
||||
assert series.tolist() == [None, None, None]
|
||||
|
||||
def test_delitem_non_existing_key(self, container_with_providers):
|
||||
with pytest.raises(KeyError, match="Key 'non_existent_key' not found"):
|
||||
del container_with_providers["non_existent_key"]
|
||||
|
||||
def test_len(self, container_with_providers):
|
||||
assert len(container_with_providers) == 3
|
||||
|
||||
def test_repr(self, container_with_providers):
|
||||
representation = repr(container_with_providers)
|
||||
assert representation.startswith("DerivedPredictionContainer(")
|
||||
assert "DerivedPredictionProvider" in representation
|
||||
|
||||
def test_to_json(self, container_with_providers):
|
||||
json_str = container_with_providers.to_json()
|
||||
container_other = DerivedPredictionContainer.from_json(json_str)
|
||||
assert container_other == container_with_providers
|
||||
|
||||
def test_from_json(self, container_with_providers):
|
||||
json_str = container_with_providers.to_json()
|
||||
container = DerivedPredictionContainer.from_json(json_str)
|
||||
assert isinstance(container, DerivedPredictionContainer)
|
||||
assert len(container.providers) == 1
|
||||
assert container.providers[0] == container_with_providers.providers[0]
|
||||
|
||||
def test_provider_by_id(self, container_with_providers):
|
||||
provider = container_with_providers.provider_by_id("DerivedPredictionProvider")
|
||||
assert isinstance(provider, DerivedPredictionProvider)
|
@@ -1,286 +0,0 @@
|
||||
"""Test Module for PV Power Forecasting Module.
|
||||
|
||||
This test module is designed to verify the functionality of the `PVForecast` class
|
||||
and its methods in the `prediction.pv_forecast` module. The tests include validation for
|
||||
forecast data processing, updating AC power measurements, retrieving forecast data,
|
||||
and caching behavior.
|
||||
|
||||
Fixtures:
|
||||
sample_forecast_data: Provides sample forecast data in JSON format for testing.
|
||||
pv_forecast_instance: Provides an instance of `PVForecast` class with sample data loaded.
|
||||
|
||||
Test Cases:
|
||||
- test_generate_cache_filename: Verifies correct cache filename generation based on URL and date.
|
||||
- test_update_ac_power_measurement: Tests updating AC power measurement for a matching date.
|
||||
- test_update_ac_power_measurement_no_match: Ensures no updates occur when there is no matching date.
|
||||
- test_get_temperature_forecast_for_date: Tests retrieving the temperature forecast for a specific date.
|
||||
- test_get_pv_forecast_for_date_range: Verifies retrieval of AC power forecast for a specified date range.
|
||||
- test_get_forecast_dataframe: Ensures forecast data can be correctly converted into a Pandas DataFrame.
|
||||
- test_cache_loading: Tests loading forecast data from a cached file to ensure caching works as expected.
|
||||
|
||||
Usage:
|
||||
This test module uses `pytest` and requires the `akkudoktoreos.prediction.pv_forecast.py` module to be present.
|
||||
Run the tests using the command: `pytest test_pv_forecast.py`.
|
||||
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from akkudoktoreos.prediction.pv_forecast import PVForecast, validate_pv_forecast_data
|
||||
from akkudoktoreos.utils.datetimeutil import to_datetime
|
||||
|
||||
DIR_TESTDATA = Path(__file__).absolute().parent.joinpath("testdata")
|
||||
|
||||
FILE_TESTDATA_PV_FORECAST_INPUT_1 = DIR_TESTDATA.joinpath("pv_forecast_input_1.json")
|
||||
FILE_TESTDATA_PV_FORECAST_RESULT_1 = DIR_TESTDATA.joinpath("pv_forecast_result_1.txt")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_forecast_data():
|
||||
"""Fixture that returns sample forecast data."""
|
||||
with open(FILE_TESTDATA_PV_FORECAST_INPUT_1, "r") as f_in:
|
||||
input_data = json.load(f_in)
|
||||
return input_data
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_forecast_report():
|
||||
"""Fixture that returns sample forecast data report."""
|
||||
with open(FILE_TESTDATA_PV_FORECAST_RESULT_1, "r") as f_res:
|
||||
input_data = f_res.read()
|
||||
return input_data
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_forecast_start(sample_forecast_data):
|
||||
"""Fixture that returns the start date of the sample forecast data."""
|
||||
forecast_start_str = sample_forecast_data["values"][0][0]["datetime"]
|
||||
assert forecast_start_str == "2024-10-06T00:00:00.000+02:00"
|
||||
|
||||
timezone_name = sample_forecast_data["meta"]["timezone"]
|
||||
assert timezone_name == "Europe/Berlin"
|
||||
|
||||
forecast_start = to_datetime(forecast_start_str, to_timezone=timezone_name, to_naiv=True)
|
||||
assert forecast_start == datetime(2024, 10, 6)
|
||||
|
||||
return forecast_start
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pv_forecast_empty_instance():
|
||||
"""Fixture that returns an empty instance of PVForecast."""
|
||||
empty_instance = PVForecast()
|
||||
assert empty_instance.get_forecast_start() is None
|
||||
|
||||
return empty_instance
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pv_forecast_instance(sample_forecast_data, sample_forecast_start):
|
||||
"""Fixture that returns an instance of PVForecast with sample data loaded."""
|
||||
pv_forecast = PVForecast(
|
||||
data=sample_forecast_data,
|
||||
forecast_start=sample_forecast_start,
|
||||
prediction_hours=48,
|
||||
)
|
||||
return pv_forecast
|
||||
|
||||
|
||||
def test_validate_pv_forecast_data(sample_forecast_data):
|
||||
"""Test validation of PV forecast data on sample data."""
|
||||
ret = validate_pv_forecast_data({})
|
||||
assert ret is None
|
||||
|
||||
ret = validate_pv_forecast_data(sample_forecast_data)
|
||||
assert ret == "Akkudoktor"
|
||||
|
||||
|
||||
def test_process_data(sample_forecast_data, sample_forecast_start):
|
||||
"""Test data processing using sample data."""
|
||||
pv_forecast_instance = PVForecast(forecast_start=sample_forecast_start)
|
||||
|
||||
# Assure the start date is correctly set by init funtion
|
||||
forecast_start = pv_forecast_instance.get_forecast_start()
|
||||
expected_start = sample_forecast_start
|
||||
assert forecast_start == expected_start
|
||||
|
||||
# Assure the prediction hours are unset
|
||||
assert pv_forecast_instance.prediction_hours is None
|
||||
|
||||
# Load forecast with sample data - throws exceptions on error
|
||||
pv_forecast_instance.process_data(data=sample_forecast_data)
|
||||
|
||||
|
||||
def test_update_ac_power_measurement(pv_forecast_instance, sample_forecast_start):
|
||||
"""Test updating AC power measurement for a specific date."""
|
||||
forecast_start = pv_forecast_instance.get_forecast_start()
|
||||
assert forecast_start == sample_forecast_start
|
||||
|
||||
updated = pv_forecast_instance.update_ac_power_measurement(1000, forecast_start)
|
||||
assert updated is True
|
||||
forecast_data = pv_forecast_instance.get_forecast_data()
|
||||
assert forecast_data[0].ac_power_measurement == 1000
|
||||
|
||||
|
||||
def test_update_ac_power_measurement_no_match(pv_forecast_instance):
|
||||
"""Test updating AC power measurement where no date matches."""
|
||||
date_time = datetime(2023, 10, 2, 1, 0, 0)
|
||||
updated = pv_forecast_instance.update_ac_power_measurement(1000, date_time)
|
||||
assert not updated
|
||||
|
||||
|
||||
def test_get_temperature_forecast_for_date(pv_forecast_instance, sample_forecast_start):
|
||||
"""Test fetching temperature forecast for a specific date."""
|
||||
forecast_temps = pv_forecast_instance.get_temperature_forecast_for_date(sample_forecast_start)
|
||||
assert len(forecast_temps) == 24
|
||||
assert forecast_temps[0] == 7.0
|
||||
assert forecast_temps[1] == 6.5
|
||||
assert forecast_temps[2] == 6.0
|
||||
|
||||
# Assure function bails out if there is no timezone name available for the system.
|
||||
tz_name = pv_forecast_instance._tz_name
|
||||
pv_forecast_instance._tz_name = None
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
forecast_temps = pv_forecast_instance.get_temperature_forecast_for_date(
|
||||
sample_forecast_start
|
||||
)
|
||||
pv_forecast_instance._tz_name = tz_name
|
||||
assert (
|
||||
exc_info.value.args[0] == "Processing without PV system timezone info ist not implemented!"
|
||||
)
|
||||
|
||||
|
||||
def test_get_temperature_for_date_range(pv_forecast_instance, sample_forecast_start):
|
||||
"""Test fetching temperature forecast for a specific date range."""
|
||||
end_date = sample_forecast_start + timedelta(hours=24)
|
||||
forecast_temps = pv_forecast_instance.get_temperature_for_date_range(
|
||||
sample_forecast_start, end_date
|
||||
)
|
||||
assert len(forecast_temps) == 48
|
||||
assert forecast_temps[0] == 7.0
|
||||
assert forecast_temps[1] == 6.5
|
||||
assert forecast_temps[2] == 6.0
|
||||
|
||||
# Assure function bails out if there is no timezone name available for the system.
|
||||
tz_name = pv_forecast_instance._tz_name
|
||||
pv_forecast_instance._tz_name = None
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
forecast_temps = pv_forecast_instance.get_temperature_for_date_range(
|
||||
sample_forecast_start, end_date
|
||||
)
|
||||
pv_forecast_instance._tz_name = tz_name
|
||||
assert (
|
||||
exc_info.value.args[0] == "Processing without PV system timezone info ist not implemented!"
|
||||
)
|
||||
|
||||
|
||||
def test_get_forecast_for_date_range(pv_forecast_instance, sample_forecast_start):
|
||||
"""Test fetching AC power forecast for a specific date range."""
|
||||
end_date = sample_forecast_start + timedelta(hours=24)
|
||||
forecast = pv_forecast_instance.get_pv_forecast_for_date_range(sample_forecast_start, end_date)
|
||||
assert len(forecast) == 48
|
||||
assert forecast[0] == 0.0
|
||||
assert forecast[1] == 0.0
|
||||
assert forecast[2] == 0.0
|
||||
|
||||
# Assure function bails out if there is no timezone name available for the system.
|
||||
tz_name = pv_forecast_instance._tz_name
|
||||
pv_forecast_instance._tz_name = None
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
forecast = pv_forecast_instance.get_pv_forecast_for_date_range(
|
||||
sample_forecast_start, end_date
|
||||
)
|
||||
pv_forecast_instance._tz_name = tz_name
|
||||
assert (
|
||||
exc_info.value.args[0] == "Processing without PV system timezone info ist not implemented!"
|
||||
)
|
||||
|
||||
|
||||
def test_get_forecast_dataframe(pv_forecast_instance):
|
||||
"""Test converting forecast data to a DataFrame."""
|
||||
df = pv_forecast_instance.get_forecast_dataframe()
|
||||
assert len(df) == 288
|
||||
assert list(df.columns) == ["date_time", "dc_power", "ac_power", "windspeed_10m", "temperature"]
|
||||
assert df.iloc[0]["dc_power"] == 0.0
|
||||
assert df.iloc[1]["ac_power"] == 0.0
|
||||
assert df.iloc[2]["temperature"] == 6.0
|
||||
|
||||
|
||||
def test_load_data_from_file(server, pv_forecast_empty_instance):
|
||||
"""Test loading data from file."""
|
||||
# load from valid address file path
|
||||
filepath = FILE_TESTDATA_PV_FORECAST_INPUT_1
|
||||
data = pv_forecast_empty_instance.load_data_from_file(filepath)
|
||||
assert len(data) > 0
|
||||
|
||||
|
||||
def test_load_data_from_url(server, pv_forecast_empty_instance):
|
||||
"""Test loading data from url."""
|
||||
# load from valid address of our server
|
||||
url = f"{server}/gesamtlast_simple?year_energy=2000&"
|
||||
data = pv_forecast_empty_instance.load_data_from_url(url)
|
||||
assert len(data) > 0
|
||||
|
||||
# load from invalid address of our server
|
||||
url = f"{server}/invalid?"
|
||||
data = pv_forecast_empty_instance.load_data_from_url(url)
|
||||
assert data == f"Failed to load data from `{url}`. Status Code: 404"
|
||||
|
||||
|
||||
def test_load_data_from_url_with_caching(
|
||||
server, pv_forecast_empty_instance, sample_forecast_data, sample_forecast_start
|
||||
):
|
||||
"""Test loading data from url with cache."""
|
||||
# load from valid address of our server
|
||||
url = f"{server}/gesamtlast_simple?year_energy=2000&"
|
||||
data = pv_forecast_empty_instance.load_data_from_url_with_caching(url)
|
||||
assert len(data) > 0
|
||||
|
||||
# load from invalid address of our server
|
||||
url = f"{server}/invalid?"
|
||||
data = pv_forecast_empty_instance.load_data_from_url_with_caching(url)
|
||||
assert data == f"Failed to load data from `{url}`. Status Code: 404"
|
||||
|
||||
|
||||
def test_report_ac_power_and_measurement(pv_forecast_instance, sample_forecast_report):
|
||||
"""Test reporting."""
|
||||
report = pv_forecast_instance.report_ac_power_and_measurement()
|
||||
assert report == sample_forecast_report
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.platform.startswith("win"), reason="'other_timezone' fixture not supported on Windows"
|
||||
)
|
||||
def test_timezone_behaviour(
|
||||
pv_forecast_instance, sample_forecast_report, sample_forecast_start, other_timezone
|
||||
):
|
||||
"""Test PVForecast in another timezone."""
|
||||
current_time = datetime.now()
|
||||
|
||||
# Test updating AC power measurement for a specific date.
|
||||
date_time = pv_forecast_instance.get_forecast_start()
|
||||
assert date_time == sample_forecast_start
|
||||
updated = pv_forecast_instance.update_ac_power_measurement(1000, date_time)
|
||||
assert updated is True
|
||||
forecast_data = pv_forecast_instance.get_forecast_data()
|
||||
assert forecast_data[0].ac_power_measurement == 1000
|
||||
|
||||
# Test fetching temperature forecast for a specific date.
|
||||
forecast_temps = pv_forecast_instance.get_temperature_forecast_for_date(sample_forecast_start)
|
||||
assert len(forecast_temps) == 24
|
||||
assert forecast_temps[0] == 7.0
|
||||
assert forecast_temps[1] == 6.5
|
||||
assert forecast_temps[2] == 6.0
|
||||
|
||||
# Test fetching AC power forecast
|
||||
end_date = sample_forecast_start + timedelta(hours=24)
|
||||
forecast = pv_forecast_instance.get_pv_forecast_for_date_range(sample_forecast_start, end_date)
|
||||
assert len(forecast) == 48
|
||||
assert forecast[0] == 1000.0 # changed before
|
||||
assert forecast[1] == 0.0
|
||||
assert forecast[2] == 0.0
|
307
tests/test_pvforecastakkudoktor.py
Normal file
307
tests/test_pvforecastakkudoktor.py
Normal file
@@ -0,0 +1,307 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from akkudoktoreos.config.config import get_config
|
||||
from akkudoktoreos.core.ems import get_ems
|
||||
from akkudoktoreos.prediction.prediction import get_prediction
|
||||
from akkudoktoreos.prediction.pvforecastakkudoktor import (
|
||||
AkkudoktorForecastHorizon,
|
||||
AkkudoktorForecastMeta,
|
||||
AkkudoktorForecastValue,
|
||||
PVForecastAkkudoktor,
|
||||
PVForecastAkkudoktorDataRecord,
|
||||
)
|
||||
from akkudoktoreos.utils.datetimeutil import compare_datetimes, to_datetime, to_duration
|
||||
|
||||
DIR_TESTDATA = Path(__file__).absolute().parent.joinpath("testdata")
|
||||
|
||||
FILE_TESTDATA_PV_FORECAST_INPUT_1 = DIR_TESTDATA.joinpath("pv_forecast_input_1.json")
|
||||
FILE_TESTDATA_PV_FORECAST_RESULT_1 = DIR_TESTDATA.joinpath("pv_forecast_result_1.txt")
|
||||
|
||||
|
||||
config_eos = get_config()
|
||||
ems_eos = get_ems()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_settings(reset_config):
|
||||
"""Fixture that adds settings data to the global config."""
|
||||
settings = {
|
||||
"prediction_hours": 48,
|
||||
"prediction_historic_hours": 24,
|
||||
"latitude": 52.52,
|
||||
"longitude": 13.405,
|
||||
"pvforecast_provider": "PVForecastAkkudoktor",
|
||||
"pvforecast0_peakpower": 5.0,
|
||||
"pvforecast0_surface_azimuth": -10,
|
||||
"pvforecast0_surface_tilt": 7,
|
||||
"pvforecast0_userhorizon": [20, 27, 22, 20],
|
||||
"pvforecast0_inverter_paco": 10000,
|
||||
"pvforecast1_peakpower": 4.8,
|
||||
"pvforecast1_surface_azimuth": -90,
|
||||
"pvforecast1_surface_tilt": 7,
|
||||
"pvforecast1_userhorizon": [30, 30, 30, 50],
|
||||
"pvforecast1_inverter_paco": 10000,
|
||||
"pvforecast2_peakpower": 1.4,
|
||||
"pvforecast2_surface_azimuth": -40,
|
||||
"pvforecast2_surface_tilt": 60,
|
||||
"pvforecast2_userhorizon": [60, 30, 0, 30],
|
||||
"pvforecast2_inverter_paco": 2000,
|
||||
"pvforecast3_peakpower": 1.6,
|
||||
"pvforecast3_surface_azimuth": 5,
|
||||
"pvforecast3_surface_tilt": 45,
|
||||
"pvforecast3_userhorizon": [45, 25, 30, 60],
|
||||
"pvforecast3_inverter_paco": 1400,
|
||||
"pvforecast4_peakpower": None,
|
||||
}
|
||||
|
||||
# Merge settings to config
|
||||
config_eos.merge_settings_from_dict(settings)
|
||||
return config_eos
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_forecast_data():
|
||||
"""Fixture that returns sample forecast data converted to pydantic model."""
|
||||
with open(FILE_TESTDATA_PV_FORECAST_INPUT_1, "r") as f_in:
|
||||
input_data = f_in.read()
|
||||
return PVForecastAkkudoktor._validate_data(input_data)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_forecast_data_raw():
|
||||
"""Fixture that returns raw sample forecast data."""
|
||||
with open(FILE_TESTDATA_PV_FORECAST_INPUT_1, "r") as f_in:
|
||||
input_data = f_in.read()
|
||||
return input_data
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_forecast_report():
|
||||
"""Fixture that returns sample forecast data report."""
|
||||
with open(FILE_TESTDATA_PV_FORECAST_RESULT_1, "r") as f_res:
|
||||
input_data = f_res.read()
|
||||
return input_data
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_forecast_start(sample_forecast_data):
|
||||
"""Fixture that returns the start date of the sample forecast data."""
|
||||
forecast_start = to_datetime(sample_forecast_data.values[0][0].datetime)
|
||||
expected_datetime = to_datetime("2024-10-06T00:00:00.000+02:00")
|
||||
assert compare_datetimes(to_datetime(forecast_start), expected_datetime).equal
|
||||
|
||||
timezone_name = sample_forecast_data.meta.timezone
|
||||
assert timezone_name == "Europe/Berlin"
|
||||
return forecast_start
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def provider():
|
||||
"""Fixture that returns the PVForecastAkkudoktor instance from the prediction."""
|
||||
prediction = get_prediction()
|
||||
provider = prediction.provider_by_id("PVForecastAkkudoktor")
|
||||
assert isinstance(provider, PVForecastAkkudoktor)
|
||||
return provider
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def provider_empty_instance():
|
||||
"""Fixture that returns an empty instance of PVForecast."""
|
||||
empty_instance = PVForecastAkkudoktor()
|
||||
empty_instance.clear()
|
||||
assert len(empty_instance) == 0
|
||||
return empty_instance
|
||||
|
||||
|
||||
# Sample data for testing
|
||||
sample_horizon = AkkudoktorForecastHorizon(altitude=30, azimuthFrom=90, azimuthTo=180)
|
||||
sample_meta = AkkudoktorForecastMeta(
|
||||
lat=52.52,
|
||||
lon=13.405,
|
||||
power=[5000],
|
||||
azimuth=[180],
|
||||
tilt=[30],
|
||||
timezone="Europe/Berlin",
|
||||
albedo=0.25,
|
||||
past_days=5,
|
||||
inverterEfficiency=0.8,
|
||||
powerInverter=[10000],
|
||||
cellCoEff=-0.36,
|
||||
range=True,
|
||||
horizont=[[sample_horizon]],
|
||||
horizontString=["sample_horizon"],
|
||||
)
|
||||
sample_value = AkkudoktorForecastValue(
|
||||
datetime="2024-11-09T12:00:00",
|
||||
dcPower=500.0,
|
||||
power=480.0,
|
||||
sunTilt=30.0,
|
||||
sunAzimuth=180.0,
|
||||
temperature=15.0,
|
||||
relativehumidity_2m=50.0,
|
||||
windspeed_10m=10.0,
|
||||
)
|
||||
sample_config_data = {
|
||||
"prediction_hours": 48,
|
||||
"prediction_historic_hours": 24,
|
||||
"latitude": 52.52,
|
||||
"longitude":13.405,
|
||||
"pvforecast_provider": "PVForecastAkkudoktor",
|
||||
"pvforecast0_peakpower": 5.0,
|
||||
"pvforecast0_surface_azimuth": 180,
|
||||
"pvforecast0_surface_tilt": 30,
|
||||
"pvforecast0_inverter_paco": 10000,
|
||||
}
|
||||
|
||||
|
||||
# Tests for AkkudoktorForecastHorizon
|
||||
def test_akkudoktor_forecast_horizon():
|
||||
horizon = AkkudoktorForecastHorizon(altitude=30, azimuthFrom=90, azimuthTo=180)
|
||||
assert horizon.altitude == 30
|
||||
assert horizon.azimuthFrom == 90
|
||||
assert horizon.azimuthTo == 180
|
||||
|
||||
|
||||
# Tests for AkkudoktorForecastMeta
|
||||
def test_akkudoktor_forecast_meta():
|
||||
meta = sample_meta
|
||||
assert meta.lat == 52.52
|
||||
assert meta.lon ==13.405
|
||||
assert meta.power == [5000]
|
||||
assert meta.tilt == [30]
|
||||
assert meta.timezone == "Europe/Berlin"
|
||||
|
||||
|
||||
# Tests for AkkudoktorForecastValue
|
||||
def test_akkudoktor_forecast_value():
|
||||
value = sample_value
|
||||
assert value.dcPower == 500.0
|
||||
assert value.power == 480.0
|
||||
assert value.temperature == 15.0
|
||||
assert value.windspeed_10m == 10.0
|
||||
|
||||
|
||||
# Tests for PVForecastAkkudoktorDataRecord
|
||||
def test_pvforecast_akkudoktor_data_record():
|
||||
record = PVForecastAkkudoktorDataRecord(
|
||||
pvforecastakkudoktor_ac_power_measured=1000.0,
|
||||
pvforecastakkudoktor_wind_speed_10m=10.0,
|
||||
pvforecastakkudoktor_temp_air=15.0,
|
||||
)
|
||||
assert record.pvforecastakkudoktor_ac_power_measured == 1000.0
|
||||
assert record.pvforecastakkudoktor_wind_speed_10m == 10.0
|
||||
assert record.pvforecastakkudoktor_temp_air == 15.0
|
||||
assert (
|
||||
record.pvforecastakkudoktor_ac_power_any == 1000.0
|
||||
) # Assuming AC power measured is preferred
|
||||
|
||||
|
||||
def test_pvforecast_akkudoktor_validate_data(provider_empty_instance, sample_forecast_data_raw):
|
||||
"""Test validation of PV forecast data on sample data."""
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match="Field: meta\nError: Field required\nType: missing\nField: values\nError: Field required\nType: missing\n",
|
||||
):
|
||||
ret = provider_empty_instance._validate_data("{}")
|
||||
data = provider_empty_instance._validate_data(sample_forecast_data_raw)
|
||||
# everything worked
|
||||
|
||||
|
||||
@patch("requests.get")
|
||||
def test_pvforecast_akkudoktor_update_with_sample_forecast(
|
||||
mock_get, sample_settings, sample_forecast_data_raw, sample_forecast_start, provider
|
||||
):
|
||||
"""Test data processing using sample forecast data."""
|
||||
# Mock response object
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.content = sample_forecast_data_raw
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
# Test that update properly inserts data records
|
||||
ems_eos.set_start_datetime(sample_forecast_start)
|
||||
provider.update_data(force_enable=True, force_update=True)
|
||||
assert compare_datetimes(provider.start_datetime, sample_forecast_start).equal
|
||||
assert compare_datetimes(provider[0].date_time, to_datetime(sample_forecast_start)).equal
|
||||
|
||||
|
||||
# Report Generation Test
|
||||
def test_report_ac_power_and_measurement(provider):
|
||||
# Set the configuration
|
||||
config = get_config()
|
||||
config.merge_settings_from_dict(sample_config_data)
|
||||
|
||||
record = PVForecastAkkudoktorDataRecord(
|
||||
pvforecastakkudoktor_ac_power_measured=900.0,
|
||||
pvforecast_dc_power=450.0,
|
||||
pvforecast_ac_power=400.0,
|
||||
)
|
||||
provider.append(record)
|
||||
|
||||
report = provider.report_ac_power_and_measurement()
|
||||
assert "DC: 450.0" in report
|
||||
assert "AC: 400.0" in report
|
||||
assert "AC sampled: 900.0" in report
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.platform.startswith("win"), reason="'other_timezone' fixture not supported on Windows"
|
||||
)
|
||||
@patch("requests.get")
|
||||
def test_timezone_behaviour(
|
||||
mock_get,
|
||||
sample_settings,
|
||||
sample_forecast_data_raw,
|
||||
sample_forecast_start,
|
||||
provider,
|
||||
set_other_timezone,
|
||||
):
|
||||
"""Test PVForecast in another timezone."""
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.content = sample_forecast_data_raw
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
# sample forecast start in other timezone
|
||||
other_timezone = set_other_timezone()
|
||||
other_start_datetime = to_datetime(sample_forecast_start, in_timezone=other_timezone)
|
||||
assert compare_datetimes(other_start_datetime, sample_forecast_start).equal
|
||||
expected_datetime = to_datetime("2024-10-06T00:00:00+0200", in_timezone=other_timezone)
|
||||
assert compare_datetimes(other_start_datetime, expected_datetime).equal
|
||||
|
||||
provider.clear()
|
||||
assert len(provider) == 0
|
||||
ems_eos.set_start_datetime(other_start_datetime)
|
||||
provider.update_data(force_update=True)
|
||||
assert compare_datetimes(provider.start_datetime, other_start_datetime).equal
|
||||
# Check wether first record starts at requested sample start time
|
||||
assert compare_datetimes(provider[0].date_time, sample_forecast_start).equal
|
||||
|
||||
# Test updating AC power measurement for a specific date.
|
||||
provider.update_value(sample_forecast_start, "pvforecastakkudoktor_ac_power_measured", 1000)
|
||||
# Check wether first record was filled with ac power measurement
|
||||
assert provider[0].pvforecastakkudoktor_ac_power_measured == 1000
|
||||
|
||||
# Test fetching temperature forecast for a specific date.
|
||||
other_end_datetime = other_start_datetime + to_duration("24 hours")
|
||||
expected_end_datetime = to_datetime("2024-10-07T00:00:00+0200", in_timezone=other_timezone)
|
||||
assert compare_datetimes(other_end_datetime, expected_end_datetime).equal
|
||||
forecast_temps = provider.key_to_series(
|
||||
"pvforecastakkudoktor_temp_air", other_start_datetime, other_end_datetime
|
||||
)
|
||||
assert len(forecast_temps) == 24
|
||||
assert forecast_temps.iloc[0] == 7.0
|
||||
assert forecast_temps.iloc[1] == 6.5
|
||||
assert forecast_temps.iloc[2] == 6.0
|
||||
|
||||
# Test fetching AC power forecast
|
||||
other_end_datetime = other_start_datetime + to_duration("48 hours")
|
||||
forecast_measured = provider.key_to_series(
|
||||
"pvforecastakkudoktor_ac_power_measured", other_start_datetime, other_end_datetime
|
||||
)
|
||||
assert len(forecast_measured) == 48
|
||||
assert forecast_measured.iloc[0] == 1000.0 # changed before
|
110
tests/test_pvforecastimport.py
Normal file
110
tests/test_pvforecastimport.py
Normal file
@@ -0,0 +1,110 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from akkudoktoreos.config.config import get_config
|
||||
from akkudoktoreos.core.ems import get_ems
|
||||
from akkudoktoreos.prediction.pvforecastimport import PVForecastImport
|
||||
from akkudoktoreos.utils.datetimeutil import compare_datetimes, to_datetime
|
||||
|
||||
DIR_TESTDATA = Path(__file__).absolute().parent.joinpath("testdata")
|
||||
|
||||
FILE_TESTDATA_PVFORECASTIMPORT_1_JSON = DIR_TESTDATA.joinpath("import_input_1.json")
|
||||
|
||||
config_eos = get_config()
|
||||
ems_eos = get_ems()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pvforecast_provider(reset_config, sample_import_1_json):
|
||||
"""Fixture to create a PVForecastProvider instance."""
|
||||
settings = {
|
||||
"pvforecast_provider": "PVForecastImport",
|
||||
"pvforecastimport_file_path": str(FILE_TESTDATA_PVFORECASTIMPORT_1_JSON),
|
||||
"pvforecastimport_json": json.dumps(sample_import_1_json),
|
||||
}
|
||||
config_eos.merge_settings_from_dict(settings)
|
||||
provider = PVForecastImport()
|
||||
assert provider.enabled() == True
|
||||
return provider
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_import_1_json():
|
||||
"""Fixture that returns sample forecast data report."""
|
||||
with open(FILE_TESTDATA_PVFORECASTIMPORT_1_JSON, "r") as f_res:
|
||||
input_data = json.load(f_res)
|
||||
return input_data
|
||||
|
||||
|
||||
# ------------------------------------------------
|
||||
# General forecast
|
||||
# ------------------------------------------------
|
||||
|
||||
|
||||
def test_singleton_instance(pvforecast_provider):
|
||||
"""Test that PVForecastForecast behaves as a singleton."""
|
||||
another_instance = PVForecastImport()
|
||||
assert pvforecast_provider is another_instance
|
||||
|
||||
|
||||
def test_invalid_provider(pvforecast_provider):
|
||||
"""Test requesting an unsupported pvforecast_provider."""
|
||||
settings = {
|
||||
"pvforecast_provider": "<invalid>",
|
||||
"pvforecastimport_file_path": str(FILE_TESTDATA_PVFORECASTIMPORT_1_JSON),
|
||||
}
|
||||
config_eos.merge_settings_from_dict(settings)
|
||||
assert pvforecast_provider.enabled() == False
|
||||
|
||||
|
||||
# ------------------------------------------------
|
||||
# Import
|
||||
# ------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"start_datetime, from_file",
|
||||
[
|
||||
("2024-11-10 00:00:00", True), # No DST in Germany
|
||||
("2024-08-10 00:00:00", True), # DST in Germany
|
||||
("2024-03-31 00:00:00", True), # DST change in Germany (23 hours/ day)
|
||||
("2024-10-27 00:00:00", True), # DST change in Germany (25 hours/ day)
|
||||
("2024-11-10 00:00:00", False), # No DST in Germany
|
||||
("2024-08-10 00:00:00", False), # DST in Germany
|
||||
("2024-03-31 00:00:00", False), # DST change in Germany (23 hours/ day)
|
||||
("2024-10-27 00:00:00", False), # DST change in Germany (25 hours/ day)
|
||||
],
|
||||
)
|
||||
def test_import(pvforecast_provider, sample_import_1_json, start_datetime, from_file):
|
||||
"""Test fetching forecast from import."""
|
||||
ems_eos.set_start_datetime(to_datetime(start_datetime, in_timezone="Europe/Berlin"))
|
||||
if from_file:
|
||||
config_eos.pvforecastimport_json = None
|
||||
assert config_eos.pvforecastimport_json is None
|
||||
else:
|
||||
config_eos.pvforecastimport_file_path = None
|
||||
assert config_eos.pvforecastimport_file_path is None
|
||||
pvforecast_provider.clear()
|
||||
|
||||
# Call the method
|
||||
pvforecast_provider.update_data()
|
||||
|
||||
# Assert: Verify the result is as expected
|
||||
assert pvforecast_provider.start_datetime is not None
|
||||
assert pvforecast_provider.total_hours is not None
|
||||
assert compare_datetimes(pvforecast_provider.start_datetime, ems_eos.start_datetime).equal
|
||||
values = sample_import_1_json["pvforecast_ac_power"]
|
||||
value_datetime_mapping = pvforecast_provider.import_datetimes(len(values))
|
||||
for i, mapping in enumerate(value_datetime_mapping):
|
||||
assert i < len(pvforecast_provider.records)
|
||||
expected_datetime, expected_value_index = mapping
|
||||
expected_value = values[expected_value_index]
|
||||
result_datetime = pvforecast_provider.records[i].date_time
|
||||
result_value = pvforecast_provider.records[i]["pvforecast_ac_power"]
|
||||
|
||||
# print(f"{i}: Expected: {expected_datetime}:{expected_value}")
|
||||
# print(f"{i}: Result: {result_datetime}:{result_value}")
|
||||
assert compare_datetimes(result_datetime, expected_datetime).equal
|
||||
assert result_value == expected_value
|
@@ -1,24 +1,17 @@
|
||||
from http import HTTPStatus
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
|
||||
from akkudoktoreos.config import CONFIG_FILE_NAME, load_config
|
||||
from akkudoktoreos.config.config import get_config
|
||||
|
||||
config_eos = get_config()
|
||||
|
||||
|
||||
def test_fixture_setup(server, tmp_path: Path) -> None:
|
||||
"""Test if the fixture sets up the server with the env var."""
|
||||
# validate correct path in server
|
||||
config = load_config(tmp_path, False)
|
||||
assert tmp_path.joinpath(CONFIG_FILE_NAME).is_file()
|
||||
cache = tmp_path / config.directories.cache
|
||||
assert cache.is_dir()
|
||||
|
||||
|
||||
def test_server(server, tmp_path: Path):
|
||||
def test_server(server):
|
||||
"""Test the server."""
|
||||
result = requests.get(f"{server}/gesamtlast_simple?year_energy=2000&")
|
||||
assert result.status_code == HTTPStatus.OK
|
||||
# validate correct path in server
|
||||
assert config_eos.data_folder_path is not None
|
||||
assert config_eos.data_folder_path.is_dir()
|
||||
|
||||
config = load_config(tmp_path, False)
|
||||
assert len(result.json()) == config.eos.prediction_hours
|
||||
result = requests.get(f"{server}/config?")
|
||||
assert result.status_code == HTTPStatus.OK
|
||||
|
@@ -4,7 +4,7 @@ from pathlib import Path
|
||||
import pytest
|
||||
from matplotlib.testing.compare import compare_images
|
||||
|
||||
from akkudoktoreos.config import AppConfig
|
||||
from akkudoktoreos.config.config import get_config
|
||||
from akkudoktoreos.visualize import visualisiere_ergebnisse
|
||||
|
||||
DIR_TESTDATA = Path(__file__).parent / "testdata"
|
||||
@@ -15,11 +15,15 @@ DIR_IMAGEDATA = DIR_TESTDATA / "images"
|
||||
"fn_in, fn_out, fn_out_base",
|
||||
[("visualize_input_1.json", "visualize_output_1.pdf", "visualize_base_output_1.pdf")],
|
||||
)
|
||||
def test_visualisiere_ergebnisse(fn_in, fn_out, fn_out_base, tmp_config: AppConfig):
|
||||
def test_visualisiere_ergebnisse(fn_in, fn_out, fn_out_base):
|
||||
with open(DIR_TESTDATA / fn_in, "r") as f:
|
||||
input_data = json.load(f)
|
||||
visualisiere_ergebnisse(config=tmp_config, **input_data)
|
||||
output_file: Path = tmp_config.working_dir / tmp_config.directories.output / fn_out
|
||||
visualisiere_ergebnisse(**input_data)
|
||||
|
||||
config = get_config()
|
||||
output_dir = config.data_output_path
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
output_file = output_dir.joinpath(fn_out)
|
||||
|
||||
assert output_file.is_file()
|
||||
assert (
|
||||
|
193
tests/test_weatherbrightsky.py
Normal file
193
tests/test_weatherbrightsky.py
Normal file
@@ -0,0 +1,193 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pandas as pd
|
||||
import pytest
|
||||
|
||||
from akkudoktoreos.core.ems import get_ems
|
||||
from akkudoktoreos.prediction.weatherbrightsky import WeatherBrightSky
|
||||
from akkudoktoreos.utils.cacheutil import CacheFileStore
|
||||
from akkudoktoreos.utils.datetimeutil import to_datetime
|
||||
|
||||
DIR_TESTDATA = Path(__file__).absolute().parent.joinpath("testdata")
|
||||
|
||||
FILE_TESTDATA_WEATHERBRIGHTSKY_1_JSON = DIR_TESTDATA.joinpath("weatherforecast_brightsky_1.json")
|
||||
FILE_TESTDATA_WEATHERBRIGHTSKY_2_JSON = DIR_TESTDATA.joinpath("weatherforecast_brightsky_2.json")
|
||||
|
||||
ems_eos = get_ems()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def weather_provider(monkeypatch):
|
||||
"""Fixture to create a WeatherProvider instance."""
|
||||
monkeypatch.setenv("weather_provider", "BrightSky")
|
||||
monkeypatch.setenv("latitude", "50.0")
|
||||
monkeypatch.setenv("longitude", "10.0")
|
||||
return WeatherBrightSky()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_brightsky_1_json():
|
||||
"""Fixture that returns sample forecast data report."""
|
||||
with open(FILE_TESTDATA_WEATHERBRIGHTSKY_1_JSON, "r") as f_res:
|
||||
input_data = json.load(f_res)
|
||||
return input_data
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_brightsky_2_json():
|
||||
"""Fixture that returns sample forecast data report."""
|
||||
with open(FILE_TESTDATA_WEATHERBRIGHTSKY_2_JSON, "r") 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(weather_provider):
|
||||
"""Test that WeatherForecast behaves as a singleton."""
|
||||
another_instance = WeatherBrightSky()
|
||||
assert weather_provider is another_instance
|
||||
|
||||
|
||||
def test_invalid_provider(weather_provider, monkeypatch):
|
||||
"""Test requesting an unsupported weather_provider."""
|
||||
monkeypatch.setenv("weather_provider", "<invalid>")
|
||||
weather_provider.config.update()
|
||||
assert weather_provider.enabled() == False
|
||||
|
||||
|
||||
def test_invalid_coordinates(weather_provider, monkeypatch):
|
||||
"""Test invalid coordinates raise ValueError."""
|
||||
monkeypatch.setenv("latitude", "1000")
|
||||
monkeypatch.setenv("longitude", "1000")
|
||||
with pytest.raises(
|
||||
ValueError, # match="Latitude '1000' and/ or longitude `1000` out of valid range."
|
||||
):
|
||||
weather_provider.config.update()
|
||||
|
||||
|
||||
# ------------------------------------------------
|
||||
# Irradiance caclulation
|
||||
# ------------------------------------------------
|
||||
|
||||
|
||||
def test_irridiance_estimate_from_cloud_cover(weather_provider):
|
||||
"""Test cloud cover to irradiance estimation."""
|
||||
cloud_cover_data = pd.Series(
|
||||
data=[20, 50, 80], index=pd.date_range("2023-10-22", periods=3, freq="h")
|
||||
)
|
||||
|
||||
ghi, dni, dhi = weather_provider.estimate_irradiance_from_cloud_cover(
|
||||
50.0, 10.0, cloud_cover_data
|
||||
)
|
||||
|
||||
assert ghi == [0, 0, 0]
|
||||
assert dhi == [0, 0, 0]
|
||||
assert dni == [0, 0, 0]
|
||||
|
||||
|
||||
# ------------------------------------------------
|
||||
# BrightSky
|
||||
# ------------------------------------------------
|
||||
|
||||
|
||||
@patch("requests.get")
|
||||
def test_request_forecast(mock_get, weather_provider, sample_brightsky_1_json):
|
||||
"""Test requesting forecast from BrightSky."""
|
||||
# Mock response object
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.content = json.dumps(sample_brightsky_1_json)
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
# Preset, as this is usually done by update()
|
||||
weather_provider.config.update()
|
||||
|
||||
# Test function
|
||||
brightsky_data = weather_provider._request_forecast()
|
||||
|
||||
assert isinstance(brightsky_data, dict)
|
||||
assert brightsky_data["weather"][0] == {
|
||||
"timestamp": "2024-10-26T00:00:00+02:00",
|
||||
"source_id": 46567,
|
||||
"precipitation": 0.0,
|
||||
"pressure_msl": 1022.9,
|
||||
"sunshine": 0.0,
|
||||
"temperature": 6.2,
|
||||
"wind_direction": 40,
|
||||
"wind_speed": 4.7,
|
||||
"cloud_cover": 100,
|
||||
"dew_point": 5.8,
|
||||
"relative_humidity": 97,
|
||||
"visibility": 140,
|
||||
"wind_gust_direction": 70,
|
||||
"wind_gust_speed": 11.9,
|
||||
"condition": "dry",
|
||||
"precipitation_probability": None,
|
||||
"precipitation_probability_6h": None,
|
||||
"solar": None,
|
||||
"fallback_source_ids": {
|
||||
"wind_gust_speed": 219419,
|
||||
"pressure_msl": 219419,
|
||||
"cloud_cover": 219419,
|
||||
"wind_gust_direction": 219419,
|
||||
"wind_direction": 219419,
|
||||
"wind_speed": 219419,
|
||||
"sunshine": 219419,
|
||||
"visibility": 219419,
|
||||
},
|
||||
"icon": "cloudy",
|
||||
}
|
||||
|
||||
|
||||
@patch("requests.get")
|
||||
def test_update_data(mock_get, weather_provider, sample_brightsky_1_json, cache_store):
|
||||
"""Test fetching forecast from BrightSky."""
|
||||
# Mock response object
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.content = json.dumps(sample_brightsky_1_json)
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
cache_store.clear(clear_all=True)
|
||||
|
||||
# Call the method
|
||||
ems_eos.set_start_datetime(to_datetime("2024-10-26 00:00:00", in_timezone="Europe/Berlin"))
|
||||
weather_provider.update_data(force_enable=True, force_update=True)
|
||||
|
||||
# Assert: Verify the result is as expected
|
||||
mock_get.assert_called_once()
|
||||
assert len(weather_provider) == 338
|
||||
|
||||
# with open(FILE_TESTDATA_WEATHERBRIGHTSKY_2_JSON, "w") as f_out:
|
||||
# f_out.write(weather_provider.to_json())
|
||||
|
||||
|
||||
# ------------------------------------------------
|
||||
# Development BrightSky
|
||||
# ------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="For development only")
|
||||
def test_brightsky_development_forecast_data(weather_provider):
|
||||
"""Fetch data from real BrightSky server."""
|
||||
# Preset, as this is usually done by update_data()
|
||||
weather_provider.start_datetime = to_datetime("2024-10-26 00:00:00")
|
||||
weather_provider.latitude = 50.0
|
||||
weather_provider.longitude = 10.0
|
||||
|
||||
brightsky_data = weather_provider._request_forecast()
|
||||
|
||||
with open(FILE_TESTDATA_WEATHERBRIGHTSKY_1_JSON, "w") as f_out:
|
||||
json.dump(brightsky_data, f_out, indent=4)
|
569
tests/test_weatherclearoutside.py
Normal file
569
tests/test_weatherclearoutside.py
Normal file
@@ -0,0 +1,569 @@
|
||||
import re
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import pvlib
|
||||
import pytest
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from akkudoktoreos.config.config import get_config
|
||||
from akkudoktoreos.core.ems import get_ems
|
||||
from akkudoktoreos.prediction.weatherclearoutside import WeatherClearOutside
|
||||
from akkudoktoreos.utils.cacheutil import CacheFileStore
|
||||
from akkudoktoreos.utils.datetimeutil import compare_datetimes, to_datetime
|
||||
|
||||
DIR_TESTDATA = Path(__file__).absolute().parent.joinpath("testdata")
|
||||
|
||||
FILE_TESTDATA_WEATHERCLEAROUTSIDE_1_HTML = DIR_TESTDATA.joinpath("weatherforecast_clearout_1.html")
|
||||
FILE_TESTDATA_WEATHERCLEAROUTSIDE_1_DATA = DIR_TESTDATA.joinpath("weatherforecast_clearout_1.json")
|
||||
|
||||
config_eos = get_config()
|
||||
ems_eos = get_ems()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def weather_provider():
|
||||
"""Fixture to create a WeatherProvider instance."""
|
||||
settings = {
|
||||
"weather_provider": "ClearOutside",
|
||||
"latitude": 50.0,
|
||||
"longitude": 10.0,
|
||||
}
|
||||
config_eos.merge_settings_from_dict(settings)
|
||||
return WeatherClearOutside()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_clearout_1_html():
|
||||
"""Fixture that returns sample forecast data report."""
|
||||
with open(FILE_TESTDATA_WEATHERCLEAROUTSIDE_1_HTML, "r") as f_res:
|
||||
input_data = f_res.read()
|
||||
return input_data
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_clearout_1_data():
|
||||
"""Fixture that returns sample forecast data."""
|
||||
with open(FILE_TESTDATA_WEATHERCLEAROUTSIDE_1_DATA, "r") as f_in:
|
||||
json_str = f_in.read()
|
||||
data = WeatherClearOutside.from_json(json_str)
|
||||
return data
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cache_store():
|
||||
"""A pytest fixture that creates a new CacheFileStore instance for testing."""
|
||||
return CacheFileStore()
|
||||
|
||||
|
||||
# ------------------------------------------------
|
||||
# General WeatherProvider
|
||||
# ------------------------------------------------
|
||||
|
||||
|
||||
def test_singleton_instance(weather_provider):
|
||||
"""Test that WeatherForecast behaves as a singleton."""
|
||||
another_instance = WeatherClearOutside()
|
||||
assert weather_provider is another_instance
|
||||
|
||||
|
||||
def test_invalid_provider(weather_provider):
|
||||
"""Test requesting an unsupported weather_provider."""
|
||||
settings = {
|
||||
"weather_provider": "<invalid>",
|
||||
}
|
||||
config_eos.merge_settings_from_dict(settings)
|
||||
assert weather_provider.enabled() == False
|
||||
|
||||
|
||||
def test_invalid_coordinates(weather_provider):
|
||||
"""Test invalid coordinates raise ValueError."""
|
||||
settings = {
|
||||
"weather_provider": "ClearOutside",
|
||||
"latitude": 1000.0,
|
||||
"longitude": 1000.0,
|
||||
}
|
||||
with pytest.raises(
|
||||
ValueError, # match="Latitude '1000' and/ or longitude `1000` out of valid range."
|
||||
):
|
||||
config_eos.merge_settings_from_dict(settings)
|
||||
|
||||
|
||||
# ------------------------------------------------
|
||||
# Irradiance caclulation
|
||||
# ------------------------------------------------
|
||||
|
||||
|
||||
def test_irridiance_estimate_from_cloud_cover(weather_provider):
|
||||
"""Test cloud cover to irradiance estimation."""
|
||||
cloud_cover_data = pd.Series(
|
||||
data=[20, 50, 80], index=pd.date_range("2023-10-22", periods=3, freq="h")
|
||||
)
|
||||
|
||||
ghi, dni, dhi = weather_provider.estimate_irradiance_from_cloud_cover(
|
||||
50.0, 10.0, cloud_cover_data
|
||||
)
|
||||
|
||||
assert ghi == [0, 0, 0]
|
||||
assert dhi == [0, 0, 0]
|
||||
assert dni == [0, 0, 0]
|
||||
|
||||
|
||||
# ------------------------------------------------
|
||||
# ClearOutside
|
||||
# ------------------------------------------------
|
||||
|
||||
|
||||
@patch("requests.get")
|
||||
def test_request_forecast(mock_get, weather_provider, sample_clearout_1_html):
|
||||
"""Test fetching forecast from ClearOutside."""
|
||||
# Mock response object
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.content = sample_clearout_1_html
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
# Preset, as this is usually done by update()
|
||||
config_eos.update()
|
||||
|
||||
# Test function
|
||||
response = weather_provider._request_forecast()
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.content == sample_clearout_1_html
|
||||
|
||||
|
||||
@patch("requests.get")
|
||||
def test_update_data(mock_get, weather_provider, sample_clearout_1_html, sample_clearout_1_data):
|
||||
# Mock response object
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.content = sample_clearout_1_html
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
expected_start = to_datetime("2024-10-26 00:00:00", in_timezone="Europe/Berlin")
|
||||
expected_end = to_datetime("2024-10-28 00:00:00", in_timezone="Europe/Berlin")
|
||||
expected_keep = to_datetime("2024-10-24 00:00:00", in_timezone="Europe/Berlin")
|
||||
|
||||
# Call the method
|
||||
ems_eos.set_start_datetime(expected_start)
|
||||
weather_provider.update_data()
|
||||
|
||||
# Check for correct prediction time window
|
||||
assert weather_provider.config.prediction_hours == 48
|
||||
assert weather_provider.config.prediction_historic_hours == 48
|
||||
assert compare_datetimes(weather_provider.start_datetime, expected_start).equal
|
||||
assert compare_datetimes(weather_provider.end_datetime, expected_end).equal
|
||||
assert compare_datetimes(weather_provider.keep_datetime, expected_keep).equal
|
||||
|
||||
# Verify the data
|
||||
assert len(weather_provider) == 165 # 6 days, 24 hours per day - 7th day 21 hours
|
||||
|
||||
# Check that specific values match the expected output
|
||||
# for i, record in enumerate(weather_data.records):
|
||||
# # Compare datetime and specific values
|
||||
# assert record.datetime == sample_clearout_1_data.records[i].datetime
|
||||
# assert record.data['total_clouds'] == sample_clearout_1_data.records[i].data['total_clouds']
|
||||
# # Check additional weather attributes as necessary
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="Test fixture to be improved")
|
||||
@patch("requests.get")
|
||||
def test_cache_forecast(mock_get, weather_provider, sample_clearout_1_html, cache_store):
|
||||
"""Test that ClearOutside forecast data is cached with TTL.
|
||||
|
||||
This can not be tested with mock_get. Mock objects are not pickable and therefor can not be
|
||||
cached to a file. Keep it for documentation.
|
||||
"""
|
||||
# Mock response object
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.content = sample_clearout_1_html
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
cache_store.clear(clear_all=True)
|
||||
|
||||
weather_provider.update_data()
|
||||
mock_get.assert_called_once()
|
||||
forecast_data_first = weather_provider.to_json()
|
||||
|
||||
weather_provider.update_data()
|
||||
forecast_data_second = weather_provider.to_json()
|
||||
# Verify that cache returns the same object without calling the method again
|
||||
assert forecast_data_first == forecast_data_second
|
||||
# A mock object is not pickable and therefor can not be chached to file
|
||||
assert mock_get.call_count == 2
|
||||
|
||||
|
||||
# ------------------------------------------------
|
||||
# Development ClearOutside
|
||||
# ------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="For development only")
|
||||
@patch("requests.get")
|
||||
def test_development_forecast_data(mock_get, weather_provider, sample_clearout_1_html):
|
||||
# Mock response object
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.content = sample_clearout_1_html
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
# Fill the instance
|
||||
weather_provider.update_data(force_enable=True)
|
||||
|
||||
with open(FILE_TESTDATA_WEATHERCLEAROUTSIDE_1_DATA, "w") as f_out:
|
||||
f_out.write(weather_provider.to_json())
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="For development only")
|
||||
def test_clearoutsides_development_scraper(weather_provider, sample_clearout_1_html):
|
||||
"""Test scraping from ClearOutside."""
|
||||
soup = BeautifulSoup(sample_clearout_1_html, "html.parser")
|
||||
|
||||
# Sample was created for the loacation
|
||||
lat = 50.0
|
||||
lon = 10.0
|
||||
|
||||
# Find generation data
|
||||
p_generated = soup.find("h2", string=lambda text: text and text.startswith("Generated:"))
|
||||
assert p_generated is not None
|
||||
|
||||
# Extract forecast start and end dates
|
||||
forecast_pattern = r"Forecast: (\d{2}/\d{2}/\d{2}) to (\d{2}/\d{2}/\d{2})"
|
||||
forecast_match = re.search(forecast_pattern, p_generated.get_text())
|
||||
if forecast_match:
|
||||
forecast_start_date = forecast_match.group(1)
|
||||
forecast_end_date = forecast_match.group(2)
|
||||
else:
|
||||
assert False
|
||||
assert forecast_start_date == "26/10/24"
|
||||
assert forecast_end_date == "01/11/24"
|
||||
|
||||
# Extract timezone offset
|
||||
timezone_pattern = r"Timezone: UTC([+-]\d+)\.(\d+)"
|
||||
timezone_match = re.search(timezone_pattern, p_generated.get_text())
|
||||
if timezone_match:
|
||||
hours = int(timezone_match.group(1))
|
||||
assert hours == 2
|
||||
# Convert the decimal part to minutes (e.g., .50 -> 30 minutes)
|
||||
minutes = int(timezone_match.group(2)) * 6 # Multiply by 6 to convert to minutes
|
||||
assert minutes == 0
|
||||
|
||||
# Create the timezone object using timedelta for the offset
|
||||
forecast_timezone = timezone(timedelta(hours=hours, minutes=minutes))
|
||||
else:
|
||||
assert False
|
||||
|
||||
forecast_start_datetime = to_datetime(
|
||||
forecast_start_date, in_timezone=forecast_timezone, to_naiv=False, to_maxtime=False
|
||||
)
|
||||
assert forecast_start_datetime == datetime(2024, 10, 26, 0, 0)
|
||||
|
||||
# Find all paragraphs with id 'day_<x>'. There should be seven.
|
||||
p_days = soup.find_all(id=re.compile(r"day_[0-9]"))
|
||||
assert len(p_days) == 7
|
||||
p_day = p_days[0]
|
||||
|
||||
# Within day_x paragraph find the details labels
|
||||
p_detail_labels = p_day.find_all(class_="fc_detail_label")
|
||||
detail_names = [p.get_text() for p in p_detail_labels]
|
||||
|
||||
assert detail_names == [
|
||||
"Total Clouds (% Sky Obscured)",
|
||||
"Low Clouds (% Sky Obscured)",
|
||||
"Medium Clouds (% Sky Obscured)",
|
||||
"High Clouds (% Sky Obscured)",
|
||||
"ISS Passover",
|
||||
"Visibility (miles)",
|
||||
"Fog (%)",
|
||||
"Precipitation Type",
|
||||
"Precipitation Probability (%)",
|
||||
"Precipitation Amount (mm)",
|
||||
"Wind Speed/Direction (mph)",
|
||||
"Chance of Frost",
|
||||
"Temperature (°C)",
|
||||
"Feels Like (°C)",
|
||||
"Dew Point (°C)",
|
||||
"Relative Humidity (%)",
|
||||
"Pressure (mb)",
|
||||
"Ozone (du)",
|
||||
]
|
||||
|
||||
# Find all the paragraphs that are associated to the details.
|
||||
# Beware there is one ul paragraph before that is not associated to a detail
|
||||
p_detail_tables = p_day.find_all("ul")
|
||||
assert len(p_detail_tables) == len(detail_names) + 1
|
||||
p_detail_tables.pop(0)
|
||||
|
||||
# Create clearout data
|
||||
clearout_data = {}
|
||||
# Add data values
|
||||
for i, detail_name in enumerate(detail_names):
|
||||
p_detail_values = p_detail_tables[i].find_all("li")
|
||||
detail_data = []
|
||||
for p_detail_value in p_detail_values:
|
||||
if (
|
||||
detail_name in ("Precipitation Type", "Chance of Frost")
|
||||
and hasattr(p_detail_value, "title")
|
||||
and p_detail_value.title
|
||||
):
|
||||
value_str = p_detail_value.title.string
|
||||
else:
|
||||
value_str = p_detail_value.get_text()
|
||||
try:
|
||||
value = float(value_str)
|
||||
except ValueError:
|
||||
value = value_str
|
||||
detail_data.append(value)
|
||||
assert len(detail_data) == 24
|
||||
clearout_data[detail_name] = detail_data
|
||||
|
||||
assert clearout_data["Temperature (°C)"] == [
|
||||
14.0,
|
||||
14.0,
|
||||
13.0,
|
||||
12.0,
|
||||
11.0,
|
||||
11.0,
|
||||
10.0,
|
||||
10.0,
|
||||
9.0,
|
||||
9.0,
|
||||
9.0,
|
||||
9.0,
|
||||
9.0,
|
||||
10.0,
|
||||
9.0,
|
||||
9.0,
|
||||
10.0,
|
||||
11.0,
|
||||
13.0,
|
||||
14.0,
|
||||
15.0,
|
||||
16.0,
|
||||
16.0,
|
||||
16.0,
|
||||
]
|
||||
assert clearout_data["Relative Humidity (%)"] == [
|
||||
59.0,
|
||||
68.0,
|
||||
75.0,
|
||||
81.0,
|
||||
84.0,
|
||||
85.0,
|
||||
85.0,
|
||||
91.0,
|
||||
91.0,
|
||||
93.0,
|
||||
93.0,
|
||||
93.0,
|
||||
93.0,
|
||||
93.0,
|
||||
95.0,
|
||||
95.0,
|
||||
93.0,
|
||||
87.0,
|
||||
81.0,
|
||||
76.0,
|
||||
70.0,
|
||||
66.0,
|
||||
66.0,
|
||||
69.0,
|
||||
]
|
||||
assert clearout_data["Wind Speed/Direction (mph)"] == [
|
||||
7.0,
|
||||
6.0,
|
||||
4.0,
|
||||
4.0,
|
||||
4.0,
|
||||
4.0,
|
||||
4.0,
|
||||
4.0,
|
||||
3.0,
|
||||
3.0,
|
||||
3.0,
|
||||
2.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
2.0,
|
||||
2.0,
|
||||
2.0,
|
||||
4.0,
|
||||
5.0,
|
||||
6.0,
|
||||
6.0,
|
||||
5.0,
|
||||
5.0,
|
||||
]
|
||||
|
||||
# Add datetimes of the scrapped data
|
||||
clearout_data["DateTime"] = [forecast_start_datetime + timedelta(hours=i) for i in range(24)]
|
||||
detail_names.append("DateTime")
|
||||
|
||||
assert len(clearout_data["DateTime"]) == 24
|
||||
assert clearout_data["DateTime"][0] == to_datetime(
|
||||
"2024-10-26 00:00:00", in_timezone=forecast_timezone
|
||||
)
|
||||
assert clearout_data["DateTime"][23] == to_datetime(
|
||||
"2024-10-26 23:00:00", in_timezone=forecast_timezone
|
||||
)
|
||||
|
||||
# Converting the cloud cover into Global Horizontal Irradiance (GHI) with a PVLib method
|
||||
offset = 35 # The default
|
||||
offset_fraction = offset / 100.0 # Adjust percentage to scaling factor
|
||||
cloud_cover = pd.Series(clearout_data["Total Clouds (% Sky Obscured)"])
|
||||
|
||||
# Convert datetime list to a pandas DatetimeIndex
|
||||
cloud_cover_times = pd.DatetimeIndex(clearout_data["DateTime"])
|
||||
|
||||
# Create a location object
|
||||
location = pvlib.location.Location(latitude=lat, longitude=lon)
|
||||
|
||||
# Get solar position and clear-sky GHI using the Ineichen model
|
||||
solpos = location.get_solarposition(cloud_cover_times)
|
||||
clear_sky = location.get_clearsky(cloud_cover_times, model="ineichen")
|
||||
|
||||
# Convert cloud cover percentage to a scaling factor
|
||||
cloud_cover_fraction = np.array(cloud_cover) / 100.0
|
||||
|
||||
# Calculate adjusted GHI with proportional offset adjustment
|
||||
adjusted_ghi = clear_sky["ghi"] * (
|
||||
offset_fraction + (1 - offset_fraction) * (1 - cloud_cover_fraction)
|
||||
)
|
||||
adjusted_ghi.fillna(0.0, inplace=True)
|
||||
|
||||
# Apply DISC model to estimate Direct Normal Irradiance (DNI) from adjusted GHI
|
||||
disc_output = pvlib.irradiance.disc(adjusted_ghi, solpos["zenith"], cloud_cover_times)
|
||||
adjusted_dni = disc_output["dni"]
|
||||
adjusted_dni.fillna(0.0, inplace=True)
|
||||
|
||||
# Calculate Diffuse Horizontal Irradiance (DHI) as DHI = GHI - DNI * cos(zenith)
|
||||
zenith_rad = np.radians(solpos["zenith"])
|
||||
adjusted_dhi = adjusted_ghi - adjusted_dni * np.cos(zenith_rad)
|
||||
adjusted_dhi.fillna(0.0, inplace=True)
|
||||
|
||||
# Add GHI, DNI, DHI to clearout data
|
||||
clearout_data["Global Horizontal Irradiance (W/m2)"] = adjusted_ghi.to_list()
|
||||
detail_names.append("Global Horizontal Irradiance (W/m2)")
|
||||
clearout_data["Direct Normal Irradiance (W/m2)"] = adjusted_dni.to_list()
|
||||
detail_names.append("Direct Normal Irradiance (W/m2)")
|
||||
clearout_data["Diffuse Horizontal Irradiance (W/m2)"] = adjusted_dhi.to_list()
|
||||
detail_names.append("Diffuse Horizontal Irradiance (W/m2)")
|
||||
|
||||
assert clearout_data["Global Horizontal Irradiance (W/m2)"] == [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
24.291000436601216,
|
||||
85.88494154645998,
|
||||
136.09269403109946,
|
||||
139.26925350542064,
|
||||
146.7174434892616,
|
||||
149.0167479382964,
|
||||
138.97458866666065,
|
||||
103.47132353697396,
|
||||
46.81279774519421,
|
||||
0.12972168074047014,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
]
|
||||
assert clearout_data["Direct Normal Irradiance (W/m2)"] == [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
10.19687368654253,
|
||||
0.0,
|
||||
0.0,
|
||||
2.9434862632289804,
|
||||
9.621272744657047,
|
||||
9.384995789935898,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
]
|
||||
assert clearout_data["Diffuse Horizontal Irradiance (W/m2)"] == [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
24.291000436601216,
|
||||
85.88494154645998,
|
||||
132.32210426501337,
|
||||
139.26925350542064,
|
||||
146.7174434892616,
|
||||
147.721968406295,
|
||||
135.32240392326145,
|
||||
100.82522311704261,
|
||||
46.81279774519421,
|
||||
0.12972168074047014,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
]
|
||||
|
||||
# Preciptable Water (PWAT) with a PVLib method
|
||||
clearout_data["Preciptable Water (cm)"] = pvlib.atmosphere.gueymard94_pw(
|
||||
pd.Series(data=clearout_data["Temperature (°C)"]),
|
||||
pd.Series(data=clearout_data["Relative Humidity (%)"]),
|
||||
).to_list()
|
||||
detail_names.append("Preciptable Water (cm)")
|
||||
|
||||
assert clearout_data["Preciptable Water (cm)"] == [
|
||||
1.5345406562673334,
|
||||
1.7686231292572652,
|
||||
1.8354895631381385,
|
||||
1.8651290310892348,
|
||||
1.8197998755611786,
|
||||
1.8414641597940502,
|
||||
1.7325709431177607,
|
||||
1.8548700685143087,
|
||||
1.7453005409540279,
|
||||
1.783658794601369,
|
||||
1.783658794601369,
|
||||
1.783658794601369,
|
||||
1.783658794601369,
|
||||
1.8956364436464912,
|
||||
1.8220170482487101,
|
||||
1.8220170482487101,
|
||||
1.8956364436464912,
|
||||
1.8847927282597918,
|
||||
1.9823287281891897,
|
||||
1.9766964385816497,
|
||||
1.9346943880237457,
|
||||
1.9381315133101413,
|
||||
1.9381315133101413,
|
||||
2.026228400278784,
|
||||
]
|
110
tests/test_weatherimport.py
Normal file
110
tests/test_weatherimport.py
Normal file
@@ -0,0 +1,110 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from akkudoktoreos.config.config import get_config
|
||||
from akkudoktoreos.core.ems import get_ems
|
||||
from akkudoktoreos.prediction.weatherimport import WeatherImport
|
||||
from akkudoktoreos.utils.datetimeutil import compare_datetimes, to_datetime
|
||||
|
||||
DIR_TESTDATA = Path(__file__).absolute().parent.joinpath("testdata")
|
||||
|
||||
FILE_TESTDATA_WEATHERIMPORT_1_JSON = DIR_TESTDATA.joinpath("import_input_1.json")
|
||||
|
||||
config_eos = get_config()
|
||||
ems_eos = get_ems()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def weather_provider(reset_config, sample_import_1_json):
|
||||
"""Fixture to create a WeatherProvider instance."""
|
||||
settings = {
|
||||
"weather_provider": "WeatherImport",
|
||||
"weatherimport_file_path": str(FILE_TESTDATA_WEATHERIMPORT_1_JSON),
|
||||
"weatherimport_json": json.dumps(sample_import_1_json),
|
||||
}
|
||||
config_eos.merge_settings_from_dict(settings)
|
||||
provider = WeatherImport()
|
||||
assert provider.enabled() == True
|
||||
return provider
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_import_1_json():
|
||||
"""Fixture that returns sample forecast data report."""
|
||||
with open(FILE_TESTDATA_WEATHERIMPORT_1_JSON, "r") as f_res:
|
||||
input_data = json.load(f_res)
|
||||
return input_data
|
||||
|
||||
|
||||
# ------------------------------------------------
|
||||
# General forecast
|
||||
# ------------------------------------------------
|
||||
|
||||
|
||||
def test_singleton_instance(weather_provider):
|
||||
"""Test that WeatherForecast behaves as a singleton."""
|
||||
another_instance = WeatherImport()
|
||||
assert weather_provider is another_instance
|
||||
|
||||
|
||||
def test_invalid_provider(weather_provider, monkeypatch):
|
||||
"""Test requesting an unsupported weather_provider."""
|
||||
settings = {
|
||||
"weather_provider": "<invalid>",
|
||||
"weatherimport_file_path": str(FILE_TESTDATA_WEATHERIMPORT_1_JSON),
|
||||
}
|
||||
config_eos.merge_settings_from_dict(settings)
|
||||
assert weather_provider.enabled() == False
|
||||
|
||||
|
||||
# ------------------------------------------------
|
||||
# Import
|
||||
# ------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"start_datetime, from_file",
|
||||
[
|
||||
("2024-11-10 00:00:00", True), # No DST in Germany
|
||||
("2024-08-10 00:00:00", True), # DST in Germany
|
||||
("2024-03-31 00:00:00", True), # DST change in Germany (23 hours/ day)
|
||||
("2024-10-27 00:00:00", True), # DST change in Germany (25 hours/ day)
|
||||
("2024-11-10 00:00:00", False), # No DST in Germany
|
||||
("2024-08-10 00:00:00", False), # DST in Germany
|
||||
("2024-03-31 00:00:00", False), # DST change in Germany (23 hours/ day)
|
||||
("2024-10-27 00:00:00", False), # DST change in Germany (25 hours/ day)
|
||||
],
|
||||
)
|
||||
def test_import(weather_provider, sample_import_1_json, start_datetime, from_file):
|
||||
"""Test fetching forecast from Import."""
|
||||
ems_eos.set_start_datetime(to_datetime(start_datetime, in_timezone="Europe/Berlin"))
|
||||
if from_file:
|
||||
config_eos.weatherimport_json = None
|
||||
assert config_eos.weatherimport_json is None
|
||||
else:
|
||||
config_eos.weatherimport_file_path = None
|
||||
assert config_eos.weatherimport_file_path is None
|
||||
weather_provider.clear()
|
||||
|
||||
# Call the method
|
||||
weather_provider.update_data()
|
||||
|
||||
# Assert: Verify the result is as expected
|
||||
assert weather_provider.start_datetime is not None
|
||||
assert weather_provider.total_hours is not None
|
||||
assert compare_datetimes(weather_provider.start_datetime, ems_eos.start_datetime).equal
|
||||
values = sample_import_1_json["weather_temp_air"]
|
||||
value_datetime_mapping = weather_provider.import_datetimes(len(values))
|
||||
for i, mapping in enumerate(value_datetime_mapping):
|
||||
assert i < len(weather_provider.records)
|
||||
expected_datetime, expected_value_index = mapping
|
||||
expected_value = values[expected_value_index]
|
||||
result_datetime = weather_provider.records[i].date_time
|
||||
result_value = weather_provider.records[i]["weather_temp_air"]
|
||||
|
||||
# print(f"{i}: Expected: {expected_datetime}:{expected_value}")
|
||||
# print(f"{i}: Result: {result_datetime}:{result_value}")
|
||||
assert compare_datetimes(result_datetime, expected_datetime).equal
|
||||
assert result_value == expected_value
|
122
tests/testdata/EOS.config.json
vendored
Normal file
122
tests/testdata/EOS.config.json
vendored
Normal file
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"config_file_path": null,
|
||||
"config_folder_path": null,
|
||||
"data_cache_path": null,
|
||||
"data_cache_subpath": null,
|
||||
"data_folder_path": null,
|
||||
"data_output_path": null,
|
||||
"data_output_subpath": null,
|
||||
"elecprice_provider": null,
|
||||
"elecpriceimport_file_path": null,
|
||||
"latitude": null,
|
||||
"load0_import_file_path": null,
|
||||
"load0_name": null,
|
||||
"load0_provider": null,
|
||||
"load1_import_file_path": null,
|
||||
"load1_name": null,
|
||||
"load1_provider": null,
|
||||
"load2_import_file_path": null,
|
||||
"load2_name": null,
|
||||
"load2_provider": null,
|
||||
"load3_import_file_path": null,
|
||||
"load3_name": null,
|
||||
"load3_provider": null,
|
||||
"load4_import_file_path": null,
|
||||
"load4_name": null,
|
||||
"load4_provider": null,
|
||||
"loadakkudoktor_year_energy": null,
|
||||
"longitude": null,
|
||||
"optimization_ev_available_charge_rates_percent": [],
|
||||
"optimization_hours": 24,
|
||||
"optimization_penalty": null,
|
||||
"prediction_historic_hours": 48,
|
||||
"prediction_hours": 48,
|
||||
"pvforecast0_albedo": null,
|
||||
"pvforecast0_inverter_model": null,
|
||||
"pvforecast0_inverter_paco": null,
|
||||
"pvforecast0_loss": null,
|
||||
"pvforecast0_module_model": null,
|
||||
"pvforecast0_modules_per_string": null,
|
||||
"pvforecast0_mountingplace": "free",
|
||||
"pvforecast0_optimal_surface_tilt": false,
|
||||
"pvforecast0_optimalangles": false,
|
||||
"pvforecast0_peakpower": null,
|
||||
"pvforecast0_pvtechchoice": "crystSi",
|
||||
"pvforecast0_strings_per_inverter": null,
|
||||
"pvforecast0_surface_azimuth": 180,
|
||||
"pvforecast0_surface_tilt": 0,
|
||||
"pvforecast0_trackingtype": 0,
|
||||
"pvforecast0_userhorizon": null,
|
||||
"pvforecast1_albedo": null,
|
||||
"pvforecast1_inverter_model": null,
|
||||
"pvforecast1_inverter_paco": null,
|
||||
"pvforecast1_loss": 0,
|
||||
"pvforecast1_module_model": null,
|
||||
"pvforecast1_modules_per_string": null,
|
||||
"pvforecast1_mountingplace": "free",
|
||||
"pvforecast1_optimal_surface_tilt": false,
|
||||
"pvforecast1_optimalangles": false,
|
||||
"pvforecast1_peakpower": null,
|
||||
"pvforecast1_pvtechchoice": "crystSi",
|
||||
"pvforecast1_strings_per_inverter": null,
|
||||
"pvforecast1_surface_azimuth": 180,
|
||||
"pvforecast1_surface_tilt": 0,
|
||||
"pvforecast1_trackingtype": 0,
|
||||
"pvforecast1_userhorizon": null,
|
||||
"pvforecast2_albedo": null,
|
||||
"pvforecast2_inverter_model": null,
|
||||
"pvforecast2_inverter_paco": null,
|
||||
"pvforecast2_loss": 0,
|
||||
"pvforecast2_module_model": null,
|
||||
"pvforecast2_modules_per_string": null,
|
||||
"pvforecast2_mountingplace": "free",
|
||||
"pvforecast2_optimal_surface_tilt": false,
|
||||
"pvforecast2_optimalangles": false,
|
||||
"pvforecast2_peakpower": null,
|
||||
"pvforecast2_pvtechchoice": "crystSi",
|
||||
"pvforecast2_strings_per_inverter": null,
|
||||
"pvforecast2_surface_azimuth": 180,
|
||||
"pvforecast2_surface_tilt": 0,
|
||||
"pvforecast2_trackingtype": 0,
|
||||
"pvforecast2_userhorizon": null,
|
||||
"pvforecast3_albedo": null,
|
||||
"pvforecast3_inverter_model": null,
|
||||
"pvforecast3_inverter_paco": null,
|
||||
"pvforecast3_loss": 0,
|
||||
"pvforecast3_module_model": null,
|
||||
"pvforecast3_modules_per_string": null,
|
||||
"pvforecast3_mountingplace": "free",
|
||||
"pvforecast3_optimal_surface_tilt": false,
|
||||
"pvforecast3_optimalangles": false,
|
||||
"pvforecast3_peakpower": null,
|
||||
"pvforecast3_pvtechchoice": "crystSi",
|
||||
"pvforecast3_strings_per_inverter": null,
|
||||
"pvforecast3_surface_azimuth": 180,
|
||||
"pvforecast3_surface_tilt": 0,
|
||||
"pvforecast3_trackingtype": 0,
|
||||
"pvforecast3_userhorizon": null,
|
||||
"pvforecast4_albedo": null,
|
||||
"pvforecast4_inverter_model": null,
|
||||
"pvforecast4_inverter_paco": null,
|
||||
"pvforecast4_loss": 0,
|
||||
"pvforecast4_module_model": null,
|
||||
"pvforecast4_modules_per_string": null,
|
||||
"pvforecast4_mountingplace": "free",
|
||||
"pvforecast4_optimal_surface_tilt": false,
|
||||
"pvforecast4_optimalangles": false,
|
||||
"pvforecast4_peakpower": null,
|
||||
"pvforecast4_pvtechchoice": "crystSi",
|
||||
"pvforecast4_strings_per_inverter": null,
|
||||
"pvforecast4_surface_azimuth": 180,
|
||||
"pvforecast4_surface_tilt": 0,
|
||||
"pvforecast4_trackingtype": 0,
|
||||
"pvforecast4_userhorizon": null,
|
||||
"pvforecast_provider": null,
|
||||
"pvforecastimport_file_path": null,
|
||||
"server_fastapi_host": "0.0.0.0",
|
||||
"server_fastapi_port": 8503,
|
||||
"server_fasthtml_host": "0.0.0.0",
|
||||
"server_fasthtml_port": 8504,
|
||||
"weather_provider": null,
|
||||
"weatherimport_file_path": null
|
||||
}
|
1
tests/testdata/elecpriceforecast_akkudoktor_1.json
vendored
Normal file
1
tests/testdata/elecpriceforecast_akkudoktor_1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
30
tests/testdata/import_input_1.json
vendored
Normal file
30
tests/testdata/import_input_1.json
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"load0_mean": [
|
||||
676.71, 876.19, 527.13, 468.88, 531.38, 517.95, 483.15, 472.28, 1011.68, 995.00,
|
||||
1053.07, 1063.91, 1320.56, 1132.03, 1163.67, 1176.82, 1216.22, 1103.78, 1129.12,
|
||||
1178.71, 1050.98, 988.56, 912.38, 704.61, 516.37, 868.05, 694.34, 608.79, 556.31,
|
||||
488.89, 506.91, 804.89, 1141.98, 1056.97, 992.46, 1155.99, 827.01, 1257.98, 1232.67,
|
||||
871.26, 860.88, 1158.03, 1222.72, 1221.04, 949.99, 987.01, 733.99, 592.97
|
||||
],
|
||||
"elecprice_marketprice": [
|
||||
0.3384, 0.3318, 0.3284, 0.3283, 0.3289, 0.3334, 0.3290,
|
||||
0.3302, 0.3042, 0.2430, 0.2280, 0.2212, 0.2093, 0.1879,
|
||||
0.1838, 0.2004, 0.2198, 0.2270, 0.2997, 0.3195, 0.3081,
|
||||
0.2969, 0.2921, 0.2780, 0.3384, 0.3318, 0.3284, 0.3283,
|
||||
0.3289, 0.3334, 0.3290, 0.3302, 0.3042, 0.2430, 0.2280,
|
||||
0.2212, 0.2093, 0.1879, 0.1838, 0.2004, 0.2198, 0.2270,
|
||||
0.2997, 0.3195, 0.3081, 0.2969, 0.2921, 0.2780
|
||||
],
|
||||
"pvforecast_ac_power": [
|
||||
0, 0, 0, 0, 0, 0, 0, 8.05, 352.91, 728.51, 930.28, 1043.25, 1106.74, 1161.69,
|
||||
6018.82, 5519.07, 3969.88, 3017.96, 1943.07, 1007.17, 319.67, 7.88, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 5.04, 335.59, 705.32, 1121.12, 1604.79, 2157.38, 1433.25, 5718.49,
|
||||
4553.96, 3027.55, 2574.46, 1720.4, 963.4, 383.3, 0, 0, 0
|
||||
],
|
||||
"weather_temp_air": [
|
||||
18.3, 17.8, 16.9, 16.2, 15.6, 15.1, 14.6, 14.2, 14.3, 14.8, 15.7, 16.7,
|
||||
17.4, 18.0, 18.6, 19.2, 19.1, 18.7, 18.5, 17.7, 16.2, 14.6, 13.6, 13.0,
|
||||
12.6, 12.2, 11.7, 11.6, 11.3, 11.0, 10.7, 10.2, 11.4, 14.4, 16.4, 18.3,
|
||||
19.5, 20.7, 21.9, 22.7, 23.1, 23.1, 22.8, 21.8, 20.2, 19.1, 18.0, 17.4
|
||||
]
|
||||
}
|
10592
tests/testdata/weatherforecast_brightsky_1.json
vendored
Normal file
10592
tests/testdata/weatherforecast_brightsky_1.json
vendored
Normal file
File diff suppressed because it is too large
Load Diff
340
tests/testdata/weatherforecast_brightsky_2.json
vendored
Normal file
340
tests/testdata/weatherforecast_brightsky_2.json
vendored
Normal file
@@ -0,0 +1,340 @@
|
||||
[
|
||||
"{\"date_time\":\"2024-10-25T23:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":140.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.5700713856497344,\"wind_speed\":4.7,\"wind_direction\":40.0,\"frost_chance\":null,\"temp_air\":6.2,\"feels_like\":null,\"dew_point\":5.8,\"relative_humidity\":97.0,\"pressure\":1022.9,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-26T00:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":90.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.6249985327562004,\"wind_speed\":6.1,\"wind_direction\":10.0,\"frost_chance\":null,\"temp_air\":6.6,\"feels_like\":null,\"dew_point\":6.3,\"relative_humidity\":98.0,\"pressure\":1023.1,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-26T01:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":740.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.7334253361156824,\"wind_speed\":3.6,\"wind_direction\":20.0,\"frost_chance\":null,\"temp_air\":7.5,\"feels_like\":null,\"dew_point\":7.3,\"relative_humidity\":99.0,\"pressure\":1023.0,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-26T02:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":580.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.8455721653466037,\"wind_speed\":2.9,\"wind_direction\":50.0,\"frost_chance\":null,\"temp_air\":8.7,\"feels_like\":null,\"dew_point\":8.5,\"relative_humidity\":98.0,\"pressure\":1022.9,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-26T03:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":140.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.9376328848315296,\"wind_speed\":4.7,\"wind_direction\":10.0,\"frost_chance\":null,\"temp_air\":9.5,\"feels_like\":null,\"dew_point\":9.3,\"relative_humidity\":98.0,\"pressure\":1022.6,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-26T04:00:00+01:00\",\"total_clouds\":87.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":250.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.9532243896939583,\"wind_speed\":4.7,\"wind_direction\":80.0,\"frost_chance\":null,\"temp_air\":9.8,\"feels_like\":null,\"dew_point\":9.4,\"relative_humidity\":97.0,\"pressure\":1022.6,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-26T05:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":2280.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.9807797336643354,\"wind_speed\":6.1,\"wind_direction\":70.0,\"frost_chance\":null,\"temp_air\":10.2,\"feels_like\":null,\"dew_point\":9.5,\"relative_humidity\":96.0,\"pressure\":1022.6,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-26T06:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":2650.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.9601466114386656,\"wind_speed\":7.6,\"wind_direction\":40.0,\"frost_chance\":null,\"temp_air\":10.2,\"feels_like\":null,\"dew_point\":9.4,\"relative_humidity\":95.0,\"pressure\":1022.3,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-26T07:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":2290.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.9395134892129953,\"wind_speed\":8.3,\"wind_direction\":40.0,\"frost_chance\":null,\"temp_air\":10.2,\"feels_like\":null,\"dew_point\":9.3,\"relative_humidity\":94.0,\"pressure\":1022.3,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-26T08:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":2530.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.9721291734030872,\"wind_speed\":7.6,\"wind_direction\":60.0,\"frost_chance\":null,\"temp_air\":10.3,\"feels_like\":null,\"dew_point\":9.5,\"relative_humidity\":95.0,\"pressure\":1022.4,\"ozone\":null,\"ghi\":22.22705922303379,\"dni\":0.0,\"dhi\":22.22705922303379}",
|
||||
"{\"date_time\":\"2024-10-26T09:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":1660.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.9601466114386656,\"wind_speed\":6.8,\"wind_direction\":80.0,\"frost_chance\":null,\"temp_air\":10.2,\"feels_like\":null,\"dew_point\":9.4,\"relative_humidity\":95.0,\"pressure\":1022.4,\"ozone\":null,\"ghi\":68.16265202099999,\"dni\":0.0,\"dhi\":68.16265202099999}",
|
||||
"{\"date_time\":\"2024-10-26T10:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":4420.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.9395134892129953,\"wind_speed\":7.2,\"wind_direction\":70.0,\"frost_chance\":null,\"temp_air\":10.2,\"feels_like\":null,\"dew_point\":9.3,\"relative_humidity\":94.0,\"pressure\":1022.1,\"ozone\":null,\"ghi\":108.0100746278567,\"dni\":0.0,\"dhi\":108.0100746278567}",
|
||||
"{\"date_time\":\"2024-10-26T11:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":2010.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.9513699189462126,\"wind_speed\":7.6,\"wind_direction\":50.0,\"frost_chance\":null,\"temp_air\":10.3,\"feels_like\":null,\"dew_point\":9.3,\"relative_humidity\":94.0,\"pressure\":1021.8,\"ozone\":null,\"ghi\":134.2816493853918,\"dni\":0.0,\"dhi\":134.2816493853918}",
|
||||
"{\"date_time\":\"2024-10-26T12:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":4340.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.8683329011187144,\"wind_speed\":7.9,\"wind_direction\":40.0,\"frost_chance\":null,\"temp_air\":10.3,\"feels_like\":null,\"dew_point\":8.8,\"relative_humidity\":90.0,\"pressure\":1021.2,\"ozone\":null,\"ghi\":144.04237088707308,\"dni\":0.0,\"dhi\":144.04237088707308}",
|
||||
"{\"date_time\":\"2024-10-26T13:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":9060.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.9164012929980223,\"wind_speed\":7.6,\"wind_direction\":40.0,\"frost_chance\":null,\"temp_air\":10.9,\"feels_like\":null,\"dew_point\":9.2,\"relative_humidity\":89.0,\"pressure\":1020.8,\"ozone\":null,\"ghi\":136.35519419190516,\"dni\":0.0,\"dhi\":136.35519419190516}",
|
||||
"{\"date_time\":\"2024-10-26T14:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":13650.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.979008294319185,\"wind_speed\":6.8,\"wind_direction\":40.0,\"frost_chance\":null,\"temp_air\":11.8,\"feels_like\":null,\"dew_point\":9.7,\"relative_humidity\":87.0,\"pressure\":1020.0,\"ozone\":null,\"ghi\":111.94730962791996,\"dni\":0.0,\"dhi\":111.94730962791996}",
|
||||
"{\"date_time\":\"2024-10-26T15:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":17560.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.9946143405085994,\"wind_speed\":7.2,\"wind_direction\":20.0,\"frost_chance\":null,\"temp_air\":12.9,\"feels_like\":null,\"dew_point\":9.8,\"relative_humidity\":82.0,\"pressure\":1019.8,\"ozone\":null,\"ghi\":73.45834328182735,\"dni\":0.0,\"dhi\":73.45834328182735}",
|
||||
"{\"date_time\":\"2024-10-26T16:00:00+01:00\",\"total_clouds\":87.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":17960.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.9099689648264042,\"wind_speed\":8.3,\"wind_direction\":30.0,\"frost_chance\":null,\"temp_air\":12.8,\"feels_like\":null,\"dew_point\":9.3,\"relative_humidity\":79.0,\"pressure\":1019.2,\"ozone\":null,\"ghi\":34.07062080450064,\"dni\":0.0,\"dhi\":34.07062080450064}",
|
||||
"{\"date_time\":\"2024-10-26T17:00:00+01:00\",\"total_clouds\":62.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":14910.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.843603353612786,\"wind_speed\":6.8,\"wind_direction\":70.0,\"frost_chance\":null,\"temp_air\":9.9,\"feels_like\":null,\"dew_point\":8.5,\"relative_humidity\":91.0,\"pressure\":1018.8,\"ozone\":null,\"ghi\":0.11256372587508819,\"dni\":0.0,\"dhi\":0.11256372587508819}",
|
||||
"{\"date_time\":\"2024-10-26T18:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":6400.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.7277309224807302,\"wind_speed\":5.4,\"wind_direction\":40.0,\"frost_chance\":null,\"temp_air\":8.3,\"feels_like\":null,\"dew_point\":7.5,\"relative_humidity\":94.0,\"pressure\":1018.7,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-26T19:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":720.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.6911147392871873,\"wind_speed\":5.4,\"wind_direction\":20.0,\"frost_chance\":null,\"temp_air\":7.6,\"feels_like\":null,\"dew_point\":6.9,\"relative_humidity\":96.0,\"pressure\":1018.7,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-26T20:00:00+01:00\",\"total_clouds\":87.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":5730.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.6378354583834347,\"wind_speed\":4.7,\"wind_direction\":20.0,\"frost_chance\":null,\"temp_air\":6.9,\"feels_like\":null,\"dew_point\":6.4,\"relative_humidity\":97.0,\"pressure\":1018.7,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-26T21:00:00+01:00\",\"total_clouds\":87.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":90.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.618159751885058,\"wind_speed\":4.7,\"wind_direction\":340.0,\"frost_chance\":null,\"temp_air\":6.7,\"feels_like\":null,\"dew_point\":6.3,\"relative_humidity\":97.0,\"pressure\":1018.9,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-26T22:00:00+01:00\",\"total_clouds\":87.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":90.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.7087305178214287,\"wind_speed\":7.2,\"wind_direction\":360.0,\"frost_chance\":null,\"temp_air\":7.6,\"feels_like\":null,\"dew_point\":7.1,\"relative_humidity\":97.0,\"pressure\":1019.3,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-26T23:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":200.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.764491154873937,\"wind_speed\":10.4,\"wind_direction\":360.0,\"frost_chance\":null,\"temp_air\":8.3,\"feels_like\":null,\"dew_point\":7.6,\"relative_humidity\":96.0,\"pressure\":1019.0,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-27T00:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":3610.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.830030033835469,\"wind_speed\":8.3,\"wind_direction\":10.0,\"frost_chance\":null,\"temp_air\":8.9,\"feels_like\":null,\"dew_point\":8.3,\"relative_humidity\":96.0,\"pressure\":1019.0,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-27T01:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":5770.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.8928153879508465,\"wind_speed\":9.0,\"wind_direction\":10.0,\"frost_chance\":null,\"temp_air\":9.8,\"feels_like\":null,\"dew_point\":8.9,\"relative_humidity\":94.0,\"pressure\":1018.7,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-27T02:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":5980.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.945096824920762,\"wind_speed\":7.2,\"wind_direction\":360.0,\"frost_chance\":null,\"temp_air\":10.6,\"feels_like\":null,\"dew_point\":9.3,\"relative_humidity\":92.0,\"pressure\":1018.8,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-27T03:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":7950.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.947556370182495,\"wind_speed\":6.5,\"wind_direction\":360.0,\"frost_chance\":null,\"temp_air\":10.8,\"feels_like\":null,\"dew_point\":9.4,\"relative_humidity\":91.0,\"pressure\":1018.2,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-27T04:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":6120.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.9903598068898019,\"wind_speed\":4.7,\"wind_direction\":360.0,\"frost_chance\":null,\"temp_air\":10.8,\"feels_like\":null,\"dew_point\":9.7,\"relative_humidity\":93.0,\"pressure\":1018.2,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-27T05:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":4830.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.987381538505997,\"wind_speed\":4.3,\"wind_direction\":10.0,\"frost_chance\":null,\"temp_air\":10.6,\"feels_like\":null,\"dew_point\":9.6,\"relative_humidity\":94.0,\"pressure\":1018.4,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-27T06:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":5360.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.9963174492677347,\"wind_speed\":3.6,\"wind_direction\":350.0,\"frost_chance\":null,\"temp_air\":10.5,\"feels_like\":null,\"dew_point\":9.8,\"relative_humidity\":95.0,\"pressure\":1018.9,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-27T07:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":4660.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.0085238952986137,\"wind_speed\":2.2,\"wind_direction\":190.0,\"frost_chance\":null,\"temp_air\":10.6,\"feels_like\":null,\"dew_point\":9.8,\"relative_humidity\":95.0,\"pressure\":1019.8,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-27T08:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":5570.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.024064286986675,\"wind_speed\":3.2,\"wind_direction\":180.0,\"frost_chance\":null,\"temp_air\":10.9,\"feels_like\":null,\"dew_point\":10.0,\"relative_humidity\":94.0,\"pressure\":1020.5,\"ozone\":null,\"ghi\":20.901591088639343,\"dni\":0.0,\"dhi\":20.901591088639343}",
|
||||
"{\"date_time\":\"2024-10-27T09:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":7670.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.020123074823889,\"wind_speed\":7.2,\"wind_direction\":190.0,\"frost_chance\":null,\"temp_air\":11.4,\"feels_like\":null,\"dew_point\":10.1,\"relative_humidity\":91.0,\"pressure\":1021.1,\"ozone\":null,\"ghi\":66.41841804602629,\"dni\":0.0,\"dhi\":66.41841804602629}",
|
||||
"{\"date_time\":\"2024-10-27T10:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":8790.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.0744781114397735,\"wind_speed\":11.2,\"wind_direction\":190.0,\"frost_chance\":null,\"temp_air\":12.2,\"feels_like\":null,\"dew_point\":10.4,\"relative_humidity\":89.0,\"pressure\":1021.4,\"ozone\":null,\"ghi\":106.12345605852113,\"dni\":0.0,\"dhi\":106.12345605852113}",
|
||||
"{\"date_time\":\"2024-10-27T11:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":8040.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.067588035893061,\"wind_speed\":10.8,\"wind_direction\":200.0,\"frost_chance\":null,\"temp_air\":12.9,\"feels_like\":null,\"dew_point\":10.4,\"relative_humidity\":85.0,\"pressure\":1022.0,\"ozone\":null,\"ghi\":132.31929512932624,\"dni\":0.0,\"dhi\":132.31929512932624}",
|
||||
"{\"date_time\":\"2024-10-27T12:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":9300.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.0813625709358203,\"wind_speed\":11.5,\"wind_direction\":220.0,\"frost_chance\":null,\"temp_air\":13.4,\"feels_like\":null,\"dew_point\":10.6,\"relative_humidity\":83.0,\"pressure\":1022.6,\"ozone\":null,\"ghi\":142.03807516868267,\"dni\":0.0,\"dhi\":142.03807516868267}",
|
||||
"{\"date_time\":\"2024-10-27T13:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":9560.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.119817892495729,\"wind_speed\":10.8,\"wind_direction\":230.0,\"frost_chance\":null,\"temp_air\":13.9,\"feels_like\":null,\"dew_point\":10.9,\"relative_humidity\":82.0,\"pressure\":1022.7,\"ozone\":null,\"ghi\":134.33853283469773,\"dni\":0.0,\"dhi\":134.33853283469773}",
|
||||
"{\"date_time\":\"2024-10-27T14:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":12400.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.119593668836106,\"wind_speed\":10.4,\"wind_direction\":220.0,\"frost_chance\":null,\"temp_air\":14.1,\"feels_like\":null,\"dew_point\":11.0,\"relative_humidity\":81.0,\"pressure\":1022.9,\"ozone\":null,\"ghi\":109.95561941571053,\"dni\":0.0,\"dhi\":109.95561941571053}",
|
||||
"{\"date_time\":\"2024-10-27T15:00:00+01:00\",\"total_clouds\":87.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":12100.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.1843956516771077,\"wind_speed\":11.9,\"wind_direction\":200.0,\"frost_chance\":null,\"temp_air\":14.8,\"feels_like\":null,\"dew_point\":11.4,\"relative_humidity\":80.0,\"pressure\":1023.0,\"ozone\":null,\"ghi\":88.84019629738314,\"dni\":0.0,\"dhi\":88.84019629738314}",
|
||||
"{\"date_time\":\"2024-10-27T16:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":13590.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.183440809341084,\"wind_speed\":13.3,\"wind_direction\":200.0,\"frost_chance\":null,\"temp_air\":15.0,\"feels_like\":null,\"dew_point\":11.4,\"relative_humidity\":79.0,\"pressure\":1023.8,\"ozone\":null,\"ghi\":25.90303659201319,\"dni\":0.0,\"dhi\":25.90303659201319}",
|
||||
"{\"date_time\":\"2024-10-27T17:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":14720.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.1291678932402403,\"wind_speed\":8.6,\"wind_direction\":220.0,\"frost_chance\":null,\"temp_air\":13.0,\"feels_like\":null,\"dew_point\":11.0,\"relative_humidity\":87.0,\"pressure\":1024.4,\"ozone\":null,\"ghi\":0.027781191847857583,\"dni\":0.0,\"dhi\":0.027781191847857583}",
|
||||
"{\"date_time\":\"2024-10-27T18:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":14300.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.0574147959693145,\"wind_speed\":9.0,\"wind_direction\":190.0,\"frost_chance\":null,\"temp_air\":11.7,\"feels_like\":null,\"dew_point\":10.3,\"relative_humidity\":91.0,\"pressure\":1025.0,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-27T19:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":12740.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.0324781084809054,\"wind_speed\":7.2,\"wind_direction\":180.0,\"frost_chance\":null,\"temp_air\":11.5,\"feels_like\":null,\"dew_point\":10.0,\"relative_humidity\":91.0,\"pressure\":1025.59,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-27T20:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":12320.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.9995340646535906,\"wind_speed\":7.6,\"wind_direction\":180.0,\"frost_chance\":null,\"temp_air\":10.7,\"feels_like\":null,\"dew_point\":9.8,\"relative_humidity\":94.0,\"pressure\":1026.0,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-27T21:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":10820.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.1311188481658605,\"wind_speed\":9.0,\"wind_direction\":180.0,\"frost_chance\":null,\"temp_air\":11.4,\"feels_like\":null,\"dew_point\":10.8,\"relative_humidity\":96.0,\"pressure\":1026.5,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-27T22:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":8040.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.1609860685094553,\"wind_speed\":9.4,\"wind_direction\":180.0,\"frost_chance\":null,\"temp_air\":11.8,\"feels_like\":null,\"dew_point\":11.0,\"relative_humidity\":95.0,\"pressure\":1027.0,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-27T23:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":4860.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.174201406952952,\"wind_speed\":8.6,\"wind_direction\":170.0,\"frost_chance\":null,\"temp_air\":11.9,\"feels_like\":null,\"dew_point\":11.2,\"relative_humidity\":95.0,\"pressure\":1027.0,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-28T00:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":3910.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.174201406952952,\"wind_speed\":11.9,\"wind_direction\":180.0,\"frost_chance\":null,\"temp_air\":11.9,\"feels_like\":null,\"dew_point\":11.1,\"relative_humidity\":95.0,\"pressure\":1027.0,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-28T01:00:00+01:00\",\"total_clouds\":87.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":7410.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.1284287457539426,\"wind_speed\":12.6,\"wind_direction\":180.0,\"frost_chance\":null,\"temp_air\":11.9,\"feels_like\":null,\"dew_point\":10.9,\"relative_humidity\":93.0,\"pressure\":1027.0,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-28T02:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":4020.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.174201406952952,\"wind_speed\":5.8,\"wind_direction\":190.0,\"frost_chance\":null,\"temp_air\":11.9,\"feels_like\":null,\"dew_point\":11.1,\"relative_humidity\":95.0,\"pressure\":1027.09,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-28T03:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":3500.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.125241657374896,\"wind_speed\":6.8,\"wind_direction\":190.0,\"frost_chance\":null,\"temp_air\":11.7,\"feels_like\":null,\"dew_point\":10.7,\"relative_humidity\":94.0,\"pressure\":1027.0,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-28T04:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":2770.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.1089196934974663,\"wind_speed\":5.0,\"wind_direction\":210.0,\"frost_chance\":null,\"temp_air\":11.4,\"feels_like\":null,\"dew_point\":10.6,\"relative_humidity\":95.0,\"pressure\":1026.59,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-28T05:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":2300.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.0519715153105804,\"wind_speed\":2.9,\"wind_direction\":230.0,\"frost_chance\":null,\"temp_air\":11.3,\"feels_like\":null,\"dew_point\":10.3,\"relative_humidity\":93.0,\"pressure\":1026.4,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-28T06:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":3330.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.045596885784406,\"wind_speed\":5.8,\"wind_direction\":180.0,\"frost_chance\":null,\"temp_air\":10.9,\"feels_like\":null,\"dew_point\":10.2,\"relative_humidity\":95.0,\"pressure\":1026.3,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-28T07:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":3100.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.0545649619507635,\"wind_speed\":8.3,\"wind_direction\":180.0,\"frost_chance\":null,\"temp_air\":10.8,\"feels_like\":null,\"dew_point\":10.1,\"relative_humidity\":96.0,\"pressure\":1026.59,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-28T08:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":2900.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.020805703639267,\"wind_speed\":6.5,\"wind_direction\":210.0,\"frost_chance\":null,\"temp_air\":10.7,\"feels_like\":null,\"dew_point\":9.9,\"relative_humidity\":95.0,\"pressure\":1027.09,\"ozone\":null,\"ghi\":19.602868340686165,\"dni\":0.0,\"dhi\":19.602868340686165}",
|
||||
"{\"date_time\":\"2024-10-28T09:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":1960.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.0581070021227617,\"wind_speed\":8.3,\"wind_direction\":200.0,\"frost_chance\":null,\"temp_air\":11.0,\"feels_like\":null,\"dew_point\":10.3,\"relative_humidity\":95.0,\"pressure\":1027.2,\"ozone\":null,\"ghi\":64.68374862563667,\"dni\":0.0,\"dhi\":64.68374862563667}",
|
||||
"{\"date_time\":\"2024-10-28T10:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":4930.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.017567894096528,\"wind_speed\":9.4,\"wind_direction\":200.0,\"frost_chance\":null,\"temp_air\":11.2,\"feels_like\":null,\"dew_point\":10.0,\"relative_humidity\":92.0,\"pressure\":1027.3,\"ozone\":null,\"ghi\":104.24512318045389,\"dni\":0.0,\"dhi\":104.24512318045389}",
|
||||
"{\"date_time\":\"2024-10-28T11:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":5410.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.0574147959693145,\"wind_speed\":10.8,\"wind_direction\":200.0,\"frost_chance\":null,\"temp_air\":11.7,\"feels_like\":null,\"dew_point\":10.3,\"relative_humidity\":91.0,\"pressure\":1027.4,\"ozone\":null,\"ghi\":130.36720205010565,\"dni\":0.0,\"dhi\":130.36720205010565}",
|
||||
"{\"date_time\":\"2024-10-28T12:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":5520.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.0744781114397735,\"wind_speed\":10.1,\"wind_direction\":200.0,\"frost_chance\":null,\"temp_air\":12.2,\"feels_like\":null,\"dew_point\":10.4,\"relative_humidity\":89.0,\"pressure\":1027.2,\"ozone\":null,\"ghi\":140.04739059314485,\"dni\":0.0,\"dhi\":140.04739059314485}",
|
||||
"{\"date_time\":\"2024-10-28T13:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":6840.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.1175533778696294,\"wind_speed\":9.4,\"wind_direction\":180.0,\"frost_chance\":null,\"temp_air\":13.1,\"feels_like\":null,\"dew_point\":10.8,\"relative_humidity\":86.0,\"pressure\":1026.59,\"ozone\":null,\"ghi\":132.3396471104131,\"dni\":0.0,\"dhi\":132.3396471104131}",
|
||||
"{\"date_time\":\"2024-10-28T14:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":11920.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.158848465791691,\"wind_speed\":11.9,\"wind_direction\":190.0,\"frost_chance\":null,\"temp_air\":14.2,\"feels_like\":null,\"dew_point\":11.2,\"relative_humidity\":82.0,\"pressure\":1026.0,\"ozone\":null,\"ghi\":107.98652507155819,\"dni\":0.0,\"dhi\":107.98652507155819}",
|
||||
"{\"date_time\":\"2024-10-28T15:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":13760.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.1586051188053688,\"wind_speed\":7.6,\"wind_direction\":210.0,\"frost_chance\":null,\"temp_air\":14.4,\"feels_like\":null,\"dew_point\":11.2,\"relative_humidity\":81.0,\"pressure\":1025.8,\"ozone\":null,\"ghi\":69.69629401139778,\"dni\":0.0,\"dhi\":69.69629401139778}",
|
||||
"{\"date_time\":\"2024-10-28T16:00:00+01:00\",\"total_clouds\":87.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":21080.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.172012702785343,\"wind_speed\":7.2,\"wind_direction\":200.0,\"frost_chance\":null,\"temp_air\":14.3,\"feels_like\":null,\"dew_point\":11.4,\"relative_humidity\":82.0,\"pressure\":1025.4,\"ozone\":null,\"ghi\":30.299076442472742,\"dni\":0.0,\"dhi\":30.299076442472742}",
|
||||
"{\"date_time\":\"2024-10-28T17:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":16720.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.0723655900991496,\"wind_speed\":8.6,\"wind_direction\":190.0,\"frost_chance\":null,\"temp_air\":12.0,\"feels_like\":null,\"dew_point\":10.5,\"relative_humidity\":90.0,\"pressure\":1025.2,\"ozone\":null,\"ghi\":0.007313988512718714,\"dni\":0.0,\"dhi\":0.007313988512718714}",
|
||||
"{\"date_time\":\"2024-10-28T18:00:00+01:00\",\"total_clouds\":75.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":8950.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.9542897134936768,\"wind_speed\":3.6,\"wind_direction\":280.0,\"frost_chance\":null,\"temp_air\":10.5,\"feels_like\":null,\"dew_point\":9.5,\"relative_humidity\":93.0,\"pressure\":1025.59,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-28T19:00:00+01:00\",\"total_clouds\":87.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":7810.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.8443259335596536,\"wind_speed\":3.2,\"wind_direction\":330.0,\"frost_chance\":null,\"temp_air\":9.2,\"feels_like\":null,\"dew_point\":8.5,\"relative_humidity\":95.0,\"pressure\":1026.0,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-28T20:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":90.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.8378811294487585,\"wind_speed\":2.5,\"wind_direction\":170.0,\"frost_chance\":null,\"temp_air\":8.8,\"feels_like\":null,\"dew_point\":8.3,\"relative_humidity\":97.0,\"pressure\":1026.4,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-28T21:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":100.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.925871372365559,\"wind_speed\":2.5,\"wind_direction\":190.0,\"frost_chance\":null,\"temp_air\":9.4,\"feels_like\":null,\"dew_point\":9.1,\"relative_humidity\":98.0,\"pressure\":1026.5,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-28T22:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":60.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.801251387267144,\"wind_speed\":2.9,\"wind_direction\":200.0,\"frost_chance\":null,\"temp_air\":8.3,\"feels_like\":null,\"dew_point\":8.0,\"relative_humidity\":98.0,\"pressure\":1026.8,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-28T23:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":90.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.7720765939942027,\"wind_speed\":3.2,\"wind_direction\":170.0,\"frost_chance\":null,\"temp_air\":8.2,\"feels_like\":null,\"dew_point\":7.8,\"relative_humidity\":97.0,\"pressure\":1026.8,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-29T00:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":80.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.8455721653466037,\"wind_speed\":3.6,\"wind_direction\":200.0,\"frost_chance\":null,\"temp_air\":8.7,\"feels_like\":null,\"dew_point\":8.4,\"relative_humidity\":98.0,\"pressure\":1026.8,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-29T01:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":160.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.779508453411574,\"wind_speed\":2.2,\"wind_direction\":330.0,\"frost_chance\":null,\"temp_air\":8.1,\"feels_like\":null,\"dew_point\":7.8,\"relative_humidity\":98.0,\"pressure\":1026.8,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-29T02:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":100.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.7580400895133157,\"wind_speed\":2.5,\"wind_direction\":10.0,\"frost_chance\":null,\"temp_air\":7.9,\"feels_like\":null,\"dew_point\":7.5,\"relative_humidity\":98.0,\"pressure\":1026.7,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-29T03:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":120.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.8196315034637476,\"wind_speed\":1.4,\"wind_direction\":0.0,\"frost_chance\":null,\"temp_air\":8.3,\"feels_like\":null,\"dew_point\":8.1,\"relative_humidity\":99.0,\"pressure\":1026.59,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-29T04:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":100.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.7976667029361821,\"wind_speed\":1.8,\"wind_direction\":320.0,\"frost_chance\":null,\"temp_air\":8.1,\"feels_like\":null,\"dew_point\":8.0,\"relative_humidity\":99.0,\"pressure\":1026.59,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-29T05:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":110.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.786788492758286,\"wind_speed\":1.4,\"wind_direction\":20.0,\"frost_chance\":null,\"temp_air\":8.0,\"feels_like\":null,\"dew_point\":7.9,\"relative_humidity\":99.0,\"pressure\":1026.9,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-29T06:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":110.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.7022172735352934,\"wind_speed\":2.2,\"wind_direction\":240.0,\"frost_chance\":null,\"temp_air\":7.2,\"feels_like\":null,\"dew_point\":7.1,\"relative_humidity\":99.0,\"pressure\":1027.0,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-29T07:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":110.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.722955851908333,\"wind_speed\":2.9,\"wind_direction\":10.0,\"frost_chance\":null,\"temp_air\":7.4,\"feels_like\":null,\"dew_point\":7.3,\"relative_humidity\":99.0,\"pressure\":1027.3,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-29T08:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":130.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.7652386971092027,\"wind_speed\":3.2,\"wind_direction\":350.0,\"frost_chance\":null,\"temp_air\":7.8,\"feels_like\":null,\"dew_point\":7.6,\"relative_humidity\":99.0,\"pressure\":1027.5,\"ozone\":null,\"ghi\":18.332702353547074,\"dni\":0.0,\"dhi\":18.332702353547074}",
|
||||
"{\"date_time\":\"2024-10-29T09:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":170.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.8644045343807525,\"wind_speed\":4.3,\"wind_direction\":350.0,\"frost_chance\":null,\"temp_air\":8.7,\"feels_like\":null,\"dew_point\":8.5,\"relative_humidity\":99.0,\"pressure\":1027.59,\"ozone\":null,\"ghi\":62.95962823548937,\"dni\":0.0,\"dhi\":62.95962823548937}",
|
||||
"{\"date_time\":\"2024-10-29T10:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":340.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.9141831337652606,\"wind_speed\":2.5,\"wind_direction\":340.0,\"frost_chance\":null,\"temp_air\":9.3,\"feels_like\":null,\"dew_point\":9.0,\"relative_humidity\":98.0,\"pressure\":1027.59,\"ozone\":null,\"ghi\":102.37603626073972,\"dni\":0.0,\"dhi\":102.37603626073972}",
|
||||
"{\"date_time\":\"2024-10-29T11:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":270.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.013647682316836,\"wind_speed\":3.6,\"wind_direction\":10.0,\"frost_chance\":null,\"temp_air\":10.3,\"feels_like\":null,\"dew_point\":9.9,\"relative_humidity\":97.0,\"pressure\":1027.5,\"ozone\":null,\"ghi\":128.42638331065623,\"dni\":0.0,\"dhi\":128.42638331065623}",
|
||||
"{\"date_time\":\"2024-10-29T12:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":320.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.1313720084431926,\"wind_speed\":5.0,\"wind_direction\":170.0,\"frost_chance\":null,\"temp_air\":12.1,\"feels_like\":null,\"dew_point\":10.9,\"relative_humidity\":92.0,\"pressure\":1027.2,\"ozone\":null,\"ghi\":138.07137517499854,\"dni\":0.0,\"dhi\":138.07137517499854}",
|
||||
"{\"date_time\":\"2024-10-29T13:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":11100.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.291852348361256,\"wind_speed\":6.1,\"wind_direction\":160.0,\"frost_chance\":null,\"temp_air\":14.4,\"feels_like\":null,\"dew_point\":12.1,\"relative_humidity\":86.0,\"pressure\":1026.59,\"ozone\":null,\"ghi\":130.35961341605747,\"dni\":0.0,\"dhi\":130.35961341605747}",
|
||||
"{\"date_time\":\"2024-10-29T14:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":16900.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.3255616039808618,\"wind_speed\":5.0,\"wind_direction\":170.0,\"frost_chance\":null,\"temp_air\":16.9,\"feels_like\":null,\"dew_point\":12.4,\"relative_humidity\":75.0,\"pressure\":1025.9,\"ozone\":null,\"ghi\":106.04108723155429,\"dni\":0.0,\"dhi\":106.04108723155429}",
|
||||
"{\"date_time\":\"2024-10-29T15:00:00+01:00\",\"total_clouds\":87.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":34440.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.1836631339806405,\"wind_speed\":6.5,\"wind_direction\":180.0,\"frost_chance\":null,\"temp_air\":17.0,\"feels_like\":null,\"dew_point\":11.6,\"relative_humidity\":70.0,\"pressure\":1025.7,\"ozone\":null,\"ghi\":84.24287206656912,\"dni\":0.0,\"dhi\":84.24287206656912}",
|
||||
"{\"date_time\":\"2024-10-29T16:00:00+01:00\",\"total_clouds\":37.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":58950.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.2475055388051737,\"wind_speed\":4.3,\"wind_direction\":210.0,\"frost_chance\":null,\"temp_air\":15.9,\"feels_like\":null,\"dew_point\":11.9,\"relative_humidity\":77.0,\"pressure\":1025.4,\"ozone\":null,\"ghi\":49.81579897492729,\"dni\":14.538665837772964,\"dhi\":47.79892049551232}",
|
||||
"{\"date_time\":\"2024-10-29T17:00:00+01:00\",\"total_clouds\":50.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":47830.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.0652816544204247,\"wind_speed\":10.4,\"wind_direction\":250.0,\"frost_chance\":null,\"temp_air\":12.5,\"feels_like\":null,\"dew_point\":10.4,\"relative_humidity\":87.0,\"pressure\":1025.59,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-29T18:00:00+01:00\",\"total_clouds\":0.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":41010.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.020123074823889,\"wind_speed\":6.1,\"wind_direction\":260.0,\"frost_chance\":null,\"temp_air\":11.4,\"feels_like\":null,\"dew_point\":10.0,\"relative_humidity\":91.0,\"pressure\":1025.9,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-29T19:00:00+01:00\",\"total_clouds\":0.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":18250.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.9662391817133793,\"wind_speed\":5.4,\"wind_direction\":200.0,\"frost_chance\":null,\"temp_air\":10.6,\"feels_like\":null,\"dew_point\":9.5,\"relative_humidity\":93.0,\"pressure\":1026.2,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-29T20:00:00+01:00\",\"total_clouds\":12.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":7840.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.0394979799019253,\"wind_speed\":6.5,\"wind_direction\":230.0,\"frost_chance\":null,\"temp_air\":11.2,\"feels_like\":null,\"dew_point\":10.2,\"relative_humidity\":93.0,\"pressure\":1026.59,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-29T21:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":7370.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.7702426892100076,\"wind_speed\":5.0,\"wind_direction\":210.0,\"frost_chance\":null,\"temp_air\":8.7,\"feels_like\":null,\"dew_point\":7.8,\"relative_humidity\":94.0,\"pressure\":1026.5,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-29T22:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":3850.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.8220170482487101,\"wind_speed\":4.3,\"wind_direction\":360.0,\"frost_chance\":null,\"temp_air\":9.0,\"feels_like\":null,\"dew_point\":8.3,\"relative_humidity\":95.0,\"pressure\":1026.59,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-29T23:00:00+01:00\",\"total_clouds\":87.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":7260.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.8443259335596536,\"wind_speed\":4.3,\"wind_direction\":350.0,\"frost_chance\":null,\"temp_air\":9.2,\"feels_like\":null,\"dew_point\":8.5,\"relative_humidity\":95.0,\"pressure\":1026.59,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-30T00:00:00+01:00\",\"total_clouds\":50.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":180.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.729577309288815,\"wind_speed\":3.6,\"wind_direction\":30.0,\"frost_chance\":null,\"temp_air\":7.8,\"feels_like\":null,\"dew_point\":7.3,\"relative_humidity\":97.0,\"pressure\":1026.8,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-30T01:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":70.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.6378354583834347,\"wind_speed\":2.2,\"wind_direction\":240.0,\"frost_chance\":null,\"temp_air\":6.9,\"feels_like\":null,\"dew_point\":6.5,\"relative_humidity\":97.0,\"pressure\":1027.0,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-30T02:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":100.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.7263462963556704,\"wind_speed\":2.9,\"wind_direction\":360.0,\"frost_chance\":null,\"temp_air\":7.6,\"feels_like\":null,\"dew_point\":7.3,\"relative_humidity\":98.0,\"pressure\":1027.2,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-30T03:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":100.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.6952547605843942,\"wind_speed\":1.8,\"wind_direction\":20.0,\"frost_chance\":null,\"temp_air\":7.3,\"feels_like\":null,\"dew_point\":7.1,\"relative_humidity\":98.0,\"pressure\":1027.09,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-30T04:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":90.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.8268830865919616,\"wind_speed\":2.2,\"wind_direction\":350.0,\"frost_chance\":null,\"temp_air\":8.2,\"feels_like\":null,\"dew_point\":8.2,\"relative_humidity\":100.0,\"pressure\":1027.4,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-30T05:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":150.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.8492109088214057,\"wind_speed\":1.4,\"wind_direction\":290.0,\"frost_chance\":null,\"temp_air\":8.4,\"feels_like\":null,\"dew_point\":8.3,\"relative_humidity\":100.0,\"pressure\":1027.9,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-30T06:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":7840.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.8872184723928271,\"wind_speed\":2.5,\"wind_direction\":150.0,\"frost_chance\":null,\"temp_air\":8.9,\"feels_like\":null,\"dew_point\":8.8,\"relative_humidity\":99.0,\"pressure\":1027.8,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-30T07:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":2810.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.8872184723928271,\"wind_speed\":2.5,\"wind_direction\":290.0,\"frost_chance\":null,\"temp_air\":8.9,\"feels_like\":null,\"dew_point\":8.7,\"relative_humidity\":99.0,\"pressure\":1028.0,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-30T08:00:00+01:00\",\"total_clouds\":87.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":460.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.921981762341113,\"wind_speed\":2.2,\"wind_direction\":200.0,\"frost_chance\":null,\"temp_air\":9.2,\"feels_like\":null,\"dew_point\":9.0,\"relative_humidity\":99.0,\"pressure\":1028.3,\"ozone\":null,\"ghi\":21.219671542362477,\"dni\":0.0,\"dhi\":21.219671542362477}",
|
||||
"{\"date_time\":\"2024-10-30T09:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":1620.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.9813912463341716,\"wind_speed\":2.5,\"wind_direction\":170.0,\"frost_chance\":null,\"temp_air\":9.7,\"feels_like\":null,\"dew_point\":9.6,\"relative_humidity\":99.0,\"pressure\":1028.4,\"ozone\":null,\"ghi\":61.24705643100921,\"dni\":0.0,\"dhi\":61.24705643100921}",
|
||||
"{\"date_time\":\"2024-10-30T10:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":13580.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.0886168395153506,\"wind_speed\":1.8,\"wind_direction\":180.0,\"frost_chance\":null,\"temp_air\":10.4,\"feels_like\":null,\"dew_point\":10.4,\"relative_humidity\":100.0,\"pressure\":1028.7,\"ozone\":null,\"ghi\":100.51716584295312,\"dni\":0.0,\"dhi\":100.51716584295312}",
|
||||
"{\"date_time\":\"2024-10-30T11:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":22490.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.0364427178898907,\"wind_speed\":5.4,\"wind_direction\":180.0,\"frost_chance\":null,\"temp_air\":11.0,\"feels_like\":null,\"dew_point\":10.2,\"relative_humidity\":94.0,\"pressure\":1028.59,\"ozone\":null,\"ghi\":126.49785855652105,\"dni\":0.0,\"dhi\":126.49785855652105}",
|
||||
"{\"date_time\":\"2024-10-30T12:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":17130.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.108204921394897,\"wind_speed\":5.4,\"wind_direction\":170.0,\"frost_chance\":null,\"temp_air\":12.1,\"feels_like\":null,\"dew_point\":10.7,\"relative_humidity\":91.0,\"pressure\":1028.59,\"ozone\":null,\"ghi\":136.11108832250144,\"dni\":0.0,\"dhi\":136.11108832250144}",
|
||||
"{\"date_time\":\"2024-10-30T13:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":27620.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.0744781114397735,\"wind_speed\":5.8,\"wind_direction\":180.0,\"frost_chance\":null,\"temp_air\":12.2,\"feels_like\":null,\"dew_point\":10.4,\"relative_humidity\":89.0,\"pressure\":1028.0,\"ozone\":null,\"ghi\":128.39950312276366,\"dni\":0.0,\"dhi\":128.39950312276366}",
|
||||
"{\"date_time\":\"2024-10-30T14:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":23300.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.0792067212034273,\"wind_speed\":10.8,\"wind_direction\":170.0,\"frost_chance\":null,\"temp_air\":12.8,\"feels_like\":null,\"dew_point\":10.6,\"relative_humidity\":86.0,\"pressure\":1028.0,\"ozone\":null,\"ghi\":104.12035323578755,\"dni\":0.0,\"dhi\":104.12035323578755}",
|
||||
"{\"date_time\":\"2024-10-30T15:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":17420.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.067588035893061,\"wind_speed\":10.4,\"wind_direction\":160.0,\"frost_chance\":null,\"temp_air\":12.9,\"feels_like\":null,\"dew_point\":10.5,\"relative_humidity\":85.0,\"pressure\":1027.7,\"ozone\":null,\"ghi\":66.05384442265539,\"dni\":0.0,\"dhi\":66.05384442265539}",
|
||||
"{\"date_time\":\"2024-10-30T16:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":12780.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.116237166149368,\"wind_speed\":9.0,\"wind_direction\":170.0,\"frost_chance\":null,\"temp_air\":12.9,\"feels_like\":null,\"dew_point\":10.7,\"relative_humidity\":87.0,\"pressure\":1028.0,\"ozone\":null,\"ghi\":21.554012703673507,\"dni\":0.0,\"dhi\":21.554012703673507}",
|
||||
"{\"date_time\":\"2024-10-30T17:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":9610.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.077906151217825,\"wind_speed\":5.0,\"wind_direction\":150.0,\"frost_chance\":null,\"temp_air\":12.6,\"feels_like\":null,\"dew_point\":10.5,\"relative_humidity\":87.0,\"pressure\":1027.9,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-30T18:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":10940.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.0723655900991496,\"wind_speed\":3.2,\"wind_direction\":160.0,\"frost_chance\":null,\"temp_air\":12.0,\"feels_like\":null,\"dew_point\":10.4,\"relative_humidity\":90.0,\"pressure\":1028.4,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-30T19:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":7710.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.04490856047528,\"wind_speed\":4.0,\"wind_direction\":180.0,\"frost_chance\":null,\"temp_air\":11.6,\"feels_like\":null,\"dew_point\":10.1,\"relative_humidity\":91.0,\"pressure\":1028.59,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-30T20:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":3170.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.0324781084809054,\"wind_speed\":3.2,\"wind_direction\":180.0,\"frost_chance\":null,\"temp_air\":11.5,\"feels_like\":null,\"dew_point\":10.1,\"relative_humidity\":91.0,\"pressure\":1028.9,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-30T21:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":3950.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.020123074823889,\"wind_speed\":6.8,\"wind_direction\":180.0,\"frost_chance\":null,\"temp_air\":11.4,\"feels_like\":null,\"dew_point\":10.0,\"relative_humidity\":91.0,\"pressure\":1028.8,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-30T22:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":2590.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.0423222294922834,\"wind_speed\":4.3,\"wind_direction\":190.0,\"frost_chance\":null,\"temp_air\":11.4,\"feels_like\":null,\"dew_point\":10.2,\"relative_humidity\":92.0,\"pressure\":1028.59,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-30T23:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":2940.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.0394979799019253,\"wind_speed\":2.5,\"wind_direction\":190.0,\"frost_chance\":null,\"temp_air\":11.2,\"feels_like\":null,\"dew_point\":10.2,\"relative_humidity\":93.0,\"pressure\":1028.9,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-31T00:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":1970.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.9424136607492761,\"wind_speed\":3.2,\"wind_direction\":200.0,\"frost_chance\":null,\"temp_air\":10.4,\"feels_like\":null,\"dew_point\":9.4,\"relative_humidity\":93.0,\"pressure\":1028.9,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-31T01:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":2510.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.9395134892129953,\"wind_speed\":4.7,\"wind_direction\":200.0,\"frost_chance\":null,\"temp_air\":10.2,\"feels_like\":null,\"dew_point\":9.3,\"relative_humidity\":94.0,\"pressure\":1028.9,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-31T02:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":2060.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.0085238952986137,\"wind_speed\":6.1,\"wind_direction\":190.0,\"frost_chance\":null,\"temp_air\":10.6,\"feels_like\":null,\"dew_point\":9.9,\"relative_humidity\":95.0,\"pressure\":1028.9,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-31T03:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":240.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.0050721659347364,\"wind_speed\":5.8,\"wind_direction\":190.0,\"frost_chance\":null,\"temp_air\":10.4,\"feels_like\":null,\"dew_point\":9.8,\"relative_humidity\":96.0,\"pressure\":1028.7,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-31T04:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":2970.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.0173313171547624,\"wind_speed\":6.8,\"wind_direction\":190.0,\"frost_chance\":null,\"temp_air\":10.5,\"feels_like\":null,\"dew_point\":9.9,\"relative_humidity\":96.0,\"pressure\":1028.5,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-31T05:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":3290.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.9963174492677347,\"wind_speed\":3.6,\"wind_direction\":210.0,\"frost_chance\":null,\"temp_air\":10.5,\"feels_like\":null,\"dew_point\":9.8,\"relative_humidity\":95.0,\"pressure\":1029.0,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-31T06:00:00+01:00\",\"total_clouds\":87.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":5990.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":2.0173313171547624,\"wind_speed\":4.3,\"wind_direction\":200.0,\"frost_chance\":null,\"temp_air\":10.5,\"feels_like\":null,\"dew_point\":10.0,\"relative_humidity\":96.0,\"pressure\":1028.7,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-31T07:00:00+01:00\",\"total_clouds\":87.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":170.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.9753035813807054,\"wind_speed\":5.4,\"wind_direction\":190.0,\"frost_chance\":null,\"temp_air\":10.5,\"feels_like\":null,\"dew_point\":9.7,\"relative_humidity\":94.0,\"pressure\":1029.0,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-31T08:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":5700.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.9513699189462126,\"wind_speed\":7.6,\"wind_direction\":190.0,\"frost_chance\":null,\"temp_air\":10.3,\"feels_like\":null,\"dew_point\":9.4,\"relative_humidity\":94.0,\"pressure\":1029.2,\"ozone\":null,\"ghi\":15.885486574503958,\"dni\":0.0,\"dhi\":15.885486574503958}",
|
||||
"{\"date_time\":\"2024-10-31T09:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":8690.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.9835068514961423,\"wind_speed\":5.0,\"wind_direction\":200.0,\"frost_chance\":null,\"temp_air\":11.1,\"feels_like\":null,\"dew_point\":9.6,\"relative_humidity\":91.0,\"pressure\":1029.5,\"ozone\":null,\"ghi\":59.547045720693454,\"dni\":0.0,\"dhi\":59.547045720693454}",
|
||||
"{\"date_time\":\"2024-10-31T10:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":15600.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.9682244315574093,\"wind_speed\":2.2,\"wind_direction\":200.0,\"frost_chance\":null,\"temp_air\":11.9,\"feels_like\":null,\"dew_point\":9.7,\"relative_humidity\":86.0,\"pressure\":1029.59,\"ozone\":null,\"ghi\":98.66949090664382,\"dni\":0.0,\"dhi\":98.66949090664382}",
|
||||
"{\"date_time\":\"2024-10-31T11:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":25190.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.9682244315574093,\"wind_speed\":2.2,\"wind_direction\":210.0,\"frost_chance\":null,\"temp_air\":11.9,\"feels_like\":null,\"dew_point\":9.6,\"relative_humidity\":86.0,\"pressure\":1029.3,\"ozone\":null,\"ghi\":124.5826522461644,\"dni\":0.0,\"dhi\":124.5826522461644}",
|
||||
"{\"date_time\":\"2024-10-31T12:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":33140.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.9459652102522924,\"wind_speed\":7.6,\"wind_direction\":200.0,\"frost_chance\":null,\"temp_air\":12.9,\"feels_like\":null,\"dew_point\":9.6,\"relative_humidity\":80.0,\"pressure\":1028.9,\"ozone\":null,\"ghi\":134.16758927009357,\"dni\":0.0,\"dhi\":134.16758927009357}",
|
||||
"{\"date_time\":\"2024-10-31T13:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":36550.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.827634823721784,\"wind_speed\":9.0,\"wind_direction\":190.0,\"frost_chance\":null,\"temp_air\":13.6,\"feels_like\":null,\"dew_point\":8.7,\"relative_humidity\":72.0,\"pressure\":1028.4,\"ozone\":null,\"ghi\":126.46038107187944,\"dni\":0.0,\"dhi\":126.46038107187944}",
|
||||
"{\"date_time\":\"2024-10-31T14:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":57100.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.8500141411416122,\"wind_speed\":9.0,\"wind_direction\":210.0,\"frost_chance\":null,\"temp_air\":13.8,\"feels_like\":null,\"dew_point\":8.8,\"relative_humidity\":72.0,\"pressure\":1027.9,\"ozone\":null,\"ghi\":102.22535560233246,\"dni\":0.0,\"dhi\":102.22535560233246}",
|
||||
"{\"date_time\":\"2024-10-31T15:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":52570.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.8317476150435485,\"wind_speed\":5.4,\"wind_direction\":190.0,\"frost_chance\":null,\"temp_air\":14.1,\"feels_like\":null,\"dew_point\":8.7,\"relative_humidity\":70.0,\"pressure\":1027.7,\"ozone\":null,\"ghi\":64.2799263126679,\"dni\":0.0,\"dhi\":64.2799263126679}",
|
||||
"{\"date_time\":\"2024-10-31T16:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":45660.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.861303515362104,\"wind_speed\":5.8,\"wind_direction\":180.0,\"frost_chance\":null,\"temp_air\":13.9,\"feels_like\":null,\"dew_point\":8.9,\"relative_humidity\":72.0,\"pressure\":1027.4,\"ozone\":null,\"ghi\":20.199874599546952,\"dni\":0.0,\"dhi\":20.199874599546952}",
|
||||
"{\"date_time\":\"2024-10-31T17:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":51810.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.848666949739678,\"wind_speed\":3.6,\"wind_direction\":230.0,\"frost_chance\":null,\"temp_air\":12.9,\"feels_like\":null,\"dew_point\":8.7,\"relative_humidity\":76.0,\"pressure\":1027.3,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-31T18:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":44110.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.8876079019771812,\"wind_speed\":2.9,\"wind_direction\":230.0,\"frost_chance\":null,\"temp_air\":11.6,\"feels_like\":null,\"dew_point\":9.0,\"relative_humidity\":84.0,\"pressure\":1027.3,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-31T19:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":48080.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.9313264561503112,\"wind_speed\":2.2,\"wind_direction\":110.0,\"frost_chance\":null,\"temp_air\":11.4,\"feels_like\":null,\"dew_point\":9.2,\"relative_humidity\":87.0,\"pressure\":1027.2,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-31T20:00:00+01:00\",\"total_clouds\":87.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":30700.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.9443700269600115,\"wind_speed\":3.6,\"wind_direction\":180.0,\"frost_chance\":null,\"temp_air\":11.7,\"feels_like\":null,\"dew_point\":9.4,\"relative_humidity\":86.0,\"pressure\":1027.59,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-31T21:00:00+01:00\",\"total_clouds\":87.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":25400.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.9313264561503112,\"wind_speed\":2.9,\"wind_direction\":170.0,\"frost_chance\":null,\"temp_air\":11.4,\"feels_like\":null,\"dew_point\":9.4,\"relative_humidity\":87.0,\"pressure\":1027.4,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-31T22:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":25670.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.9298475508749398,\"wind_speed\":4.3,\"wind_direction\":180.0,\"frost_chance\":null,\"temp_air\":11.2,\"feels_like\":null,\"dew_point\":9.3,\"relative_humidity\":88.0,\"pressure\":1027.4,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-10-31T23:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":24450.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.9281212967255343,\"wind_speed\":2.9,\"wind_direction\":230.0,\"frost_chance\":null,\"temp_air\":11.0,\"feels_like\":null,\"dew_point\":9.3,\"relative_humidity\":89.0,\"pressure\":1027.0,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-01T00:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":24100.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.947556370182495,\"wind_speed\":2.9,\"wind_direction\":210.0,\"frost_chance\":null,\"temp_air\":10.8,\"feels_like\":null,\"dew_point\":9.4,\"relative_humidity\":91.0,\"pressure\":1026.8,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-01T01:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":20630.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.9753035813807054,\"wind_speed\":5.0,\"wind_direction\":190.0,\"frost_chance\":null,\"temp_air\":10.5,\"feels_like\":null,\"dew_point\":9.5,\"relative_humidity\":94.0,\"pressure\":1026.4,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-01T02:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":24020.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.9807797336643354,\"wind_speed\":6.5,\"wind_direction\":210.0,\"frost_chance\":null,\"temp_air\":10.2,\"feels_like\":null,\"dew_point\":9.6,\"relative_humidity\":96.0,\"pressure\":1026.2,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-01T03:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":8730.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.9807797336643354,\"wind_speed\":9.4,\"wind_direction\":210.0,\"frost_chance\":null,\"temp_air\":10.2,\"feels_like\":null,\"dew_point\":9.6,\"relative_humidity\":96.0,\"pressure\":1026.09,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-01T04:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":5560.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.9687457153823724,\"wind_speed\":9.0,\"wind_direction\":250.0,\"frost_chance\":null,\"temp_air\":10.1,\"feels_like\":null,\"dew_point\":9.6,\"relative_humidity\":96.0,\"pressure\":1025.9,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-01T05:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":5030.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.1,\"preciptable_water\":1.9807797336643354,\"wind_speed\":8.3,\"wind_direction\":250.0,\"frost_chance\":null,\"temp_air\":10.2,\"feels_like\":null,\"dew_point\":9.6,\"relative_humidity\":96.0,\"pressure\":1026.0,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-01T06:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":7340.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.1,\"preciptable_water\":1.9449002411739278,\"wind_speed\":7.9,\"wind_direction\":270.0,\"frost_chance\":null,\"temp_air\":9.9,\"feels_like\":null,\"dew_point\":9.2,\"relative_humidity\":96.0,\"pressure\":1025.8,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-01T07:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":6340.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.8669161262727356,\"wind_speed\":3.2,\"wind_direction\":260.0,\"frost_chance\":null,\"temp_air\":9.4,\"feels_like\":null,\"dew_point\":8.7,\"relative_humidity\":95.0,\"pressure\":1025.7,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-01T08:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":12030.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.8360532099381073,\"wind_speed\":5.8,\"wind_direction\":220.0,\"frost_chance\":null,\"temp_air\":9.3,\"feels_like\":null,\"dew_point\":8.5,\"relative_humidity\":94.0,\"pressure\":1025.7,\"ozone\":null,\"ghi\":14.712237798164347,\"dni\":0.0,\"dhi\":14.712237798164347}",
|
||||
"{\"date_time\":\"2024-11-01T09:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":29210.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.8301128504372512,\"wind_speed\":11.9,\"wind_direction\":220.0,\"frost_chance\":null,\"temp_air\":9.6,\"feels_like\":null,\"dew_point\":8.4,\"relative_humidity\":92.0,\"pressure\":1026.0,\"ozone\":null,\"ghi\":57.8606202914984,\"dni\":0.0,\"dhi\":57.8606202914984}",
|
||||
"{\"date_time\":\"2024-11-01T10:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":24570.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.8548700685143087,\"wind_speed\":12.2,\"wind_direction\":210.0,\"frost_chance\":null,\"temp_air\":10.0,\"feels_like\":null,\"dew_point\":8.6,\"relative_humidity\":91.0,\"pressure\":1025.9,\"ozone\":null,\"ghi\":96.83399790992624,\"dni\":0.0,\"dhi\":96.83399790992624}",
|
||||
"{\"date_time\":\"2024-11-01T11:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":19290.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.8475736466618398,\"wind_speed\":11.9,\"wind_direction\":200.0,\"frost_chance\":null,\"temp_air\":10.3,\"feels_like\":null,\"dew_point\":8.6,\"relative_humidity\":89.0,\"pressure\":1026.3,\"ozone\":null,\"ghi\":122.68179272930055,\"dni\":0.0,\"dhi\":122.68179272930055}",
|
||||
"{\"date_time\":\"2024-11-01T12:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":38750.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.8492203740585325,\"wind_speed\":12.6,\"wind_direction\":220.0,\"frost_chance\":null,\"temp_air\":10.5,\"feels_like\":null,\"dew_point\":8.6,\"relative_humidity\":88.0,\"pressure\":1025.8,\"ozone\":null,\"ghi\":132.24193611040087,\"dni\":0.0,\"dhi\":132.24193611040087}",
|
||||
"{\"date_time\":\"2024-11-01T13:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":47950.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.850632591753855,\"wind_speed\":13.3,\"wind_direction\":220.0,\"frost_chance\":null,\"temp_air\":10.7,\"feels_like\":null,\"dew_point\":8.6,\"relative_humidity\":87.0,\"pressure\":1025.2,\"ozone\":null,\"ghi\":124.54330452319319,\"dni\":0.0,\"dhi\":124.54330452319319}",
|
||||
"{\"date_time\":\"2024-11-01T14:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":39370.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.8087382990093692,\"wind_speed\":10.4,\"wind_direction\":240.0,\"frost_chance\":null,\"temp_air\":10.9,\"feels_like\":null,\"dew_point\":8.3,\"relative_humidity\":84.0,\"pressure\":1024.7,\"ozone\":null,\"ghi\":100.35711080592202,\"dni\":0.0,\"dhi\":100.35711080592202}",
|
||||
"{\"date_time\":\"2024-11-01T15:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":45050.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.8201971218479545,\"wind_speed\":10.1,\"wind_direction\":240.0,\"frost_chance\":null,\"temp_air\":11.2,\"feels_like\":null,\"dew_point\":8.5,\"relative_humidity\":83.0,\"pressure\":1024.3,\"ozone\":null,\"ghi\":62.53879690593976,\"dni\":0.0,\"dhi\":62.53879690593976}",
|
||||
"{\"date_time\":\"2024-11-01T16:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":50820.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.8313294168900875,\"wind_speed\":9.0,\"wind_direction\":220.0,\"frost_chance\":null,\"temp_air\":11.3,\"feels_like\":null,\"dew_point\":8.5,\"relative_humidity\":83.0,\"pressure\":1024.0,\"ozone\":null,\"ghi\":18.895062599949565,\"dni\":0.0,\"dhi\":18.895062599949565}",
|
||||
"{\"date_time\":\"2024-11-01T17:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":44670.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.7977443417069183,\"wind_speed\":9.0,\"wind_direction\":200.0,\"frost_chance\":null,\"temp_air\":10.8,\"feels_like\":null,\"dew_point\":8.2,\"relative_humidity\":84.0,\"pressure\":1024.0,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-01T18:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":49650.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.7548156137872095,\"wind_speed\":5.4,\"wind_direction\":240.0,\"frost_chance\":null,\"temp_air\":10.6,\"feels_like\":null,\"dew_point\":7.9,\"relative_humidity\":83.0,\"pressure\":1024.4,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-01T19:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":34320.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.785295883291216,\"wind_speed\":9.0,\"wind_direction\":270.0,\"frost_chance\":null,\"temp_air\":10.3,\"feels_like\":null,\"dew_point\":8.0,\"relative_humidity\":86.0,\"pressure\":1025.0,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-01T20:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":28820.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.7021447330910096,\"wind_speed\":13.3,\"wind_direction\":300.0,\"frost_chance\":null,\"temp_air\":10.1,\"feels_like\":null,\"dew_point\":7.3,\"relative_humidity\":83.0,\"pressure\":1025.59,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-01T21:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":27910.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.6511793809783977,\"wind_speed\":12.2,\"wind_direction\":290.0,\"frost_chance\":null,\"temp_air\":9.8,\"feels_like\":null,\"dew_point\":6.9,\"relative_humidity\":82.0,\"pressure\":1025.9,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-01T22:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":27860.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.621195919413435,\"wind_speed\":10.4,\"wind_direction\":280.0,\"frost_chance\":null,\"temp_air\":9.3,\"feels_like\":null,\"dew_point\":6.6,\"relative_humidity\":83.0,\"pressure\":1026.5,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-01T23:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":26550.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.620878591918624,\"wind_speed\":7.9,\"wind_direction\":300.0,\"frost_chance\":null,\"temp_air\":9.1,\"feels_like\":null,\"dew_point\":6.5,\"relative_humidity\":84.0,\"pressure\":1026.8,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-02T00:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":33360.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.601582418205307,\"wind_speed\":6.1,\"wind_direction\":330.0,\"frost_chance\":null,\"temp_air\":9.1,\"feels_like\":null,\"dew_point\":6.4,\"relative_humidity\":83.0,\"pressure\":1027.09,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-02T01:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":30030.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.639401905310941,\"wind_speed\":4.0,\"wind_direction\":230.0,\"frost_chance\":null,\"temp_air\":8.9,\"feels_like\":null,\"dew_point\":6.7,\"relative_humidity\":86.0,\"pressure\":1027.2,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-02T02:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":34110.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.6572484750051133,\"wind_speed\":4.7,\"wind_direction\":210.0,\"frost_chance\":null,\"temp_air\":8.7,\"feels_like\":null,\"dew_point\":6.8,\"relative_humidity\":88.0,\"pressure\":1027.4,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-02T03:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":27390.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.6572484750051133,\"wind_speed\":5.8,\"wind_direction\":200.0,\"frost_chance\":null,\"temp_air\":8.7,\"feels_like\":null,\"dew_point\":6.8,\"relative_humidity\":88.0,\"pressure\":1027.3,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-02T04:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":25060.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.6484088480622887,\"wind_speed\":2.9,\"wind_direction\":190.0,\"frost_chance\":null,\"temp_air\":8.8,\"feels_like\":null,\"dew_point\":6.8,\"relative_humidity\":87.0,\"pressure\":1027.8,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-02T05:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":23760.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.70525053247823,\"wind_speed\":4.3,\"wind_direction\":210.0,\"frost_chance\":null,\"temp_air\":8.8,\"feels_like\":null,\"dew_point\":7.2,\"relative_humidity\":90.0,\"pressure\":1028.0,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-02T06:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":21300.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.6863033043395828,\"wind_speed\":5.8,\"wind_direction\":190.0,\"frost_chance\":null,\"temp_air\":8.8,\"feels_like\":null,\"dew_point\":7.1,\"relative_humidity\":89.0,\"pressure\":1028.09,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-02T07:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":18210.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.70525053247823,\"wind_speed\":4.0,\"wind_direction\":170.0,\"frost_chance\":null,\"temp_air\":8.8,\"feels_like\":null,\"dew_point\":7.3,\"relative_humidity\":90.0,\"pressure\":1028.8,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-02T08:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":18570.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.7173594604852087,\"wind_speed\":1.8,\"wind_direction\":260.0,\"frost_chance\":null,\"temp_air\":9.1,\"feels_like\":null,\"dew_point\":7.4,\"relative_humidity\":89.0,\"pressure\":1029.4,\"ozone\":null,\"ghi\":13.575130473514132,\"dni\":0.0,\"dhi\":13.575130473514132}",
|
||||
"{\"date_time\":\"2024-11-02T09:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":19450.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.7306501955221831,\"wind_speed\":2.5,\"wind_direction\":180.0,\"frost_chance\":null,\"temp_air\":9.6,\"feels_like\":null,\"dew_point\":7.6,\"relative_humidity\":87.0,\"pressure\":1030.0,\"ozone\":null,\"ghi\":56.188815268002756,\"dni\":0.0,\"dhi\":56.188815268002756}",
|
||||
"{\"date_time\":\"2024-11-02T10:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":20370.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.7861787703974465,\"wind_speed\":1.8,\"wind_direction\":170.0,\"frost_chance\":null,\"temp_air\":10.5,\"feels_like\":null,\"dew_point\":8.2,\"relative_humidity\":85.0,\"pressure\":1030.5,\"ozone\":null,\"ghi\":95.01168041969699,\"dni\":0.0,\"dhi\":95.01168041969699}",
|
||||
"{\"date_time\":\"2024-11-02T11:00:00+01:00\",\"total_clouds\":87.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":32720.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.7981315281399455,\"wind_speed\":1.8,\"wind_direction\":150.0,\"frost_chance\":null,\"temp_air\":11.4,\"feels_like\":null,\"dew_point\":8.3,\"relative_humidity\":81.0,\"pressure\":1030.4,\"ozone\":null,\"ghi\":149.95999291205823,\"dni\":12.813701117903975,\"dhi\":144.84157493139483}",
|
||||
"{\"date_time\":\"2024-11-02T12:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":63890.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.7119416707379567,\"wind_speed\":4.0,\"wind_direction\":110.0,\"frost_chance\":null,\"temp_air\":12.3,\"feels_like\":null,\"dew_point\":7.6,\"relative_humidity\":73.0,\"pressure\":1030.4,\"ozone\":null,\"ghi\":130.33518534986166,\"dni\":0.0,\"dhi\":130.33518534986166}",
|
||||
"{\"date_time\":\"2024-11-02T13:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":75000.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.7341189545032323,\"wind_speed\":5.0,\"wind_direction\":90.0,\"frost_chance\":null,\"temp_air\":13.2,\"feels_like\":null,\"dew_point\":7.8,\"relative_humidity\":70.0,\"pressure\":1030.09,\"ozone\":null,\"ghi\":122.64932256772778,\"dni\":0.0,\"dhi\":122.64932256772778}",
|
||||
"{\"date_time\":\"2024-11-02T14:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":70510.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.7674144274726324,\"wind_speed\":4.0,\"wind_direction\":110.0,\"frost_chance\":null,\"temp_air\":12.6,\"feels_like\":null,\"dew_point\":8.2,\"relative_humidity\":74.0,\"pressure\":1029.8,\"ozone\":null,\"ghi\":98.5166184540087,\"dni\":0.0,\"dhi\":98.5166184540087}",
|
||||
"{\"date_time\":\"2024-11-02T15:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":73670.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.815182384971893,\"wind_speed\":5.0,\"wind_direction\":50.0,\"frost_chance\":null,\"temp_air\":12.6,\"feels_like\":null,\"dew_point\":8.4,\"relative_humidity\":76.0,\"pressure\":1029.7,\"ozone\":null,\"ghi\":60.83135423410029,\"dni\":0.0,\"dhi\":60.83135423410029}",
|
||||
"{\"date_time\":\"2024-11-02T16:00:00+01:00\",\"total_clouds\":87.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":75000.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.850307043871555,\"wind_speed\":7.6,\"wind_direction\":60.0,\"frost_chance\":null,\"temp_air\":12.7,\"feels_like\":null,\"dew_point\":8.7,\"relative_humidity\":77.0,\"pressure\":1030.0,\"ozone\":null,\"ghi\":21.89924464521845,\"dni\":0.0,\"dhi\":21.89924464521845}",
|
||||
"{\"date_time\":\"2024-11-02T17:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":75000.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.8087382990093692,\"wind_speed\":9.7,\"wind_direction\":60.0,\"frost_chance\":null,\"temp_air\":10.9,\"feels_like\":null,\"dew_point\":8.3,\"relative_humidity\":84.0,\"pressure\":1030.2,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-02T18:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":31130.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.8833512151215333,\"wind_speed\":10.4,\"wind_direction\":50.0,\"frost_chance\":null,\"temp_air\":10.8,\"feels_like\":null,\"dew_point\":9.0,\"relative_humidity\":88.0,\"pressure\":1030.8,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-02T19:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":75000.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.8745229585567942,\"wind_speed\":11.5,\"wind_direction\":50.0,\"frost_chance\":null,\"temp_air\":11.1,\"feels_like\":null,\"dew_point\":8.9,\"relative_humidity\":86.0,\"pressure\":1031.4,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-02T20:00:00+01:00\",\"total_clouds\":37.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":75000.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.6919160225049534,\"wind_speed\":11.5,\"wind_direction\":50.0,\"frost_chance\":null,\"temp_air\":10.2,\"feels_like\":null,\"dew_point\":7.3,\"relative_humidity\":82.0,\"pressure\":1031.9,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-02T21:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":73550.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.5255496443180916,\"wind_speed\":8.6,\"wind_direction\":30.0,\"frost_chance\":null,\"temp_air\":8.3,\"feels_like\":null,\"dew_point\":5.6,\"relative_humidity\":83.0,\"pressure\":1032.7,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-02T22:00:00+01:00\",\"total_clouds\":87.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":64570.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.4958879070461668,\"wind_speed\":9.0,\"wind_direction\":50.0,\"frost_chance\":null,\"temp_air\":7.2,\"feels_like\":null,\"dew_point\":5.2,\"relative_humidity\":87.0,\"pressure\":1032.8,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-02T23:00:00+01:00\",\"total_clouds\":87.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":66050.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.390501273322162,\"wind_speed\":5.0,\"wind_direction\":40.0,\"frost_chance\":null,\"temp_air\":5.8,\"feels_like\":null,\"dew_point\":4.0,\"relative_humidity\":88.0,\"pressure\":1033.09,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-03T00:00:00+01:00\",\"total_clouds\":87.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":68320.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.4252928707378512,\"wind_speed\":5.8,\"wind_direction\":30.0,\"frost_chance\":null,\"temp_air\":6.4,\"feels_like\":null,\"dew_point\":4.4,\"relative_humidity\":87.0,\"pressure\":1033.2,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-03T01:00:00+01:00\",\"total_clouds\":37.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":62300.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.3403838896380875,\"wind_speed\":7.2,\"wind_direction\":40.0,\"frost_chance\":null,\"temp_air\":5.0,\"feels_like\":null,\"dew_point\":3.4,\"relative_humidity\":89.0,\"pressure\":1033.4,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-03T02:00:00+01:00\",\"total_clouds\":87.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":52300.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.3623270721453318,\"wind_speed\":6.8,\"wind_direction\":20.0,\"frost_chance\":null,\"temp_air\":4.9,\"feels_like\":null,\"dew_point\":3.6,\"relative_humidity\":91.0,\"pressure\":1033.3,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-03T03:00:00+01:00\",\"total_clouds\":75.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":46680.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.2794609296319466,\"wind_speed\":9.0,\"wind_direction\":10.0,\"frost_chance\":null,\"temp_air\":3.3,\"feels_like\":null,\"dew_point\":2.5,\"relative_humidity\":94.0,\"pressure\":1033.3,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-03T04:00:00+01:00\",\"total_clouds\":87.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":25080.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.199274182505457,\"wind_speed\":7.6,\"wind_direction\":10.0,\"frost_chance\":null,\"temp_air\":2.2,\"feels_like\":null,\"dew_point\":1.3,\"relative_humidity\":94.0,\"pressure\":1033.0,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-03T05:00:00+01:00\",\"total_clouds\":37.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":31420.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.2392275984186982,\"wind_speed\":7.6,\"wind_direction\":10.0,\"frost_chance\":null,\"temp_air\":2.4,\"feels_like\":null,\"dew_point\":1.8,\"relative_humidity\":96.0,\"pressure\":1032.9,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-03T06:00:00+01:00\",\"total_clouds\":12.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":7340.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.2105500551096946,\"wind_speed\":5.8,\"wind_direction\":10.0,\"frost_chance\":null,\"temp_air\":2.0,\"feels_like\":null,\"dew_point\":1.4,\"relative_humidity\":96.0,\"pressure\":1032.8,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-03T07:00:00+01:00\",\"total_clouds\":37.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":14380.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.1434707306695828,\"wind_speed\":6.1,\"wind_direction\":360.0,\"frost_chance\":null,\"temp_air\":1.2,\"feels_like\":null,\"dew_point\":0.5,\"relative_humidity\":95.0,\"pressure\":1032.9,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-03T08:00:00+01:00\",\"total_clouds\":25.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":190.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.1488384034632042,\"wind_speed\":6.1,\"wind_direction\":360.0,\"frost_chance\":null,\"temp_air\":1.1,\"feels_like\":null,\"dew_point\":0.5,\"relative_humidity\":96.0,\"pressure\":1033.09,\"ozone\":null,\"ghi\":29.853526900099773,\"dni\":0.0,\"dhi\":29.853526900099773}",
|
||||
"{\"date_time\":\"2024-11-03T09:00:00+01:00\",\"total_clouds\":0.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":14830.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.1982804081056098,\"wind_speed\":6.5,\"wind_direction\":360.0,\"frost_chance\":null,\"temp_air\":3.5,\"feels_like\":null,\"dew_point\":1.6,\"relative_humidity\":87.0,\"pressure\":1033.5,\"ozone\":null,\"ghi\":155.80764565813612,\"dni\":359.7647285960036,\"dhi\":73.0052545725479}",
|
||||
"{\"date_time\":\"2024-11-03T10:00:00+01:00\",\"total_clouds\":0.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":29490.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.3676770322583323,\"wind_speed\":3.6,\"wind_direction\":10.0,\"frost_chance\":null,\"temp_air\":6.9,\"feels_like\":null,\"dew_point\":3.9,\"relative_humidity\":81.0,\"pressure\":1033.09,\"ozone\":null,\"ghi\":266.29582531703034,\"dni\":542.6987046512027,\"dhi\":86.93886764577462}",
|
||||
"{\"date_time\":\"2024-11-03T11:00:00+01:00\",\"total_clouds\":37.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":35910.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.413965797496583,\"wind_speed\":3.2,\"wind_direction\":350.0,\"frost_chance\":null,\"temp_air\":8.5,\"feels_like\":null,\"dew_point\":4.5,\"relative_humidity\":76.0,\"pressure\":1032.59,\"ozone\":null,\"ghi\":258.0721216984891,\"dni\":250.78797925254707,\"dhi\":159.12427101947398}",
|
||||
"{\"date_time\":\"2024-11-03T12:00:00+01:00\",\"total_clouds\":62.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":40750.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.441145619265592,\"wind_speed\":5.0,\"wind_direction\":30.0,\"frost_chance\":null,\"temp_air\":10.4,\"feels_like\":null,\"dew_point\":5.0,\"relative_humidity\":69.0,\"pressure\":1032.0,\"ozone\":null,\"ghi\":219.09625671981206,\"dni\":99.04311158893928,\"dhi\":177.70064914620303}",
|
||||
"{\"date_time\":\"2024-11-03T13:00:00+01:00\",\"total_clouds\":37.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":45690.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.510497819567314,\"wind_speed\":4.3,\"wind_direction\":60.0,\"frost_chance\":null,\"temp_air\":11.9,\"feels_like\":null,\"dew_point\":5.8,\"relative_humidity\":66.0,\"pressure\":1031.3,\"ozone\":null,\"ghi\":262.09146259237264,\"dni\":252.71731147093138,\"dhi\":161.2289221818615}",
|
||||
"{\"date_time\":\"2024-11-03T14:00:00+01:00\",\"total_clouds\":25.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":20640.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.4989629850536337,\"wind_speed\":4.7,\"wind_direction\":90.0,\"frost_chance\":null,\"temp_air\":12.8,\"feels_like\":null,\"dew_point\":5.8,\"relative_humidity\":62.0,\"pressure\":1030.7,\"ozone\":null,\"ghi\":231.40091702563853,\"dni\":315.5806670947059,\"dhi\":124.32565938039977}",
|
||||
"{\"date_time\":\"2024-11-03T15:00:00+01:00\",\"total_clouds\":37.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":39980.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.5138875813494541,\"wind_speed\":2.9,\"wind_direction\":70.0,\"frost_chance\":null,\"temp_air\":12.7,\"feels_like\":null,\"dew_point\":5.8,\"relative_humidity\":63.0,\"pressure\":1030.3,\"ozone\":null,\"ghi\":128.3738640423681,\"dni\":143.98607922934983,\"dhi\":93.44607846301753}",
|
||||
"{\"date_time\":\"2024-11-03T16:00:00+01:00\",\"total_clouds\":25.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":52750.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.510497819567314,\"wind_speed\":3.2,\"wind_direction\":90.0,\"frost_chance\":null,\"temp_air\":11.9,\"feels_like\":null,\"dew_point\":5.8,\"relative_humidity\":66.0,\"pressure\":1029.8,\"ozone\":null,\"ghi\":39.33002179401603,\"dni\":3.9206199292686486,\"dhi\":38.87702761430884}",
|
||||
"{\"date_time\":\"2024-11-03T17:00:00+01:00\",\"total_clouds\":87.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":49460.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.4883850499964026,\"wind_speed\":4.0,\"wind_direction\":90.0,\"frost_chance\":null,\"temp_air\":8.5,\"feels_like\":null,\"dew_point\":5.3,\"relative_humidity\":80.0,\"pressure\":1029.8,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-03T18:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":26380.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.398891798445423,\"wind_speed\":3.2,\"wind_direction\":70.0,\"frost_chance\":null,\"temp_air\":5.9,\"feels_like\":null,\"dew_point\":4.1,\"relative_humidity\":88.0,\"pressure\":1030.2,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-03T19:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":32120.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.3609257229104745,\"wind_speed\":5.0,\"wind_direction\":50.0,\"frost_chance\":null,\"temp_air\":4.7,\"feels_like\":null,\"dew_point\":3.5,\"relative_humidity\":92.0,\"pressure\":1030.59,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-03T20:00:00+01:00\",\"total_clouds\":87.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":24750.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.2733596216558292,\"wind_speed\":5.8,\"wind_direction\":20.0,\"frost_chance\":null,\"temp_air\":3.4,\"feels_like\":null,\"dew_point\":2.4,\"relative_humidity\":93.0,\"pressure\":1030.9,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-03T21:00:00+01:00\",\"total_clouds\":25.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":16220.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.226318977601837,\"wind_speed\":5.8,\"wind_direction\":40.0,\"frost_chance\":null,\"temp_air\":2.4,\"feels_like\":null,\"dew_point\":1.6,\"relative_humidity\":95.0,\"pressure\":1031.0,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-03T22:00:00+01:00\",\"total_clouds\":0.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":8920.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.1965035965509712,\"wind_speed\":6.1,\"wind_direction\":10.0,\"frost_chance\":null,\"temp_air\":1.8,\"feels_like\":null,\"dew_point\":1.2,\"relative_humidity\":96.0,\"pressure\":1031.4,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-03T23:00:00+01:00\",\"total_clouds\":0.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":6530.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.1368713367604626,\"wind_speed\":7.2,\"wind_direction\":360.0,\"frost_chance\":null,\"temp_air\":1.1,\"feels_like\":null,\"dew_point\":0.4,\"relative_humidity\":95.0,\"pressure\":1031.7,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-04T00:00:00+01:00\",\"total_clouds\":0.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":690.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.1356387287723233,\"wind_speed\":7.2,\"wind_direction\":10.0,\"frost_chance\":null,\"temp_air\":0.9,\"feels_like\":null,\"dew_point\":0.4,\"relative_humidity\":96.0,\"pressure\":1031.8,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-04T01:00:00+01:00\",\"total_clouds\":0.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":290.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.090861721609437,\"wind_speed\":7.2,\"wind_direction\":350.0,\"frost_chance\":null,\"temp_air\":0.2,\"feels_like\":null,\"dew_point\":-0.3,\"relative_humidity\":96.0,\"pressure\":1031.59,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-04T02:00:00+01:00\",\"total_clouds\":37.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":480.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.0835031721133945,\"wind_speed\":7.2,\"wind_direction\":10.0,\"frost_chance\":null,\"temp_air\":-0.1,\"feels_like\":null,\"dew_point\":-0.5,\"relative_humidity\":97.0,\"pressure\":1031.8,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-04T03:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":110.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.1009339806416942,\"wind_speed\":6.8,\"wind_direction\":20.0,\"frost_chance\":null,\"temp_air\":0.0,\"feels_like\":null,\"dew_point\":-0.3,\"relative_humidity\":98.0,\"pressure\":1031.7,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-04T04:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":110.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.1135880074763005,\"wind_speed\":6.8,\"wind_direction\":10.0,\"frost_chance\":null,\"temp_air\":0.2,\"feels_like\":null,\"dew_point\":-0.1,\"relative_humidity\":98.0,\"pressure\":1031.7,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-04T05:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":160.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.1264206880151613,\"wind_speed\":4.7,\"wind_direction\":360.0,\"frost_chance\":null,\"temp_air\":0.4,\"feels_like\":null,\"dew_point\":0.2,\"relative_humidity\":98.0,\"pressure\":1031.59,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-04T06:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":120.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.1460092992885946,\"wind_speed\":4.3,\"wind_direction\":10.0,\"frost_chance\":null,\"temp_air\":0.7,\"feels_like\":null,\"dew_point\":0.5,\"relative_humidity\":98.0,\"pressure\":1031.59,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-04T07:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":120.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.157703271730315,\"wind_speed\":5.4,\"wind_direction\":360.0,\"frost_chance\":null,\"temp_air\":0.7,\"feels_like\":null,\"dew_point\":0.6,\"relative_humidity\":99.0,\"pressure\":1031.7,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-04T08:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":90.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.1541137262196406,\"wind_speed\":5.0,\"wind_direction\":360.0,\"frost_chance\":null,\"temp_air\":1.0,\"feels_like\":null,\"dew_point\":0.6,\"relative_humidity\":97.0,\"pressure\":1032.0,\"ozone\":null,\"ghi\":11.417075555130713,\"dni\":0.0,\"dhi\":11.417075555130713}",
|
||||
"{\"date_time\":\"2024-11-04T09:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":120.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.22143075481245,\"wind_speed\":5.0,\"wind_direction\":20.0,\"frost_chance\":null,\"temp_air\":1.8,\"feels_like\":null,\"dew_point\":1.6,\"relative_humidity\":98.0,\"pressure\":1032.2,\"ozone\":null,\"ghi\":52.89325642044978,\"dni\":0.0,\"dhi\":52.89325642044978}",
|
||||
"{\"date_time\":\"2024-11-04T10:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":150.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.3316895356787095,\"wind_speed\":5.0,\"wind_direction\":360.0,\"frost_chance\":null,\"temp_air\":3.1,\"feels_like\":null,\"dew_point\":2.9,\"relative_humidity\":99.0,\"pressure\":1032.0,\"ozone\":null,\"ghi\":91.41057965841121,\"dni\":0.0,\"dhi\":91.41057965841121}",
|
||||
"{\"date_time\":\"2024-11-04T11:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":120.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.3555118553110441,\"wind_speed\":4.0,\"wind_direction\":350.0,\"frost_chance\":null,\"temp_air\":3.4,\"feels_like\":null,\"dew_point\":3.2,\"relative_humidity\":99.0,\"pressure\":1031.7,\"ozone\":null,\"ghi\":117.07563013335894,\"dni\":0.0,\"dhi\":117.07563013335894}",
|
||||
"{\"date_time\":\"2024-11-04T12:00:00+01:00\",\"total_clouds\":87.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":350.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.411639131909973,\"wind_speed\":3.6,\"wind_direction\":350.0,\"frost_chance\":null,\"temp_air\":4.6,\"feels_like\":null,\"dew_point\":4.0,\"relative_humidity\":96.0,\"pressure\":1030.9,\"ozone\":null,\"ghi\":157.14326578481194,\"dni\":14.639827403702798,\"dhi\":151.09589042551917}",
|
||||
"{\"date_time\":\"2024-11-04T13:00:00+01:00\",\"total_clouds\":0.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":21410.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.4587699149332078,\"wind_speed\":4.3,\"wind_direction\":30.0,\"frost_chance\":null,\"temp_air\":5.5,\"feels_like\":null,\"dew_point\":4.6,\"relative_humidity\":94.0,\"pressure\":1029.9,\"ozone\":null,\"ghi\":339.81370340762766,\"dni\":626.3444200231016,\"dhi\":92.86155820300033}",
|
||||
"{\"date_time\":\"2024-11-04T14:00:00+01:00\",\"total_clouds\":0.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":39060.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.5252929600670635,\"wind_speed\":2.9,\"wind_direction\":10.0,\"frost_chance\":null,\"temp_air\":8.1,\"feels_like\":null,\"dew_point\":5.5,\"relative_humidity\":84.0,\"pressure\":1029.0,\"ozone\":null,\"ghi\":271.20800751093515,\"dni\":549.6301264862547,\"dhi\":87.32285840607426}",
|
||||
"{\"date_time\":\"2024-11-04T15:00:00+01:00\",\"total_clouds\":0.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":36810.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.5619693663437841,\"wind_speed\":5.4,\"wind_direction\":50.0,\"frost_chance\":null,\"temp_air\":9.5,\"feels_like\":null,\"dew_point\":6.0,\"relative_humidity\":79.0,\"pressure\":1028.5,\"ozone\":null,\"ghi\":164.34557743320204,\"dni\":378.3659028909279,\"dhi\":74.29470308711969}",
|
||||
"{\"date_time\":\"2024-11-04T16:00:00+01:00\",\"total_clouds\":0.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":61290.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.5325727324790135,\"wind_speed\":5.4,\"wind_direction\":40.0,\"frost_chance\":null,\"temp_air\":7.6,\"feels_like\":null,\"dew_point\":5.6,\"relative_humidity\":87.0,\"pressure\":1028.09,\"ozone\":null,\"ghi\":43.66802704534112,\"dni\":44.64467562847794,\"dhi\":38.705191642431814}",
|
||||
"{\"date_time\":\"2024-11-04T17:00:00+01:00\",\"total_clouds\":0.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":45940.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.3447696273196648,\"wind_speed\":4.3,\"wind_direction\":40.0,\"frost_chance\":null,\"temp_air\":4.5,\"feels_like\":null,\"dew_point\":3.3,\"relative_humidity\":92.0,\"pressure\":1028.2,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-04T18:00:00+01:00\",\"total_clouds\":0.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":44410.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.2349798532829575,\"wind_speed\":4.3,\"wind_direction\":40.0,\"frost_chance\":null,\"temp_air\":2.7,\"feels_like\":null,\"dew_point\":1.8,\"relative_humidity\":94.0,\"pressure\":1028.2,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-04T19:00:00+01:00\",\"total_clouds\":12.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":44940.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.2247906544736582,\"wind_speed\":6.1,\"wind_direction\":10.0,\"frost_chance\":null,\"temp_air\":2.2,\"feels_like\":null,\"dew_point\":1.7,\"relative_humidity\":96.0,\"pressure\":1028.59,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-04T20:00:00+01:00\",\"total_clouds\":0.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":47840.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.2160391817564757,\"wind_speed\":7.6,\"wind_direction\":360.0,\"frost_chance\":null,\"temp_air\":1.9,\"feels_like\":null,\"dew_point\":1.5,\"relative_humidity\":97.0,\"pressure\":1029.0,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-04T21:00:00+01:00\",\"total_clouds\":0.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":37930.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.2430135737816375,\"wind_speed\":7.2,\"wind_direction\":360.0,\"frost_chance\":null,\"temp_air\":2.1,\"feels_like\":null,\"dew_point\":1.9,\"relative_humidity\":98.0,\"pressure\":1029.09,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-04T22:00:00+01:00\",\"total_clouds\":0.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":18950.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.2503071264418593,\"wind_speed\":4.0,\"wind_direction\":10.0,\"frost_chance\":null,\"temp_air\":2.2,\"feels_like\":null,\"dew_point\":1.9,\"relative_humidity\":98.0,\"pressure\":1029.2,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-04T23:00:00+01:00\",\"total_clouds\":0.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":12300.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.255697385759001,\"wind_speed\":7.6,\"wind_direction\":10.0,\"frost_chance\":null,\"temp_air\":2.1,\"feels_like\":null,\"dew_point\":1.9,\"relative_humidity\":99.0,\"pressure\":1029.0,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-05T00:00:00+01:00\",\"total_clouds\":12.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":6660.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.2430135737816375,\"wind_speed\":7.6,\"wind_direction\":10.0,\"frost_chance\":null,\"temp_air\":2.1,\"feels_like\":null,\"dew_point\":1.8,\"relative_humidity\":98.0,\"pressure\":1028.8,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-05T01:00:00+01:00\",\"total_clouds\":12.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":1020.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.1847396035714295,\"wind_speed\":8.6,\"wind_direction\":360.0,\"frost_chance\":null,\"temp_air\":1.1,\"feels_like\":null,\"dew_point\":0.9,\"relative_humidity\":99.0,\"pressure\":1028.7,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-05T02:00:00+01:00\",\"total_clouds\":75.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":4820.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.0896999604310647,\"wind_speed\":7.9,\"wind_direction\":10.0,\"frost_chance\":null,\"temp_air\":0.0,\"feels_like\":null,\"dew_point\":-0.4,\"relative_humidity\":97.0,\"pressure\":1028.59,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-05T03:00:00+01:00\",\"total_clouds\":87.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":230.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.0472255738551546,\"wind_speed\":7.9,\"wind_direction\":360.0,\"frost_chance\":null,\"temp_air\":-0.7,\"feels_like\":null,\"dew_point\":-1.1,\"relative_humidity\":97.0,\"pressure\":1028.5,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-05T04:00:00+01:00\",\"total_clouds\":87.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":1170.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.0402709520121391,\"wind_speed\":6.8,\"wind_direction\":10.0,\"frost_chance\":null,\"temp_air\":-1.0,\"feels_like\":null,\"dew_point\":-1.3,\"relative_humidity\":98.0,\"pressure\":1028.3,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-05T05:00:00+01:00\",\"total_clouds\":0.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":430.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":0.9964253886852408,\"wind_speed\":7.2,\"wind_direction\":10.0,\"frost_chance\":null,\"temp_air\":-1.4,\"feels_like\":null,\"dew_point\":-1.9,\"relative_humidity\":96.0,\"pressure\":1028.5,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-05T06:00:00+01:00\",\"total_clouds\":25.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":200.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":0.9908715007922109,\"wind_speed\":7.9,\"wind_direction\":10.0,\"frost_chance\":null,\"temp_air\":-1.5,\"feels_like\":null,\"dew_point\":-2.0,\"relative_humidity\":96.0,\"pressure\":1028.7,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-05T07:00:00+01:00\",\"total_clouds\":37.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":180.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":0.9845964358395034,\"wind_speed\":7.2,\"wind_direction\":360.0,\"frost_chance\":null,\"temp_air\":-1.8,\"feels_like\":null,\"dew_point\":-2.2,\"relative_humidity\":97.0,\"pressure\":1028.8,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-05T08:00:00+01:00\",\"total_clouds\":87.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":250.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.01904093258332,\"wind_speed\":7.2,\"wind_direction\":360.0,\"frost_chance\":null,\"temp_air\":-1.0,\"feels_like\":null,\"dew_point\":-1.5,\"relative_humidity\":96.0,\"pressure\":1029.2,\"ozone\":null,\"ghi\":12.910799386398546,\"dni\":0.0,\"dhi\":12.910799386398546}",
|
||||
"{\"date_time\":\"2024-11-05T09:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":150.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.1949683522039627,\"wind_speed\":4.7,\"wind_direction\":10.0,\"frost_chance\":null,\"temp_air\":1.6,\"feels_like\":null,\"dew_point\":1.1,\"relative_humidity\":97.0,\"pressure\":1029.09,\"ozone\":null,\"ghi\":51.27161617785122,\"dni\":0.0,\"dhi\":51.27161617785122}",
|
||||
"{\"date_time\":\"2024-11-05T10:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":2630.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.2612560203740841,\"wind_speed\":3.2,\"wind_direction\":10.0,\"frost_chance\":null,\"temp_air\":2.7,\"feels_like\":null,\"dew_point\":2.2,\"relative_humidity\":96.0,\"pressure\":1028.7,\"ozone\":null,\"ghi\":89.63381300017787,\"dni\":0.0,\"dhi\":89.63381300017787}",
|
||||
"{\"date_time\":\"2024-11-05T11:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":11070.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.2885323670339885,\"wind_speed\":5.8,\"wind_direction\":60.0,\"frost_chance\":null,\"temp_air\":3.6,\"feels_like\":null,\"dew_point\":2.6,\"relative_humidity\":93.0,\"pressure\":1028.2,\"ozone\":null,\"ghi\":115.24250625572647,\"dni\":0.0,\"dhi\":115.24250625572647}",
|
||||
"{\"date_time\":\"2024-11-05T12:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":12260.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.3288271029735363,\"wind_speed\":7.9,\"wind_direction\":100.0,\"frost_chance\":null,\"temp_air\":4.3,\"feels_like\":null,\"dew_point\":3.2,\"relative_humidity\":92.0,\"pressure\":1028.0,\"ozone\":null,\"ghi\":124.73888203411694,\"dni\":0.0,\"dhi\":124.73888203411694}",
|
||||
"{\"date_time\":\"2024-11-05T13:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":13640.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.3461330520092738,\"wind_speed\":12.6,\"wind_direction\":160.0,\"frost_chance\":null,\"temp_air\":4.7,\"feels_like\":null,\"dew_point\":3.4,\"relative_humidity\":91.0,\"pressure\":1027.7,\"ozone\":null,\"ghi\":117.11630570621142,\"dni\":0.0,\"dhi\":117.11630570621142}",
|
||||
"{\"date_time\":\"2024-11-05T14:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":14990.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.380031254206577,\"wind_speed\":10.8,\"wind_direction\":170.0,\"frost_chance\":null,\"temp_air\":5.3,\"feels_like\":null,\"dew_point\":3.8,\"relative_humidity\":90.0,\"pressure\":1027.5,\"ozone\":null,\"ghi\":93.17139021360312,\"dni\":0.0,\"dhi\":93.17139021360312}",
|
||||
"{\"date_time\":\"2024-11-05T15:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":14400.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.3738841320739368,\"wind_speed\":8.6,\"wind_direction\":160.0,\"frost_chance\":null,\"temp_air\":5.6,\"feels_like\":null,\"dew_point\":3.8,\"relative_humidity\":88.0,\"pressure\":1027.3,\"ozone\":null,\"ghi\":55.91961682093465,\"dni\":0.0,\"dhi\":55.91961682093465}",
|
||||
"{\"date_time\":\"2024-11-05T16:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":15490.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.381175770521867,\"wind_speed\":6.1,\"wind_direction\":150.0,\"frost_chance\":null,\"temp_air\":5.5,\"feels_like\":null,\"dew_point\":3.8,\"relative_humidity\":89.0,\"pressure\":1027.3,\"ozone\":null,\"ghi\":14.182910983589771,\"dni\":0.0,\"dhi\":14.182910983589771}",
|
||||
"{\"date_time\":\"2024-11-05T17:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":12550.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.4260322960134626,\"wind_speed\":5.4,\"wind_direction\":140.0,\"frost_chance\":null,\"temp_air\":5.3,\"feels_like\":null,\"dew_point\":4.2,\"relative_humidity\":93.0,\"pressure\":1027.2,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-05T18:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":3050.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.4479915832345371,\"wind_speed\":4.0,\"wind_direction\":100.0,\"frost_chance\":null,\"temp_air\":5.2,\"feels_like\":null,\"dew_point\":4.4,\"relative_humidity\":95.0,\"pressure\":1027.5,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-05T19:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":470.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.4346133748222865,\"wind_speed\":3.2,\"wind_direction\":360.0,\"frost_chance\":null,\"temp_air\":5.4,\"feels_like\":null,\"dew_point\":4.3,\"relative_humidity\":93.0,\"pressure\":1027.4,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-05T20:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":320.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.4346133748222865,\"wind_speed\":5.0,\"wind_direction\":350.0,\"frost_chance\":null,\"temp_air\":5.4,\"feels_like\":null,\"dew_point\":4.4,\"relative_humidity\":93.0,\"pressure\":1027.5,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-05T21:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":350.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.4587699149332078,\"wind_speed\":4.0,\"wind_direction\":350.0,\"frost_chance\":null,\"temp_air\":5.5,\"feels_like\":null,\"dew_point\":4.7,\"relative_humidity\":94.0,\"pressure\":1027.59,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-05T22:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":200.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.4808912256230051,\"wind_speed\":4.0,\"wind_direction\":10.0,\"frost_chance\":null,\"temp_air\":5.4,\"feels_like\":null,\"dew_point\":4.8,\"relative_humidity\":96.0,\"pressure\":1027.5,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-05T23:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":170.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.4873670184226437,\"wind_speed\":3.6,\"wind_direction\":10.0,\"frost_chance\":null,\"temp_air\":5.3,\"feels_like\":null,\"dew_point\":4.9,\"relative_humidity\":97.0,\"pressure\":1027.7,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-06T00:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":210.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.4784756165657906,\"wind_speed\":2.5,\"wind_direction\":10.0,\"frost_chance\":null,\"temp_air\":5.2,\"feels_like\":null,\"dew_point\":4.8,\"relative_humidity\":97.0,\"pressure\":1027.9,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-06T01:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":170.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.4696426588157527,\"wind_speed\":2.5,\"wind_direction\":10.0,\"frost_chance\":null,\"temp_air\":5.1,\"feels_like\":null,\"dew_point\":4.7,\"relative_humidity\":97.0,\"pressure\":1028.0,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-06T02:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":690.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.4696426588157527,\"wind_speed\":3.2,\"wind_direction\":200.0,\"frost_chance\":null,\"temp_air\":5.1,\"feels_like\":null,\"dew_point\":4.7,\"relative_humidity\":97.0,\"pressure\":1028.0,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-06T03:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":900.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.452150835143925,\"wind_speed\":5.8,\"wind_direction\":200.0,\"frost_chance\":null,\"temp_air\":4.9,\"feels_like\":null,\"dew_point\":4.5,\"relative_humidity\":97.0,\"pressure\":1028.09,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-06T04:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":2150.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.1,\"preciptable_water\":1.4458073416320942,\"wind_speed\":5.0,\"wind_direction\":190.0,\"frost_chance\":null,\"temp_air\":5.0,\"feels_like\":null,\"dew_point\":4.4,\"relative_humidity\":96.0,\"pressure\":1028.09,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-06T05:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":2410.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.4608678347740953,\"wind_speed\":2.5,\"wind_direction\":210.0,\"frost_chance\":null,\"temp_air\":5.0,\"feels_like\":null,\"dew_point\":4.5,\"relative_humidity\":97.0,\"pressure\":1028.5,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-06T06:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":3520.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.484793614061276,\"wind_speed\":1.8,\"wind_direction\":180.0,\"frost_chance\":null,\"temp_air\":5.1,\"feels_like\":null,\"dew_point\":4.8,\"relative_humidity\":98.0,\"pressure\":1028.7,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-06T07:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":900.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.4632335999001638,\"wind_speed\":2.2,\"wind_direction\":160.0,\"frost_chance\":null,\"temp_air\":5.2,\"feels_like\":null,\"dew_point\":4.6,\"relative_humidity\":96.0,\"pressure\":1029.2,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-06T08:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":12750.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.4808912256230051,\"wind_speed\":1.8,\"wind_direction\":120.0,\"frost_chance\":null,\"temp_air\":5.4,\"feels_like\":null,\"dew_point\":4.9,\"relative_humidity\":96.0,\"pressure\":1029.7,\"ozone\":null,\"ghi\":9.426582342676907,\"dni\":0.0,\"dhi\":9.426582342676907}",
|
||||
"{\"date_time\":\"2024-11-06T09:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":6980.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.4831703698525456,\"wind_speed\":2.2,\"wind_direction\":340.0,\"frost_chance\":null,\"temp_air\":5.6,\"feels_like\":null,\"dew_point\":4.8,\"relative_humidity\":95.0,\"pressure\":1030.5,\"ozone\":null,\"ghi\":49.66881557471828,\"dni\":0.0,\"dhi\":49.66881557471828}",
|
||||
"{\"date_time\":\"2024-11-06T10:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":15760.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.4233298143003077,\"wind_speed\":3.2,\"wind_direction\":360.0,\"frost_chance\":null,\"temp_air\":6.0,\"feels_like\":null,\"dew_point\":4.3,\"relative_humidity\":89.0,\"pressure\":1030.9,\"ozone\":null,\"ghi\":87.87424898748947,\"dni\":0.0,\"dhi\":87.87424898748947}",
|
||||
"{\"date_time\":\"2024-11-06T11:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":29630.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.4252928707378512,\"wind_speed\":5.0,\"wind_direction\":30.0,\"frost_chance\":null,\"temp_air\":6.4,\"feels_like\":null,\"dew_point\":4.4,\"relative_humidity\":87.0,\"pressure\":1031.5,\"ozone\":null,\"ghi\":113.42891032649719,\"dni\":0.0,\"dhi\":113.42891032649719}",
|
||||
"{\"date_time\":\"2024-11-06T12:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":19110.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.409437502900786,\"wind_speed\":3.6,\"wind_direction\":20.0,\"frost_chance\":null,\"temp_air\":6.6,\"feels_like\":null,\"dew_point\":4.2,\"relative_humidity\":85.0,\"pressure\":1031.59,\"ozone\":null,\"ghi\":122.91825781571202,\"dni\":0.0,\"dhi\":122.91825781571202}",
|
||||
"{\"date_time\":\"2024-11-06T13:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":25120.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.3846109217160805,\"wind_speed\":5.0,\"wind_direction\":360.0,\"frost_chance\":null,\"temp_air\":6.7,\"feels_like\":null,\"dew_point\":4.0,\"relative_humidity\":83.0,\"pressure\":1031.7,\"ozone\":null,\"ghi\":115.32501526045236,\"dni\":0.0,\"dhi\":115.32501526045236}",
|
||||
"{\"date_time\":\"2024-11-06T14:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":28400.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.4012929810138648,\"wind_speed\":4.0,\"wind_direction\":340.0,\"frost_chance\":null,\"temp_air\":6.7,\"feels_like\":null,\"dew_point\":4.2,\"relative_humidity\":84.0,\"pressure\":1031.5,\"ozone\":null,\"ghi\":91.45155097121383,\"dni\":0.0,\"dhi\":91.45155097121383}",
|
||||
"{\"date_time\":\"2024-11-06T15:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":25950.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.3844732659494836,\"wind_speed\":4.3,\"wind_direction\":90.0,\"frost_chance\":null,\"temp_air\":6.5,\"feels_like\":null,\"dew_point\":4.1,\"relative_humidity\":84.0,\"pressure\":1031.59,\"ozone\":null,\"ghi\":54.35521490956087,\"dni\":0.0,\"dhi\":54.35521490956087}",
|
||||
"{\"date_time\":\"2024-11-06T16:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":22070.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.3678703280167073,\"wind_speed\":4.7,\"wind_direction\":90.0,\"frost_chance\":null,\"temp_air\":6.3,\"feels_like\":null,\"dew_point\":3.8,\"relative_humidity\":84.0,\"pressure\":1031.8,\"ozone\":null,\"ghi\":13.133993037937158,\"dni\":0.0,\"dhi\":13.133993037937158}",
|
||||
"{\"date_time\":\"2024-11-06T17:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":22360.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.36757098972718,\"wind_speed\":6.1,\"wind_direction\":100.0,\"frost_chance\":null,\"temp_air\":6.1,\"feels_like\":null,\"dew_point\":3.8,\"relative_humidity\":85.0,\"pressure\":1032.09,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-06T18:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":17470.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.3829953007358158,\"wind_speed\":7.6,\"wind_direction\":110.0,\"frost_chance\":null,\"temp_air\":5.9,\"feels_like\":null,\"dew_point\":3.9,\"relative_humidity\":87.0,\"pressure\":1032.3,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-06T19:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":18050.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.3114348533433033,\"wind_speed\":2.2,\"wind_direction\":50.0,\"frost_chance\":null,\"temp_air\":5.6,\"feels_like\":null,\"dew_point\":3.1,\"relative_humidity\":84.0,\"pressure\":1032.59,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-06T20:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":16940.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.3112057726870359,\"wind_speed\":4.7,\"wind_direction\":50.0,\"frost_chance\":null,\"temp_air\":5.4,\"feels_like\":null,\"dew_point\":3.1,\"relative_humidity\":85.0,\"pressure\":1032.8,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-06T21:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":16260.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.3266317229539424,\"wind_speed\":5.8,\"wind_direction\":60.0,\"frost_chance\":null,\"temp_air\":5.4,\"feels_like\":null,\"dew_point\":3.3,\"relative_humidity\":86.0,\"pressure\":1033.2,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-06T22:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":14540.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.3112057726870359,\"wind_speed\":4.3,\"wind_direction\":60.0,\"frost_chance\":null,\"temp_air\":5.4,\"feels_like\":null,\"dew_point\":3.0,\"relative_humidity\":85.0,\"pressure\":1033.59,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-06T23:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":14150.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.2803293999126435,\"wind_speed\":2.9,\"wind_direction\":50.0,\"frost_chance\":null,\"temp_air\":5.2,\"feels_like\":null,\"dew_point\":2.8,\"relative_humidity\":84.0,\"pressure\":1033.7,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-07T00:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":17300.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.2878311958694741,\"wind_speed\":6.5,\"wind_direction\":70.0,\"frost_chance\":null,\"temp_air\":5.1,\"feels_like\":null,\"dew_point\":2.8,\"relative_humidity\":85.0,\"pressure\":1033.8,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-07T01:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":20750.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.2952024102120847,\"wind_speed\":6.5,\"wind_direction\":50.0,\"frost_chance\":null,\"temp_air\":5.0,\"feels_like\":null,\"dew_point\":2.9,\"relative_humidity\":86.0,\"pressure\":1033.7,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-07T02:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":19330.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.2725033091467384,\"wind_speed\":5.4,\"wind_direction\":40.0,\"frost_chance\":null,\"temp_air\":4.9,\"feels_like\":null,\"dew_point\":2.7,\"relative_humidity\":85.0,\"pressure\":1033.7,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-07T03:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":17310.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.2500209307860815,\"wind_speed\":5.0,\"wind_direction\":30.0,\"frost_chance\":null,\"temp_air\":5.0,\"feels_like\":null,\"dew_point\":2.4,\"relative_humidity\":83.0,\"pressure\":1033.59,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-07T04:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":28980.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.2878311958694741,\"wind_speed\":7.2,\"wind_direction\":80.0,\"frost_chance\":null,\"temp_air\":5.1,\"feels_like\":null,\"dew_point\":2.8,\"relative_humidity\":85.0,\"pressure\":1033.5,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-07T05:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":22600.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.2726802406239508,\"wind_speed\":7.6,\"wind_direction\":90.0,\"frost_chance\":null,\"temp_air\":5.1,\"feels_like\":null,\"dew_point\":2.7,\"relative_humidity\":84.0,\"pressure\":1033.4,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-07T06:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":21600.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.2952024102120847,\"wind_speed\":5.4,\"wind_direction\":40.0,\"frost_chance\":null,\"temp_air\":5.0,\"feels_like\":null,\"dew_point\":2.8,\"relative_humidity\":86.0,\"pressure\":1033.2,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-07T07:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":19120.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.2803293999126435,\"wind_speed\":5.8,\"wind_direction\":40.0,\"frost_chance\":null,\"temp_air\":5.2,\"feels_like\":null,\"dew_point\":2.7,\"relative_humidity\":84.0,\"pressure\":1033.3,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-07T08:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":16970.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.3112057726870359,\"wind_speed\":5.4,\"wind_direction\":30.0,\"frost_chance\":null,\"temp_air\":5.4,\"feels_like\":null,\"dew_point\":3.1,\"relative_humidity\":85.0,\"pressure\":1033.8,\"ozone\":null,\"ghi\":8.498732233274223,\"dni\":0.0,\"dhi\":8.498732233274223}",
|
||||
"{\"date_time\":\"2024-11-07T09:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":20190.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.3036333508504807,\"wind_speed\":6.8,\"wind_direction\":60.0,\"frost_chance\":null,\"temp_air\":5.7,\"feels_like\":null,\"dew_point\":3.1,\"relative_humidity\":83.0,\"pressure\":1034.2,\"ozone\":null,\"ghi\":48.08590935974426,\"dni\":0.0,\"dhi\":48.08590935974426}",
|
||||
"{\"date_time\":\"2024-11-07T10:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":20410.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.311382525535115,\"wind_speed\":6.1,\"wind_direction\":70.0,\"frost_chance\":null,\"temp_air\":6.0,\"feels_like\":null,\"dew_point\":3.1,\"relative_humidity\":82.0,\"pressure\":1034.4,\"ozone\":null,\"ghi\":86.13289253240517,\"dni\":0.0,\"dhi\":86.13289253240517}",
|
||||
"{\"date_time\":\"2024-11-07T11:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":23940.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.3032147078576655,\"wind_speed\":6.8,\"wind_direction\":60.0,\"frost_chance\":null,\"temp_air\":6.1,\"feels_like\":null,\"dew_point\":3.1,\"relative_humidity\":81.0,\"pressure\":1034.5,\"ozone\":null,\"ghi\":111.63587311647375,\"dni\":0.0,\"dhi\":111.63587311647375}",
|
||||
"{\"date_time\":\"2024-11-07T12:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":22710.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.3106141340118171,\"wind_speed\":7.9,\"wind_direction\":90.0,\"frost_chance\":null,\"temp_air\":6.4,\"feels_like\":null,\"dew_point\":3.3,\"relative_humidity\":80.0,\"pressure\":1034.0,\"ozone\":null,\"ghi\":121.12176907062565,\"dni\":0.0,\"dhi\":121.12176907062565}",
|
||||
"{\"date_time\":\"2024-11-07T13:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":22430.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.2701653045869423,\"wind_speed\":9.4,\"wind_direction\":80.0,\"frost_chance\":null,\"temp_air\":6.3,\"feels_like\":null,\"dew_point\":2.7,\"relative_humidity\":78.0,\"pressure\":1033.7,\"ozone\":null,\"ghi\":113.56192265137761,\"dni\":0.0,\"dhi\":113.56192265137761}",
|
||||
"{\"date_time\":\"2024-11-07T14:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":23210.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.3020641429763,\"wind_speed\":7.9,\"wind_direction\":50.0,\"frost_chance\":null,\"temp_air\":6.5,\"feels_like\":null,\"dew_point\":3.2,\"relative_humidity\":79.0,\"pressure\":1033.3,\"ozone\":null,\"ghi\":89.7641924158202,\"dni\":0.0,\"dhi\":89.7641924158202}",
|
||||
"{\"date_time\":\"2024-11-07T15:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":23220.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.2845185659293759,\"wind_speed\":7.2,\"wind_direction\":50.0,\"frost_chance\":null,\"temp_air\":6.7,\"feels_like\":null,\"dew_point\":3.0,\"relative_humidity\":77.0,\"pressure\":1032.9,\"ozone\":null,\"ghi\":52.82846758743223,\"dni\":0.0,\"dhi\":52.82846758743223}",
|
||||
"{\"date_time\":\"2024-11-07T16:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":24990.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.293366179132486,\"wind_speed\":8.3,\"wind_direction\":50.0,\"frost_chance\":null,\"temp_air\":6.6,\"feels_like\":null,\"dew_point\":3.1,\"relative_humidity\":78.0,\"pressure\":1032.8,\"ozone\":null,\"ghi\":12.137164591797857,\"dni\":0.0,\"dhi\":12.137164591797857}",
|
||||
"{\"date_time\":\"2024-11-07T17:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":23620.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.2855823183816633,\"wind_speed\":5.8,\"wind_direction\":60.0,\"frost_chance\":null,\"temp_air\":6.5,\"feels_like\":null,\"dew_point\":2.9,\"relative_humidity\":78.0,\"pressure\":1032.7,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-07T18:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":20330.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.2949042355874099,\"wind_speed\":5.0,\"wind_direction\":60.0,\"frost_chance\":null,\"temp_air\":6.2,\"feels_like\":null,\"dew_point\":3.1,\"relative_humidity\":80.0,\"pressure\":1032.8,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-07T19:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":23590.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.2625316296977247,\"wind_speed\":4.0,\"wind_direction\":70.0,\"frost_chance\":null,\"temp_air\":6.2,\"feels_like\":null,\"dew_point\":2.7,\"relative_humidity\":78.0,\"pressure\":1032.9,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-07T20:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":18610.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.3032147078576655,\"wind_speed\":6.1,\"wind_direction\":70.0,\"frost_chance\":null,\"temp_air\":6.1,\"feels_like\":null,\"dew_point\":3.1,\"relative_humidity\":81.0,\"pressure\":1033.0,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-07T21:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":15020.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.311382525535115,\"wind_speed\":9.0,\"wind_direction\":80.0,\"frost_chance\":null,\"temp_air\":6.0,\"feels_like\":null,\"dew_point\":3.1,\"relative_humidity\":82.0,\"pressure\":1033.2,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-07T22:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":14560.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.311382525535115,\"wind_speed\":9.4,\"wind_direction\":80.0,\"frost_chance\":null,\"temp_air\":6.0,\"feels_like\":null,\"dew_point\":3.1,\"relative_humidity\":82.0,\"pressure\":1033.4,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-07T23:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":14290.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.2956943683229234,\"wind_speed\":9.4,\"wind_direction\":60.0,\"frost_chance\":null,\"temp_air\":5.8,\"feels_like\":null,\"dew_point\":2.9,\"relative_humidity\":82.0,\"pressure\":1033.09,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-08T00:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":13230.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.2879269249366194,\"wind_speed\":9.0,\"wind_direction\":60.0,\"frost_chance\":null,\"temp_air\":5.7,\"feels_like\":null,\"dew_point\":2.8,\"relative_humidity\":82.0,\"pressure\":1033.09,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-08T01:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":10600.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.295822533660645,\"wind_speed\":6.8,\"wind_direction\":80.0,\"frost_chance\":null,\"temp_air\":5.6,\"feels_like\":null,\"dew_point\":2.9,\"relative_humidity\":83.0,\"pressure\":1033.0,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-08T02:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":10300.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.2726954899905094,\"wind_speed\":9.0,\"wind_direction\":50.0,\"frost_chance\":null,\"temp_air\":5.3,\"feels_like\":null,\"dew_point\":2.7,\"relative_humidity\":83.0,\"pressure\":1032.59,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-08T03:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":10800.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.2726802406239508,\"wind_speed\":9.0,\"wind_direction\":40.0,\"frost_chance\":null,\"temp_air\":5.1,\"feels_like\":null,\"dew_point\":2.7,\"relative_humidity\":84.0,\"pressure\":1032.0,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-08T04:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":10600.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.25003374788666,\"wind_speed\":9.0,\"wind_direction\":50.0,\"frost_chance\":null,\"temp_air\":4.8,\"feels_like\":null,\"dew_point\":2.3,\"relative_humidity\":84.0,\"pressure\":1031.5,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-08T05:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":11200.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.257532681980306,\"wind_speed\":11.9,\"wind_direction\":40.0,\"frost_chance\":null,\"temp_air\":4.9,\"feels_like\":null,\"dew_point\":2.4,\"relative_humidity\":84.0,\"pressure\":1031.4,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-08T06:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":11200.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.2351842404212265,\"wind_speed\":9.0,\"wind_direction\":30.0,\"frost_chance\":null,\"temp_air\":4.6,\"feels_like\":null,\"dew_point\":2.2,\"relative_humidity\":84.0,\"pressure\":1031.09,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-08T07:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":13800.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.22783313798752,\"wind_speed\":10.1,\"wind_direction\":30.0,\"frost_chance\":null,\"temp_air\":4.5,\"feels_like\":null,\"dew_point\":2.1,\"relative_humidity\":84.0,\"pressure\":1030.7,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-08T08:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":14300.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.2351842404212265,\"wind_speed\":9.0,\"wind_direction\":20.0,\"frost_chance\":null,\"temp_air\":4.6,\"feels_like\":null,\"dew_point\":2.1,\"relative_humidity\":84.0,\"pressure\":1030.59,\"ozone\":null,\"ghi\":7.618065280685061,\"dni\":0.0,\"dhi\":7.618065280685061}",
|
||||
"{\"date_time\":\"2024-11-08T09:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":13200.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.257532681980306,\"wind_speed\":2.9,\"wind_direction\":350.0,\"frost_chance\":null,\"temp_air\":4.9,\"feels_like\":null,\"dew_point\":2.5,\"relative_humidity\":84.0,\"pressure\":1030.5,\"ozone\":null,\"ghi\":46.52393962571433,\"dni\":0.0,\"dhi\":46.52393962571433}",
|
||||
"{\"date_time\":\"2024-11-08T10:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":12300.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.2952024102120847,\"wind_speed\":5.0,\"wind_direction\":40.0,\"frost_chance\":null,\"temp_air\":5.0,\"feels_like\":null,\"dew_point\":2.8,\"relative_humidity\":86.0,\"pressure\":1030.3,\"ozone\":null,\"ghi\":84.41073757319909,\"dni\":0.0,\"dhi\":84.41073757319909}",
|
||||
"{\"date_time\":\"2024-11-08T11:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":11900.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.3635859720970902,\"wind_speed\":6.8,\"wind_direction\":60.0,\"frost_chance\":null,\"temp_air\":5.1,\"feels_like\":null,\"dew_point\":3.6,\"relative_humidity\":90.0,\"pressure\":1030.2,\"ozone\":null,\"ghi\":109.86441465473348,\"dni\":0.0,\"dhi\":109.86441465473348}",
|
||||
"{\"date_time\":\"2024-11-08T12:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":9500.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.4695070274881938,\"wind_speed\":7.9,\"wind_direction\":70.0,\"frost_chance\":null,\"temp_air\":5.8,\"feels_like\":null,\"dew_point\":4.7,\"relative_humidity\":93.0,\"pressure\":1029.5,\"ozone\":null,\"ghi\":119.35043772145661,\"dni\":0.0,\"dhi\":119.35043772145661}",
|
||||
"{\"date_time\":\"2024-11-08T13:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":14500.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.498143692589727,\"wind_speed\":6.1,\"wind_direction\":70.0,\"frost_chance\":null,\"temp_air\":6.3,\"feels_like\":null,\"dew_point\":5.1,\"relative_humidity\":92.0,\"pressure\":1028.8,\"ozone\":null,\"ghi\":111.82801067882036,\"dni\":0.0,\"dhi\":111.82801067882036}",
|
||||
"{\"date_time\":\"2024-11-08T14:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":17100.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.1,\"preciptable_water\":1.4923455913067147,\"wind_speed\":11.2,\"wind_direction\":90.0,\"frost_chance\":null,\"temp_air\":6.6,\"feels_like\":null,\"dew_point\":5.1,\"relative_humidity\":90.0,\"pressure\":1028.4,\"ozone\":null,\"ghi\":88.1102014938565,\"dni\":0.0,\"dhi\":88.1102014938565}",
|
||||
"{\"date_time\":\"2024-11-08T15:00:00+01:00\",\"total_clouds\":88.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":10200.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.4801944829988296,\"wind_speed\":14.0,\"wind_direction\":130.0,\"frost_chance\":null,\"temp_air\":6.1,\"feels_like\":null,\"dew_point\":4.9,\"relative_humidity\":92.0,\"pressure\":1028.4,\"ozone\":null,\"ghi\":62.78155778969431,\"dni\":0.0,\"dhi\":62.78155778969431}",
|
||||
"{\"date_time\":\"2024-11-08T16:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":12000.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.456767265035836,\"wind_speed\":13.0,\"wind_direction\":120.0,\"frost_chance\":null,\"temp_air\":6.2,\"feels_like\":null,\"dew_point\":4.7,\"relative_humidity\":90.0,\"pressure\":1028.3,\"ozone\":null,\"ghi\":11.192374342113588,\"dni\":0.0,\"dhi\":11.192374342113588}",
|
||||
"{\"date_time\":\"2024-11-08T17:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":7900.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.4243946591461507,\"wind_speed\":11.2,\"wind_direction\":110.0,\"frost_chance\":null,\"temp_air\":6.2,\"feels_like\":null,\"dew_point\":4.4,\"relative_humidity\":88.0,\"pressure\":1028.2,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-08T18:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":9200.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.3913448746531096,\"wind_speed\":15.1,\"wind_direction\":130.0,\"frost_chance\":null,\"temp_air\":6.0,\"feels_like\":null,\"dew_point\":4.0,\"relative_humidity\":87.0,\"pressure\":1028.7,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-08T19:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":9500.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.3829953007358158,\"wind_speed\":9.0,\"wind_direction\":140.0,\"frost_chance\":null,\"temp_air\":5.9,\"feels_like\":null,\"dew_point\":3.9,\"relative_humidity\":87.0,\"pressure\":1028.59,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-08T20:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":10100.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.3588989716557491,\"wind_speed\":9.0,\"wind_direction\":140.0,\"frost_chance\":null,\"temp_air\":5.8,\"feels_like\":null,\"dew_point\":3.7,\"relative_humidity\":86.0,\"pressure\":1028.59,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-08T21:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":9900.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.3430978208225428,\"wind_speed\":9.0,\"wind_direction\":140.0,\"frost_chance\":null,\"temp_air\":5.8,\"feels_like\":null,\"dew_point\":3.5,\"relative_humidity\":85.0,\"pressure\":1028.59,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-08T22:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":17700.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.295822533660645,\"wind_speed\":10.1,\"wind_direction\":150.0,\"frost_chance\":null,\"temp_air\":5.6,\"feels_like\":null,\"dew_point\":3.0,\"relative_humidity\":83.0,\"pressure\":1028.7,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-08T23:00:00+01:00\",\"total_clouds\":100.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":15500.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":null,\"precip_amt\":0.0,\"preciptable_water\":1.2957798224201298,\"wind_speed\":10.1,\"wind_direction\":130.0,\"frost_chance\":null,\"temp_air\":5.4,\"feels_like\":null,\"dew_point\":2.9,\"relative_humidity\":84.0,\"pressure\":1028.7,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}",
|
||||
"{\"date_time\":\"2024-11-09T00:00:00+01:00\",\"total_clouds\":86.0,\"low_clouds\":null,\"medium_clouds\":null,\"high_clouds\":null,\"visibility\":8700.0,\"fog\":null,\"precip_type\":null,\"precip_prob\":1.0,\"precip_amt\":0.0,\"preciptable_water\":null,\"wind_speed\":5.5,\"wind_direction\":87.0,\"frost_chance\":null,\"temp_air\":5.3,\"feels_like\":null,\"dew_point\":2.8,\"relative_humidity\":null,\"pressure\":1027.4,\"ozone\":null,\"ghi\":0.0,\"dni\":0.0,\"dhi\":0.0}"
|
||||
]
|
2221
tests/testdata/weatherforecast_clearout_1.html
vendored
Normal file
2221
tests/testdata/weatherforecast_clearout_1.html
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
tests/testdata/weatherforecast_clearout_1.json
vendored
Normal file
1
tests/testdata/weatherforecast_clearout_1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user