feature: Pv forecast refactoring (#354)

* refactoring
This commit is contained in:
Normann 2025-01-12 00:00:14 +01:00 committed by GitHub
parent b43bf105aa
commit 1bf49c8c52
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -201,25 +201,38 @@ class PVForecastAkkudoktor(PVForecastProvider):
def _url(self) -> str: def _url(self) -> str:
"""Build akkudoktor.net API request URL.""" """Build akkudoktor.net API request URL."""
url = f"https://api.akkudoktor.net/forecast?lat={self.config.latitude}&lon={self.config.longitude}&" base_url = "https://api.akkudoktor.net/forecast"
planes_peakpower = self.config.pvforecast_planes_peakpower query_params = [
planes_azimuth = self.config.pvforecast_planes_azimuth f"lat={self.config.latitude}",
planes_tilt = self.config.pvforecast_planes_tilt f"lon={self.config.longitude}",
planes_inverter_paco = self.config.pvforecast_planes_inverter_paco ]
planes_userhorizon = self.config.pvforecast_planes_userhorizon
for i, plane in enumerate(self.config.pvforecast_planes): for i in range(len(self.config.pvforecast_planes)):
url += f"power={int(planes_peakpower[i]*1000)}&" query_params.append(f"power={int(self.config.pvforecast_planes_peakpower[i] * 1000)}")
url += f"azimuth={int(planes_azimuth[i])}&" query_params.append(f"azimuth={int(self.config.pvforecast_planes_azimuth[i])}")
url += f"tilt={int(planes_tilt[i])}&" query_params.append(f"tilt={int(self.config.pvforecast_planes_tilt[i])}")
url += f"powerInverter={int(planes_inverter_paco[i])}&" query_params.append(
url += "horizont=" f"powerInverter={int(self.config.pvforecast_planes_inverter_paco[i])}"
for horizon in planes_userhorizon[i]: )
url += f"{int(horizon)}," horizon_values = ",".join(
url = url[:-1] # remove trailing comma str(int(h)) for h in self.config.pvforecast_planes_userhorizon[i]
url += "&" )
url += "past_days=5&cellCoEff=-0.36&inverterEfficiency=0.8&albedo=0.25&" query_params.append(f"horizont={horizon_values}")
url += f"timezone={self.config.timezone}&"
url += "hourly=relativehumidity_2m%2Cwindspeed_10m" # Append fixed query parameters
query_params.extend(
[
"past_days=5",
"cellCoEff=-0.36",
"inverterEfficiency=0.8",
"albedo=0.25",
f"timezone={self.config.timezone}",
"hourly=relativehumidity_2m%2Cwindspeed_10m",
]
)
# Join all query parameters with `&`
url = f"{base_url}?{'&'.join(query_params)}"
logger.debug(f"Akkudoktor URL: {url}") logger.debug(f"Akkudoktor URL: {url}")
return url return url
@ -252,7 +265,7 @@ class PVForecastAkkudoktor(PVForecastProvider):
`PVForecastAkkudoktorDataRecord`. `PVForecastAkkudoktorDataRecord`.
""" """
# Assure we have something to request PV power for. # Assure we have something to request PV power for.
if len(self.config.pvforecast_planes) == 0: if not self.config.pvforecast_planes:
# No planes for PV # No planes for PV
error_msg = "Requested PV forecast, but no planes configured." error_msg = "Requested PV forecast, but no planes configured."
logger.error(f"Configuration error: {error_msg}") logger.error(f"Configuration error: {error_msg}")
@ -269,34 +282,36 @@ class PVForecastAkkudoktor(PVForecastProvider):
# Assumption that all lists are the same length and are ordered chronologically # Assumption that all lists are the same length and are ordered chronologically
# in ascending order and have the same timestamps. # in ascending order and have the same timestamps.
values_len = len(akkudoktor_data.values[0]) if len(akkudoktor_data.values[0]) < self.config.prediction_hours:
if values_len < self.config.prediction_hours:
# Expect one value set per prediction hour # Expect one value set per prediction hour
error_msg = ( error_msg = (
f"The forecast must cover at least {self.config.prediction_hours} hours, " f"The forecast must cover at least {self.config.prediction_hours} hours, "
f"but only {values_len} data sets are given in forecast data." f"but only {len(akkudoktor_data.values[0])} data sets are given in forecast data."
) )
logger.error(f"Akkudoktor schema change: {error_msg}") logger.error(f"Akkudoktor schema change: {error_msg}")
raise ValueError(error_msg) raise ValueError(error_msg)
for i in range(values_len): assert self.start_datetime # mypy fix
original_datetime = akkudoktor_data.values[0][i].datetime
# Iterate over forecast data points
for forecast_values in zip(*akkudoktor_data.values):
original_datetime = forecast_values[0].datetime
dt = to_datetime(original_datetime, in_timezone=self.config.timezone) dt = to_datetime(original_datetime, in_timezone=self.config.timezone)
# We provide prediction starting at start of day, to be compatible to old system. # Skip outdated forecast data
if compare_datetimes(dt, self.start_datetime.start_of("day")).lt: if compare_datetimes(dt, self.start_datetime.start_of("day")).lt:
# forecast data is too old
continue continue
sum_dc_power = sum(values[i].dcPower for values in akkudoktor_data.values) sum_dc_power = sum(values.dcPower for values in forecast_values)
sum_ac_power = sum(values[i].power for values in akkudoktor_data.values) sum_ac_power = sum(values.power for values in forecast_values)
data = { data = {
"pvforecast_dc_power": sum_dc_power, "pvforecast_dc_power": sum_dc_power,
"pvforecast_ac_power": sum_ac_power, "pvforecast_ac_power": sum_ac_power,
"pvforecastakkudoktor_wind_speed_10m": akkudoktor_data.values[0][i].windspeed_10m, "pvforecastakkudoktor_wind_speed_10m": forecast_values[0].windspeed_10m,
"pvforecastakkudoktor_temp_air": akkudoktor_data.values[0][i].temperature, "pvforecastakkudoktor_temp_air": forecast_values[0].temperature,
} }
self.update_value(dt, data) self.update_value(dt, data)
if len(self) < self.config.prediction_hours: if len(self) < self.config.prediction_hours:
@ -307,35 +322,37 @@ class PVForecastAkkudoktor(PVForecastProvider):
) )
def report_ac_power_and_measurement(self) -> str: def report_ac_power_and_measurement(self) -> str:
"""Report DC/ AC power, and AC power measurement for each forecast hour. """Generate a report of DC power, forecasted AC power, measured AC power, and other AC power values.
For each forecast entry, the time, DC power, forecasted AC power, measured AC power For each forecast entry, the following details are included:
(if available), and the value returned by the `get_ac_power` method is provided. - Time of the forecast
- DC power
- Forecasted AC power
- Measured AC power (if available)
- Value returned by `get_ac_power` (if available)
Returns: Returns:
str: The report. str: A formatted report containing details for each forecast entry.
""" """
rep = ""
def format_value(value: float | None) -> str:
"""Helper to format values as rounded strings or 'N/A' if None."""
return f"{round(value, 2)}" if value is not None else "N/A"
report_lines = []
for record in self.records: for record in self.records:
date_time = record.date_time date_time = record.date_time
dc_pow = round(record.pvforecast_dc_power, 2) if record.pvforecast_dc_power else None dc_power = format_value(record.pvforecast_dc_power)
ac_pow = round(record.pvforecast_ac_power, 2) if record.pvforecast_ac_power else None ac_power = format_value(record.pvforecast_ac_power)
ac_pow_measurement = ( ac_power_measured = format_value(record.pvforecastakkudoktor_ac_power_measured)
round(record.pvforecastakkudoktor_ac_power_measured, 2) ac_power_any = format_value(record.pvforecastakkudoktor_ac_power_any)
if record.pvforecastakkudoktor_ac_power_measured
else None report_lines.append(
f"Date&Time: {date_time}, DC: {dc_power}, AC: {ac_power}, "
f"AC sampled: {ac_power_measured}, AC any: {ac_power_any}"
) )
ac_pow_any = (
round(record.pvforecastakkudoktor_ac_power_any, 2) return "\n".join(report_lines)
if record.pvforecastakkudoktor_ac_power_any
else None
)
rep += (
f"Date&Time: {date_time}, DC: {dc_pow}, AC: {ac_pow}, "
f"AC sampled: {ac_pow_measurement}, AC any: {ac_pow_any}"
"\n"
)
return rep
# Example of how to use the PVForecastAkkudoktor class # Example of how to use the PVForecastAkkudoktor class