Skip to content

Commit d25a66e

Browse files
committed
Add feature: integrate WeatherXu API
1 parent 6578d5e commit d25a66e

File tree

2 files changed

+354
-0
lines changed

2 files changed

+354
-0
lines changed

README.md

+8
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,14 @@ go install github.com/schachmat/wego@latest
5757
location=New York
5858
wwo-api-key=YOUR_WORLDWEATHERONLINE_API_KEY_HERE
5959
```
60+
0. __With a [WeatherXu](https://weatherxu.com/) account__
61+
* You can create an account and get a free API key by [signing up](https://weatherxu.com/register)
62+
* Update the following `.wegorc` config variables to fit your needs:
63+
```
64+
backend=weatherxu
65+
location=21.033333,105.849998
66+
weatherxu-api-key=YOUR_WEATHERXU_API_KEY_HERE
67+
```
6068
0. You may want to adjust other preferences like `days`, `units` and `…-lang` as
6169
well. Save the file.
6270
0. Run `wego` once again and you should get the weather forecast for the current

backends/weatherxu.go

+346
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
1+
package backends
2+
3+
import (
4+
"encoding/json"
5+
"flag"
6+
"fmt"
7+
"io"
8+
"log"
9+
"net/http"
10+
"regexp"
11+
"strings"
12+
"time"
13+
14+
"github.com/schachmat/wego/iface"
15+
)
16+
17+
type weatherXuConfig struct {
18+
apiKey string
19+
lang string
20+
debug bool
21+
}
22+
23+
// WeatherXuResponse represents the main response structure
24+
type weatherXuResponse struct {
25+
Success bool `json:"success"`
26+
Error ErrorData `json:"error,omitempty"`
27+
Data struct {
28+
Dt int64 `json:"dt"`
29+
Latitude float64 `json:"latitude"`
30+
Longitude float64 `json:"longitude"`
31+
Timezone string `json:"timezone"`
32+
TimezoneAbbreviation string `json:"timezone_abbreviation"`
33+
TimezoneOffset int `json:"timezone_offset"`
34+
Units string `json:"units"`
35+
Currently Current `json:"currently"`
36+
37+
Hourly WeatherXuHourly `json:"hourly"`
38+
Daily WeatherXuDaily `json:"daily"`
39+
} `json:"data"`
40+
}
41+
42+
// ErrorData represents error response from API
43+
type ErrorData struct {
44+
StatusCode int `json:"statusCode"`
45+
Message string `json:"message"`
46+
}
47+
48+
// Current represents current weather conditions
49+
type Current struct {
50+
ApparentTemperature float64 `json:"apparentTemperature"`
51+
CloudCover float64 `json:"cloudCover"`
52+
DewPoint float64 `json:"dewPoint"`
53+
Humidity float64 `json:"humidity"`
54+
Icon string `json:"icon"`
55+
PrecipIntensity float64 `json:"precipIntensity"`
56+
Pressure float64 `json:"pressure"`
57+
Temperature float64 `json:"temperature"`
58+
UvIndex int `json:"uvIndex"`
59+
Visibility float64 `json:"visibility"`
60+
WindDirection float64 `json:"windDirection"`
61+
WindGust float64 `json:"windGust"`
62+
WindSpeed float64 `json:"windSpeed"`
63+
}
64+
65+
// Hourly represents hourly forecast data
66+
type WeatherXuHourly struct {
67+
Data []HourlyData `json:"data"`
68+
}
69+
70+
// HourlyData represents weather data for a specific hour
71+
type HourlyData struct {
72+
ApparentTemperature float64 `json:"apparentTemperature"`
73+
CloudCover float64 `json:"cloudCover"`
74+
DewPoint float64 `json:"dewPoint"`
75+
ForecastStart int64 `json:"forecastStart"`
76+
Humidity float64 `json:"humidity"`
77+
Icon string `json:"icon"`
78+
PrecipIntensity float64 `json:"precipIntensity"`
79+
PrecipProbability float64 `json:"precipProbability"`
80+
Pressure float64 `json:"pressure"`
81+
Temperature float64 `json:"temperature"`
82+
UvIndex int `json:"uvIndex"`
83+
Visibility float64 `json:"visibility"`
84+
WindDirection float64 `json:"windDirection"`
85+
WindGust float64 `json:"windGust"`
86+
WindSpeed float64 `json:"windSpeed"`
87+
}
88+
89+
// Daily represents daily forecast data
90+
type WeatherXuDaily struct {
91+
Data []DailyData `json:"data"`
92+
}
93+
94+
// DailyData represents weather data for a specific day
95+
type DailyData struct {
96+
ApparentTemperatureAvg float64 `json:"apparentTemperatureAvg"`
97+
ApparentTemperatureMax float64 `json:"apparentTemperatureMax"`
98+
ApparentTemperatureMin float64 `json:"apparentTemperatureMin"`
99+
CloudCover float64 `json:"cloudCover"`
100+
DewPointAvg float64 `json:"dewPointAvg"`
101+
DewPointMax float64 `json:"dewPointMax"`
102+
DewPointMin float64 `json:"dewPointMin"`
103+
ForecastEnd int64 `json:"forecastEnd"`
104+
ForecastStart int64 `json:"forecastStart"`
105+
Humidity float64 `json:"humidity"`
106+
Icon string `json:"icon"`
107+
MoonPhase float64 `json:"moonPhase"`
108+
PrecipIntensity float64 `json:"precipIntensity"`
109+
PrecipProbability float64 `json:"precipProbability"`
110+
Pressure float64 `json:"pressure"`
111+
SunriseTime int64 `json:"sunriseTime"`
112+
SunsetTime int64 `json:"sunsetTime"`
113+
TemperatureAvg float64 `json:"temperatureAvg"`
114+
TemperatureMax float64 `json:"temperatureMax"`
115+
TemperatureMin float64 `json:"temperatureMin"`
116+
UvIndexMax int `json:"uvIndexMax"`
117+
Visibility float64 `json:"visibility"`
118+
WindDirectionAvg float64 `json:"windDirectionAvg"`
119+
WindGustAvg float64 `json:"windGustAvg"`
120+
WindGustMax float64 `json:"windGustMax"`
121+
WindGustMin float64 `json:"windGustMin"`
122+
WindSpeedAvg float64 `json:"windSpeedAvg"`
123+
WindSpeedMax float64 `json:"windSpeedMax"`
124+
WindSpeedMin float64 `json:"windSpeedMin"`
125+
}
126+
127+
const (
128+
weatherXuURI = "https://api.weatherxu.com/v1/weather?%s"
129+
)
130+
131+
var (
132+
weatherXuCodemap = map[string]iface.WeatherCode{
133+
"clear": iface.CodeSunny,
134+
"partly_cloudy": iface.CodePartlyCloudy,
135+
"mostly_cloudy": iface.CodeCloudy,
136+
"cloudy": iface.CodeVeryCloudy,
137+
"light_rain": iface.CodeLightShowers,
138+
"rain": iface.CodeHeavyRain,
139+
"heavy_rain": iface.CodeHeavyRain,
140+
"freezing_rain": iface.CodeLightSleet,
141+
"thunderstorm": iface.CodeThunderyHeavyRain,
142+
"sleet": iface.CodeLightSleet,
143+
"light_snow": iface.CodeLightSnow,
144+
"snow": iface.CodeHeavySnow,
145+
"heavy_snow": iface.CodeHeavySnow,
146+
"hail": iface.CodeHeavyShowers,
147+
"windy": iface.CodeCloudy,
148+
"fog": iface.CodeFog,
149+
"mist": iface.CodeFog,
150+
"haze": iface.CodeFog,
151+
"smoke": iface.CodeFog,
152+
"tornado": iface.CodeThunderyHeavyRain,
153+
"tropical_storm": iface.CodeThunderyHeavyRain,
154+
"hurricane": iface.CodeThunderyHeavyRain,
155+
"sandstorm": iface.CodeVeryCloudy,
156+
"blizzard": iface.CodeHeavySnowShowers,
157+
}
158+
)
159+
160+
func (c *weatherXuConfig) Setup() {
161+
flag.StringVar(&c.apiKey, "weatherxu-api-key", "", "weatherxu backend: the api `KEY` to use")
162+
flag.StringVar(&c.lang, "weatherxu-lang", "en", "weatherxu backend: the `LANGUAGE` to request from weatherxu")
163+
flag.BoolVar(&c.debug, "weatherxu-debug", false, "weatherxu backend: print raw requests and responses")
164+
}
165+
166+
func (c *weatherXuConfig) parseDaily(dailyInfo WeatherXuDaily, hourlyInfo WeatherXuHourly) []iface.Day {
167+
var forecast []iface.Day
168+
var result []iface.Day
169+
for _, day := range dailyInfo.Data {
170+
forecast = append(forecast, c.parseDay(day))
171+
}
172+
for _, hourlyData := range hourlyInfo.Data {
173+
dayIndex := findDailyPeriod(hourlyData.ForecastStart, dailyInfo.Data)
174+
if dayIndex == -1 {
175+
continue
176+
}
177+
178+
forecast[dayIndex].Slots = append(forecast[dayIndex].Slots, c.parseCurCondHourly(hourlyData))
179+
}
180+
for _, day := range forecast {
181+
if len(day.Slots) > 0 {
182+
result = append(result, day)
183+
}
184+
}
185+
return result
186+
}
187+
188+
func findDailyPeriod(hourlyTime int64, dailyData []DailyData) int {
189+
left := 0
190+
right := len(dailyData) - 1
191+
for left <= right {
192+
mid := (left + right) / 2
193+
if dailyData[mid].ForecastStart <= hourlyTime && hourlyTime <= dailyData[mid].ForecastEnd {
194+
return mid
195+
}
196+
if dailyData[mid].ForecastStart > hourlyTime {
197+
right = mid - 1
198+
} else {
199+
left = mid + 1
200+
}
201+
}
202+
return -1
203+
}
204+
func (c *weatherXuConfig) parseCurCondHourly(hourlyData HourlyData) (ret iface.Cond) {
205+
ret.Time = time.Unix(hourlyData.ForecastStart, 0)
206+
ret.Code = iface.CodeUnknown
207+
if val, ok := weatherXuCodemap[hourlyData.Icon]; ok {
208+
ret.Code = val
209+
ret.Desc = hourlyData.Icon
210+
}
211+
// Convert and set temperature values
212+
var feelsLike float32 = float32(hourlyData.ApparentTemperature)
213+
ret.FeelsLikeC = &feelsLike
214+
var temp float32 = float32(hourlyData.Temperature)
215+
ret.TempC = &temp
216+
217+
// Convert and set atmospheric conditions
218+
var humidity int = int(hourlyData.Humidity * 100)
219+
ret.Humidity = &humidity
220+
var visibility float32 = float32(hourlyData.Visibility)
221+
ret.VisibleDistM = &visibility
222+
223+
// Add wind information
224+
var windSpeed float32 = float32(hourlyData.WindSpeed)
225+
ret.WindspeedKmph = &windSpeed
226+
var windGust float32 = float32(hourlyData.WindGust)
227+
ret.WindGustKmph = &windGust
228+
var windDir int = int(hourlyData.WindDirection)
229+
ret.WinddirDegree = &windDir
230+
231+
var precipM float32 = float32(hourlyData.PrecipIntensity)
232+
ret.PrecipM = &precipM
233+
var precipProb int = int(hourlyData.PrecipProbability * 100)
234+
ret.ChanceOfRainPercent = &precipProb
235+
236+
return ret
237+
}
238+
func (c *weatherXuConfig) parseCurCond(dt int64, current Current) (ret iface.Cond) {
239+
// Set timestamp
240+
ret.Time = time.Unix(dt, 0)
241+
// Map weather code
242+
ret.Code = iface.CodeUnknown
243+
if val, ok := weatherXuCodemap[current.Icon]; ok {
244+
ret.Code = val
245+
ret.Desc = current.Icon
246+
247+
}
248+
249+
// Convert and set temperature values
250+
var feelsLike float32 = float32(current.ApparentTemperature)
251+
ret.FeelsLikeC = &feelsLike
252+
var temp float32 = float32(current.Temperature)
253+
ret.TempC = &temp
254+
255+
// Convert and set atmospheric conditions
256+
var humidity int = int(current.Humidity * 100) // Convert to percentage
257+
ret.Humidity = &humidity
258+
var visibility float32 = float32(current.Visibility)
259+
ret.VisibleDistM = &visibility
260+
261+
// Add wind information
262+
var windSpeed float32 = float32(current.WindSpeed)
263+
ret.WindspeedKmph = &windSpeed
264+
var windDir int = int(current.WindDirection)
265+
ret.WinddirDegree = &windDir
266+
267+
return ret
268+
}
269+
270+
func (c *weatherXuConfig) parseDay(dailyData DailyData) (ret iface.Day) {
271+
ret.Date = time.Unix(dailyData.ForecastStart, 0)
272+
ret.Astronomy.Sunrise = time.Unix(dailyData.SunriseTime, 0)
273+
ret.Astronomy.Sunset = time.Unix(dailyData.SunsetTime, 0)
274+
return ret
275+
}
276+
277+
func (c *weatherXuConfig) fetch(url string) (*weatherXuResponse, error) {
278+
client := &http.Client{}
279+
req, err := http.NewRequest("GET", url, nil)
280+
if err != nil {
281+
return nil, fmt.Errorf("error creating request: %v", err)
282+
}
283+
284+
// Add API key to header
285+
req.Header.Add("X-API-KEY", c.apiKey)
286+
if c.debug {
287+
fmt.Printf("Fetching %s\n", url)
288+
}
289+
290+
res, err := client.Do(req)
291+
if err != nil {
292+
return nil, fmt.Errorf("unable to get (%s) %v", url, err)
293+
}
294+
defer res.Body.Close()
295+
296+
body, err := io.ReadAll(res.Body)
297+
if err != nil {
298+
return nil, fmt.Errorf("unable to read response body (%s): %v", url, err)
299+
}
300+
301+
if c.debug {
302+
fmt.Printf("Response (%s):\n%s\n", url, string(body))
303+
}
304+
305+
var resp weatherXuResponse
306+
if err = json.Unmarshal(body, &resp); err != nil {
307+
return nil, fmt.Errorf("unable to unmarshal response (%s): %v\nThe json body is: %s", url, err, string(body))
308+
}
309+
if !resp.Success {
310+
return nil, fmt.Errorf("error: %s", resp.Error.Message)
311+
}
312+
return &resp, nil
313+
}
314+
315+
func (c *weatherXuConfig) Fetch(location string, numdays int) iface.Data {
316+
var ret iface.Data
317+
loc := ""
318+
319+
if len(c.apiKey) == 0 {
320+
log.Fatal("No weatherxu.com API key specified.\nYou have to register for one at https://weatherxu.com/")
321+
}
322+
if matched, err := regexp.MatchString(`^-?[0-9]*(\.[0-9]+)?,-?[0-9]*(\.[0-9]+)?$`, location); !matched || err != nil {
323+
log.Fatalf("Error: The weatherxu backend only supports latitude,longitude pairs as location.\n", location)
324+
}
325+
326+
s := strings.Split(location, ",")
327+
loc = fmt.Sprintf("lat=%s&lon=%s", s[0], s[1])
328+
requestUrl := fmt.Sprintf(weatherXuURI, loc)
329+
resp, err := c.fetch(requestUrl)
330+
if err != nil {
331+
log.Fatalf("Failed to fetch weather data: %v\n", err)
332+
}
333+
ret.Current = c.parseCurCond(resp.Data.Dt, resp.Data.Currently)
334+
if err != nil {
335+
log.Fatalf("Failed to fetch weather data: %v\n", err)
336+
}
337+
ret.Forecast = c.parseDaily(resp.Data.Daily, resp.Data.Hourly)
338+
ret.GeoLoc = &iface.LatLon{Latitude: float32(resp.Data.Latitude), Longitude: float32(resp.Data.Longitude)}
339+
ret.Location = location
340+
341+
return ret
342+
}
343+
344+
func init() {
345+
iface.AllBackends["weatherxu"] = &weatherXuConfig{}
346+
}

0 commit comments

Comments
 (0)