* Mypy: Initial support

 * Add to pre-commit (currently installs own deps, could maybe changed
   to poetry venv in the future to reuse environment and don't need
   duplicated types deps).
 * Add type hints.

* Mypy: Add missing annotations
This commit is contained in:
Dominique Lasserre
2024-11-26 22:28:05 +01:00
committed by GitHub
parent 2a163569bc
commit 1163ddb4ac
31 changed files with 637 additions and 531 deletions

View File

@@ -1,14 +1,15 @@
from datetime import datetime
from typing import Dict, List, Optional, Union
from typing import Any, Dict, Optional, Union
import numpy as np
from pydantic import BaseModel, Field, model_validator
from pydantic import BaseModel, Field, field_validator, model_validator
from typing_extensions import Self
from akkudoktoreos.config import EOSConfig
from akkudoktoreos.devices.battery import PVAkku
from akkudoktoreos.devices.generic import HomeAppliance
from akkudoktoreos.devices.inverter import Wechselrichter
from akkudoktoreos.utils.utils import NumpyEncoder
class EnergieManagementSystemParameters(BaseModel):
@@ -41,14 +42,67 @@ class EnergieManagementSystemParameters(BaseModel):
return self
class SimulationResult(BaseModel):
"""This object contains the results of the simulation and provides insights into various parameters over the entire forecast period."""
Last_Wh_pro_Stunde: list[Optional[float]] = Field(description="TBD")
EAuto_SoC_pro_Stunde: list[Optional[float]] = Field(
description="The state of charge of the EV for each hour."
)
Einnahmen_Euro_pro_Stunde: list[Optional[float]] = Field(
description="The revenue from grid feed-in or other sources in euros per hour."
)
Gesamt_Verluste: float = Field(
description="The total losses in watt-hours over the entire period."
)
Gesamtbilanz_Euro: float = Field(
description="The total balance of revenues minus costs in euros."
)
Gesamteinnahmen_Euro: float = Field(description="The total revenues in euros.")
Gesamtkosten_Euro: float = Field(description="The total costs in euros.")
Home_appliance_wh_per_hour: list[Optional[float]] = Field(
description="The energy consumption of a household appliance in watt-hours per hour."
)
Kosten_Euro_pro_Stunde: list[Optional[float]] = Field(
description="The costs in euros per hour."
)
Netzbezug_Wh_pro_Stunde: list[Optional[float]] = Field(
description="The grid energy drawn in watt-hours per hour."
)
Netzeinspeisung_Wh_pro_Stunde: list[Optional[float]] = Field(
description="The energy fed into the grid in watt-hours per hour."
)
Verluste_Pro_Stunde: list[Optional[float]] = Field(
description="The losses in watt-hours per hour."
)
akku_soc_pro_stunde: list[Optional[float]] = Field(
description="The state of charge of the battery (not the EV) in percentage per hour."
)
@field_validator(
"Last_Wh_pro_Stunde",
"Netzeinspeisung_Wh_pro_Stunde",
"akku_soc_pro_stunde",
"Netzbezug_Wh_pro_Stunde",
"Kosten_Euro_pro_Stunde",
"Einnahmen_Euro_pro_Stunde",
"EAuto_SoC_pro_Stunde",
"Verluste_Pro_Stunde",
"Home_appliance_wh_per_hour",
mode="before",
)
def convert_numpy(cls, field: Any) -> Any:
return NumpyEncoder.convert_numpy(field)[0]
class EnergieManagementSystem:
def __init__(
self,
config: EOSConfig,
parameters: EnergieManagementSystemParameters,
wechselrichter: Wechselrichter,
eauto: Optional[PVAkku] = None,
home_appliance: Optional[HomeAppliance] = None,
wechselrichter: Optional[Wechselrichter] = None,
):
self.akku = wechselrichter.akku
self.gesamtlast = np.array(parameters.gesamtlast, float)
@@ -66,7 +120,7 @@ class EnergieManagementSystem:
self.dc_charge_hours = np.full(config.prediction_hours, 1)
self.ev_charge_hours = np.full(config.prediction_hours, 0)
def set_akku_discharge_hours(self, ds: List[int]) -> None:
def set_akku_discharge_hours(self, ds: np.ndarray) -> None:
self.akku.set_discharge_per_hour(ds)
def set_akku_ac_charge_hours(self, ds: np.ndarray) -> None:
@@ -75,22 +129,24 @@ class EnergieManagementSystem:
def set_akku_dc_charge_hours(self, ds: np.ndarray) -> None:
self.dc_charge_hours = ds
def set_ev_charge_hours(self, ds: List[int]) -> None:
def set_ev_charge_hours(self, ds: np.ndarray) -> None:
self.ev_charge_hours = ds
def set_home_appliance_start(self, ds: List[int], global_start_hour: int = 0) -> None:
self.home_appliance.set_starting_time(ds, global_start_hour=global_start_hour)
def set_home_appliance_start(self, start_hour: int, global_start_hour: int = 0) -> None:
assert self.home_appliance is not None
self.home_appliance.set_starting_time(start_hour, global_start_hour=global_start_hour)
def reset(self) -> None:
self.eauto.reset()
if self.eauto:
self.eauto.reset()
self.akku.reset()
def simuliere_ab_jetzt(self) -> dict:
def simuliere_ab_jetzt(self) -> dict[str, Any]:
jetzt = datetime.now()
start_stunde = jetzt.hour
return self.simuliere(start_stunde)
def simuliere(self, start_stunde: int) -> dict:
def simuliere(self, start_stunde: int) -> dict[str, Any]:
"""hour.
akku_soc_pro_stunde begin of the hour, initial hour state!

View File

@@ -2,11 +2,13 @@ import numpy as np
class Gesamtlast:
def __init__(self, prediction_hours=24):
self.lasten = {} # Contains names and load arrays for different sources
def __init__(self, prediction_hours: int = 24):
self.lasten: dict[
str, np.ndarray
] = {} # Contains names and load arrays for different sources
self.prediction_hours = prediction_hours
def hinzufuegen(self, name, last_array):
def hinzufuegen(self, name: str, last_array: np.ndarray) -> None:
"""Adds an array of loads for a specific source.
:param name: Name of the load source (e.g., "Household", "Heat Pump")
@@ -16,13 +18,13 @@ class Gesamtlast:
raise ValueError(f"Total load inconsistent lengths in arrays: {name} {len(last_array)}")
self.lasten[name] = last_array
def gesamtlast_berechnen(self):
def gesamtlast_berechnen(self) -> np.ndarray:
"""Calculates the total load for each hour and returns an array of total loads.
:return: Array of total loads, where each entry corresponds to an hour
"""
if not self.lasten:
return []
return np.ndarray(0)
# Assumption: All load arrays have the same length
stunden = len(next(iter(self.lasten.values())))

View File

@@ -1,28 +1,30 @@
from datetime import datetime
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from sklearn.metrics import mean_squared_error, r2_score
from akkudoktoreos.prediction.load_forecast import LoadForecast
class LoadPredictionAdjuster:
def __init__(self, measured_data, predicted_data, load_forecast):
def __init__(
self, measured_data: pd.DataFrame, predicted_data: pd.DataFrame, load_forecast: LoadForecast
):
self.measured_data = measured_data
self.predicted_data = predicted_data
self.load_forecast = load_forecast
self.merged_data = self._merge_data()
self.train_data = None
self.test_data = None
self.weekday_diff = None
self.weekend_diff = None
def _remove_outliers(self, data, threshold=2):
def _remove_outliers(self, data: pd.DataFrame, threshold: int = 2) -> pd.DataFrame:
# Calculate the Z-Score of the 'Last' data
data["Z-Score"] = np.abs((data["Last"] - data["Last"].mean()) / data["Last"].std())
# Filter the data based on the threshold
filtered_data = data[data["Z-Score"] < threshold]
return filtered_data.drop(columns=["Z-Score"])
def _merge_data(self):
def _merge_data(self) -> pd.DataFrame:
# Convert the time column in both DataFrames to datetime
self.predicted_data["time"] = pd.to_datetime(self.predicted_data["time"])
self.measured_data["time"] = pd.to_datetime(self.measured_data["time"])
@@ -47,7 +49,9 @@ class LoadPredictionAdjuster:
merged_data["DayOfWeek"] = merged_data["time"].dt.dayofweek
return merged_data
def calculate_weighted_mean(self, train_period_weeks=9, test_period_weeks=1):
def calculate_weighted_mean(
self, train_period_weeks: int = 9, test_period_weeks: int = 1
) -> None:
self.merged_data = self._remove_outliers(self.merged_data)
train_end_date = self.merged_data["time"].max() - pd.Timedelta(weeks=test_period_weeks)
train_start_date = train_end_date - pd.Timedelta(weeks=train_period_weeks)
@@ -79,27 +83,27 @@ class LoadPredictionAdjuster:
weekends_train_data.groupby("Hour").apply(self._weighted_mean_diff).dropna()
)
def _weighted_mean_diff(self, data):
def _weighted_mean_diff(self, data: pd.DataFrame) -> float:
train_end_date = self.train_data["time"].max()
weights = 1 / (train_end_date - data["time"]).dt.days.replace(0, np.nan)
weighted_mean = (data["Difference"] * weights).sum() / weights.sum()
return weighted_mean
def adjust_predictions(self):
def adjust_predictions(self) -> None:
self.train_data["Adjusted Pred"] = self.train_data.apply(self._adjust_row, axis=1)
self.test_data["Adjusted Pred"] = self.test_data.apply(self._adjust_row, axis=1)
def _adjust_row(self, row):
def _adjust_row(self, row: pd.Series) -> pd.Series:
if row["DayOfWeek"] < 5:
return row["Last Pred"] + self.weekday_diff.get(row["Hour"], 0)
else:
return row["Last Pred"] + self.weekend_diff.get(row["Hour"], 0)
def plot_results(self):
def plot_results(self) -> None:
self._plot_data(self.train_data, "Training")
self._plot_data(self.test_data, "Testing")
def _plot_data(self, data, data_type):
def _plot_data(self, data: pd.DataFrame, data_type: str) -> None:
plt.figure(figsize=(14, 7))
plt.plot(data["time"], data["Last"], label=f"Actual Last - {data_type}", color="blue")
plt.plot(
@@ -123,13 +127,13 @@ class LoadPredictionAdjuster:
plt.grid(True)
plt.show()
def evaluate_model(self):
def evaluate_model(self) -> None:
mse = mean_squared_error(self.test_data["Last"], self.test_data["Adjusted Pred"])
r2 = r2_score(self.test_data["Last"], self.test_data["Adjusted Pred"])
print(f"Mean Squared Error: {mse}")
print(f"R-squared: {r2}")
def predict_next_hours(self, hours_ahead):
def predict_next_hours(self, hours_ahead: int) -> pd.DataFrame:
last_date = self.merged_data["time"].max()
future_dates = [last_date + pd.Timedelta(hours=i) for i in range(1, hours_ahead + 1)]
future_df = pd.DataFrame({"time": future_dates})
@@ -139,7 +143,7 @@ class LoadPredictionAdjuster:
future_df["Adjusted Pred"] = future_df.apply(self._adjust_row, axis=1)
return future_df
def _forecast_next_hours(self, timestamp):
def _forecast_next_hours(self, timestamp: datetime) -> float:
date_str = timestamp.strftime("%Y-%m-%d")
hour = timestamp.hour
daily_forecast = self.load_forecast.get_daily_stats(date_str)

View File

@@ -1,4 +1,5 @@
from datetime import datetime
from pathlib import Path
import numpy as np
@@ -6,14 +7,12 @@ import numpy as np
class LoadForecast:
def __init__(self, filepath=None, year_energy=None):
def __init__(self, filepath: str | Path, year_energy: float):
self.filepath = filepath
self.data = None
self.data_year_energy = None
self.year_energy = year_energy
self.load_data()
def get_daily_stats(self, date_str):
def get_daily_stats(self, date_str: str) -> np.ndarray:
"""Returns the 24-hour profile with mean and standard deviation for a given date.
:param date_str: Date as a string in the format "YYYY-MM-DD"
@@ -29,7 +28,7 @@ class LoadForecast:
daily_stats = self.data_year_energy[day_of_year - 1] # -1 because indexing starts at 0
return daily_stats
def get_hourly_stats(self, date_str, hour):
def get_hourly_stats(self, date_str: str, hour: int) -> np.ndarray:
"""Returns the mean and standard deviation for a specific hour of a given date.
:param date_str: Date as a string in the format "YYYY-MM-DD"
@@ -47,7 +46,7 @@ class LoadForecast:
return hourly_stats
def get_stats_for_date_range(self, start_date_str, end_date_str):
def get_stats_for_date_range(self, start_date_str: str, end_date_str: str) -> np.ndarray:
"""Returns the means and standard deviations for a date range.
:param start_date_str: Start date as a string in the format "YYYY-MM-DD"
@@ -69,7 +68,7 @@ class LoadForecast:
stats_for_range = stats_for_range.reshape(stats_for_range.shape[0], -1)
return stats_for_range
def load_data(self):
def load_data(self) -> None:
"""Loads data from the specified file."""
try:
data = np.load(self.filepath)
@@ -81,11 +80,12 @@ class LoadForecast:
except Exception as e:
print(f"An error occurred while loading data: {e}")
def get_price_data(self):
def get_price_data(self) -> None:
"""Returns price data (currently not implemented)."""
return self.price_data
raise NotImplementedError
# return self.price_data
def _convert_to_datetime(self, date_str):
def _convert_to_datetime(self, date_str: str) -> datetime:
"""Converts a date string to a datetime object."""
return datetime.strptime(date_str, "%Y-%m-%d")

View File

@@ -3,6 +3,7 @@ import json
import zoneinfo
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any, Sequence
import numpy as np
import requests
@@ -10,7 +11,7 @@ import requests
from akkudoktoreos.config import AppConfig, SetupIncomplete
def repeat_to_shape(array, target_shape):
def repeat_to_shape(array: np.ndarray, target_shape: Sequence[int]) -> np.ndarray:
# Check if the array fits the target shape
if len(target_shape) != array.ndim:
raise ValueError("Array and target shape must have the same number of dimensions")
@@ -25,7 +26,11 @@ def repeat_to_shape(array, target_shape):
class HourlyElectricityPriceForecast:
def __init__(
self, source: str | Path, config: AppConfig, charges=0.000228, use_cache=True
self,
source: str | Path,
config: AppConfig,
charges: float = 0.000228,
use_cache: bool = True,
): # 228
self.cache_dir = config.working_dir / config.directories.cache
self.use_cache = use_cache
@@ -37,7 +42,7 @@ class HourlyElectricityPriceForecast:
self.charges = charges
self.prediction_hours = config.eos.prediction_hours
def load_data(self, source: str | Path):
def load_data(self, source: str | Path) -> list[dict[str, Any]]:
cache_file = self.get_cache_file(source)
if isinstance(source, str):
if cache_file.is_file() and not self.is_cache_expired() and self.use_cache:
@@ -61,12 +66,14 @@ class HourlyElectricityPriceForecast:
raise ValueError(f"Input is not a valid path: {source}")
return json_data["values"]
def get_cache_file(self, url):
def get_cache_file(self, url: str | Path) -> Path:
if isinstance(url, Path):
url = str(url)
hash_object = hashlib.sha256(url.encode())
hex_dig = hash_object.hexdigest()
return self.cache_dir / f"cache_{hex_dig}.json"
def is_cache_expired(self):
def is_cache_expired(self) -> bool:
if not self.cache_time_file.is_file():
return True
with self.cache_time_file.open("r") as file:
@@ -74,11 +81,11 @@ class HourlyElectricityPriceForecast:
last_cache_time = datetime.strptime(timestamp_str, "%Y-%m-%d %H:%M:%S")
return datetime.now() - last_cache_time > timedelta(hours=1)
def update_cache_timestamp(self):
def update_cache_timestamp(self) -> None:
with self.cache_time_file.open("w") as file:
file.write(datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
def get_price_for_date(self, date_str):
def get_price_for_date(self, date_str: str) -> np.ndarray:
"""Returns all prices for the specified date, including the price from 00:00 of the previous day."""
# Convert date string to datetime object
date_obj = datetime.strptime(date_str, "%Y-%m-%d")
@@ -108,7 +115,7 @@ class HourlyElectricityPriceForecast:
return np.array(date_prices) / (1000.0 * 100.0) + self.charges
def get_price_for_daterange(self, start_date_str, end_date_str):
def get_price_for_daterange(self, start_date_str: str, end_date_str: str) -> np.ndarray:
"""Returns all prices between the start and end dates."""
print(start_date_str)
print(end_date_str)
@@ -117,7 +124,7 @@ class HourlyElectricityPriceForecast:
start_date = start_date_utc.astimezone(zoneinfo.ZoneInfo("Europe/Berlin"))
end_date = end_date_utc.astimezone(zoneinfo.ZoneInfo("Europe/Berlin"))
price_list = []
price_list: list[float] = []
while start_date < end_date:
date_str = start_date.strftime("%Y-%m-%d")
@@ -127,8 +134,10 @@ class HourlyElectricityPriceForecast:
price_list.extend(daily_prices)
start_date += timedelta(days=1)
price_list_np = np.array(price_list)
# If prediction hours are greater than 0, reshape the price list
if self.prediction_hours > 0:
price_list = repeat_to_shape(np.array(price_list), (self.prediction_hours,))
price_list_np = repeat_to_shape(price_list_np, (self.prediction_hours,))
return price_list
return price_list_np

View File

@@ -21,7 +21,7 @@ Example:
)
# Update the AC power measurement for a specific date and time
forecast.update_ac_power_measurement(date_time=datetime.now(), ac_power_measurement=1000)
forecast.update_ac_power_measurement(ac_power_measurement=1000, date_time=datetime.now())
# Print the forecast data with DC and AC power details
forecast.print_ac_power_and_measurement()
@@ -36,7 +36,8 @@ Attributes:
import json
from datetime import date, datetime
from typing import List, Optional, Union
from pathlib import Path
from typing import Any, List, Optional, Union
import numpy as np
import pandas as pd
@@ -89,21 +90,20 @@ class AkkudoktorForecast(BaseModel):
values: List[List[AkkudoktorForecastValue]]
def validate_pv_forecast_data(data) -> str:
def validate_pv_forecast_data(data: dict[str, Any]) -> Optional[str]:
"""Validate PV forecast data."""
data_type = None
error_msg = ""
try:
AkkudoktorForecast.model_validate(data)
data_type = "Akkudoktor"
except ValidationError as e:
error_msg = ""
for error in e.errors():
field = " -> ".join(str(x) for x in error["loc"])
message = error["msg"]
error_type = error["type"]
error_msg += f"Field: {field}\nError: {message}\nType: {error_type}\n"
logger.debug(f"Validation did not succeed: {error_msg}")
return None
return data_type
@@ -167,7 +167,7 @@ class ForecastData:
"""
return self.dc_power
def ac_power_measurement(self) -> float:
def get_ac_power_measurement(self) -> Optional[float]:
"""Returns the measured AC power.
It returns the measured AC power if available; otherwise None.
@@ -191,7 +191,7 @@ class ForecastData:
else:
return self.ac_power
def get_windspeed_10m(self) -> float:
def get_windspeed_10m(self) -> Optional[float]:
"""Returns the wind speed at 10 meters altitude.
Returns:
@@ -199,7 +199,7 @@ class ForecastData:
"""
return self.windspeed_10m
def get_temperature(self) -> float:
def get_temperature(self) -> Optional[float]:
"""Returns the temperature.
Returns:
@@ -227,10 +227,10 @@ class PVForecast:
def __init__(
self,
data: Optional[dict] = None,
filepath: Optional[str] = None,
data: Optional[dict[str, Any]] = None,
filepath: Optional[str | Path] = None,
url: Optional[str] = None,
forecast_start: Union[datetime, date, str, int, float] = None,
forecast_start: Union[datetime, date, str, int, float, None] = None,
prediction_hours: Optional[int] = None,
):
"""Initializes a `PVForecast` instance.
@@ -253,16 +253,15 @@ class PVForecast:
Example:
forecast = PVForecast(data=my_forecast_data, forecast_start="2024-10-13", prediction_hours=72)
"""
self.meta = {}
self.forecast_data = []
self.current_measurement = None
self.meta: dict[str, Any] = {}
self.forecast_data: list[ForecastData] = []
self.current_measurement: Optional[float] = None
self.data = data
self.filepath = filepath
self.url = url
self._forecast_start: Optional[datetime] = None
if forecast_start:
self._forecast_start = to_datetime(forecast_start, to_naiv=True, to_maxtime=False)
else:
self._forecast_start = None
self.prediction_hours = prediction_hours
self._tz_name = None
@@ -277,8 +276,8 @@ class PVForecast:
def update_ac_power_measurement(
self,
ac_power_measurement: float,
date_time: Union[datetime, date, str, int, float, None] = None,
ac_power_measurement=None,
) -> bool:
"""Updates the AC power measurement for a specific time.
@@ -309,10 +308,10 @@ class PVForecast:
def process_data(
self,
data: Optional[dict] = None,
filepath: Optional[str] = None,
data: Optional[dict[str, Any]] = None,
filepath: Optional[str | Path] = None,
url: Optional[str] = None,
forecast_start: Union[datetime, date, str, int, float] = None,
forecast_start: Union[datetime, date, str, int, float, None] = None,
prediction_hours: Optional[int] = None,
) -> None:
"""Processes the forecast data from the provided source (in-memory `data`, `filepath`, or `url`).
@@ -368,6 +367,7 @@ class PVForecast:
) # Invalid path
else:
raise ValueError("No prediction input data available.")
assert data is not None # make mypy happy
# Validate input data to be of a known format
data_format = validate_pv_forecast_data(data)
if data_format != "Akkudoktor":
@@ -390,7 +390,7 @@ class PVForecast:
# --------------------------------------------
# From here Akkudoktor PV forecast data format
# ---------------------------------------------
self.meta = data.get("meta")
self.meta = data.get("meta", {})
all_values = data.get("values")
# timezone of the PV system
@@ -454,7 +454,7 @@ class PVForecast:
self._forecast_start = self.forecast_data[0].get_date_time()
logger.debug(f"Forecast start adapted to {self._forecast_start}")
def load_data_from_file(self, filepath: str) -> dict:
def load_data_from_file(self, filepath: str | Path) -> dict[str, Any]:
"""Loads forecast data from a file.
Args:
@@ -467,7 +467,7 @@ class PVForecast:
data = json.load(file)
return data
def load_data_from_url(self, url: str) -> dict:
def load_data_from_url(self, url: str) -> dict[str, Any]:
"""Loads forecast data from a URL.
Example:
@@ -488,7 +488,7 @@ class PVForecast:
return data
@cache_in_file() # use binary mode by default as we have python objects not text
def load_data_from_url_with_caching(self, url: str, until_date=None) -> dict:
def load_data_from_url_with_caching(self, url: str) -> dict[str, Any]:
"""Loads data from a URL or from the cache if available.
Args:
@@ -506,7 +506,7 @@ class PVForecast:
logger.error(data)
return data
def get_forecast_data(self):
def get_forecast_data(self) -> list[ForecastData]:
"""Returns the forecast data.
Returns:
@@ -516,7 +516,7 @@ class PVForecast:
def get_temperature_forecast_for_date(
self, input_date: Union[datetime, date, str, int, float, None]
):
) -> np.ndarray:
"""Returns the temperature forecast for a specific date.
Args:
@@ -543,7 +543,7 @@ class PVForecast:
self,
start_date: Union[datetime, date, str, int, float, None],
end_date: Union[datetime, date, str, int, float, None],
):
) -> np.ndarray:
"""Returns the PV forecast for a date range.
Args:
@@ -575,7 +575,7 @@ class PVForecast:
self,
start_date: Union[datetime, date, str, int, float, None],
end_date: Union[datetime, date, str, int, float, None],
):
) -> np.ndarray:
"""Returns the temperature forecast for a given date range.
Args:
@@ -601,7 +601,7 @@ class PVForecast:
temperature_forecast = [data.get_temperature() for data in date_range_forecast]
return np.array(temperature_forecast)[: self.prediction_hours]
def get_forecast_dataframe(self):
def get_forecast_dataframe(self) -> pd.DataFrame:
"""Converts the forecast data into a Pandas DataFrame.
Returns:
@@ -623,7 +623,7 @@ class PVForecast:
df = pd.DataFrame(data)
return df
def get_forecast_start(self) -> datetime:
def get_forecast_start(self) -> Optional[datetime]:
"""Return the start of the forecast data in local timezone.
Returns:
@@ -678,5 +678,5 @@ if __name__ == "__main__":
"past_days=5&cellCoEff=-0.36&inverterEfficiency=0.8&albedo=0.25&timezone=Europe%2FBerlin&"
"hourly=relativehumidity_2m%2Cwindspeed_10m",
)
forecast.update_ac_power_measurement(date_time=datetime.now(), ac_power_measurement=1000)
forecast.update_ac_power_measurement(ac_power_measurement=1000, date_time=datetime.now())
print(forecast.report_ac_power_and_measurement())