all repos

viye @ 486dcc1

my shot at reimplementing xiki

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

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