diff options
Diffstat (limited to 'main.go')
-rw-r--r-- | main.go | 316 |
1 files changed, 316 insertions, 0 deletions
@@ -0,0 +1,316 @@ +// 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\n", w.precipitationPerc, w.windspeedMph) + if *verbose { + fmt.Printf("%+v\n\n", w) + } + } +} |