Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions backends/open-meteo.com.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
8 changes: 4 additions & 4 deletions backends/openweathermap.org.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
Expand Down
246 changes: 246 additions & 0 deletions backends/weatherapi.com.go
Original file line number Diff line number Diff line change
@@ -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{}
}
58 changes: 45 additions & 13 deletions frontends/ascii-art-table.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion iface/iface.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down