Spaces:
Sleeping
Sleeping
rickyt
commited on
Commit
·
7d80fff
1
Parent(s):
7b5ada2
tomorrow
Browse files
app.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
# app.py
|
| 2 |
-
# Gradio app:
|
| 3 |
# Requirements: pip install gradio requests pandas python-dateutil python-dotenv
|
| 4 |
|
| 5 |
import os
|
|
@@ -37,178 +37,86 @@ def _fmt(x, nd=1):
|
|
| 37 |
return escape(str(x))
|
| 38 |
|
| 39 |
def _heat(value, vmax, hue="blue"):
|
| 40 |
-
"""Inline, subtle heat-tint
|
| 41 |
try:
|
| 42 |
v = float(value or 0.0)
|
| 43 |
except:
|
| 44 |
v = 0.0
|
| 45 |
pct = max(0.0, min(1.0, v / max(vmax, 1e-9)))
|
| 46 |
if hue == "blue":
|
| 47 |
-
# blue overlay
|
| 48 |
return f"background: rgba(59, 130, 246, {0.18*pct});"
|
| 49 |
if hue == "green":
|
| 50 |
-
# green overlay
|
| 51 |
return f"background: rgba(34, 197, 94, {0.18*pct});"
|
| 52 |
return ""
|
| 53 |
|
| 54 |
-
def build_pretty_table(df: pd.DataFrame) ->
|
| 55 |
if df is None or len(df) == 0:
|
| 56 |
-
return "<p>No data.</p>"
|
| 57 |
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
df["TMRW_mm"] = pd.to_numeric(df.get("TMRW_PrecipRate_mmph"), errors="coerce")
|
| 61 |
-
df["XW_PoP"] = pd.to_numeric(df.get("XW_RAIN_PROB_%"), errors="coerce")
|
| 62 |
-
df["TMRW_PoP"]= pd.to_numeric(df.get("TMRW_RAIN_PROB_%"), errors="coerce")
|
| 63 |
|
| 64 |
-
# Rain alert rows
|
| 65 |
-
rain_flag = (
|
| 66 |
-
((df["XW_mm"] >= 1.0) | (df["TMRW_mm"] >= 1.0)) |
|
| 67 |
-
((df["XW_PoP"] >= 70) | (df["TMRW_PoP"] >= 70))
|
| 68 |
-
)
|
| 69 |
-
|
| 70 |
-
# Next rain (either provider hits threshold first)
|
| 71 |
-
def _first_rain_row():
|
| 72 |
-
for _, row in df.iterrows():
|
| 73 |
-
if (row.get("XW_mm", 0) >= 1) or (row.get("TMRW_mm", 0) >= 1) or \
|
| 74 |
-
(row.get("XW_PoP", 0) >= 70) or (row.get("TMRW_PoP", 0) >= 70):
|
| 75 |
-
return row.get("Local Time")
|
| 76 |
-
return None
|
| 77 |
-
next_rain = _first_rain_row()
|
| 78 |
-
|
| 79 |
-
# Peak rain (max of providers)
|
| 80 |
-
xw_vals = df["XW_mm"].fillna(0).to_numpy() if "XW_mm" in df else np.zeros(len(df))
|
| 81 |
-
tm_vals = df["TMRW_mm"].fillna(0).to_numpy() if "TMRW_mm" in df else np.zeros(len(df))
|
| 82 |
-
df["peak_proxy"] = np.maximum(xw_vals, tm_vals)
|
| 83 |
-
if df["peak_proxy"].max() > 0:
|
| 84 |
-
peak_idx = int(df["peak_proxy"].idxmax())
|
| 85 |
-
peak_when = df.iloc[peak_idx]["Local Time"]
|
| 86 |
-
peak_amt = float(df.iloc[peak_idx]["peak_proxy"])
|
| 87 |
-
else:
|
| 88 |
-
peak_when, peak_amt = None, 0.0
|
| 89 |
-
|
| 90 |
-
summary = {
|
| 91 |
-
"next_rain": next_rain,
|
| 92 |
-
"peak_when": peak_when,
|
| 93 |
-
"peak_mmph": round(peak_amt, 1),
|
| 94 |
-
"has_rain": bool(next_rain),
|
| 95 |
-
}
|
| 96 |
-
|
| 97 |
-
# --- Grouped headers (2 rows) ---
|
| 98 |
rows_html = []
|
| 99 |
rows_html.append(
|
| 100 |
-
"<thead>"
|
| 101 |
-
"<
|
| 102 |
-
"<th
|
| 103 |
-
"<th
|
| 104 |
-
"<th
|
| 105 |
-
"
|
| 106 |
-
"<
|
| 107 |
-
"<th
|
| 108 |
-
"
|
| 109 |
-
"<th class='xw-col'>Rain mm</th>"
|
| 110 |
-
"<th class='tm-col'>Weather</th>"
|
| 111 |
-
"<th class='tm-col'>Rain Prob. %</th>"
|
| 112 |
-
"<th class='tm-col'>Rain mm/h</th>"
|
| 113 |
-
"</tr>"
|
| 114 |
-
"</thead>"
|
| 115 |
)
|
| 116 |
|
| 117 |
-
vmax_pop = max(70, np.nanmax([df["XW_PoP"].max(), df["TMRW_PoP"].max()]))
|
| 118 |
-
vmax_mm = max(2.0, np.nanmax([df["XW_mm"].max(), df["TMRW_mm"].max()]))
|
| 119 |
-
|
| 120 |
body = []
|
| 121 |
-
for
|
| 122 |
-
|
| 123 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
cells = [
|
| 125 |
f"<td>{escape(str(r.get('Local Time','')))}</td>",
|
| 126 |
-
f"<td
|
| 127 |
-
f"<td
|
| 128 |
-
f"<td
|
| 129 |
-
f"<td
|
| 130 |
-
f"<td
|
| 131 |
-
f"<td
|
| 132 |
]
|
| 133 |
-
body.append(f"<tr
|
| 134 |
|
| 135 |
-
|
| 136 |
-
table_css = """
|
| 137 |
<style>
|
| 138 |
-
:
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
--tm-cell: #f9fff9; /* body green tint */
|
| 147 |
-
--alert-bg: rgba(255, 193, 7, 0.12); /* amber */
|
| 148 |
-
}
|
| 149 |
-
@media (prefers-color-scheme: dark) {
|
| 150 |
-
:root {
|
| 151 |
-
--bg: #0b1220;
|
| 152 |
-
--fg: #e5e7eb;
|
| 153 |
-
--muted: #131c2b;
|
| 154 |
-
--border: #243042;
|
| 155 |
-
--xw-head: #10335f;
|
| 156 |
-
--tm-head: #0f3b26;
|
| 157 |
-
--xw-cell: #0f213e;
|
| 158 |
-
--tm-cell: #0f2a1a;
|
| 159 |
-
--alert-bg: rgba(255, 193, 7, 0.18);
|
| 160 |
-
}
|
| 161 |
-
}
|
| 162 |
-
|
| 163 |
-
.wx-scope { color: var(--fg); background: transparent; }
|
| 164 |
-
.wx-table { width: 100%; border-collapse: collapse; font-family: ui-sans-serif, system-ui, -apple-system; }
|
| 165 |
-
.wx-table th, .wx-table td {
|
| 166 |
-
padding: 10px 8px; border-bottom: 1px solid var(--border);
|
| 167 |
-
font-size: 14px; vertical-align: middle; text-align: center; color: var(--fg);
|
| 168 |
-
}
|
| 169 |
-
.wx-table thead th { position: sticky; top: 0; z-index: 1; }
|
| 170 |
-
.wx-table th.local-col { background: var(--muted); }
|
| 171 |
-
.wx-table th.xw-col { background: var(--xw-head); }
|
| 172 |
-
.wx-table th.tm-col { background: var(--tm-head); }
|
| 173 |
-
.xw-cell { background: var(--xw-cell); }
|
| 174 |
-
.tm-cell { background: var(--tm-cell); }
|
| 175 |
-
|
| 176 |
-
.wx-chips { display:flex; gap:8px; flex-wrap: wrap; margin: 10px 0 6px; }
|
| 177 |
-
.chip { padding:6px 10px; border-radius: 999px; background: var(--muted); border:1px solid var(--border); font-size: 13px; color: var(--fg); }
|
| 178 |
-
.chip.good { box-shadow: inset 0 0 0 9999px rgba(34,197,94,0.12); }
|
| 179 |
-
.chip.warn { box-shadow: inset 0 0 0 9999px rgba(255,193,7,0.12); }
|
| 180 |
</style>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
"""
|
|
|
|
| 182 |
|
| 183 |
-
chips = []
|
| 184 |
-
chips.append(f"<span class='chip {'warn' if summary['has_rain'] else 'good'}'>"
|
| 185 |
-
f"{'Next rain: ' + escape(summary['next_rain']) if summary['has_rain'] else 'No rain signal in next 24h'}</span>")
|
| 186 |
-
if summary["peak_mmph"] > 0:
|
| 187 |
-
chips.append(f"<span class='chip'>Peak ~{summary['peak_mmph']} mm/h at {escape(summary['peak_when'] or '')}</span>")
|
| 188 |
-
|
| 189 |
-
header_html = "<div class='wx-chips'>" + "".join(chips) + "</div>"
|
| 190 |
-
|
| 191 |
-
table_html = f"""
|
| 192 |
-
{table_css}
|
| 193 |
-
<div class="wx-scope">
|
| 194 |
-
{header_html}
|
| 195 |
-
<table class="wx-table">
|
| 196 |
-
{''.join(rows_html)}
|
| 197 |
-
<tbody>
|
| 198 |
-
{''.join(body)}
|
| 199 |
-
</tbody>
|
| 200 |
-
</table>
|
| 201 |
-
</div>
|
| 202 |
-
"""
|
| 203 |
-
return table_html, summary
|
| 204 |
|
| 205 |
# -------------------
|
| 206 |
# API Configs
|
| 207 |
# -------------------
|
| 208 |
-
XW_BASE_URL = "https://data.api.xweather.com/forecasts/"
|
| 209 |
-
CLIENT_ID = os.getenv("XWEATHER_CLIENT_ID") or "BlZV8kShcnDxJ2ugQ3b65"
|
| 210 |
-
CLIENT_SECRET = os.getenv("XWEATHER_CLIENT_SECRET") or "JYvA8vAJJqEO6yP5QixQw59V3oUKqO9HHvj7ZI2R"
|
| 211 |
-
|
| 212 |
TMRW_BASE = "https://api.tomorrow.io/v4/weather/forecast"
|
| 213 |
TMRW_API_KEY = os.getenv("TOMORROW_API_KEY") or "teKj9Rkys1UzWxKBEs36pAR8paCXnPW6"
|
| 214 |
|
|
@@ -228,37 +136,6 @@ def _safe_to_timestamp(iso: str):
|
|
| 228 |
try: return pd.to_datetime(iso)
|
| 229 |
except Exception: return None
|
| 230 |
|
| 231 |
-
# ---------- Xweather ----------
|
| 232 |
-
def fetch_xweather_hourly(location: str, limit: int = 24):
|
| 233 |
-
url = XW_BASE_URL + requests.utils.quote(location.strip(), safe=",")
|
| 234 |
-
params = {"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET, "filter": "1hr", "limit": limit}
|
| 235 |
-
r = requests.get(url, params=params, timeout=20)
|
| 236 |
-
r.raise_for_status()
|
| 237 |
-
data = r.json()
|
| 238 |
-
recs = data.get("response") if isinstance(data, dict) and "response" in data else data
|
| 239 |
-
rec = recs[0] if isinstance(recs, list) and recs else data
|
| 240 |
-
periods = rec.get("periods") or []
|
| 241 |
-
tz_str = (rec.get("profile") or {}).get("tz", "")
|
| 242 |
-
rows = []
|
| 243 |
-
for p in periods[:limit]:
|
| 244 |
-
iso = p.get("dateTimeISO") or p.get("validTime")
|
| 245 |
-
t_utc = _safe_to_timestamp(iso)
|
| 246 |
-
if isinstance(t_utc, pd.Timestamp) and t_utc.tzinfo:
|
| 247 |
-
t_utc = t_utc.tz_convert("UTC")
|
| 248 |
-
rows.append({
|
| 249 |
-
"time_utc": t_utc,
|
| 250 |
-
"XW_Weather": p.get("weather") or p.get("weatherPrimary"),
|
| 251 |
-
"XW_Humidity_%": p.get("humidity"),
|
| 252 |
-
"XW_RAIN_PROB_%": p.get("pop"),
|
| 253 |
-
"XW_Precip_mm": p.get("precipMM"),
|
| 254 |
-
"XW_Visibility_km": p.get("visibilityKM"),
|
| 255 |
-
"XW_Cloud_%": p.get("sky"),
|
| 256 |
-
"XW_TZ": tz_str,
|
| 257 |
-
})
|
| 258 |
-
df = pd.DataFrame(rows)
|
| 259 |
-
df["time_hour_utc"] = pd.to_datetime(df["time_utc"]).dt.tz_convert("UTC").dt.floor("H")
|
| 260 |
-
return df
|
| 261 |
-
|
| 262 |
# ---------- Tomorrow.io ----------
|
| 263 |
def fetch_tomorrow_hourly(latlon: str, hours: int = 24):
|
| 264 |
params = {"location": latlon.strip(), "timesteps": "hourly", "units": "metric", "apikey": TMRW_API_KEY}
|
|
@@ -272,52 +149,41 @@ def fetch_tomorrow_hourly(latlon: str, hours: int = 24):
|
|
| 272 |
v = h.get("values") or {}
|
| 273 |
code = v.get("weatherCode")
|
| 274 |
rows.append({
|
| 275 |
-
"time_utc": t_utc
|
| 276 |
"TMRW_Weather": TMRW_WEATHER_CODE.get(code, str(code) if code is not None else None),
|
| 277 |
"TMRW_Humidity_%": v.get("humidity"),
|
| 278 |
"TMRW_RAIN_PROB_%": v.get("precipitationProbability"),
|
| 279 |
-
"TMRW_PrecipRate_mmph": v.get("rainIntensity"),
|
| 280 |
"TMRW_Visibility_km": v.get("visibility"),
|
| 281 |
"TMRW_Cloud_%": v.get("cloudCover"),
|
| 282 |
})
|
| 283 |
df = pd.DataFrame(rows)
|
| 284 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 285 |
return df
|
| 286 |
|
| 287 |
-
# ---------- Merge ----------
|
| 288 |
-
def build_side_by_side(location: str):
|
| 289 |
-
xw = fetch_xweather_hourly(location, limit=24)
|
| 290 |
-
tm = fetch_tomorrow_hourly(location, hours=24)
|
| 291 |
-
merged = pd.merge(xw, tm, on="time_hour_utc", how="outer", sort=True)
|
| 292 |
-
try:
|
| 293 |
-
local_tz = ZoneInfo("Asia/Singapore") # UTC+8
|
| 294 |
-
merged["Local Time"] = (
|
| 295 |
-
pd.to_datetime(merged["time_hour_utc"])
|
| 296 |
-
.dt.tz_convert(local_tz)
|
| 297 |
-
.dt.strftime("%Y-%m-%d %H:%M %Z")
|
| 298 |
-
)
|
| 299 |
-
except Exception:
|
| 300 |
-
merged["Local Time"] = merged["time_hour_utc"].astype(str)
|
| 301 |
-
return merged, f"**Location:** {location} • Rows: {len(merged)} • Sources: Xweather + Tomorrow.io (UTC+8)"
|
| 302 |
-
|
| 303 |
# ---------- Gradio UI ----------
|
| 304 |
with gr.Blocks(fill_height=True) as demo:
|
| 305 |
-
gr.Markdown("## Hourly Forecast
|
| 306 |
with gr.Row():
|
| 307 |
loc = gr.Textbox(label="Location (lat,lon)", placeholder="e.g., -6.21,106.85", value="0.46876,116.16879")
|
| 308 |
-
btn = gr.Button("
|
| 309 |
|
| 310 |
-
out_table = gr.HTML(label="Hourly
|
| 311 |
out_info = gr.Markdown()
|
| 312 |
dl_btn = gr.DownloadButton(label="Download CSV", value=None)
|
| 313 |
|
| 314 |
def _run(loc_s):
|
| 315 |
try:
|
| 316 |
-
df
|
| 317 |
-
html
|
| 318 |
-
csv_path = "
|
| 319 |
df.to_csv(csv_path, index=False)
|
| 320 |
-
return html,
|
| 321 |
except Exception as ex:
|
| 322 |
return f"<pre>{escape(str(ex))}</pre>", "", None
|
| 323 |
|
|
|
|
| 1 |
# app.py
|
| 2 |
+
# Gradio app: Tomorrow.io hourly (next 24 hours)
|
| 3 |
# Requirements: pip install gradio requests pandas python-dateutil python-dotenv
|
| 4 |
|
| 5 |
import os
|
|
|
|
| 37 |
return escape(str(x))
|
| 38 |
|
| 39 |
def _heat(value, vmax, hue="blue"):
|
| 40 |
+
"""Inline, subtle heat-tint"""
|
| 41 |
try:
|
| 42 |
v = float(value or 0.0)
|
| 43 |
except:
|
| 44 |
v = 0.0
|
| 45 |
pct = max(0.0, min(1.0, v / max(vmax, 1e-9)))
|
| 46 |
if hue == "blue":
|
|
|
|
| 47 |
return f"background: rgba(59, 130, 246, {0.18*pct});"
|
| 48 |
if hue == "green":
|
|
|
|
| 49 |
return f"background: rgba(34, 197, 94, {0.18*pct});"
|
| 50 |
return ""
|
| 51 |
|
| 52 |
+
def build_pretty_table(df: pd.DataFrame) -> str:
|
| 53 |
if df is None or len(df) == 0:
|
| 54 |
+
return "<p>No data.</p>"
|
| 55 |
|
| 56 |
+
vmax_pop = max(70, np.nanmax(df["TMRW_RAIN_PROB_%"]))
|
| 57 |
+
vmax_mm = max(2.0, np.nanmax(df["TMRW_PrecipRate_mmph"]))
|
|
|
|
|
|
|
|
|
|
| 58 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
rows_html = []
|
| 60 |
rows_html.append(
|
| 61 |
+
"<thead><tr>"
|
| 62 |
+
"<th>Local Time</th>"
|
| 63 |
+
"<th>Weather</th>"
|
| 64 |
+
"<th>Rain Prob. %</th>"
|
| 65 |
+
"<th>Rain mm/h</th>"
|
| 66 |
+
"<th>Humidity %</th>"
|
| 67 |
+
"<th>Visibility km</th>"
|
| 68 |
+
"<th>Cloud %</th>"
|
| 69 |
+
"</tr></thead>"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
)
|
| 71 |
|
|
|
|
|
|
|
|
|
|
| 72 |
body = []
|
| 73 |
+
for _, r in df.iterrows():
|
| 74 |
+
prob = r.get("TMRW_RAIN_PROB_%") or 0
|
| 75 |
+
rain = r.get("TMRW_PrecipRate_mmph") or 0
|
| 76 |
+
|
| 77 |
+
# ✅ Apply green highlight rule
|
| 78 |
+
row_class = ""
|
| 79 |
+
try:
|
| 80 |
+
if (rain >= 1) or (prob > 50 and rain >= 1):
|
| 81 |
+
row_class = "highlight-green"
|
| 82 |
+
except Exception:
|
| 83 |
+
row_class = ""
|
| 84 |
+
|
| 85 |
cells = [
|
| 86 |
f"<td>{escape(str(r.get('Local Time','')))}</td>",
|
| 87 |
+
f"<td>{_wx_emoji(r.get('TMRW_Weather'))}</td>",
|
| 88 |
+
f"<td style='{_heat(prob, vmax_pop, 'green')}'>{_fmt(prob)}</td>",
|
| 89 |
+
f"<td style='{_heat(rain, vmax_mm, 'blue')}'>{_fmt(rain)}</td>",
|
| 90 |
+
f"<td>{_fmt(r.get('TMRW_Humidity_%'))}</td>",
|
| 91 |
+
f"<td>{_fmt(r.get('TMRW_Visibility_km'))}</td>",
|
| 92 |
+
f"<td>{_fmt(r.get('TMRW_Cloud_%'))}</td>",
|
| 93 |
]
|
| 94 |
+
body.append(f"<tr class='{row_class}'>" + "".join(cells) + "</tr>")
|
| 95 |
|
| 96 |
+
table_html = f"""
|
|
|
|
| 97 |
<style>
|
| 98 |
+
.wx-table {{ width: 100%; border-collapse: collapse; font-family: ui-sans-serif, system-ui; }}
|
| 99 |
+
.wx-table th, .wx-table td {{
|
| 100 |
+
padding: 8px; border-bottom: 1px solid #ccc; text-align: center;
|
| 101 |
+
}}
|
| 102 |
+
/* ✅ Full-row green highlight */
|
| 103 |
+
tr.highlight-green td {{
|
| 104 |
+
background: rgba(34,197,94,0.35) !important;
|
| 105 |
+
}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
</style>
|
| 107 |
+
<table class="wx-table">
|
| 108 |
+
{''.join(rows_html)}
|
| 109 |
+
<tbody>
|
| 110 |
+
{''.join(body)}
|
| 111 |
+
</tbody>
|
| 112 |
+
</table>
|
| 113 |
"""
|
| 114 |
+
return table_html
|
| 115 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
|
| 117 |
# -------------------
|
| 118 |
# API Configs
|
| 119 |
# -------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
TMRW_BASE = "https://api.tomorrow.io/v4/weather/forecast"
|
| 121 |
TMRW_API_KEY = os.getenv("TOMORROW_API_KEY") or "teKj9Rkys1UzWxKBEs36pAR8paCXnPW6"
|
| 122 |
|
|
|
|
| 136 |
try: return pd.to_datetime(iso)
|
| 137 |
except Exception: return None
|
| 138 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
# ---------- Tomorrow.io ----------
|
| 140 |
def fetch_tomorrow_hourly(latlon: str, hours: int = 24):
|
| 141 |
params = {"location": latlon.strip(), "timesteps": "hourly", "units": "metric", "apikey": TMRW_API_KEY}
|
|
|
|
| 149 |
v = h.get("values") or {}
|
| 150 |
code = v.get("weatherCode")
|
| 151 |
rows.append({
|
| 152 |
+
"time_utc": t_utc,
|
| 153 |
"TMRW_Weather": TMRW_WEATHER_CODE.get(code, str(code) if code is not None else None),
|
| 154 |
"TMRW_Humidity_%": v.get("humidity"),
|
| 155 |
"TMRW_RAIN_PROB_%": v.get("precipitationProbability"),
|
| 156 |
+
"TMRW_PrecipRate_mmph": v.get("rainIntensity"),
|
| 157 |
"TMRW_Visibility_km": v.get("visibility"),
|
| 158 |
"TMRW_Cloud_%": v.get("cloudCover"),
|
| 159 |
})
|
| 160 |
df = pd.DataFrame(rows)
|
| 161 |
+
local_tz = ZoneInfo("Asia/Singapore")
|
| 162 |
+
df["Local Time"] = (
|
| 163 |
+
pd.to_datetime(df["time_utc"])
|
| 164 |
+
.dt.tz_convert(local_tz)
|
| 165 |
+
.dt.strftime("%Y-%m-%d %H:%M %Z")
|
| 166 |
+
)
|
| 167 |
return df
|
| 168 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
# ---------- Gradio UI ----------
|
| 170 |
with gr.Blocks(fill_height=True) as demo:
|
| 171 |
+
gr.Markdown("## Tomorrow.io Hourly Forecast (Next 24h)")
|
| 172 |
with gr.Row():
|
| 173 |
loc = gr.Textbox(label="Location (lat,lon)", placeholder="e.g., -6.21,106.85", value="0.46876,116.16879")
|
| 174 |
+
btn = gr.Button("Get Forecast", variant="primary")
|
| 175 |
|
| 176 |
+
out_table = gr.HTML(label="Hourly forecast table")
|
| 177 |
out_info = gr.Markdown()
|
| 178 |
dl_btn = gr.DownloadButton(label="Download CSV", value=None)
|
| 179 |
|
| 180 |
def _run(loc_s):
|
| 181 |
try:
|
| 182 |
+
df = fetch_tomorrow_hourly(loc_s, hours=24)
|
| 183 |
+
html = build_pretty_table(df)
|
| 184 |
+
csv_path = "forecast_tomorrow.csv"
|
| 185 |
df.to_csv(csv_path, index=False)
|
| 186 |
+
return html, f"**Location:** {loc_s} • Rows: {len(df)} • Source: Tomorrow.io", csv_path
|
| 187 |
except Exception as ex:
|
| 188 |
return f"<pre>{escape(str(ex))}</pre>", "", None
|
| 189 |
|