mirror of
https://github.com/tiennm99/tiennm99bot.git
synced 2026-10-11 12:28:54 +00:00
/thuyvan shows the 5-day tide-peak forecast at Phú An and Nhà Bè from the Đài KTTV Nam Bộ bulletin PDF, the Open-Meteo rain forecast, and live VNDMS river gauges within 30 km. /thuyvan_subscribe opts a chat into a 10:30 ICT push, retried at 12:30, sent only when a forecast peak reaches báo động I (1.40 m) or a day's rain reaches 50 mm. A missing or stale bulletin fails the push instead of reading as no risk. The thoitiet module is renamed to weather; the /thoitiet* commands keep their names. BREAKING CHANGE: the module key is now "weather". A MODULES list that names thoitiet fails at startup and must name weather instead.
175 lines
5.8 KiB
Go
175 lines
5.8 KiB
Go
package weather
|
||
|
||
import (
|
||
"fmt"
|
||
"math"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
// sourceLine credits the data source, as Open-Meteo's CC BY 4.0 licence asks.
|
||
const sourceLine = "Nguồn: Open-Meteo"
|
||
|
||
type condition struct {
|
||
label string
|
||
emoji string
|
||
}
|
||
|
||
// wmoConditions maps WMO weather interpretation codes, the "weather_code"
|
||
// field Open-Meteo returns, to a Vietnamese label.
|
||
var wmoConditions = map[int]condition{
|
||
0: {"Trời quang", "☀️"},
|
||
1: {"Ít mây", "🌤️"},
|
||
2: {"Mây rải rác", "⛅"},
|
||
3: {"Nhiều mây", "☁️"},
|
||
45: {"Sương mù", "🌫️"},
|
||
48: {"Sương mù đóng băng", "🌫️"},
|
||
51: {"Mưa phùn nhẹ", "🌦️"},
|
||
53: {"Mưa phùn", "🌦️"},
|
||
55: {"Mưa phùn dày", "🌧️"},
|
||
56: {"Mưa phùn băng nhẹ", "🌧️"},
|
||
57: {"Mưa phùn băng", "🌧️"},
|
||
61: {"Mưa nhẹ", "🌧️"},
|
||
63: {"Mưa vừa", "🌧️"},
|
||
65: {"Mưa to", "🌧️"},
|
||
66: {"Mưa băng nhẹ", "🌧️"},
|
||
67: {"Mưa băng", "🌧️"},
|
||
71: {"Tuyết nhẹ", "🌨️"},
|
||
73: {"Tuyết vừa", "🌨️"},
|
||
75: {"Tuyết dày", "❄️"},
|
||
77: {"Tuyết hạt", "🌨️"},
|
||
80: {"Mưa rào nhẹ", "🌦️"},
|
||
81: {"Mưa rào", "🌦️"},
|
||
82: {"Mưa rào rất to", "⛈️"},
|
||
85: {"Mưa tuyết nhẹ", "🌨️"},
|
||
86: {"Mưa tuyết", "🌨️"},
|
||
95: {"Dông", "⛈️"},
|
||
96: {"Dông kèm mưa đá nhẹ", "⛈️"},
|
||
99: {"Dông kèm mưa đá", "⛈️"},
|
||
}
|
||
|
||
func conditionFor(code int) condition {
|
||
if c, ok := wmoConditions[code]; ok {
|
||
return c
|
||
}
|
||
return condition{"Không rõ", "🌡️"}
|
||
}
|
||
|
||
// vnWeekdays are the short Vietnamese weekday names, indexed by time.Weekday.
|
||
var vnWeekdays = [...]string{"CN", "T2", "T3", "T4", "T5", "T6", "T7"}
|
||
|
||
// hourlyViewHours is how many upcoming hours /thoitiet lists.
|
||
const hourlyViewHours = 6
|
||
|
||
// formatHourly renders current conditions followed by one block per upcoming
|
||
// hour.
|
||
func formatHourly(p place, f forecast) string {
|
||
c := f.Current
|
||
cur := conditionFor(c.WeatherCode)
|
||
h := f.Hourly
|
||
idx := f.upcomingHours(hourlyViewHours)
|
||
var sb strings.Builder
|
||
fmt.Fprintf(&sb, "🕐 Thời tiết %d giờ tới — %s\n", len(idx), displayName(p))
|
||
fmt.Fprintf(&sb, "Hiện tại %s: %s (cảm giác %s), %s %s\n", clock(c.Time), temp(c.Temperature),
|
||
temp(c.ApparentTemperature), cur.label, cur.emoji)
|
||
for _, i := range idx {
|
||
hc := conditionFor(h.WeatherCode[i])
|
||
fmt.Fprintf(&sb, "\n%s %s %s, %s (cảm giác %s)\n", clock(h.Time[i]), hc.emoji, hc.label,
|
||
temp(h.Temperature[i]), temp(h.ApparentTemperature[i]))
|
||
fmt.Fprintf(&sb, "Mưa %d%% (%s mm), độ ẩm %d%%, gió %s km/h\n", round(h.PrecipitationProbability[i]),
|
||
decimal(h.Precipitation[i]), round(h.Humidity[i]), decimal(h.WindSpeed[i]))
|
||
}
|
||
sb.WriteString("\n" + sourceLine)
|
||
return sb.String()
|
||
}
|
||
|
||
// formatToday renders current conditions plus today's daily summary.
|
||
func formatToday(p place, f forecast) string {
|
||
c := f.Current
|
||
cur := conditionFor(c.WeatherCode)
|
||
var sb strings.Builder
|
||
fmt.Fprintf(&sb, "%s Thời tiết hôm nay %s — %s\n", conditionFor(f.Daily.WeatherCode[0]).emoji,
|
||
shortDate(f.Daily.Time[0]), displayName(p))
|
||
fmt.Fprintf(&sb, "Hiện tại: %s (cảm giác %s), %s %s\n", temp(c.Temperature), temp(c.ApparentTemperature),
|
||
cur.label, cur.emoji)
|
||
fmt.Fprintf(&sb, "Độ ẩm %d%%, gió %s km/h\n", round(c.Humidity), decimal(c.WindSpeed))
|
||
writeDay(&sb, "Cả ngày", f.Daily, 0)
|
||
sb.WriteString(sourceLine)
|
||
return sb.String()
|
||
}
|
||
|
||
// formatTomorrow renders tomorrow's daily summary.
|
||
func formatTomorrow(p place, f forecast) string {
|
||
d := f.Daily
|
||
var sb strings.Builder
|
||
fmt.Fprintf(&sb, "%s Thời tiết ngày mai %s %s — %s\n", conditionFor(d.WeatherCode[1]).emoji,
|
||
weekday(d.Time[1]), shortDate(d.Time[1]), displayName(p))
|
||
writeDay(&sb, "Dự báo", d, 1)
|
||
sb.WriteString(sourceLine)
|
||
return sb.String()
|
||
}
|
||
|
||
// formatWeek renders one line per day for every day in the forecast.
|
||
func formatWeek(p place, f forecast) string {
|
||
d := f.Daily
|
||
n := d.days()
|
||
var sb strings.Builder
|
||
fmt.Fprintf(&sb, "📅 Thời tiết %d ngày tới — %s\n", n, displayName(p))
|
||
for i := range n {
|
||
c := conditionFor(d.WeatherCode[i])
|
||
fmt.Fprintf(&sb, "%s %s: %s–%s %s %s, mưa %d%%\n", weekday(d.Time[i]), shortDate(d.Time[i]),
|
||
tempValue(d.TemperatureMin[i]), temp(d.TemperatureMax[i]), c.emoji, c.label,
|
||
round(d.PrecipitationProbability[i]))
|
||
}
|
||
sb.WriteString(sourceLine)
|
||
return sb.String()
|
||
}
|
||
|
||
// writeDay renders the daily lines shared by the today and tomorrow views.
|
||
func writeDay(sb *strings.Builder, label string, d dailyWeather, i int) {
|
||
c := conditionFor(d.WeatherCode[i])
|
||
fmt.Fprintf(sb, "%s: %s–%s, %s %s\n", label, tempValue(d.TemperatureMin[i]), temp(d.TemperatureMax[i]),
|
||
c.label, c.emoji)
|
||
fmt.Fprintf(sb, "Khả năng mưa %d%% (%s mm), UV %s\n", round(d.PrecipitationProbability[i]),
|
||
decimal(d.PrecipitationSum[i]), decimal(d.UVIndexMax[i]))
|
||
fmt.Fprintf(sb, "Mặt trời mọc %s, lặn %s\n", clock(d.Sunrise[i]), clock(d.Sunset[i]))
|
||
}
|
||
|
||
func round(v float64) int { return int(math.Round(v)) }
|
||
|
||
func tempValue(v float64) string { return strconv.Itoa(round(v)) }
|
||
|
||
func temp(v float64) string { return tempValue(v) + "°C" }
|
||
|
||
// decimal renders one decimal place with the Vietnamese comma separator.
|
||
func decimal(v float64) string {
|
||
return strings.Replace(strconv.FormatFloat(v, 'f', 1, 64), ".", ",", 1)
|
||
}
|
||
|
||
// shortDate turns Open-Meteo's "2026-10-01" into "01/10".
|
||
func shortDate(iso string) string {
|
||
t, err := time.Parse(time.DateOnly, iso)
|
||
if err != nil {
|
||
return iso
|
||
}
|
||
return t.Format("02/01")
|
||
}
|
||
|
||
func weekday(iso string) string {
|
||
t, err := time.Parse(time.DateOnly, iso)
|
||
if err != nil {
|
||
return ""
|
||
}
|
||
return vnWeekdays[t.Weekday()]
|
||
}
|
||
|
||
// clock turns Open-Meteo's local "2026-10-01T05:42" into "05:42".
|
||
func clock(iso string) string {
|
||
t, err := time.Parse("2006-01-02T15:04", iso)
|
||
if err != nil {
|
||
return iso
|
||
}
|
||
return t.Format("15:04")
|
||
}
|