|
23 | 23 | from pvlib.pvsystem import PVSystem |
24 | 24 | from pvlib.temperature import TEMPERATURE_MODEL_PARAMETERS |
25 | 25 | from requests import get |
| 26 | +from requests.exceptions import RequestException |
26 | 27 |
|
27 | 28 | from sklearn.pipeline import make_pipeline |
28 | 29 | from sklearn.preprocessing import StandardScaler |
@@ -215,6 +216,92 @@ def __init__( |
215 | 216 | 0 : self.params["passed_data"]["prediction_horizon"] |
216 | 217 | ] |
217 | 218 |
|
| 219 | + def get_cached_open_meteo_forecast_json( |
| 220 | + self, |
| 221 | + max_age: Optional[int] = 30, |
| 222 | + ) -> dict: |
| 223 | + r""" |
| 224 | + Get weather forecast json from Open-Meteo and cache it for re-use. |
| 225 | + The response json is cached in the local file system and returned |
| 226 | + on subsequent calls until it is older than max_age, at which point |
| 227 | + attempts will be made to replace it with a new version. |
| 228 | + The cached version will not be overwritten until a new version has |
| 229 | + been successfully fetched from Open-Meteo. |
| 230 | + In the event of connectivity issues, the cached version will continue |
| 231 | + to be returned until such time as a new version can be successfully |
| 232 | + fetched from Open-Meteo. |
| 233 | + If you want to force reload, pass max_age value of zero. |
| 234 | +
|
| 235 | + :param max_age: The maximum age of the cached json file, in minutes, |
| 236 | + before it is discarded and a new version fetched from Open-Meteo. |
| 237 | + Defaults to 30 minutes. |
| 238 | + :type max_age: int, optional |
| 239 | + :return: The json containing the Open-Meteo forecast data |
| 240 | + :rtype: dict |
| 241 | +
|
| 242 | + """ |
| 243 | + json_path = os.path.abspath( |
| 244 | + self.emhass_conf["data_path"] / "cached-open-meteo-forecast.json" |
| 245 | + ) |
| 246 | + # The cached JSON file is always loaded, if it exists, as it is also a fallback |
| 247 | + # in case the REST API call to Open-Meteo fails - the cached JSON will continue to |
| 248 | + # be used until it can successfully fetch a new version from Open-Meteo. |
| 249 | + data = None |
| 250 | + use_cache = False |
| 251 | + if os.path.exists(json_path): |
| 252 | + delta = datetime.now() - datetime.fromtimestamp(os.path.getmtime(json_path)) |
| 253 | + json_age = int(delta / timedelta(seconds=60)) |
| 254 | + use_cache = json_age < max_age |
| 255 | + self.logger.info("Loading existing cached Open-Meteo JSON file: %s", json_path) |
| 256 | + with open(json_path) as json_file: |
| 257 | + data = json.load(json_file) |
| 258 | + if use_cache: |
| 259 | + self.logger.info("The cached Open-Meteo JSON file is recent (age=%.0fm, max_age=%sm)", |
| 260 | + json_age, max_age) |
| 261 | + else: |
| 262 | + self.logger.info("The cached Open-Meteo JSON file is old (age=%.0fm, max_age=%sm)", |
| 263 | + json_age, max_age) |
| 264 | + |
| 265 | + if not use_cache: |
| 266 | + self.logger.info("Fetching a new weather forecast from Open-Meteo") |
| 267 | + headers = {"User-Agent": "EMHASS", "Accept": "application/json"} |
| 268 | + url = ( |
| 269 | + "https://api.open-meteo.com/v1/forecast?" |
| 270 | + + "latitude=" |
| 271 | + + str(round(self.lat, 2)) |
| 272 | + + "&longitude=" |
| 273 | + + str(round(self.lon, 2)) |
| 274 | + + "&minutely_15=" |
| 275 | + + "temperature_2m," |
| 276 | + + "relative_humidity_2m," |
| 277 | + + "rain," |
| 278 | + + "cloud_cover," |
| 279 | + + "wind_speed_10m," |
| 280 | + + "shortwave_radiation_instant," |
| 281 | + + "diffuse_radiation_instant," |
| 282 | + + "direct_normal_irradiance_instant" |
| 283 | + + "&timezone=" |
| 284 | + + quote(str(self.time_zone), safe="") |
| 285 | + ) |
| 286 | + try: |
| 287 | + response = get(url, headers=headers) |
| 288 | + self.logger.debug("Returned HTTP status code: %s", response.status_code) |
| 289 | + response.raise_for_status() |
| 290 | + """import bz2 # Uncomment to save a serialized data for tests |
| 291 | + import _pickle as cPickle |
| 292 | + with bz2.BZ2File("data/test_response_openmeteo_get_method.pbz2", "w") as f: |
| 293 | + cPickle.dump(response, f)""" |
| 294 | + data = response.json() |
| 295 | + self.logger.info("Saving response in Open-Meteo JSON cache file: %s", json_path) |
| 296 | + with open(json_path, "w") as json_file: |
| 297 | + json.dump(response.json(), json_file, indent=2) |
| 298 | + except RequestException: |
| 299 | + self.logger.error("Failed to fetch weather forecast from Open-Meteo", exc_info=True) |
| 300 | + if data is not None: |
| 301 | + self.logger.warning("Returning old cached data until next Open-Meteo attempt") |
| 302 | + |
| 303 | + return data |
| 304 | + |
218 | 305 | def get_weather_forecast( |
219 | 306 | self, |
220 | 307 | method: Optional[str] = "open-meteo", |
@@ -248,32 +335,7 @@ def get_weather_forecast( |
248 | 335 | method == "open-meteo" or method == "scrapper" |
249 | 336 | ): # The scrapper option is being left here for backward compatibility |
250 | 337 | if not os.path.isfile(w_forecast_cache_path): |
251 | | - headers = {"User-Agent": "EMHASS", "Accept": "application/json"} |
252 | | - url = ( |
253 | | - "https://api.open-meteo.com/v1/forecast?" |
254 | | - + "latitude=" |
255 | | - + str(round(self.lat, 2)) |
256 | | - + "&longitude=" |
257 | | - + str(round(self.lon, 2)) |
258 | | - + "&minutely_15=" |
259 | | - + "temperature_2m," |
260 | | - + "relative_humidity_2m," |
261 | | - + "rain," |
262 | | - + "cloud_cover," |
263 | | - + "wind_speed_10m," |
264 | | - + "shortwave_radiation_instant," |
265 | | - + "diffuse_radiation_instant," |
266 | | - + "direct_normal_irradiance_instant" |
267 | | - + "&timezone=" |
268 | | - + quote(str(self.time_zone), safe="") |
269 | | - ) |
270 | | - response = get(url, headers=headers) |
271 | | - """import bz2 # Uncomment to save a serialized data for tests |
272 | | - import _pickle as cPickle |
273 | | - with bz2.BZ2File("data/test_response_openmeteo_get_method.pbz2", "w") as f: |
274 | | - cPickle.dump(response, f)""" |
275 | | - |
276 | | - data_raw = response.json() |
| 338 | + data_raw = self.get_cached_open_meteo_forecast_json(self.optim_conf["open_meteo_cache_max_age"]) |
277 | 339 | data_15min = pd.DataFrame.from_dict(data_raw["minutely_15"]) |
278 | 340 | data_15min["time"] = pd.to_datetime(data_15min["time"]) |
279 | 341 | data_15min.set_index("time", inplace=True) |
|
0 commit comments