Spaces:
Sleeping
Sleeping
rickyt
commited on
Commit
·
be94c44
1
Parent(s):
7d80fff
tomorrow
Browse files
app.py
CHANGED
|
@@ -1,193 +1,128 @@
|
|
| 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
|
| 6 |
import requests
|
| 7 |
import pandas as pd
|
| 8 |
import gradio as gr
|
|
|
|
| 9 |
from zoneinfo import ZoneInfo
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
#
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
if
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
""
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 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 |
-
|
| 123 |
-
TMRW_WEATHER_CODE = {
|
| 124 |
-
0: "Unknown", 1000: "Clear", 1100: "Mostly Clear", 1101: "Partly Cloudy", 1102: "Mostly Cloudy",
|
| 125 |
-
1001: "Cloudy", 2000: "Fog", 2100: "Light Fog",
|
| 126 |
-
4000: "Drizzle", 4001: "Rain", 4200: "Light Rain", 4201: "Heavy Rain",
|
| 127 |
-
5000: "Snow", 5001: "Flurries", 5100: "Light Snow", 5101: "Heavy Snow",
|
| 128 |
-
6000: "Freezing Drizzle", 6001: "Freezing Rain", 6200: "Light Freezing Rain",
|
| 129 |
-
6201: "Heavy Freezing Rain", 7000: "Ice Pellets",
|
| 130 |
-
7101: "Heavy Ice Pellets", 7102: "Light Ice Pellets",
|
| 131 |
-
8000: "Thunderstorm"
|
| 132 |
-
}
|
| 133 |
-
|
| 134 |
-
def _safe_to_timestamp(iso: str):
|
| 135 |
-
if not iso: return None
|
| 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}
|
| 142 |
-
r = requests.get(TMRW_BASE, params=params, timeout=20)
|
| 143 |
-
r.raise_for_status()
|
| 144 |
-
data = r.json()
|
| 145 |
-
hourly = (data.get("timelines") or {}).get("hourly") or []
|
| 146 |
rows = []
|
| 147 |
-
for
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
return df
|
| 168 |
|
| 169 |
-
#
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 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 |
|
| 190 |
-
btn.click(
|
| 191 |
|
| 192 |
if __name__ == "__main__":
|
| 193 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import os
|
| 2 |
import requests
|
| 3 |
import pandas as pd
|
| 4 |
import gradio as gr
|
| 5 |
+
from datetime import datetime, timedelta, timezone
|
| 6 |
from zoneinfo import ZoneInfo
|
| 7 |
+
|
| 8 |
+
# --- CONFIG ---
|
| 9 |
+
TOMORROW_API_KEY = os.getenv("TOMORROW_API_KEY", "teKj9Rkys1UzWxKBEs36pAR8paCXnPW6")
|
| 10 |
+
METEOMATICS_USERNAME = os.getenv("METEO_USERNAME", "ptbukitteknologidigital_tudjuka_ricky")
|
| 11 |
+
METEOMATICS_PASSWORD = os.getenv("METEO_PASSWORD", "u5n25MwaviEG3O3v8q94")
|
| 12 |
+
|
| 13 |
+
DEFAULT_LAT = 0.46876
|
| 14 |
+
DEFAULT_LON = 116.16879
|
| 15 |
+
LOCAL_TZ = ZoneInfo("Asia/Kuala_Lumpur") # ✅ changed from Asia/Jakarta to Asia/Kuala_Lumpur
|
| 16 |
+
|
| 17 |
+
MM_VAR_RAIN = "precip_1h:mm"
|
| 18 |
+
MM_VAR_PROB = "prob_precip_1h:p"
|
| 19 |
+
DEFAULT_MODEL = "ecmwf-ifs"
|
| 20 |
+
|
| 21 |
+
# --- Tomorrow.io fetch ---
|
| 22 |
+
def fetch_tomorrow(lat, lon, hours=24):
|
| 23 |
+
if not TOMORROW_API_KEY:
|
| 24 |
+
raise RuntimeError("Missing TOMORROW_API_KEY")
|
| 25 |
+
|
| 26 |
+
url = "https://api.tomorrow.io/v4/weather/forecast"
|
| 27 |
+
params = {
|
| 28 |
+
"location": f"{lat},{lon}",
|
| 29 |
+
"units": "metric",
|
| 30 |
+
"fields": "rainIntensity,precipitationProbability",
|
| 31 |
+
"apikey": TOMORROW_API_KEY,
|
| 32 |
+
}
|
| 33 |
+
r = requests.get(url, params=params, timeout=25)
|
| 34 |
+
if r.status_code != 200:
|
| 35 |
+
raise RuntimeError(f"Tomorrow.io API error: {r.status_code} {r.text}")
|
| 36 |
+
|
| 37 |
+
data = r.json().get("timelines", {}).get("hourly", [])
|
| 38 |
+
if not data:
|
| 39 |
+
raise RuntimeError("Tomorrow.io: no hourly data returned")
|
| 40 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
rows = []
|
| 42 |
+
for p in data[:hours]:
|
| 43 |
+
ts = pd.to_datetime(p["time"], utc=True).tz_convert(LOCAL_TZ)
|
| 44 |
+
vals = p.get("values", {})
|
| 45 |
+
rain = float(vals.get("rainIntensity", 0.0) or 0.0)
|
| 46 |
+
prob = float(vals.get("precipitationProbability", 0.0) or 0.0)
|
| 47 |
+
rows.append({"time": ts, "tio_rain": rain, "tio_prob": prob})
|
| 48 |
+
return pd.DataFrame(rows)
|
| 49 |
+
|
| 50 |
+
# --- Meteomatics fetch ---
|
| 51 |
+
def fetch_meteomatics(lat, lon, hours=24):
|
| 52 |
+
if not METEOMATICS_USERNAME or not METEOMATICS_PASSWORD:
|
| 53 |
+
raise RuntimeError("Missing METEOMATICS credentials")
|
| 54 |
+
|
| 55 |
+
start = datetime.now(timezone.utc).replace(minute=0, second=0, microsecond=0)
|
| 56 |
+
end = start + timedelta(hours=hours - 1)
|
| 57 |
+
timepath = f"{start.isoformat().replace('+00:00','Z')}--{end.isoformat().replace('+00:00','Z')}:PT1H"
|
| 58 |
+
url = f"https://api.meteomatics.com/{timepath}/{MM_VAR_RAIN},{MM_VAR_PROB}/{lat},{lon}/json"
|
| 59 |
+
params = {"model": DEFAULT_MODEL}
|
| 60 |
+
|
| 61 |
+
r = requests.get(url, auth=(METEOMATICS_USERNAME, METEOMATICS_PASSWORD), params=params, timeout=25)
|
| 62 |
+
if r.status_code != 200:
|
| 63 |
+
raise RuntimeError(f"Meteomatics API error: {r.status_code} {r.text}")
|
| 64 |
+
|
| 65 |
+
js = r.json().get("data", [])
|
| 66 |
+
if not js:
|
| 67 |
+
raise RuntimeError("Meteomatics: no data returned")
|
| 68 |
+
|
| 69 |
+
time_map = {}
|
| 70 |
+
for entry in js:
|
| 71 |
+
var = entry.get("parameter")
|
| 72 |
+
for d in entry["coordinates"][0]["dates"]:
|
| 73 |
+
ts = pd.to_datetime(d["date"], utc=True).tz_convert(LOCAL_TZ)
|
| 74 |
+
val = float(d.get("value", 0.0) or 0.0)
|
| 75 |
+
rec = time_map.setdefault(ts, {})
|
| 76 |
+
if var == MM_VAR_RAIN:
|
| 77 |
+
rec["mm_rain"] = val
|
| 78 |
+
elif var == MM_VAR_PROB:
|
| 79 |
+
rec["mm_prob"] = val
|
| 80 |
+
|
| 81 |
+
df = pd.DataFrame([
|
| 82 |
+
{"time": t, "mm_rain": rec.get("mm_rain", 0.0), "mm_prob": rec.get("mm_prob", 0.0)}
|
| 83 |
+
for t, rec in sorted(time_map.items())
|
| 84 |
+
])
|
| 85 |
return df
|
| 86 |
|
| 87 |
+
# --- Combine & table ---
|
| 88 |
+
def build_table(lat, lon):
|
| 89 |
+
df_tio = fetch_tomorrow(lat, lon, 24)
|
| 90 |
+
df_mm = fetch_meteomatics(lat, lon, 24)
|
| 91 |
+
df = pd.merge(df_tio, df_mm, on="time", how="outer").sort_values("time")
|
| 92 |
+
|
| 93 |
+
table = pd.DataFrame({
|
| 94 |
+
"Time (Asia/Kuala_Lumpur)": df["time"].dt.strftime("%Y-%m-%d %H:%M"),
|
| 95 |
+
"Tomorrow.io Rain (mm/hr)": df["tio_rain"].round(3),
|
| 96 |
+
"Tomorrow.io Prob (%)": df["tio_prob"].round(1),
|
| 97 |
+
"Meteomatics Rain (mm/hr)": df["mm_rain"].round(3),
|
| 98 |
+
"Meteomatics Prob (%)": df["mm_prob"].round(1),
|
| 99 |
+
})
|
| 100 |
+
return table
|
| 101 |
+
|
| 102 |
+
def run_table(lat, lon):
|
| 103 |
+
try:
|
| 104 |
+
table = build_table(lat, lon)
|
| 105 |
+
info = f"<small>Location: {lat:.5f}, {lon:.5f} | Model: <b>{DEFAULT_MODEL}</b> | Timezone: <b>Asia/Kuala_Lumpur</b> | Units: <b>mm/hr</b> | Horizon: 24 hours</small>"
|
| 106 |
+
return table, info
|
| 107 |
+
except Exception as e:
|
| 108 |
+
return None, f"<p><b>Error:</b> {e}</p>"
|
| 109 |
|
| 110 |
+
# --- Gradio UI ---
|
| 111 |
+
with gr.Blocks(title="Next 24 Hours — Tomorrow.io + Meteomatics Table (mm/hr)") as demo:
|
| 112 |
+
gr.Markdown("## 🌧️ Next 24 Hours — **Tomorrow.io** & **Meteomatics** (All in mm/hr)\n"
|
| 113 |
+
"Side-by-side hourly rain and probability forecasts for the next 24 hours.")
|
| 114 |
+
|
| 115 |
+
with gr.Row():
|
| 116 |
+
lat = gr.Number(label="Latitude", value=DEFAULT_LAT)
|
| 117 |
+
lon = gr.Number(label="Longitude", value=DEFAULT_LON)
|
| 118 |
|
| 119 |
+
btn = gr.Button("Fetch 24h Table")
|
| 120 |
+
table_out = gr.Dataframe(interactive=False, wrap=True)
|
| 121 |
+
info_out = gr.HTML()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
|
| 123 |
+
btn.click(run_table, inputs=[lat, lon], outputs=[table_out, info_out])
|
| 124 |
|
| 125 |
if __name__ == "__main__":
|
| 126 |
+
os.environ["GRADIO_ANALYTICS_ENABLED"]="false"
|
| 127 |
+
os.environ["HF_HUB_DISABLE_TELEMETRY"]="1"
|
| 128 |
+
demo.launch(server_name="127.0.0.1", server_port=7861, show_error=True, inbrowser=False, debug=True)
|