diff --git a/README.md b/README.md index 5c9dea9..8838bc6 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,14 @@ go install github.com/schachmat/wego@latest location=New York wwo-api-key=YOUR_WORLDWEATHERONLINE_API_KEY_HERE ``` +0. __With a [WeatherAPI](https://www.weatherapi.com/) account__ + * You can create an account and get a free API key by [signing up](https://www.weatherapi.com/signup.aspx) + * Update the following `.wegorc` config variables to fit your needs: + ``` + backend=weatherapi + location=New York + weather-api-key=YOUR_WEATHERAPI_API_KEY_HERE + ``` 0. You may want to adjust other preferences like `days`, `units` and `…-lang` as well. Save the file. 0. Run `wego` once again and you should get the weather forecast for the current diff --git a/backends/open-meteo.com.go b/backends/open-meteo.com.go index 696ba1e..76747ff 100644 --- a/backends/open-meteo.com.go +++ b/backends/open-meteo.com.go @@ -213,12 +213,13 @@ func (opmeteo *openmeteoConfig) Fetch(location string, numdays int) iface.Data { forecast := opmeteo.parseDaily(resp.Hourly) + for i, _ := range forecast { + forecast[i].Astronomy.Sunset = time.Unix(resp.Daily.Sunset[i], 0) + forecast[i].Astronomy.Sunrise = time.Unix(resp.Daily.Sunrise[i], 0) + } if len(forecast) > 0 { - forecast[0].Astronomy.Sunset = time.Unix(resp.Daily.Sunset[0], 0) - forecast[0].Astronomy.Sunrise = time.Unix(resp.Daily.Sunrise[0], 0) ret.Forecast = forecast } - return ret } diff --git a/backends/openweathermap.org.go b/backends/openweathermap.org.go index 1eae102..0ca04c4 100644 --- a/backends/openweathermap.org.go +++ b/backends/openweathermap.org.go @@ -22,12 +22,12 @@ type openWeatherConfig struct { type openWeatherResponse struct { Cod string `json:"cod"` City struct { - Name string `json:"name"` - Country string `json:"country"` - TimeZone int64 `json: "timezone"` + Name string `json:"name"` + Country string `json:"country"` + TimeZone int64 `json: "timezone"` // sunrise/sunset are once per call SunRise int64 `json: "sunrise"` - SunSet int64 `json: "sunset"` + SunSet int64 `json: "sunset"` } `json:"city"` List []dataBlock `json:"list"` } diff --git a/backends/weatherapi.com.go b/backends/weatherapi.com.go new file mode 100644 index 0000000..4a4b2fa --- /dev/null +++ b/backends/weatherapi.com.go @@ -0,0 +1,246 @@ +package backends + +import ( + "encoding/json" + "flag" + "fmt" + "io" + "log" + "net/http" + "strconv" + "time" + + "github.com/schachmat/wego/iface" +) + +type weatherApiResponse struct { + Location struct { + Name string `json:"name"` + Country string `json:"country"` + } `json:"location"` + Current currentCond `json:"current"` + Forecast struct { + List []forecastBlock `json:"forecastday"` + } `json:"forecast"` +} + +type currentCond struct { + TempC float32 `json:"temp_c"` + FeelsLikeC float32 `json:"feelslike_c"` + Humidity int `json:"humidity"` + Condition struct { + Code int `json:"code"` + Desc string `json:"text"` + } `json:"condition"` + WindspeedKmph *float32 `json:"wind_kph"` + WinddirDegree int `json:"wind_degree"` + ChanceOfRainPercent int `json:"chance_of_rain"` +} + +type forecastBlock struct { + DateEpoch int64 `json:"date_epoch"` + Hour []hourlyWeather `json:"hour"` +} + +type hourlyWeather struct { + TimeEpoch int64 `json:"time_epoch"` + TempC float32 `json:"temp_c"` + FeelsLikeC float32 `json:"feelslike_c"` + Humidity int `json:"humidity"` + Condition struct { + Code int `json:"code"` + Desc string `json:"text"` + } `json:"condition"` + WindspeedKmph *float32 `json:"wind_kph"` + WinddirDegree int `json:"wind_degree"` + ChanceOfRainPercent int `json:"chance_of_rain"` +} + +type weatherApiConfig struct { + apiKey string + debug bool + lang string +} + +const ( + weatherApiURI = "https://api.weatherapi.com/v1/forecast.json?key=%s&q=%s&days=%s&aqi=no&alerts=no&lang=%s" +) + +var ( + codemapping = map[int]iface.WeatherCode{ + 1000: iface.CodeSunny, + 1003: iface.CodePartlyCloudy, + 1006: iface.CodeCloudy, + 1009: iface.CodeVeryCloudy, + 1030: iface.CodeVeryCloudy, + 1063: iface.CodeLightRain, + 1066: iface.CodeLightSnow, + 1069: iface.CodeLightSleet, + 1071: iface.CodeLightShowers, + 1072: iface.CodeLightShowers, + 1087: iface.CodeThunderyShowers, + 1114: iface.CodeHeavySnow, + 1117: iface.CodeHeavySnowShowers, + 1135: iface.CodeFog, + 1147: iface.CodeFog, + 1150: iface.CodeLightRain, + 1153: iface.CodeLightRain, + 1168: iface.CodeLightRain, + 1171: iface.CodeLightRain, + 1180: iface.CodeLightRain, + 1183: iface.CodeLightRain, + 1186: iface.CodeHeavyRain, + 1189: iface.CodeHeavyRain, + 1192: iface.CodeHeavyShowers, + 1195: iface.CodeHeavyRain, + 1198: iface.CodeLightRain, + 1201: iface.CodeHeavyRain, + 1204: iface.CodeLightSleet, + 1207: iface.CodeLightSleetShowers, + 1210: iface.CodeLightSnow, + 1213: iface.CodeLightSnow, + 1216: iface.CodeHeavySnow, + 1219: iface.CodeHeavySnow, + 1222: iface.CodeHeavySnow, + 1225: iface.CodeHeavySnow, + 1237: iface.CodeHeavySnow, + 1240: iface.CodeLightShowers, + 1243: iface.CodeHeavyShowers, + 1246: iface.CodeThunderyShowers, + 1249: iface.CodeLightSleetShowers, + 1252: iface.CodeLightSleetShowers, + 1255: iface.CodeLightSnowShowers, + 1258: iface.CodeHeavySnowShowers, + 1261: iface.CodeLightSnowShowers, + 1264: iface.CodeHeavySnowShowers, + 1273: iface.CodeThunderyShowers, + 1276: iface.CodeThunderyHeavyRain, + 1279: iface.CodeThunderySnowShowers, + 1282: iface.CodeThunderySnowShowers, + } +) + +func (c *weatherApiConfig) Setup() { + flag.StringVar(&c.apiKey, "weather-api-key", "", "weatherapi backend: the api `Key` to use") + flag.StringVar(&c.lang, "weather-lang", "en", "weatherapi backend: the `LANGUAGE` to request from weatherapi") + flag.BoolVar(&c.debug, "weather-debug", false, "weatherapi backend: print raw requests and responses") +} + +func (c *weatherApiConfig) fetch(url string) (*weatherApiResponse, error) { + res, err := http.Get(url) + if c.debug { + fmt.Printf("Fetching %s\n", url) + } + if err != nil { + return nil, fmt.Errorf("Unable to get (%s) %v", url, err) + } + defer res.Body.Close() + body, err := io.ReadAll(res.Body) + if err != nil { + return nil, fmt.Errorf("Unable to read response body (%s): %v", url, err) + } + + if c.debug { + fmt.Printf("Response (%s):\n%s\n", url, string(body)) + } + + var resp weatherApiResponse + if err := json.Unmarshal(body, &resp); err != nil { + return nil, fmt.Errorf("Unable to unmarshal response (%s): %v\nThe json body is: %s", url, err, string(body)) + } + + return &resp, nil +} + +func (c *weatherApiConfig) parseDaily(dataBlock []forecastBlock, numdays int) []iface.Day { + var ret []iface.Day + + for i, day := range dataBlock { + if i == numdays { + break + } + newDay := new(iface.Day) + newDay.Date = time.Unix(day.DateEpoch, 0) + for _, hour := range day.Hour { + slot, err := c.parseCond(hour) + if err != nil { + log.Println("Error parsing hourly weather condition:", err) + continue + } + newDay.Slots = append(newDay.Slots, slot) + } + ret = append(ret, *newDay) + } + + return ret +} + +func (c *weatherApiConfig) parseCond(forecastInfo hourlyWeather) (iface.Cond, error) { + var ret iface.Cond + + ret.Code = iface.CodeUnknown + ret.Desc = forecastInfo.Condition.Desc + ret.Humidity = &(forecastInfo.Humidity) + ret.TempC = &(forecastInfo.TempC) + ret.FeelsLikeC = &(forecastInfo.FeelsLikeC) + ret.WindspeedKmph = forecastInfo.WindspeedKmph + ret.WinddirDegree = &forecastInfo.WinddirDegree + ret.ChanceOfRainPercent = &forecastInfo.ChanceOfRainPercent + + if val, ok := codemapping[forecastInfo.Condition.Code]; ok { + ret.Code = val + } + + ret.Time = time.Unix(forecastInfo.TimeEpoch, 0) + + return ret, nil +} + +func (c *weatherApiConfig) parseCurCond(forecastInfo currentCond) (iface.Cond, error) { + var ret iface.Cond + + ret.Code = iface.CodeUnknown + ret.Desc = forecastInfo.Condition.Desc + ret.Humidity = &(forecastInfo.Humidity) + ret.TempC = &(forecastInfo.TempC) + ret.FeelsLikeC = &(forecastInfo.FeelsLikeC) + ret.WindspeedKmph = forecastInfo.WindspeedKmph + ret.WinddirDegree = &forecastInfo.WinddirDegree + ret.ChanceOfRainPercent = &forecastInfo.ChanceOfRainPercent + + if val, ok := codemapping[forecastInfo.Condition.Code]; ok { + ret.Code = val + } + + return ret, nil +} + +func (c *weatherApiConfig) Fetch(location string, numdays int) iface.Data { + var ret iface.Data + + if len(c.apiKey) == 0 { + log.Fatal("No weatherapi.com API key specified.\nYou have to register for one at https://weatherapi.com/signup.aspx") + } + + resp, err := c.fetch(fmt.Sprintf(weatherApiURI, c.apiKey, location, strconv.Itoa(numdays), c.lang)) + if err != nil { + log.Fatalf("Failed to fetch weather data: %v\n", err) + } + ret.Current, err = c.parseCurCond(resp.Current) + ret.Location = fmt.Sprintf("%s, %s", resp.Location.Name, resp.Location.Country) + + if err != nil { + log.Fatalf("Failed to fetch weather data: %v\n", err) + } + + if numdays == 0 { + return ret + } + ret.Forecast = c.parseDaily(resp.Forecast.List, numdays) + + return ret +} + +func init() { + iface.AllBackends["weatherapi"] = &weatherApiConfig{} +} diff --git a/frontends/ascii-art-table.go b/frontends/ascii-art-table.go index 01a85aa..f3cfacc 100644 --- a/frontends/ascii-art-table.go +++ b/frontends/ascii-art-table.go @@ -18,10 +18,12 @@ import ( type aatConfig struct { coords bool monochrome bool - unit iface.UnitSystem + compact bool + + unit iface.UnitSystem } -//TODO: replace s parameter with printf interface? +// TODO: replace s parameter with printf interface? func aatPad(s string, mustLen int) (ret string) { ansiEsc := regexp.MustCompile("\033.*?m") ret = s @@ -283,9 +285,13 @@ func (c *aatConfig) formatCond(cur []string, cond iface.Cond, current bool) (ret }, } - icon, ok := codes[cond.Code] - if !ok { - log.Fatalln("aat-frontend: The following weather code has no icon:", cond.Code) + icon := make([]string, 5) + if !c.compact { + var ok bool + icon, ok = codes[cond.Code] + if !ok { + log.Fatalln("aat-frontend: The following weather code has no icon:", cond.Code) + } } desc := cond.Desc @@ -352,19 +358,45 @@ func (c *aatConfig) printDay(day iface.Day) (ret []string) { } dateFmt := "┤ " + day.Date.Format("Mon 02. Jan") + " ├" - ret = append([]string{ - " ┌─────────────┐ ", - "┌──────────────────────────────┬───────────────────────" + dateFmt + "───────────────────────┬──────────────────────────────┐", - "│ Morning │ Noon └──────┬──────┘ Evening │ Night │", - "├──────────────────────────────┼──────────────────────────────┼──────────────────────────────┼──────────────────────────────┤"}, - ret...) - return append(ret, - "└──────────────────────────────┴──────────────────────────────┴──────────────────────────────┴──────────────────────────────┘") + if !c.compact { + ret = append([]string{ + " ┌─────────────┐ ", + "┌──────────────────────────────┬───────────────────────" + dateFmt + "───────────────────────┬──────────────────────────────┐", + "│ Morning │ Noon └──────┬──────┘ Evening │ Night │", + "├──────────────────────────────┼──────────────────────────────┼──────────────────────────────┼──────────────────────────────┤"}, + ret...) + ret = append(ret, + "└──────────────────────────────┴──────────────────────────────┴──────────────────────────────┴──────────────────────────────┘") + } else { + merge := func(src string, into string) string { + ret := []rune(into) + for k, v := range src { + ret[k] = v + } + return string(ret) + } + + spaces := (len(ret[0]) / 4) - 3 + bar := strings.Repeat("─", spaces) + + ret = append([]string{ + day.Date.Format("Mon 02. Jan"), + "┌" + merge("Morning", bar) + "┬" + merge("Noon", bar) + "┬" + merge("Evening", bar) + "┬" + merge("Night", bar) + "┐", + }, ret...) + + ret = append(ret, + "└"+bar+"┴"+bar+"┴"+bar+"┴"+bar+"┘", + ) + } + + return ret } func (c *aatConfig) Setup() { flag.BoolVar(&c.coords, "aat-coords", false, "aat-frontend: Show geo coordinates") flag.BoolVar(&c.monochrome, "aat-monochrome", false, "aat-frontend: Monochrome output") + + flag.BoolVar(&c.compact, "aat-compact", false, "aat-frontend: Compact output") } func (c *aatConfig) Render(r iface.Data, unitSystem iface.UnitSystem) { diff --git a/iface/iface.go b/iface/iface.go index 0d4ea91..63b654a 100644 --- a/iface/iface.go +++ b/iface/iface.go @@ -123,7 +123,7 @@ func (u UnitSystem) Temp(tempC float32) (res float32, unit string) { } else if u == UnitsImperial { return tempC*1.8 + 32, "°F" } else if u == UnitsSi { - return tempC + 273.16, "°K" + return tempC + 273.16, "K" } log.Fatalln("Unknown unit system:", u) return