Initial commit
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user