feat: implement interactive temperature chart dashboard with runway trend visualization and multi-source data collection support
This commit is contained in:
@@ -32,6 +32,15 @@ def _safe_float(value: Any) -> Optional[float]:
|
||||
return None
|
||||
|
||||
|
||||
def _display_temp_from_c(temp_c: Any, use_fahrenheit: bool) -> Optional[float]:
|
||||
numeric = _safe_float(temp_c)
|
||||
if numeric is None:
|
||||
return None
|
||||
if use_fahrenheit:
|
||||
return round(numeric * 9.0 / 5.0 + 32.0, 1)
|
||||
return round(numeric, 1)
|
||||
|
||||
|
||||
def _parse_obs_datetime(
|
||||
value: Any,
|
||||
epoch_value: Any = None,
|
||||
@@ -248,11 +257,18 @@ def _airport_primary_from_raw(city: str, raw: Dict[str, Any]) -> Dict[str, Any]:
|
||||
|
||||
# High-frequency realtime sources take priority over plain METAR.
|
||||
madis = raw.get("madis_hfmetar_current") or {}
|
||||
if madis.get("temp_c") is not None:
|
||||
madis_temp_c = _safe_float(madis.get("temp_c"))
|
||||
madis_temp = _safe_float(madis.get("temp"))
|
||||
if madis_temp is None and madis_temp_c is not None:
|
||||
madis_temp = _display_temp_from_c(
|
||||
madis_temp_c,
|
||||
bool(meta.get("use_fahrenheit")),
|
||||
)
|
||||
if madis_temp is not None:
|
||||
return _normalize_station_row(
|
||||
station_code=meta.get("icao") or madis.get("icao"),
|
||||
station_label=meta.get("airport_name") or meta.get("icao"),
|
||||
temp=madis["temp_c"],
|
||||
temp=madis_temp,
|
||||
obs_time=madis.get("obs_time") or metar.get("observation_time"),
|
||||
source_code="madis_hfmetar",
|
||||
source_label="NOAA MADIS",
|
||||
@@ -260,6 +276,8 @@ def _airport_primary_from_raw(city: str, raw: Dict[str, Any]) -> Dict[str, Any]:
|
||||
is_airport_station=True,
|
||||
is_settlement_anchor=False,
|
||||
extra={
|
||||
"temp_c": madis_temp_c,
|
||||
"unit": "fahrenheit" if bool(meta.get("use_fahrenheit")) else "celsius",
|
||||
"max_so_far": _safe_float(current.get("max_temp_so_far")),
|
||||
"max_temp_time": current.get("max_temp_time"),
|
||||
"obs_age_min": None,
|
||||
|
||||
@@ -925,6 +925,16 @@ class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSour
|
||||
def _uses_fahrenheit(self, city_lower: str) -> bool:
|
||||
return city_lower in self.US_CITIES
|
||||
|
||||
@staticmethod
|
||||
def _display_temp_from_c(temp_c: Any, use_fahrenheit: bool) -> Optional[float]:
|
||||
try:
|
||||
numeric = float(temp_c)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
if use_fahrenheit:
|
||||
return round(numeric * 9.0 / 5.0 + 32.0, 1)
|
||||
return round(numeric, 1)
|
||||
|
||||
def _supports_aviationweather(self, city_lower: str) -> bool:
|
||||
city_meta = self.CITY_REGISTRY.get(str(city_lower or "").strip().lower(), {}) or {}
|
||||
settlement_source = str(city_meta.get("settlement_source") or "").strip().lower()
|
||||
@@ -1451,7 +1461,7 @@ class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSour
|
||||
logger.warning("AMSC AWOS attach failed city={}: {}", city_lower, exc)
|
||||
|
||||
def _attach_madis_hfmetar_data(
|
||||
self, results: Dict, city_lower: str
|
||||
self, results: Dict, city_lower: str, use_fahrenheit: bool
|
||||
) -> None:
|
||||
"""Fetch MADIS HFMETAR data and attach matching US station observations."""
|
||||
# Only run for US cities (ICAO starts with K)
|
||||
@@ -1476,19 +1486,33 @@ class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSour
|
||||
# Find this city's station in the MADIS results
|
||||
for obs in obs_list:
|
||||
if obs["icao"] == icao:
|
||||
temp_c = self._display_temp_from_c(obs.get("temp_c"), False)
|
||||
display_temp = self._display_temp_from_c(
|
||||
temp_c,
|
||||
use_fahrenheit,
|
||||
)
|
||||
if temp_c is None or display_temp is None:
|
||||
continue
|
||||
display_unit = "fahrenheit" if use_fahrenheit else "celsius"
|
||||
# Inject into results so it flows through to airport_primary
|
||||
results["madis_hfmetar_current"] = obs
|
||||
results["madis_hfmetar_current"] = {
|
||||
**obs,
|
||||
"temp_c": temp_c,
|
||||
"temp": display_temp,
|
||||
"unit": display_unit,
|
||||
}
|
||||
self._emit_temperature_patch_if_changed(
|
||||
city_lower,
|
||||
obs.get("temp_c"),
|
||||
display_temp,
|
||||
obs.get("obs_time"),
|
||||
source="madis_hfmetar",
|
||||
extra={"temp_c": temp_c, "unit": display_unit},
|
||||
)
|
||||
try:
|
||||
DBManager().append_airport_obs(
|
||||
icao=icao,
|
||||
city=city_lower,
|
||||
temp_c=obs["temp_c"],
|
||||
temp_c=temp_c,
|
||||
wind_kt=obs.get("wind_kt"),
|
||||
pressure_hpa=obs.get("pressure_hpa"),
|
||||
obs_time=obs["obs_time"] or datetime.now().isoformat(),
|
||||
@@ -1686,7 +1710,7 @@ class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSour
|
||||
)
|
||||
self._attach_korean_amos_data(results, city_lower, use_fahrenheit)
|
||||
self._attach_china_amsc_awos_data(results, city_lower, use_fahrenheit)
|
||||
self._attach_madis_hfmetar_data(results, city_lower)
|
||||
self._attach_madis_hfmetar_data(results, city_lower, use_fahrenheit)
|
||||
self._attach_singapore_mss_data(results, city_lower)
|
||||
self._attach_israel_ims_data(results, city_lower)
|
||||
self._attach_saudi_ncm_data(results, city_lower)
|
||||
@@ -1738,7 +1762,7 @@ class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSour
|
||||
)
|
||||
self._attach_korean_amos_data(results, city_lower, use_fahrenheit)
|
||||
self._attach_china_amsc_awos_data(results, city_lower, use_fahrenheit)
|
||||
self._attach_madis_hfmetar_data(results, city_lower)
|
||||
self._attach_madis_hfmetar_data(results, city_lower, use_fahrenheit)
|
||||
self._attach_singapore_mss_data(results, city_lower)
|
||||
self._attach_israel_ims_data(results, city_lower)
|
||||
self._attach_saudi_ncm_data(results, city_lower)
|
||||
|
||||
Reference in New Issue
Block a user