feat: implement AMOS real-time weather data collection and add corresponding unit and frontend tests
This commit is contained in:
@@ -10,7 +10,7 @@ import re
|
||||
import os
|
||||
import time
|
||||
from html import unescape
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from loguru import logger
|
||||
@@ -75,6 +75,42 @@ def _amos_extract_metar_qnh(metar_line: str) -> Optional[float]:
|
||||
return None
|
||||
|
||||
|
||||
def _amos_extract_observation_time(text: str, icao: str) -> tuple[Optional[str], Optional[str]]:
|
||||
"""Return AMOS page timestamp as (UTC ISO, KST local display)."""
|
||||
icao_pattern = re.escape(str(icao or "").strip().upper())
|
||||
patterns = []
|
||||
if icao_pattern:
|
||||
patterns.append(
|
||||
rf"\({icao_pattern}\)\s*(\d{{4}})년\s*(\d{{1,2}})월\s*(\d{{1,2}})일\s*(\d{{1,2}}):(\d{{2}})\s*KST"
|
||||
)
|
||||
patterns.append(
|
||||
r"(\d{4})년\s*(\d{1,2})월\s*(\d{1,2})일\s*(\d{1,2}):(\d{2})\s*KST"
|
||||
)
|
||||
|
||||
for pattern in patterns:
|
||||
match = re.search(pattern, text or "", re.I)
|
||||
if not match:
|
||||
continue
|
||||
try:
|
||||
year, month, day, hour, minute = [int(part) for part in match.groups()[:5]]
|
||||
local_dt = datetime(
|
||||
year,
|
||||
month,
|
||||
day,
|
||||
hour,
|
||||
minute,
|
||||
tzinfo=timezone(timedelta(hours=9)),
|
||||
)
|
||||
utc_dt = local_dt.astimezone(timezone.utc).replace(microsecond=0)
|
||||
return (
|
||||
utc_dt.isoformat().replace("+00:00", "Z"),
|
||||
local_dt.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
return None, None
|
||||
|
||||
|
||||
def _amos_extract_metar_wind(metar_line: str) -> Optional[float]:
|
||||
"""Extract wind speed in knots from METAR like '22014KT'."""
|
||||
match = re.search(r"\b(\d{3})(\d{2,3})KT\b", metar_line)
|
||||
@@ -513,6 +549,14 @@ class AmosStationSourceMixin:
|
||||
mid = len(sorted_p) // 2
|
||||
pressure_hpa = float(sorted_p[mid]) if len(sorted_p) % 2 else float((sorted_p[mid-1] + sorted_p[mid]) / 2)
|
||||
|
||||
observation_time, observation_time_local = _amos_extract_observation_time(html, icao)
|
||||
if not observation_time:
|
||||
now_utc = datetime.now(timezone.utc).replace(microsecond=0)
|
||||
observation_time = now_utc.isoformat().replace("+00:00", "Z")
|
||||
observation_time_local = now_utc.astimezone(
|
||||
timezone(timedelta(hours=9))
|
||||
).strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
temp = round(temp_c * 9 / 5 + 32, 1) if use_fahrenheit and temp_c is not None else temp_c
|
||||
dew = round(dew_c * 9 / 5 + 32, 1) if use_fahrenheit and dew_c is not None else dew_c
|
||||
|
||||
@@ -541,7 +585,8 @@ class AmosStationSourceMixin:
|
||||
"runway_obs": runway_data if runway_data.get("temperatures") else None,
|
||||
"observation_source": "AMOS runway sensors",
|
||||
"observation_source_zh": "AMOS 跑道传感器",
|
||||
"observation_time": datetime.now().strftime("%Y-%m-%dT%H:%M:%S"),
|
||||
"observation_time": observation_time,
|
||||
"observation_time_local": observation_time_local,
|
||||
}
|
||||
|
||||
# Compute runway temp range for compact display ("14.6~15.2")
|
||||
|
||||
Reference in New Issue
Block a user