Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 [WeatherXu](https://weatherxu.com/) account__
* You can create an account and get a free API key by [signing up](https://weatherxu.com/register)
* Update the following `.wegorc` config variables to fit your needs:
```
backend=weatherxu
location=21.033333,105.849998
weatherxu-api-key=YOUR_WEATHERXU_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
349 changes: 349 additions & 0 deletions backends/weatherxu.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,349 @@
package backends

import (
"encoding/json"
"flag"
"fmt"
"io"
"log"
"net/http"
"regexp"
"strings"
"time"

"github.com/schachmat/wego/iface"
)

type weatherXuConfig struct {
apiKey string
lang string
debug bool
}

// WeatherXuResponse represents the main response structure
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says "WeatherXuResponse" but the actual type is weatherXuResponse (unexported). This is confusing and will trigger golint-style checks; either rename the type to WeatherXuResponse or update/remove the comment so it matches the identifier.

Suggested change
// WeatherXuResponse represents the main response structure
// weatherXuResponse represents the main response structure

Copilot uses AI. Check for mistakes.
type weatherXuResponse struct {
Success bool `json:"success"`
Error ErrorData `json:"error,omitempty"`
Data struct {
Dt int64 `json:"dt"`
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
Timezone string `json:"timezone"`
TimezoneAbbreviation string `json:"timezone_abbreviation"`
TimezoneOffset int `json:"timezone_offset"`
Units string `json:"units"`
Currently Current `json:"currently"`

Hourly WeatherXuHourly `json:"hourly"`
Daily WeatherXuDaily `json:"daily"`
} `json:"data"`
}

// ErrorData represents error response from API
type ErrorData struct {
StatusCode int `json:"statusCode"`
Message string `json:"message"`
}

// Current represents current weather conditions
type Current struct {
ApparentTemperature float64 `json:"apparentTemperature"`
CloudCover float64 `json:"cloudCover"`
DewPoint float64 `json:"dewPoint"`
Humidity float64 `json:"humidity"`
Icon string `json:"icon"`
PrecipIntensity float64 `json:"precipIntensity"`
Pressure float64 `json:"pressure"`
Temperature float64 `json:"temperature"`
UvIndex int `json:"uvIndex"`
Visibility float64 `json:"visibility"`
WindDirection float64 `json:"windDirection"`
WindGust float64 `json:"windGust"`
WindSpeed float64 `json:"windSpeed"`
}

// Hourly represents hourly forecast data
type WeatherXuHourly struct {
Data []HourlyData `json:"data"`
}

// HourlyData represents weather data for a specific hour
type HourlyData struct {
ApparentTemperature float64 `json:"apparentTemperature"`
CloudCover float64 `json:"cloudCover"`
DewPoint float64 `json:"dewPoint"`
ForecastStart int64 `json:"forecastStart"`
Humidity float64 `json:"humidity"`
Icon string `json:"icon"`
PrecipIntensity float64 `json:"precipIntensity"`
PrecipProbability float64 `json:"precipProbability"`
Pressure float64 `json:"pressure"`
Temperature float64 `json:"temperature"`
UvIndex int `json:"uvIndex"`
Visibility float64 `json:"visibility"`
WindDirection float64 `json:"windDirection"`
WindGust float64 `json:"windGust"`
WindSpeed float64 `json:"windSpeed"`
}

// Daily represents daily forecast data
type WeatherXuDaily struct {
Data []DailyData `json:"data"`
}

// DailyData represents weather data for a specific day
type DailyData struct {
ApparentTemperatureAvg float64 `json:"apparentTemperatureAvg"`
ApparentTemperatureMax float64 `json:"apparentTemperatureMax"`
ApparentTemperatureMin float64 `json:"apparentTemperatureMin"`
CloudCover float64 `json:"cloudCover"`
DewPointAvg float64 `json:"dewPointAvg"`
DewPointMax float64 `json:"dewPointMax"`
DewPointMin float64 `json:"dewPointMin"`
ForecastEnd int64 `json:"forecastEnd"`
ForecastStart int64 `json:"forecastStart"`
Humidity float64 `json:"humidity"`
Icon string `json:"icon"`
MoonPhase float64 `json:"moonPhase"`
PrecipIntensity float64 `json:"precipIntensity"`
PrecipProbability float64 `json:"precipProbability"`
Pressure float64 `json:"pressure"`
SunriseTime int64 `json:"sunriseTime"`
SunsetTime int64 `json:"sunsetTime"`
TemperatureAvg float64 `json:"temperatureAvg"`
TemperatureMax float64 `json:"temperatureMax"`
TemperatureMin float64 `json:"temperatureMin"`
UvIndexMax int `json:"uvIndexMax"`
Visibility float64 `json:"visibility"`
WindDirectionAvg float64 `json:"windDirectionAvg"`
WindGustAvg float64 `json:"windGustAvg"`
WindGustMax float64 `json:"windGustMax"`
WindGustMin float64 `json:"windGustMin"`
WindSpeedAvg float64 `json:"windSpeedAvg"`
WindSpeedMax float64 `json:"windSpeedMax"`
WindSpeedMin float64 `json:"windSpeedMin"`
}

type WeatherXuCodemap struct {
Code iface.WeatherCode
Desc string
}

const (
weatherXuURI = "https://api.weatherxu.com/v1/weather?%s"
)

var (
weatherXuCodemap = map[string]WeatherXuCodemap{
"clear": {iface.CodeSunny, "Clear"},
"partly_cloudy": {iface.CodePartlyCloudy, "Partly Cloudy"},
"mostly_cloudy": {iface.CodeCloudy, "Cloudy"},
"cloudy": {iface.CodeVeryCloudy, "Cloudy"},
"light_rain": {iface.CodeLightShowers, "Light Rain"},
"rain": {iface.CodeHeavyRain, "Rain"},
"heavy_rain": {iface.CodeHeavyRain, "Heavy Rain"},
"freezing_rain": {iface.CodeLightSleet, "Freezing Rain"},
"thunderstorm": {iface.CodeThunderyHeavyRain, "Thunderstorm"},
"sleet": {iface.CodeLightSleet, "Sleet"},
"light_snow": {iface.CodeLightSnow, "Light Snow"},
"snow": {iface.CodeHeavySnow, "Snow"},
"heavy_snow": {iface.CodeHeavySnow, "Heavy Snow"},
"hail": {iface.CodeHeavyShowers, "Hail"},
"windy": {iface.CodeCloudy, "Windy"},
"fog": {iface.CodeFog, "Fog"},
"mist": {iface.CodeFog, "Mist"},
"haze": {iface.CodeFog, "Haze"},
"smoke": {iface.CodeFog, "Smoke"},
"tornado": {iface.CodeThunderyHeavyRain, "Tornado"},
"tropical_storm": {iface.CodeThunderyHeavyRain, "Tropical Storm"},
"hurricane": {iface.CodeThunderyHeavyRain, "Hurricane"},
"sandstorm": {iface.CodeVeryCloudy, "Sandstorm"},
"blizzard": {iface.CodeHeavySnowShowers, "Blizzard"},
}
)

func (c *weatherXuConfig) Setup() {
flag.StringVar(&c.apiKey, "weatherxu-api-key", "", "weatherxu backend: the api `KEY` to use")
flag.StringVar(&c.lang, "weatherxu-lang", "en", "weatherxu backend: the `LANGUAGE` to request from weatherxu")
flag.BoolVar(&c.debug, "weatherxu-debug", false, "weatherxu backend: print raw requests and responses")
Comment on lines +165 to +168
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-weatherxu-lang is defined and stored on the config, but c.lang is never used when building the WeatherXu request. Either remove the flag/config field, or pass the language through to the API (query parameter/header) so the option actually changes the response.

Copilot uses AI. Check for mistakes.
}

func (c *weatherXuConfig) parseDaily(dailyInfo WeatherXuDaily, hourlyInfo WeatherXuHourly) []iface.Day {
var forecast []iface.Day
var result []iface.Day
for _, day := range dailyInfo.Data {
forecast = append(forecast, c.parseDay(day))
}
for _, hourlyData := range hourlyInfo.Data {
dayIndex := findDailyPeriod(hourlyData.ForecastStart, dailyInfo.Data)
if dayIndex == -1 {
continue
}

forecast[dayIndex].Slots = append(forecast[dayIndex].Slots, c.parseCurCondHourly(hourlyData))
}
for _, day := range forecast {
if len(day.Slots) > 0 {
result = append(result, day)
}
}
return result
}

func findDailyPeriod(hourlyTime int64, dailyData []DailyData) int {
left := 0
right := len(dailyData) - 1
for left <= right {
mid := (left + right) / 2
if dailyData[mid].ForecastStart <= hourlyTime && hourlyTime <= dailyData[mid].ForecastEnd {
return mid
}
if dailyData[mid].ForecastStart > hourlyTime {
right = mid - 1
} else {
left = mid + 1
}
}
return -1
}
func (c *weatherXuConfig) parseCurCondHourly(hourlyData HourlyData) (ret iface.Cond) {
ret.Time = time.Unix(hourlyData.ForecastStart, 0)
ret.Code = iface.CodeUnknown
if val, ok := weatherXuCodemap[hourlyData.Icon]; ok {
ret.Code = val.Code
ret.Desc = val.Desc
}
// Convert and set temperature values
var feelsLike float32 = float32(hourlyData.ApparentTemperature)
ret.FeelsLikeC = &feelsLike
var temp float32 = float32(hourlyData.Temperature)
ret.TempC = &temp

// Convert and set atmospheric conditions
var humidity int = int(hourlyData.Humidity * 100)
ret.Humidity = &humidity
var visibility float32 = float32(hourlyData.Visibility)
ret.VisibleDistM = &visibility

// Add wind information
var windSpeed float32 = float32(hourlyData.WindSpeed)
ret.WindspeedKmph = &windSpeed
var windGust float32 = float32(hourlyData.WindGust)
ret.WindGustKmph = &windGust
var windDir int = int(hourlyData.WindDirection)
ret.WinddirDegree = &windDir

var precipM float32 = float32(hourlyData.PrecipIntensity)
ret.PrecipM = &precipM
var precipProb int = int(hourlyData.PrecipProbability * 100)
ret.ChanceOfRainPercent = &precipProb

return ret
}
func (c *weatherXuConfig) parseCurCond(dt int64, current Current) (ret iface.Cond) {
// Set timestamp
ret.Time = time.Unix(dt, 0)
// Map weather code
ret.Code = iface.CodeUnknown
if val, ok := weatherXuCodemap[current.Icon]; ok {
ret.Code = val.Code
ret.Desc = val.Desc

}

// Convert and set temperature values
var feelsLike float32 = float32(current.ApparentTemperature)
ret.FeelsLikeC = &feelsLike
var temp float32 = float32(current.Temperature)
ret.TempC = &temp

// Convert and set atmospheric conditions
var humidity int = int(current.Humidity * 100) // Convert to percentage
ret.Humidity = &humidity
var visibility float32 = float32(current.Visibility)
ret.VisibleDistM = &visibility

// Add wind information
var windSpeed float32 = float32(current.WindSpeed)
ret.WindspeedKmph = &windSpeed
var windDir int = int(current.WindDirection)
ret.WinddirDegree = &windDir

return ret
}

func (c *weatherXuConfig) parseDay(dailyData DailyData) (ret iface.Day) {
ret.Date = time.Unix(dailyData.ForecastStart, 0)
ret.Astronomy.Sunrise = time.Unix(dailyData.SunriseTime, 0)
ret.Astronomy.Sunset = time.Unix(dailyData.SunsetTime, 0)
return ret
}

func (c *weatherXuConfig) fetch(url string) (*weatherXuResponse, error) {
client := &http.Client{}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, fmt.Errorf("error creating request: %v", err)
}

// Add API key to header
req.Header.Add("X-API-KEY", c.apiKey)
if c.debug {
fmt.Printf("Fetching %s\n", url)
}

res, err := client.Do(req)
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 weatherXuResponse
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))
}
if !resp.Success {
return nil, fmt.Errorf("error: %s", resp.Error.Message)
}
return &resp, nil
}

func (c *weatherXuConfig) Fetch(location string, numdays int) iface.Data {
var ret iface.Data
loc := ""

if len(c.apiKey) == 0 {
log.Fatal("No weatherxu.com API key specified.\nYou have to register for one at https://weatherxu.com/")
}
if matched, err := regexp.MatchString(`^-?[0-9]*(\.[0-9]+)?,-?[0-9]*(\.[0-9]+)?$`, location); !matched || err != nil {
log.Fatalf("Error: The weatherxu backend only supports latitude,longitude pairs as location %s.\n", location)
}
Comment on lines +327 to +329
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The latitude/longitude validation regex allows empty values (e.g. "," matches because [0-9]* can be empty). Consider requiring at least one digit for both lat and lon (use + instead of *) so invalid locations fail fast with a clear error.

Copilot uses AI. Check for mistakes.

s := strings.Split(location, ",")
loc = fmt.Sprintf("lat=%s&lon=%s", s[0], s[1])
requestUrl := fmt.Sprintf(weatherXuURI, loc)
resp, err := c.fetch(requestUrl)
if err != nil {
Comment on lines +331 to +335
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

numdays is never applied: the request URL does not include any forecast-days/limit parameter, and the parsed forecast is returned as-is. This breaks the global -days/config behavior; either request only numdays from WeatherXu (if supported) or trim ret.Forecast to numdays before returning.

Copilot uses AI. Check for mistakes.
log.Fatalf("Failed to fetch weather data: %v\n", err)
}
ret.Current = c.parseCurCond(resp.Data.Dt, resp.Data.Currently)

ret.Forecast = c.parseDaily(resp.Data.Daily, resp.Data.Hourly)
ret.GeoLoc = &iface.LatLon{Latitude: float32(resp.Data.Latitude), Longitude: float32(resp.Data.Longitude)}
ret.Location = location

return ret
}

func init() {
iface.AllBackends["weatherxu"] = &weatherXuConfig{}
}