fix: use settlement runway endpoint temperatures
This commit is contained in:
@@ -33,11 +33,25 @@ AMSC_AWOS_AIRPORTS: Dict[str, Dict[str, str]] = {
|
||||
"qingdao": {"icao": "ZSQD", "label": "Qingdao Jiaodong"},
|
||||
}
|
||||
|
||||
AMSC_SETTLEMENT_RUNWAY_TARGETS: Dict[str, str] = {
|
||||
"shanghai": "35R",
|
||||
"chengdu": "02L",
|
||||
"chongqing": "02L",
|
||||
"guangzhou": "02L",
|
||||
"wuhan": "04",
|
||||
"beijing": "01",
|
||||
"qingdao": "34",
|
||||
}
|
||||
|
||||
|
||||
def _amsc_supported_city_codes() -> Dict[str, str]:
|
||||
return {city: meta["icao"] for city, meta in AMSC_AWOS_AIRPORTS.items()}
|
||||
|
||||
|
||||
def _amsc_normalize_runway(value: Any) -> str:
|
||||
return str(value or "").strip().upper().replace(" ", "")
|
||||
|
||||
|
||||
def _amsc_safe_float(value: Any) -> Optional[float]:
|
||||
if value is None:
|
||||
return None
|
||||
@@ -54,13 +68,43 @@ def _amsc_safe_float(value: Any) -> Optional[float]:
|
||||
|
||||
|
||||
def _amsc_split_runway_pair(label: str) -> tuple[str, str]:
|
||||
parts = [part.strip() for part in str(label or "").split("/") if part.strip()]
|
||||
parts = [_amsc_normalize_runway(part) for part in str(label or "").split("/") if part.strip()]
|
||||
if len(parts) >= 2:
|
||||
return parts[0], parts[1]
|
||||
runway = str(label or "").strip() or "--"
|
||||
runway = _amsc_normalize_runway(label) or "--"
|
||||
return runway, runway
|
||||
|
||||
|
||||
def _amsc_settlement_endpoint(
|
||||
city_key: str,
|
||||
runway_pair: tuple[str, str],
|
||||
*,
|
||||
tdz_temp: Optional[float],
|
||||
end_temp: Optional[float],
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
target = _amsc_normalize_runway(AMSC_SETTLEMENT_RUNWAY_TARGETS.get(city_key))
|
||||
if not target:
|
||||
return None
|
||||
first = _amsc_normalize_runway(runway_pair[0])
|
||||
second = _amsc_normalize_runway(runway_pair[1])
|
||||
if target == first:
|
||||
temp = tdz_temp if tdz_temp is not None else end_temp
|
||||
position = "tdz" if tdz_temp is not None else "end_fallback"
|
||||
elif target == second:
|
||||
temp = end_temp if end_temp is not None else tdz_temp
|
||||
position = "end" if end_temp is not None else "tdz_fallback"
|
||||
else:
|
||||
return None
|
||||
if temp is None:
|
||||
return None
|
||||
return {
|
||||
"runway": target,
|
||||
"pair": f"{first}/{second}",
|
||||
"position": position,
|
||||
"temp": temp,
|
||||
}
|
||||
|
||||
|
||||
def _amsc_wind_dir(*candidates: Any) -> Optional[int]:
|
||||
"""Parse wind direction from AMSC fields (degrees)."""
|
||||
for value in candidates:
|
||||
@@ -138,8 +182,24 @@ def _amsc_parse_wind_plate_payload(
|
||||
if raw_metar is None and raw_row.get("METAR"):
|
||||
raw_metar = str(raw_row.get("METAR"))
|
||||
|
||||
runway_pairs.append(_amsc_split_runway_pair(runway_label))
|
||||
best_temp = tdz if tdz is not None else max(points)
|
||||
runway_pair = _amsc_split_runway_pair(runway_label)
|
||||
settlement_endpoint = _amsc_settlement_endpoint(
|
||||
city_key,
|
||||
runway_pair,
|
||||
tdz_temp=tdz,
|
||||
end_temp=end,
|
||||
)
|
||||
runway_pairs.append(runway_pair)
|
||||
target_temp = (
|
||||
settlement_endpoint["temp"]
|
||||
if settlement_endpoint is not None
|
||||
else max(points)
|
||||
)
|
||||
best_temp = (
|
||||
settlement_endpoint["temp"]
|
||||
if settlement_endpoint is not None
|
||||
else tdz if tdz is not None else max(points)
|
||||
)
|
||||
runway_temps.append((best_temp, None))
|
||||
valid_values.extend(points)
|
||||
|
||||
@@ -167,21 +227,29 @@ def _amsc_parse_wind_plate_payload(
|
||||
raw_row.get("TDZ_HUMID") or raw_row.get("END_HUMID") or raw_row.get("MID_HUMID")
|
||||
)
|
||||
|
||||
target_max = max(points)
|
||||
point_temperatures.append(
|
||||
{
|
||||
"runway": runway_label,
|
||||
"tdz_temp": tdz,
|
||||
"mid_temp": mid,
|
||||
"end_temp": end,
|
||||
"target_runway_max": target_max,
|
||||
"wind_dir": wind_dir,
|
||||
"wind_speed": wind_speed,
|
||||
"rvr": rvr,
|
||||
"mor": mor,
|
||||
"humidity": humidity,
|
||||
}
|
||||
)
|
||||
point = {
|
||||
"runway": f"{runway_pair[0]}/{runway_pair[1]}",
|
||||
"temp": target_temp,
|
||||
"tdz_temp": tdz,
|
||||
"mid_temp": mid,
|
||||
"end_temp": end,
|
||||
"target_runway_max": target_temp,
|
||||
"wind_dir": wind_dir,
|
||||
"wind_speed": wind_speed,
|
||||
"rvr": rvr,
|
||||
"mor": mor,
|
||||
"humidity": humidity,
|
||||
}
|
||||
if settlement_endpoint is not None:
|
||||
point.update(
|
||||
{
|
||||
"is_settlement": True,
|
||||
"settlement_runway": settlement_endpoint["runway"],
|
||||
"settlement_runway_position": settlement_endpoint["position"],
|
||||
"settlement_runway_temp": settlement_endpoint["temp"],
|
||||
}
|
||||
)
|
||||
point_temperatures.append(point)
|
||||
|
||||
if not valid_values or not runway_pairs:
|
||||
return None
|
||||
@@ -189,11 +257,28 @@ def _amsc_parse_wind_plate_payload(
|
||||
max_temp = round(max(valid_values), 1)
|
||||
min_temp = round(min(valid_values), 1)
|
||||
avg_temp = round(sum(valid_values) / len(valid_values), 1)
|
||||
settlement_point = next(
|
||||
(
|
||||
point
|
||||
for point in point_temperatures
|
||||
if point.get("is_settlement") and point.get("settlement_runway_temp") is not None
|
||||
),
|
||||
None,
|
||||
)
|
||||
display_temp = (
|
||||
round(float(settlement_point["settlement_runway_temp"]), 1)
|
||||
if settlement_point is not None
|
||||
else max_temp
|
||||
)
|
||||
|
||||
return {
|
||||
"temp": max_temp,
|
||||
"temp_c": max_temp,
|
||||
"temp_source": "runway_max",
|
||||
"temp": display_temp,
|
||||
"temp_c": display_temp,
|
||||
"temp_source": "settlement_runway_endpoint" if settlement_point else "runway_max",
|
||||
"settlement_runway": settlement_point.get("settlement_runway") if settlement_point else None,
|
||||
"settlement_runway_pair": settlement_point.get("runway") if settlement_point else None,
|
||||
"settlement_runway_position": settlement_point.get("settlement_runway_position") if settlement_point else None,
|
||||
"settlement_runway_temp": display_temp if settlement_point else None,
|
||||
"runway_temps": runway_temps,
|
||||
"runway_temp_range": (min_temp, max_temp),
|
||||
"runway_temp_avg": avg_temp,
|
||||
|
||||
+127
-22
@@ -619,9 +619,20 @@ SETTLEMENT_RUNWAY_PAIRS: Dict[str, Set[Tuple[str, str]]] = {
|
||||
"chengdu": {("02L", "20R")},
|
||||
"chongqing": {("02L", "20R")},
|
||||
"wuhan": {("04", "22")},
|
||||
"qingdao": {("16", "34")},
|
||||
"seoul": {("15R", "33L")},
|
||||
}
|
||||
|
||||
SETTLEMENT_RUNWAY_TARGETS: Dict[str, str] = {
|
||||
"shanghai": "35R",
|
||||
"chengdu": "02L",
|
||||
"chongqing": "02L",
|
||||
"guangzhou": "02L",
|
||||
"wuhan": "04",
|
||||
"beijing": "01",
|
||||
"qingdao": "34",
|
||||
}
|
||||
|
||||
# All cities with active runway observation data (AMSC AWOS / AMOS).
|
||||
RUNWAY_OBSERVATION_CITIES = {
|
||||
"shanghai", "beijing", "guangzhou",
|
||||
@@ -736,6 +747,94 @@ def _focus_runway_pairs_for_city(city: str) -> Set[Tuple[str, str]]:
|
||||
return {_runway_pair_key(a, b) for a, b in FOCUS_RUNWAY_PAIRS.get(city, set())}
|
||||
|
||||
|
||||
def _settlement_runway_target_for_city(city: str) -> str:
|
||||
city_key = (city or "").strip().lower()
|
||||
return _normalize_runway_label(SETTLEMENT_RUNWAY_TARGETS.get(city_key))
|
||||
|
||||
|
||||
def _runway_pair_from_point(pair: Any, point: Any) -> Tuple[str, str]:
|
||||
if isinstance(point, dict):
|
||||
rw = str(point.get("runway") or "")
|
||||
parts = [_normalize_runway_label(p) for p in rw.split("/") if p.strip()]
|
||||
if len(parts) >= 2:
|
||||
return parts[0], parts[1]
|
||||
try:
|
||||
r1, r2 = pair
|
||||
return _normalize_runway_label(r1), _normalize_runway_label(r2)
|
||||
except Exception:
|
||||
return "", ""
|
||||
|
||||
|
||||
def _settlement_endpoint_for_point(
|
||||
city: str,
|
||||
pair: Any,
|
||||
point: Any,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
if not isinstance(point, dict):
|
||||
point = {}
|
||||
r1, r2 = _runway_pair_from_point(pair, point)
|
||||
if not r1 or not r2 or not _is_settlement_runway(city, r1, r2):
|
||||
return None
|
||||
|
||||
target = _settlement_runway_target_for_city(city)
|
||||
if target:
|
||||
direct_temp = _safe_float(point.get("settlement_runway_temp"))
|
||||
direct_runway = _normalize_runway_label(point.get("settlement_runway"))
|
||||
if direct_temp is not None and (not direct_runway or direct_runway == target):
|
||||
return {
|
||||
"temp": direct_temp,
|
||||
"pair": f"{r1}/{r2}",
|
||||
"runway": target,
|
||||
"position": point.get("settlement_runway_position") or "settlement",
|
||||
"label": "settle",
|
||||
}
|
||||
|
||||
tdz = _safe_float(point.get("tdz_temp"))
|
||||
end = _safe_float(point.get("end_temp"))
|
||||
if target == r1:
|
||||
temp = tdz if tdz is not None else end
|
||||
position = "tdz" if tdz is not None else "end_fallback"
|
||||
elif target == r2:
|
||||
temp = end if end is not None else tdz
|
||||
position = "end" if end is not None else "tdz_fallback"
|
||||
else:
|
||||
return None
|
||||
if temp is None:
|
||||
return None
|
||||
return {
|
||||
"temp": temp,
|
||||
"pair": f"{r1}/{r2}",
|
||||
"runway": target,
|
||||
"position": position,
|
||||
"label": "settle",
|
||||
}
|
||||
|
||||
tmax = _safe_float(point.get("target_runway_max"))
|
||||
if tmax is None:
|
||||
return None
|
||||
return {
|
||||
"temp": tmax,
|
||||
"pair": f"{r1}/{r2}",
|
||||
"runway": "",
|
||||
"position": "max",
|
||||
"label": "max",
|
||||
}
|
||||
|
||||
|
||||
def _settlement_endpoint_from_obs(
|
||||
city: str,
|
||||
runway_pairs: List[Any],
|
||||
point_temps: Optional[List[Any]] = None,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
points = point_temps or []
|
||||
for i, pair in enumerate(runway_pairs or []):
|
||||
point = points[i] if i < len(points) else {}
|
||||
endpoint = _settlement_endpoint_for_point(city, pair, point)
|
||||
if endpoint is not None:
|
||||
return endpoint
|
||||
return None
|
||||
|
||||
|
||||
def _select_focus_runway_obs(
|
||||
city: str,
|
||||
runway_pairs: List[Any],
|
||||
@@ -849,6 +948,9 @@ def _focused_runway_max(city: str, city_weather: Dict[str, Any]) -> Optional[flo
|
||||
runway_temps,
|
||||
runway_obs.get("point_temperatures") or [],
|
||||
)
|
||||
endpoint = _settlement_endpoint_from_obs(city, runway_pairs, _points)
|
||||
if endpoint is not None:
|
||||
return float(endpoint["temp"])
|
||||
del runway_pairs
|
||||
valid = [float(t) for (t, _d) in runway_temps if t is not None]
|
||||
return max(valid) if valid else None
|
||||
@@ -1016,21 +1118,12 @@ def _build_airport_status_message(
|
||||
has_runway = bool(runway_pairs and (runway_temps or point_temps))
|
||||
amos_icao = amos.get("icao") or HIGH_FREQ_AIRPORT_ICAO.get(city, "")
|
||||
settlement_pair = _settlement_runway_for_city(city)
|
||||
settlement_endpoint = _settlement_endpoint_from_obs(city, runway_pairs, point_temps)
|
||||
|
||||
# ── Display temp: settlement runway max first, then airport temp ──
|
||||
settlement_temp: Optional[float] = None
|
||||
# ── Display temp: settlement endpoint first, then airport temp ──
|
||||
display_temp: Optional[float] = None
|
||||
if point_temps:
|
||||
for pt in point_temps:
|
||||
rw = str(pt.get("runway") or "")
|
||||
rw_parts = [p.strip() for p in rw.split("/") if p.strip()]
|
||||
if settlement_pair and len(rw_parts) >= 2 and _runway_pair_key(rw_parts[0], rw_parts[1]) == _runway_pair_key(*settlement_pair):
|
||||
tmax = pt.get("target_runway_max")
|
||||
if tmax is not None:
|
||||
settlement_temp = float(tmax)
|
||||
break
|
||||
if settlement_temp is not None:
|
||||
display_temp = settlement_temp
|
||||
if settlement_endpoint is not None:
|
||||
display_temp = float(settlement_endpoint["temp"])
|
||||
if display_temp is None:
|
||||
if point_temps:
|
||||
valid_tmax = [float(p.get("target_runway_max")) for p in point_temps if p.get("target_runway_max") is not None]
|
||||
@@ -1069,7 +1162,12 @@ def _build_airport_status_message(
|
||||
language=language,
|
||||
)
|
||||
icao_display = f"{amos_icao} · " if amos_icao else ""
|
||||
settlement_str = f" · ★{settlement_pair[0]}/{settlement_pair[1]}" if settlement_pair else ""
|
||||
settlement_pair_label = (
|
||||
str(settlement_endpoint.get("pair"))
|
||||
if settlement_endpoint is not None and settlement_endpoint.get("pair")
|
||||
else (f"{settlement_pair[0]}/{settlement_pair[1]}" if settlement_pair else "")
|
||||
)
|
||||
settlement_str = f" · ★{settlement_pair_label}" if settlement_pair_label else ""
|
||||
header = f"{icao_display}{en_name} / {ap_name}{settlement_str}{time_suffix}" if ap_name else f"{icao_display}{en_name}{settlement_str}{time_suffix}"
|
||||
lines.append(hashtag_line)
|
||||
lines.append("")
|
||||
@@ -1093,12 +1191,15 @@ def _build_airport_status_message(
|
||||
mid = pts.get("mid_temp")
|
||||
end = pts.get("end_temp")
|
||||
is_settlement = _is_settlement_runway(city, r1, r2)
|
||||
marker = _copy(language, " ★Settlement", " ★结算") if is_settlement else ""
|
||||
marker = f" {_copy(language, '★Settlement', '★结算')}" if is_settlement else ""
|
||||
tmax = pts.get("target_runway_max")
|
||||
if tdz is not None or mid is not None or end is not None:
|
||||
line = f"{r1}/{r2}{marker} TDZ:{_fmt(tdz)} MID:{_fmt(mid)} END:{_fmt(end)}"
|
||||
if tmax is not None:
|
||||
line += f" max:{tmax:.1f}"
|
||||
settlement_line_endpoint = _settlement_endpoint_for_point(city, (r1, r2), pts) if is_settlement else None
|
||||
if settlement_line_endpoint is not None:
|
||||
line += f" settle:{float(settlement_line_endpoint['temp']):.1f}"
|
||||
elif tmax is not None:
|
||||
line += f" max:{float(tmax):.1f}"
|
||||
lines.append(line)
|
||||
else:
|
||||
temp_symbol = str(city_weather.get("temp_symbol") or "°C").strip()
|
||||
@@ -1402,14 +1503,18 @@ def _process_airport_city(
|
||||
runway_obs = (amos.get("runway_obs") or {})
|
||||
runway_pairs = runway_obs.get("runway_pairs") or []
|
||||
runway_temps = runway_obs.get("temperatures") or []
|
||||
runway_pairs, runway_temps, _point_temps = _select_focus_runway_obs(
|
||||
runway_pairs, runway_temps, point_temps = _select_focus_runway_obs(
|
||||
city, runway_pairs, runway_temps,
|
||||
runway_obs.get("point_temperatures") or [],
|
||||
)
|
||||
if runway_temps:
|
||||
valid_temps = [t for (t, _d) in runway_temps if t is not None]
|
||||
if valid_temps:
|
||||
station_temp = max(valid_temps)
|
||||
if runway_pairs and (runway_temps or point_temps):
|
||||
endpoint = _settlement_endpoint_from_obs(city, runway_pairs, point_temps)
|
||||
if endpoint is not None:
|
||||
station_temp = float(endpoint["temp"])
|
||||
else:
|
||||
valid_temps = [t for (t, _d) in runway_temps if t is not None]
|
||||
if valid_temps:
|
||||
station_temp = max(valid_temps)
|
||||
amos_obs_time = amos.get("observation_time") or ""
|
||||
if amos_obs_time:
|
||||
current_obs_time = amos_obs_time
|
||||
|
||||
Reference in New Issue
Block a user