Skip to content

Commit 240d84e

Browse files
author
tosaki
committed
feat: Improve weather alerts to predict precipitation start/stop and refactor holiday logic to find the next workday or rest day.
1 parent bd46e75 commit 240d84e

File tree

2 files changed

+146
-89
lines changed

2 files changed

+146
-89
lines changed

data_services.py

Lines changed: 131 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,25 @@ def get_data_for_time(target_dt):
266266
# 4. Weather Alert
267267
alert_msg = ""
268268
upcoming_alerts = []
269+
270+
# Precipitation code sets
271+
RAIN_CODES = {61, 63, 65, 80, 81, 82, 66, 67}
272+
SNOW_CODES = {71, 73, 75, 85, 86, 77}
273+
STORM_CODES = {95, 96, 99}
274+
DRIZZLE_CODES = {51, 53, 55}
275+
ALL_PRECIP_CODES = RAIN_CODES | SNOW_CODES | STORM_CODES | DRIZZLE_CODES
276+
277+
def get_precip_type(wcode):
278+
if wcode in RAIN_CODES:
279+
return "雨" if Config.LANGUAGE != 'EN' else "Rain"
280+
elif wcode in SNOW_CODES:
281+
return "雪" if Config.LANGUAGE != 'EN' else "Snow"
282+
elif wcode in STORM_CODES:
283+
return "雷雨" if Config.LANGUAGE != 'EN' else "T-Storm"
284+
elif wcode in DRIZZLE_CODES:
285+
return "小雨" if Config.LANGUAGE != 'EN' else "Drizzle"
286+
return None
287+
269288
try:
270289
now_hour_idx = -1
271290
for i, t_str in enumerate(times):
@@ -276,52 +295,67 @@ def get_data_for_time(target_dt):
276295

277296
if now_hour_idx != -1:
278297
max_hours = min(now_hour_idx + 49, len(times))
279-
for i in range(now_hour_idx + 1, max_hours):
280-
t_dt = datetime.datetime.fromisoformat(times[i])
281-
hours_from_now = i - now_hour_idx
282-
code = codes[i]
283-
weather_type = None
284-
285-
# Simple alert logic
286-
if code in [61, 63, 65, 80, 81, 82, 66, 67]:
287-
weather_type = "雨" if Config.LANGUAGE != 'EN' else "Rain"
288-
elif code in [71, 73, 75, 85, 86, 77]:
289-
weather_type = "雪" if Config.LANGUAGE != 'EN' else "Snow"
290-
elif code in [95, 96, 99]:
291-
weather_type = "雷雨" if Config.LANGUAGE != 'EN' else "T-Storm"
292-
elif code in [51, 53, 55]:
293-
weather_type = "小雨" if Config.LANGUAGE != 'EN' else "Drizzle"
294-
295-
if weather_type:
296-
upcoming_alerts.append((hours_from_now, weather_type, t_dt))
298+
current_code = codes[now_hour_idx] if now_hour_idx < len(codes) else code
299+
is_currently_precip = current_code in ALL_PRECIP_CODES
300+
current_precip_type = get_precip_type(current_code)
297301

298-
if upcoming_alerts:
299-
first_alert = upcoming_alerts[0]
300-
hours, wtype, alert_dt = first_alert
302+
if is_currently_precip and current_precip_type:
303+
# Currently precipitating -> find when it STOPS
304+
stop_hour = None
305+
for i in range(now_hour_idx + 1, max_hours):
306+
if codes[i] not in ALL_PRECIP_CODES:
307+
stop_hour = i - now_hour_idx
308+
break
301309

302-
label_today = "今天" if Config.LANGUAGE != 'EN' else "Today"
303-
label_tmr = "明天" if Config.LANGUAGE != 'EN' else "Tmrrw"
304-
305-
if hours <= 3:
306-
if Config.LANGUAGE == 'EN':
307-
alert_msg = f"{wtype} in {hours}h"
308-
else:
309-
alert_msg = f"{hours}H后有{wtype}"
310-
elif alert_dt.day == now_dt.day:
310+
if stop_hour:
311311
if Config.LANGUAGE == 'EN':
312-
alert_msg = f"{wtype} at {alert_dt.hour}:00"
312+
alert_msg = f"{current_precip_type} stops in {stop_hour}H"
313313
else:
314-
alert_msg = f"{label_today}{alert_dt.hour}点有{wtype}"
315-
elif alert_dt.day == now_dt.day + 1:
316-
if Config.LANGUAGE == 'EN':
317-
alert_msg = f"{wtype} tom. at {alert_dt.hour}:00"
318-
else:
319-
alert_msg = f"{label_tmr}{alert_dt.hour}点有{wtype}"
314+
alert_msg = f"{stop_hour}H后{current_precip_type}停"
320315
else:
316+
# Precipitation lasts the entire forecast window
317+
remaining = max_hours - now_hour_idx - 1
321318
if Config.LANGUAGE == 'EN':
322-
alert_msg = f"{wtype} in {hours}h"
319+
alert_msg = f"{current_precip_type} for {remaining}H+"
320+
else:
321+
alert_msg = f"{current_precip_type}将持续至少{remaining}H"
322+
else:
323+
# Not currently precipitating -> find when next precipitation STARTS
324+
for i in range(now_hour_idx + 1, max_hours):
325+
t_dt = datetime.datetime.fromisoformat(times[i])
326+
hours_from_now = i - now_hour_idx
327+
weather_type = get_precip_type(codes[i])
328+
329+
if weather_type:
330+
upcoming_alerts.append((hours_from_now, weather_type, t_dt))
331+
332+
if upcoming_alerts:
333+
first_alert = upcoming_alerts[0]
334+
hours, wtype, alert_dt = first_alert
335+
336+
label_today = "今天" if Config.LANGUAGE != 'EN' else "Today"
337+
label_tmr = "明天" if Config.LANGUAGE != 'EN' else "Tmrrw"
338+
339+
if hours <= 3:
340+
if Config.LANGUAGE == 'EN':
341+
alert_msg = f"{wtype} in {hours}h"
342+
else:
343+
alert_msg = f"{hours}H后有{wtype}"
344+
elif alert_dt.day == now_dt.day:
345+
if Config.LANGUAGE == 'EN':
346+
alert_msg = f"{wtype} at {alert_dt.hour}:00"
347+
else:
348+
alert_msg = f"{label_today}{alert_dt.hour}点有{wtype}"
349+
elif alert_dt.day == now_dt.day + 1:
350+
if Config.LANGUAGE == 'EN':
351+
alert_msg = f"{wtype} tom. at {alert_dt.hour}:00"
352+
else:
353+
alert_msg = f"{label_tmr}{alert_dt.hour}点有{wtype}"
323354
else:
324-
alert_msg = f"{hours}H后有{wtype}"
355+
if Config.LANGUAGE == 'EN':
356+
alert_msg = f"{wtype} in {hours}h"
357+
else:
358+
alert_msg = f"{hours}H后有{wtype}"
325359
except Exception as e:
326360
print(f"Alert Logic Error: {e}")
327361

@@ -611,42 +645,69 @@ def get_calendar_info():
611645
if hasattr(holidays, Config.HOLIDAY_COUNTRY):
612646
country_holidays = getattr(holidays, Config.HOLIDAY_COUNTRY)(years=now.year)
613647
else:
614-
# Fallback to SG
615648
country_holidays = holidays.SG(years=now.year)
616649
except:
617650
country_holidays = holidays.SG(years=now.year)
618651

619652
today_holiday = country_holidays.get(now.date())
620-
621-
next_date = now.date() + datetime.timedelta(days=1)
622-
next_non_working = None
623-
624-
for _ in range(30):
625-
is_weekend = next_date.weekday() >= 5
626-
is_holiday = country_holidays.get(next_date)
627-
628-
if is_weekend or is_holiday:
629-
info = is_holiday if is_holiday else ("Saturday" if next_date.weekday() == 5 else "Sunday")
630-
weekday_en = next_date.strftime("%A")
631-
653+
today_is_weekend = now.weekday() >= 5
654+
is_rest_today = bool(today_holiday) or today_is_weekend
655+
656+
# Determine today's status name when it's a rest day
657+
today_status = None
658+
if is_rest_today:
659+
if today_holiday:
660+
today_status = today_holiday
661+
else:
632662
if Config.LANGUAGE == 'EN':
633-
label = info
663+
today_status = "Saturday" if now.weekday() == 5 else "Sunday"
634664
else:
635-
# Basic translation for generic weekends if needed, mostly holiday names are in their lang
636-
# If holidays lib returns English for SG holidays, might need translation map but out of scope?
637-
# User asked "holiday origin", allowing different countries. Holidays lib usually returns local language or english.
638-
# Let's assume the library return is acceptable or we display it as is.
639-
if info == "Saturday": label = "周六"
640-
elif info == "Sunday": label = "周日"
641-
else: label = info # Use provided name
642-
643-
next_non_working = {
644-
"date": next_date.strftime("%m-%d"),
645-
"name": label,
646-
"days_away": (next_date - now.date()).days
647-
}
648-
break
649-
next_date += datetime.timedelta(days=1)
665+
today_status = "星期六" if now.weekday() == 5 else "星期日"
666+
667+
next_day = None
668+
next_date = now.date() + datetime.timedelta(days=1)
669+
670+
if is_rest_today:
671+
# Today is a rest day -> find next WORKDAY
672+
for _ in range(30):
673+
d_is_weekend = next_date.weekday() >= 5
674+
d_is_holiday = country_holidays.get(next_date)
675+
if not d_is_weekend and not d_is_holiday:
676+
# This is a workday
677+
weekday_en = next_date.strftime("%A")
678+
if Config.LANGUAGE == 'EN':
679+
label = weekday_en
680+
else:
681+
label = WEEKDAYS_CN.get(weekday_en, weekday_en)
682+
next_day = {
683+
"type": "workday",
684+
"date": next_date.strftime("%m-%d"),
685+
"name": today_status, # Show today's holiday/weekend name
686+
"days_away": (next_date - now.date()).days
687+
}
688+
break
689+
next_date += datetime.timedelta(days=1)
690+
else:
691+
# Today is a workday -> find next REST day
692+
for _ in range(30):
693+
d_is_weekend = next_date.weekday() >= 5
694+
d_is_holiday = country_holidays.get(next_date)
695+
if d_is_weekend or d_is_holiday:
696+
info = d_is_holiday if d_is_holiday else ("Saturday" if next_date.weekday() == 5 else "Sunday")
697+
if Config.LANGUAGE == 'EN':
698+
label = info
699+
else:
700+
if info == "Saturday": label = "星期六"
701+
elif info == "Sunday": label = "星期日"
702+
else: label = info
703+
next_day = {
704+
"type": "rest",
705+
"date": next_date.strftime("%m-%d"),
706+
"name": label,
707+
"days_away": (next_date - now.date()).days
708+
}
709+
break
710+
next_date += datetime.timedelta(days=1)
650711

651712
weekday_en = now.strftime("%A")
652713
if Config.LANGUAGE == 'EN':
@@ -658,6 +719,6 @@ def get_calendar_info():
658719
"date_str": now.strftime("%Y-%m-%d"),
659720
"weekday": weekday_disp,
660721
"lunar": lunar_str,
661-
"holiday": today_holiday,
662-
"next_non_working": next_non_working
722+
"is_rest_today": is_rest_today,
723+
"next_day": next_day
663724
}

templates/dashboard.html

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -52,27 +52,23 @@
5252
<!-- Top: Date Info -->
5353
<div class="flex flex-row justify-between items-start">
5454
<div>
55-
<div class="text-4xl font-bold mb-2 tracking-wide">
55+
<div class="text-6xl font-bold mb-2 tracking-wide">
5656
{{ calendar.date_str }}
5757
</div>
5858
<!-- Weekday -->
5959
<div class="{% if config.LANGUAGE == 'EN' %}text-[5.5rem]{% else %}text-[7rem]{% endif %} font-black leading-none mb-3 uppercase tracking-tight">{{ calendar.weekday }}</div>
60-
<div class="text-6xl font-bold mt-1 text-gray-800">{{ calendar.lunar }}</div>
61-
62-
{% if calendar.holiday %}
63-
<div class="inline-block bg-black text-white text-4xl font-bold px-6 py-3 mt-4">
64-
{% if config.LANGUAGE == 'EN' %}Holiday:{% else %}节假日:{% endif %} {{ calendar.holiday }}
65-
</div>
66-
{% endif %}
60+
<div class="text-4xl font-bold mt-1 text-gray-800">{{ calendar.lunar }}</div>
6761
</div>
6862

69-
<!-- Next Non-Working Day -->
70-
{% if calendar.next_non_working %}
63+
<!-- Next Rest/Work Day -->
64+
{% if calendar.next_day %}
7165
<div class="text-right pt-2">
72-
<div class="text-4xl font-bold text-gray-500 uppercase tracking-widest mb-2">{% if config.LANGUAGE == 'EN' %}Next Off{% else %}下个休息{% endif %}</div>
73-
<div class="text-6xl font-black leading-none">{{ calendar.next_non_working.date }}</div>
74-
<div class="text-4xl font-bold mt-2">{{ calendar.next_non_working.name }}</div>
75-
<div class="text-3xl font-bold text-gray-400 mt-2">{% if config.LANGUAGE == 'EN' %}In {{ calendar.next_non_working.days_away }} days{% else %}{{ calendar.next_non_working.days_away }}天后{% endif %}</div>
66+
<div class="flex items-baseline justify-end gap-4 mb-2">
67+
<span class="text-4xl font-bold text-gray-600 uppercase tracking-widest">{% if calendar.is_rest_today %}{% if config.LANGUAGE == 'EN' %}Next Work{% else %}下个工作{% endif %}{% else %}{% if config.LANGUAGE == 'EN' %}Next Off{% else %}下个休息{% endif %}{% endif %}</span>
68+
<span class="text-4xl font-bold text-gray-500">{% if config.LANGUAGE == 'EN' %}In {{ calendar.next_day.days_away }} days{% else %}{{ calendar.next_day.days_away }}天后{% endif %}</span>
69+
</div>
70+
<div class="text-6xl font-black leading-none">{{ calendar.next_day.date }}</div>
71+
<div class="text-4xl font-bold mt-2">{{ calendar.next_day.name }}</div>
7672
</div>
7773
{% endif %}
7874
</div>
@@ -140,12 +136,12 @@
140136
</div>
141137
{% elif weather.current.alert %}
142138
<!-- Rain/Snow forecast -->
143-
<div class="inline-block text-3xl font-bold text-gray-400 px-1 py-1">
139+
<div class="inline-block text-3xl font-bold text-gray-500 px-1 py-1">
144140
{{ weather.current.alert }}
145141
</div>
146142
{% else %}
147143
<!-- No rain/snow: Gray text -->
148-
<div class="inline-block text-3xl font-bold text-gray-400 px-1 py-1">
144+
<div class="inline-block text-3xl font-bold text-gray-500 px-1 py-1">
149145
{% if config.LANGUAGE == 'EN' %}No rain/snow recently{% else %}近期无雨雪{% endif %}
150146
</div>
151147
{% endif %}
@@ -155,7 +151,7 @@
155151
<!-- Right: Temperature and High/Low (Aligned) -->
156152
<div class="text-right flex flex-col items-end justify-center tabular-nums">
157153
<div class="text-[9rem] font-black leading-[0.85] tracking-tighter">{{ weather.current.temp }}°</div>
158-
<div class="text-4xl font-bold text-gray-400 mt-8 mr-12 tracking-tight">{{ weather.current.high_low }}</div>
154+
<div class="text-4xl font-bold text-gray-500 mt-8 mr-12 tracking-tight">{{ weather.current.high_low }}</div>
159155
</div>
160156
</div>
161157
</div>
@@ -204,7 +200,7 @@
204200
</span>
205201
</div>
206202
<!-- Meta info (small gray text on right) -->
207-
<span class="text-3xl font-bold text-gray-400 shrink-0 min-w-[3rem] text-right">
203+
<span class="text-3xl font-bold text-gray-500 shrink-0 min-w-[3rem] text-right">
208204
{{ story.meta }}
209205
</span>
210206
</div>
@@ -221,7 +217,7 @@
221217
</span>
222218
</div>
223219
<!-- Score (Small gray number, Black if breaking) -->
224-
<span class="text-3xl font-bold {% if story.is_breaking %}text-black{% else %}text-gray-400{% endif %} shrink-0 min-w-[3rem] text-right">
220+
<span class="text-3xl font-bold {% if story.is_breaking %}text-black{% else %}text-gray-500{% endif %} shrink-0 min-w-[3rem] text-right">
225221
{{ story.score }}
226222
</span>
227223
</div>

0 commit comments

Comments
 (0)