diff options
-rw-r--r-- | README | 72 | ||||
-rw-r--r-- | go.mod | 2 | ||||
-rw-r--r-- | main.go | 161 |
3 files changed, 150 insertions, 85 deletions
@@ -1,31 +1,41 @@ -# njw.name/weather package - -This package is a command that prints the weather forecast for a -location, using either the Met Office or BBC as a source. - -This is a Go package, and can be installed in the standard go way, -by running `git clone https://git.njw.name/bbcschedule` and -`go install .` from the bbcschedule directory. - -Note that the location must be set manually from the Go code, see -`metdefid` and `bbcdefid`. Relatedly, note that this was the first -Go program I wrote. Look elsewhere for examples of best practise. - -## Usage - - Usage: weather [-b] [-n int] [-v] - - -b use bbc as data source (default true) - -n int - number of days to show (default 2) - -v verbose: show all weather details - -## Contributions - -Any and all comments, bug reports, patches or pull requests would -be very welcomely received. Please email them to <git@njw.name>. - -## License - -This package is licensed under the GPLv3. See the LICENSE file for -more details. +# Weather - a simple tool to look up weather forecasts + +Weather is far faster than any browser based forecast request; +the weather websites nowadays are so full of surveillance +that each forecast takes around 650KB for Met Office or +8MB for BBC. Much better to just make a single request for the +forecast data in JSON format and display it, which is what this +tool does. + +Weather currently requires a location ID for the location to look +up. The defaults are hardcoded at the top of weather.go (bbcdefid +and metdefid), and I encourage you to set them to your own home +location. Otherwise, you can set the location ID with an argument +to the program. + +## Finding your location ID + +The Met Office and BBC weather providers each use different IDs, +but each are easy to discover. + +For the BBC, go to the forecast page for your location and the +ID is the final part of the page URL, for example 2653266 is the +location ID for Chelmsford, which has this page on the BBC website: +https://www.bbc.com/weather/2653266. You could also look it up with +their JSON location service, using the 'containerId' field from a +request like this: +https://open.live.bbc.co.uk/locator/locations?s=chelmsford&format=json + +For the Met Office, look up your location and use the ID from the +'nearestSspaId' field from this URL, substituting "Chelmsford" +for the location you want: +https://www.metoffice.gov.uk/plain-rest-services/location-search/Chelmsford/?max=5 + +## Notes + +Weather doesn't use any API keys or anything silly like that, +instead relying on the URLs the organisations use with their own +Javascript. + +The Met Office unfortunately forbids requests through tor, but BBC +allow them. @@ -1 +1,3 @@ module njw.name/weather + +go 1.14 @@ -1,17 +1,11 @@ -// Copyright 2020 Nick White. +// 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 -// Note that this was the first Go I wrote. Look elsewhere for -// examples of best practise. +package main -// BUG: need to allow choice of met-office source (-b 0 doesn't work how i thought it would) -// TODO: allow free-text lookups of place names, rather than ids -// TODO: convert metoffice windspeed to mph -// TODO: split output into days -// TODO: add -n flag to only output a certain number of days -// TODO: human friendly dates +// TODO: allow free-text lookups of place names, rather than ids. +// see README for details of how to do that. import ( "encoding/json" @@ -22,12 +16,20 @@ import ( "net/http" ) -const metdefid = "310118" +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 { @@ -106,19 +108,21 @@ type WeatherParams struct { WT int // Weather Type } -// TODO: complete this mapping 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", @@ -126,26 +130,38 @@ var TypeDescription = map[int]string{ 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 { - date string - time string - temperature float64 - precipitation int - weathertype int - windspeed float64 + 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 ( - bbc = flag.Bool("b", true, "use bbc as data source") - numdays = flag.Int("n", 2, "number of days to show") + src = flag.String("s", "bbc", "data source provider (valid options: 'bbc', 'metoffice')") verbose = flag.Bool("v", false, "verbose: show all weather details") ) @@ -162,16 +178,56 @@ func processBBC(b []byte) []Weather { for _, report := range f.Detailed.Reports { w.date = report.LocalDate w.time = report.Timeslot - w.temperature = float64(report.TemperatureC) - w.precipitation = report.PrecipitationProbabilityInPercent + 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.windspeed = float64(report.WindSpeedMph) + 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 @@ -182,29 +238,20 @@ func processMet(b []byte) []Weather { } for _, d := range r.BestFcst.Forecast.Location.Days { + w = parseMetWeather(d.DayValues.WeatherParameters) w.date = d.Date w.time = "Day " - w.temperature = d.DayValues.WeatherParameters.T - w.precipitation = d.DayValues.WeatherParameters.PP - w.weathertype = d.DayValues.WeatherParameters.WT - w.windspeed = d.DayValues.WeatherParameters.WS weather = append(weather, w) + w = parseMetWeather(d.NightValues.WeatherParameters) w.date = d.Date w.time = "Night " - w.temperature = d.DayValues.WeatherParameters.T - w.precipitation = d.DayValues.WeatherParameters.PP - w.weathertype = d.DayValues.WeatherParameters.WT - w.windspeed = d.DayValues.WeatherParameters.WS weather = append(weather, w) for _, t := range d.TimeSteps.TimeStep { + w = parseMetWeather(t.WeatherParameters) w.date = d.Date w.time = t.Time - w.temperature = t.WeatherParameters.T - w.precipitation = t.WeatherParameters.PP - w.weathertype = t.WeatherParameters.WT - w.windspeed = t.WeatherParameters.WS weather = append(weather, w) } } @@ -214,25 +261,35 @@ func processMet(b []byte) []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) - } else { - if *bbc { - id = bbcdefid - } else { - id = metdefid - } } - if *bbc { - resp, err = http.Get(fmt.Sprintf(bbcurl, id)) - } else { - resp, err = http.Get(fmt.Sprintf(meturl, id)) - } + resp, err = http.Get(fmt.Sprintf(url, id)) if err != nil { log.Fatal(err) } @@ -242,11 +299,7 @@ func main() { } b, err := ioutil.ReadAll(resp.Body) - if *bbc { - weather = processBBC(b) - } else { - weather = processMet(b) - } + weather = parsefunc(b) for _, w := range weather { fmt.Printf("%s %s ", w.date, w.time) @@ -254,10 +307,10 @@ func main() { if !ok { desc = fmt.Sprintf("%d", w.weathertype) } - fmt.Printf("%18s, Temp: %4.1f°C, ", desc, w.temperature) - fmt.Printf("Rain: %2d%%, Wind: %4.1fmph\n", w.precipitation, w.windspeed) + 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", w) + fmt.Printf("%+v\n\n", w) } } } |