feat: Add multi-source weather data collection and a web application for display.
This commit is contained in:
@@ -328,37 +328,40 @@ class WeatherDataCollector:
|
|||||||
# 3. 提取最近 4 条报文的多维数据(温度 + 风/云/压强,用于趋势和 shock_score)
|
# 3. 提取最近 4 条报文的多维数据(温度 + 风/云/压强,用于趋势和 shock_score)
|
||||||
recent_temps_raw = [] # [(local_time_str, temp_c), ...]
|
recent_temps_raw = [] # [(local_time_str, temp_c), ...]
|
||||||
recent_obs_raw = [] # [{time, temp, wdir, wspd, clouds, altim}, ...]
|
recent_obs_raw = [] # [{time, temp, wdir, wspd, clouds, altim}, ...]
|
||||||
for obs in data[:4]: # data 已按时间倒序
|
today_obs_raw = [] # [(local_time_str, temp_c), ...] 今天全部观测
|
||||||
|
cloud_rank_map = {
|
||||||
|
"CLR": 0, "SKC": 0, "FEW": 1, "SCT": 2, "BKN": 3, "OVC": 4,
|
||||||
|
}
|
||||||
|
for i_obs, obs in enumerate(data): # data 已按时间倒序
|
||||||
obs_temp = obs.get("temp")
|
obs_temp = obs.get("temp")
|
||||||
obs_dt_iter = _parse_rawob_time(obs)
|
obs_dt_iter = _parse_rawob_time(obs)
|
||||||
if obs_temp is not None and obs_dt_iter:
|
if obs_temp is not None and obs_dt_iter:
|
||||||
local_rt = obs_dt_iter + timedelta(seconds=utc_offset)
|
local_rt = obs_dt_iter + timedelta(seconds=utc_offset)
|
||||||
recent_temps_raw.append((local_rt.strftime("%H:%M"), obs_temp))
|
time_str = local_rt.strftime("%H:%M")
|
||||||
# 云量码映射: CLR=0, FEW=1, SCT=2, BKN=3, OVC=4
|
|
||||||
cloud_rank_map = {
|
# 收集今天全部观测点(用于图表叠加)
|
||||||
"CLR": 0,
|
if obs_dt_iter >= utc_midnight:
|
||||||
"SKC": 0,
|
today_obs_raw.append((time_str, obs_temp))
|
||||||
"FEW": 1,
|
|
||||||
"SCT": 2,
|
# 只取前4条用于趋势分析和 shock_score
|
||||||
"BKN": 3,
|
if i_obs < 4:
|
||||||
"OVC": 4,
|
recent_temps_raw.append((time_str, obs_temp))
|
||||||
}
|
clouds = obs.get("clouds", [])
|
||||||
clouds = obs.get("clouds", [])
|
max_cloud_rank = 0
|
||||||
max_cloud_rank = 0
|
for c in clouds:
|
||||||
for c in clouds:
|
rank = cloud_rank_map.get(c.get("cover", ""), 0)
|
||||||
rank = cloud_rank_map.get(c.get("cover", ""), 0)
|
if rank > max_cloud_rank:
|
||||||
if rank > max_cloud_rank:
|
max_cloud_rank = rank
|
||||||
max_cloud_rank = rank
|
recent_obs_raw.append(
|
||||||
recent_obs_raw.append(
|
{
|
||||||
{
|
"time": time_str,
|
||||||
"time": local_rt.strftime("%H:%M"),
|
"temp": obs_temp,
|
||||||
"temp": obs_temp,
|
"wdir": obs.get("wdir"),
|
||||||
"wdir": obs.get("wdir"),
|
"wspd": obs.get("wspd"),
|
||||||
"wspd": obs.get("wspd"),
|
"cloud_rank": max_cloud_rank, # 0~4
|
||||||
"cloud_rank": max_cloud_rank, # 0~4
|
"altim": obs.get("altim"),
|
||||||
"altim": obs.get("altim"),
|
}
|
||||||
}
|
)
|
||||||
)
|
|
||||||
|
|
||||||
# 转换为单位
|
# 转换为单位
|
||||||
if use_fahrenheit:
|
if use_fahrenheit:
|
||||||
@@ -366,16 +369,19 @@ class WeatherDataCollector:
|
|||||||
max_so_far = max_so_far_c * 9 / 5 + 32 if max_so_far_c > -900 else None
|
max_so_far = max_so_far_c * 9 / 5 + 32 if max_so_far_c > -900 else None
|
||||||
dewp = dewp_c * 9 / 5 + 32 if dewp_c is not None else None
|
dewp = dewp_c * 9 / 5 + 32 if dewp_c is not None else None
|
||||||
unit = "fahrenheit"
|
unit = "fahrenheit"
|
||||||
# 转换最近温度
|
|
||||||
recent_temps = [
|
recent_temps = [
|
||||||
(t, round(v * 9 / 5 + 32, 1)) for t, v in recent_temps_raw
|
(t, round(v * 9 / 5 + 32, 1)) for t, v in recent_temps_raw
|
||||||
]
|
]
|
||||||
|
today_obs = [
|
||||||
|
(t, round(v * 9 / 5 + 32, 1)) for t, v in today_obs_raw
|
||||||
|
]
|
||||||
else:
|
else:
|
||||||
temp = temp_c
|
temp = temp_c
|
||||||
max_so_far = max_so_far_c if max_so_far_c > -900 else None
|
max_so_far = max_so_far_c if max_so_far_c > -900 else None
|
||||||
dewp = dewp_c
|
dewp = dewp_c
|
||||||
unit = "celsius"
|
unit = "celsius"
|
||||||
recent_temps = [(t, v) for t, v in recent_temps_raw]
|
recent_temps = [(t, v) for t, v in recent_temps_raw]
|
||||||
|
today_obs = [(t, v) for t, v in today_obs_raw]
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
"source": "metar",
|
"source": "metar",
|
||||||
@@ -399,6 +405,7 @@ class WeatherDataCollector:
|
|||||||
"clouds": latest.get("clouds", []),
|
"clouds": latest.get("clouds", []),
|
||||||
},
|
},
|
||||||
"recent_temps": recent_temps, # 最近4条: [("15:00", 5), ("14:20", 5), ...]
|
"recent_temps": recent_temps, # 最近4条: [("15:00", 5), ("14:20", 5), ...]
|
||||||
|
"today_obs": today_obs, # 今天全部观测: [("00:00", 3), ("01:00", 2.5), ...]
|
||||||
"recent_obs": recent_obs_raw, # 最近4条多维数据(风/云/压强)
|
"recent_obs": recent_obs_raw, # 最近4条多维数据(风/云/压强)
|
||||||
"unit": unit,
|
"unit": unit,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -445,6 +445,10 @@ def _analyze(city: str) -> Dict[str, Any]:
|
|||||||
"status": peak_status,
|
"status": peak_status,
|
||||||
},
|
},
|
||||||
"hourly": today_hourly,
|
"hourly": today_hourly,
|
||||||
|
"metar_today_obs": [
|
||||||
|
{"time": t, "temp": v}
|
||||||
|
for t, v in (metar.get("today_obs", []) if metar else [])
|
||||||
|
],
|
||||||
"ai_analysis": ai_text,
|
"ai_analysis": ai_text,
|
||||||
"updated_at": datetime.now(timezone.utc).isoformat(),
|
"updated_at": datetime.now(timezone.utc).isoformat(),
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-7
@@ -476,15 +476,25 @@ function renderChart(data) {
|
|||||||
curIdx < 0 || i >= curIdx ? t : null,
|
curIdx < 0 || i >= curIdx ? t : null,
|
||||||
);
|
);
|
||||||
|
|
||||||
// METAR observation scatter points
|
// METAR observation scatter points — use full today's obs if available
|
||||||
const metarPoints = new Array(times.length).fill(null);
|
const metarPoints = new Array(times.length).fill(null);
|
||||||
const metarRecent = data.trend?.recent || [];
|
const metarSrc = data.metar_today_obs?.length
|
||||||
if (metarRecent.length > 0) {
|
? data.metar_today_obs
|
||||||
metarRecent.forEach((r) => {
|
: data.trend?.recent || [];
|
||||||
// Match METAR time (e.g. "15:00") to chart time labels
|
if (metarSrc.length > 0) {
|
||||||
const idx = times.indexOf(r.time);
|
metarSrc.forEach((r) => {
|
||||||
|
// METAR time may be "14:20" but chart labels are whole hours "14:00"
|
||||||
|
const parts = r.time.split(":");
|
||||||
|
let h = parseInt(parts[0], 10);
|
||||||
|
const m = parseInt(parts[1] || "0", 10);
|
||||||
|
if (m >= 30) h = (h + 1) % 24; // round to nearest hour
|
||||||
|
const hourKey = h.toString().padStart(2, "0") + ":00";
|
||||||
|
const idx = times.indexOf(hourKey);
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
metarPoints[idx] = r.temp;
|
// If multiple METARs map to the same hour, keep the latest (first in array)
|
||||||
|
if (metarPoints[idx] === null) {
|
||||||
|
metarPoints[idx] = r.temp;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user