all repos

viye @ 0966286a56515c571766a50f8c47a9a64703f183

my shot at reimplementing xiki

viye/internal/plumbing/weather/weather.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
weather: show date of the fetch, 1 month ago
1
package weather
2
3
import (
4
	"context"
5
	"encoding/json"
6
	"errors"
7
	"fmt"
8
	"net/http"
9
	"net/url"
10
	"strconv"
11
	"time"
12
13
	"github.com/olexsmir/viye/internal/config"
14
	"github.com/olexsmir/viye/internal/viye"
15
)
16
17
type Tool struct{}
18
19
func (Tool) Name() string               { return "weather" }
20
func (Tool) Match(c *viye.Context) bool { return c.Cmd == "weather" }
21
func (Tool) Execute(c *viye.Context) (string, error) {
22
	city, err := getCity(c)
23
	if err != nil {
24
		return "", err
25
	}
26
27
	fc, err := forecast(city)
28
	if err != nil {
29
		return "", err
30
	}
31
32
	return viye.FormatOutput(render(city, fc)), nil
33
}
34
35
func getCity(c *viye.Context) (string, error) {
36
	var city string
37
	if len(c.Args) == 1 {
38
		city = c.Args[0]
39
	} else {
40
		cfg, err := config.Load()
41
		if err != nil {
42
			return "", err
43
		}
44
		city = cfg.Get("city")
45
	}
46
	if city == "" {
47
		return "", errors.New("please provide a city option")
48
	}
49
	return city, nil
50
}
51
52
func render(city string, fc weatherResp) []string {
53
	if len(fc.Weather) < 3 || len(fc.Weather[0].Hourly) < 8 {
54
		return []string{"no forecast data"}
55
	}
56
57
	t := fc.Weather[0].Hourly
58
	lines := []string{}
59
	lines = append(lines, fmt.Sprintf("%s (%s)", city, fc.Weather[0].Date))
60
	lines = append(lines, fmt.Sprintf("Today %s %s° / %s %s° / %s %s°",
61
		emoji(t[2].Code), t[2].TempC, // morning
62
		emoji(t[4].Code), t[4].TempC, // midday
63
		emoji(t[7].Code), t[7].TempC)) // evening
64
65
	for _, d := range fc.Weather[1:3] {
66
		mi, _ := strconv.Atoi(d.Min)
67
		ma, _ := strconv.Atoi(d.Max)
68
		lines = append(lines, fmt.Sprintf("%-5s %s %.0f° (avg)",
69
			weekday(d.Date), emoji(d.Hourly[4].Code), (float64(mi)+float64(ma))/2))
70
	}
71
	return lines
72
}
73
74
func weekday(date string) string {
75
	t, _ := time.Parse("2006-01-02", date)
76
	return t.Weekday().String()[:3]
77
}
78
79
// api
80
81
type weatherResp struct {
82
	Weather []day `json:"weather"`
83
}
84
85
// hour is one 3-hourly reading; 8 readings cover a day, hours 0..21.
86
type hour struct {
87
	TempC string `json:"tempC"`
88
	Code  string `json:"weatherCode"`
89
}
90
91
type day struct {
92
	Date   string `json:"date"`
93
	Min    string `json:"mintempC"`
94
	Max    string `json:"maxtempC"`
95
	Hourly []hour `json:"hourly"`
96
}
97
98
// forecast fetches the 3-day forecast for a city via wttr.in (Open-Meteo data).
99
func forecast(city string) (weatherResp, error) {
100
	u := "https://wttr.in/" + url.QueryEscape(city) + "?format=j1"
101
	ctx, cancel := context.WithTimeout(context.Background(), viye.Timeout)
102
	defer cancel()
103
104
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
105
	if err != nil {
106
		return weatherResp{}, err
107
	}
108
	resp, err := http.DefaultClient.Do(req)
109
	if err != nil {
110
		return weatherResp{}, err
111
	}
112
	defer resp.Body.Close()
113
	if resp.StatusCode != http.StatusOK {
114
		return weatherResp{}, fmt.Errorf("api returned %s", resp.Status)
115
	}
116
117
	var out weatherResp
118
	if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
119
		return weatherResp{}, err
120
	}
121
	if len(out.Weather) == 0 {
122
		return weatherResp{}, fmt.Errorf("city %q not found", city)
123
	}
124
	return out, nil
125
}
126
127
// emoji maps a wttr.in weather code to its phenomenon emoji.
128
func emoji(code string) string {
129
	c, _ := strconv.Atoi(code)
130
	switch {
131
	case c == 113:
132
		return "☀️"
133
	case c == 116:
134
		return "🌤️"
135
	case c == 119 || c == 122:
136
		return "☁️"
137
	case c == 143 || c == 248 || c == 260:
138
		return "🌫️"
139
	case c == 179 || c == 227 || c == 230 ||
140
		c >= 317 && c <= 338 || c == 350 ||
141
		c >= 362 && c <= 377 || c == 392 || c == 395: // snow, sleet
142
		return "🌨️"
143
	case c == 176 || c == 182 || c == 185 || c == 200 ||
144
		c >= 263 && c <= 314 || c >= 353 && c <= 359 ||
145
		c == 386 || c == 389: // drizzle, rain
146
		return "🌧️"
147
	default:
148
		return "🌡️"
149
	}
150
}