Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/adding weather api #193

Open
wants to merge 23 commits into
base: master
Choose a base branch
from
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