// Copyright 2018-2021 Nick White. // Use of this source code is governed by the GPLv3 // license that can be found in the LICENSE file. package main // TODO: allow free-text lookups of place names, rather than ids. // see README for details of how to do that. import ( "encoding/json" "flag" "fmt" "io/ioutil" "log" "net/http" ) const metdefid = "310004" const bbcdefid = "2654675" const meturl = "https://www.metoffice.gov.uk/public/data/PWSCache/BestForecast/Forecast/%s.json?concise=true" const bbcurl = "https://weather-broker-cdn.api.bbci.co.uk/en/forecast/aggregated/%s" const usage = `Usage: weather [-s source] [-v] [locationid] weather shows the weather forecast for a location. Read the README for instructions on finding your location ID. ` const mpsToMphMultiplier = 2.23693629 // BBC structures type BBCResponse struct { Forecasts []struct { Detailed struct { Reports []Report } } } type Report struct { EnhancedWeatherDescription string ExtendedWeatherType int FeelsLikeTemperatureC int FeelsLikeTemperatureF int GustSpeedKph int GustSpeedMph int Humidity int LocalDate string PrecipitationProbabilityInPercent int PrecipitationProbabilityText string Pressure int TemperatureC int TemperatureF int Timeslot string TimeslotLength int Visibility string WeatherType int WeatherTypeText string WindDescription string WindDirection string WindDirectionAbbreviation string WindDirectionFull string WindSpeedKph int WindSpeedMph int } // Met Office structures type MetResponse struct { BestFcst struct { Forecast struct { Location struct { Days []Day `json:"Day"` } } } } type Day struct { Date string `json:"@date"` DayValues struct { WeatherParameters WeatherParams } NightValues struct { WeatherParameters WeatherParams } TimeSteps struct { TimeStep []struct { Time string `json:"@time"` WeatherParameters WeatherParams } } } type WeatherParams struct { AQIndex int // Air Quality F float64 // Feels Like Temperature H int // Humidity P int // Pressure PP int // Precipitation Probability T float64 // Temperature UV int // Max UV Index V int // Visibility WD string // Wind Direction WG float64 // Wind Gust WS float64 // Wind Speed WT int // Weather Type } var TypeDescription = map[int]string{ 0: "Clear Sky", 1: "Sunny", 2: "Partly Cloudy", 3: "Sunny Intervals", 4: "Unknown", 5: "Mist", 6: "Fog", 7: "Light Cloud", 8: "Thick Cloud", 9: "Light Rain Showers", 10: "Light Rain Showers", 11: "Drizzle", 12: "Light Rain", 13: "Heavy Rain Showers", 14: "Heavy Rain Showers", 15: "Heavy Rain", 16: "Sleet Showers", 17: "Sleet Showers", 18: "Sleet", 19: "Hail Showers", 20: "Hail Showers", 21: "Hail", 22: "Light Snow Showers", 23: "Light Snow Showers", 24: "Light Snow", 25: "Heavy Snow Showers", 26: "Heavy Snow Showers", 27: "Heavy Snow", 28: "Thundery Showers", 29: "Thundery Showers", 30: "Thunder", } // Our prefered struct type Weather struct { airquality int date string feelsliketempDegC float64 humidity int maxuv int precipitationPerc int pressure int temperatureDegC float64 time string visibilityMetres int weathertype int winddir string windgustMph float64 windspeedMph float64 } var ( src = flag.String("s", "bbc", "data source provider (valid options: 'bbc', 'metoffice')") verbose = flag.Bool("v", false, "verbose: show all weather details") ) func processBBC(b []byte) []Weather { var r BBCResponse var weather []Weather var w Weather err := json.Unmarshal(b, &r) if err != nil { log.Fatal(err) } for _, f := range r.Forecasts { for _, report := range f.Detailed.Reports { w.date = report.LocalDate w.time = report.Timeslot w.feelsliketempDegC = float64(report.FeelsLikeTemperatureC) w.humidity = report.Humidity w.temperatureDegC = float64(report.TemperatureC) w.precipitationPerc = report.PrecipitationProbabilityInPercent w.pressure = report.Pressure w.visibilityMetres = estimateVisibility(report.Visibility) w.weathertype = report.WeatherType w.winddir = report.WindDirectionFull w.windgustMph = float64(report.GustSpeedMph) w.windspeedMph = float64(report.WindSpeedMph) weather = append(weather, w) } } return weather } // estimateVisibility returns a rough number of meters // of vilibility based on descriptive text. func estimateVisibility(s string) int { switch s { case "Good": return 10000 case "Moderate": return 5000 case "Poor": return 500 default: return 0 } } func parseMetWeather(wp WeatherParams) Weather { var w Weather w.airquality = wp.AQIndex w.feelsliketempDegC = wp.F w.humidity = wp.H w.pressure = wp.P w.precipitationPerc = wp.PP w.temperatureDegC = wp.T w.maxuv = wp.UV w.visibilityMetres = wp.V w.weathertype = wp.WT w.winddir = wp.WD w.windgustMph = wp.WG w.windspeedMph = wp.WS * mpsToMphMultiplier return w } func processMet(b []byte) []Weather { var r MetResponse var weather []Weather var w Weather err := json.Unmarshal(b, &r) if err != nil { log.Fatal(err) } for _, d := range r.BestFcst.Forecast.Location.Days { w = parseMetWeather(d.DayValues.WeatherParameters) w.date = d.Date w.time = "Day " weather = append(weather, w) w = parseMetWeather(d.NightValues.WeatherParameters) w.date = d.Date w.time = "Night " weather = append(weather, w) for _, t := range d.TimeSteps.TimeStep { w = parseMetWeather(t.WeatherParameters) w.date = d.Date w.time = t.Time weather = append(weather, w) } } return weather } func main() { var err error var id string var url string var parsefunc func([]byte) []Weather var resp *http.Response var weather []Weather flag.Usage = func() { fmt.Fprintf(flag.CommandLine.Output(), usage) flag.PrintDefaults() } flag.Parse() switch *src { case "bbc": id = bbcdefid url = bbcurl parsefunc = processBBC case "metoffice": id = metdefid url = meturl parsefunc = processMet default: log.Fatalf("data source %s not supported; use either 'bbc' or 'metoffice'\n", *src) } if flag.NArg() > 0 { id = flag.Arg(0) } resp, err = http.Get(fmt.Sprintf(url, id)) if err != nil { log.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { log.Fatalf("HTTP status code: %d\n", resp.StatusCode) } b, err := ioutil.ReadAll(resp.Body) weather = parsefunc(b) for _, w := range weather { fmt.Printf("%s %s ", w.date, w.time) desc, ok := TypeDescription[w.weathertype] if !ok { desc = fmt.Sprintf("%d", w.weathertype) } fmt.Printf("%18s, Temp: %4.1f°C, ", desc, w.temperatureDegC) fmt.Printf("Rain: %2d%%, Wind: %4.1fmph, Gust: %4.1fmph\n", w.precipitationPerc, w.windspeedMph, w.windgustMph) if *verbose { fmt.Printf("%+v\n\n", w) } } }