|
| 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