"""FMI (Finnish Meteorological Institute) open data source. Fetches 10-minute airport weather observations from opendata.fmi.fi for Helsinki-Vantaa airport (EFHK, WMO 2974, FMISID 100968). """ from __future__ import annotations import re import os import time from datetime import datetime from typing import Any, Dict, Optional from loguru import logger from src.utils.metrics import record_source_call FMI_BASE_URL = os.getenv("FMI_BASE_URL", "").strip() or "https://opendata.fmi.fi/wfs" FMI_STATION = { "helsinki": { "fmisid": "100968", "wmo": "2974", "icao": "EFHK", "label": "Helsinki-Vantaa 10min (FMI)", "query_place": "helsinki-vantaa_airport", }, } class FmiSourceMixin: def _fmi_http_get_text(self, url: str) -> str: getter = getattr(self, "_http_get", None) if callable(getter): response = getter(url) else: response = self.session.get(url, timeout=self.timeout) response.raise_for_status() return response.text def fetch_fmi_current( self, city: str, use_fahrenheit: bool = False, ) -> Optional[Dict[str, Any]]: started = time.perf_counter() city_key = str(city or "").strip().lower() meta = FMI_STATION.get(city_key) or {} if not meta: record_source_call("fmi", "current", "unsupported_city", (time.perf_counter() - started) * 1000.0) return None cache_key = f"fmi:{city_key}:{use_fahrenheit}" now_ts = time.time() with self._fmi_cache_lock: cached = self._fmi_cache.get(cache_key) if cached and now_ts - cached["t"] < self.fmi_cache_ttl_sec: record_source_call("fmi", "current", "cache_hit", (time.perf_counter() - started) * 1000.0) return cached["d"] try: from datetime import timezone as tz place = meta["query_place"] end_time = datetime.now(tz.utc) start_time = end_time.replace(minute=end_time.minute // 10 * 10, second=0, microsecond=0) end_str = end_time.strftime("%Y-%m-%dT%H:%M:%SZ") start_str = (start_time - __import__("datetime").timedelta(minutes=20)).strftime( "%Y-%m-%dT%H:%M:%SZ" ) url = ( f"{FMI_BASE_URL}?service=WFS&version=2.0.0&request=getFeature" f"&storedquery_id=fmi::observations::weather::timevaluepair" f"&place={place}" f"¶meters=t2m,ws_10min,p_sea" f"&starttime={start_str}&endtime={end_str}" ) xml_text = self._fmi_http_get_text(url) # Parse per-parameter observation blocks blocks = re.split(r".*?(.*?)\s*(.*?)", block, re.DOTALL, ) if tvps: latest_time, latest_val = tvps[-1] obs_time = latest_time try: latest_values[param] = float(latest_val) except (ValueError, TypeError): pass temp_c = latest_values.get("t2m") wind_ms = latest_values.get("ws_10min") pressure_hpa = latest_values.get("p_sea") if temp_c is None: record_source_call("fmi", "current", "no_temperature", (time.perf_counter() - started) * 1000.0) return None # Convert wind from m/s to kt wind_kt = round(wind_ms * 1.94384, 1) if wind_ms is not None else None temp = round(temp_c * 9 / 5 + 32, 1) if use_fahrenheit else round(temp_c, 1) result = { "source": "fmi", "timestamp": datetime.utcnow().isoformat(), "station_code": meta["fmisid"], "station_name": meta["label"], "wmo": meta["wmo"], "icao": meta["icao"], "obs_time": obs_time, "current": { "temp": temp, }, "temp_c": temp_c, "wind_kt": wind_kt, "pressure_hpa": pressure_hpa, } with self._fmi_cache_lock: self._fmi_cache[cache_key] = {"d": result, "t": now_ts} record_source_call("fmi", "current", "success", (time.perf_counter() - started) * 1000.0) return result except Exception as exc: logger.warning("FMI current fetch failed city={} error={}", city_key, exc) with self._fmi_cache_lock: stale = self._fmi_cache.get(cache_key) if stale: record_source_call("fmi", "current", "stale_cache", (time.perf_counter() - started) * 1000.0) return stale["d"] record_source_call("fmi", "current", "error", (time.perf_counter() - started) * 1000.0) return None def fetch_fmi_official_nearby( self, city: str, use_fahrenheit: bool = False, ) -> list[Dict[str, Any]]: current = self.fetch_fmi_current(city, use_fahrenheit=use_fahrenheit) if not current: return [] meta = FMI_STATION.get(str(city or "").strip().lower()) or {} return [ { "name": meta.get("label") or "Helsinki-Vantaa 10min (FMI)", "station_label": meta.get("label"), "lat": 60.32937, "lon": 24.97274, "temp": (current.get("current") or {}).get("temp"), "icao": meta.get("icao"), "istNo": meta.get("fmisid"), "source": "fmi", "source_label": "FMI", "obs_time": current.get("obs_time"), } ]