# environment/smhi.py import logging from datetime import datetime import requests from tools.geometry import Coordinate, LineString, MultiPolygon, Polygon logger = logging.getLogger(__name__) class SMHI: fire_warning_api = "https://opendata-download-metfcst.smhi.se/api/category/fwif1g/version/1/daily/geotype/point/lon/{lon}/lat/{lat}/data.json" forecast_api = "https://opendata-download-metfcst.smhi.se/api/category/pmp3g/version/2/geotype/point/lon/{lon}/lat/{lat}/data.json" alerts_api = ( "https://opendata-download-warnings.smhi.se/ibww/api/version/1/warning.json" ) def __init__(self, location: Coordinate, session: requests.Session) -> None: self.location = location self._session = session self._data = {"fire_index": {}, "weather_alerts": {}, "forecast": {}} self._api = { "fire_index": self._get_fire_warning, "weather_alerts": self._get_alerts, "forecast": self._get_forecast, } def _calculate_area(self, area_type: str, area_list: list) -> bool: """Helper to check if our home coordinates fall within an SMHI warning zone.""" point = self.location.to_Point(reversed=True) match area_type: case "Polygon": return Polygon(area_list[0]).contains(point) case "LineString": return LineString(area_list).distance_to(point) <= 0.045 case "MultiPolygon": return MultiPolygon(area_list).contains(point) return False def _get_fire_warning(self) -> bool: try: resp = self._session.get( self.fire_warning_api.format( lon=self.location.long, lat=self.location.lat ), timeout=10, ) resp.raise_for_status() for param in resp.json()["timeSeries"][0]["parameters"]: if param["name"] == "fwiindex": self._data["fire_index"] = { "data": param["values"][0], "valid_time": datetime.now(), } return True except requests.RequestException as e: logger.error(f"Network error getting SMHI Fire index: {e}") except Exception as e: logger.error(f"Parsing error for SMHI Fire index: {e}") return False def _get_alerts(self) -> bool: try: resp = self._session.get(self.alerts_api, timeout=10) resp.raise_for_status() area_list = [] for warning in resp.json(): for areas in warning["warningAreas"]: is_in_area = False if areas["area"]["type"] == "FeatureCollection": for features in areas["area"]["features"]: if self._calculate_area( features["geometry"]["type"], features["geometry"]["coordinates"], ): is_in_area = True break elif self._calculate_area( areas["area"]["geometry"]["type"], areas["area"]["geometry"]["coordinates"], ): is_in_area = True if is_in_area: area_list.append( { "areaName": areas["areaName"]["sv"], "type": areas["warningLevel"]["code"], "description": areas["eventDescription"]["code"], } ) self._data["weather_alerts"] = { "data": area_list, "valid_time": datetime.now(), } return True except requests.RequestException as e: logger.error(f"Network error getting SMHI Alerts: {e}") except Exception as e: logger.error(f"Parsing error for SMHI Alerts: {e}") return False def _get_forecast(self) -> bool: try: resp = self._session.get( self.forecast_api.format(lon=self.location.long, lat=self.location.lat), timeout=10, ) resp.raise_for_status() data = resp.json()["timeSeries"][1] w_data = {x["name"]: x["values"][0] for x in data["parameters"]} self._data["forecast"] = { "data": { "wind": { "deg": w_data["wd"], "speed": round(w_data["ws"], 1), "gust": round(w_data["gust"], 1), }, "weather_symbol": w_data["Wsymb2"] - 1, }, "valid_time": datetime.now(), } return True except requests.RequestException as e: logger.error(f"Network error getting SMHI Forecast: {e}") except Exception as e: logger.error(f"Parsing error for SMHI Forecast: {e}") return False def get(self) -> dict: """Returns valid SMHI data, utilizing cached data if recently fetched.""" valid_data = {} for api_name, fetch_func in self._api.items(): cache = self._data[api_name] valid_time = cache.get("valid_time", datetime.fromtimestamp(0)) age_seconds = (datetime.now() - valid_time).total_seconds() if age_seconds <= 300: logger.debug(f'Using cached data for "{api_name}"') valid_data[api_name] = cache["data"] else: logger.debug(f'Fetching new data for "{api_name}"...') if fetch_func(): logger.debug(f'Successfully fetched new data for "{api_name}"') valid_data[api_name] = self._data[api_name]["data"] elif age_seconds <= 600: logger.warning( f'Failed fetching "{api_name}", falling back to 10-min cache.' ) valid_data[api_name] = cache["data"] else: logger.error( f'SMHI data "{api_name}" is too old and cannot be refreshed.' ) return valid_data