summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README72
-rw-r--r--go.mod2
-rw-r--r--main.go161
3 files changed, 150 insertions, 85 deletions
diff --git a/README b/README
index 19cc490..eb44f1e 100644
--- a/README
+++ b/README
@@ -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.
diff --git a/go.mod b/go.mod
index d885c93..8438b7b 100644
--- a/go.mod
+++ b/go.mod
@@ -1 +1,3 @@
module njw.name/weather
+
+go 1.14
diff --git a/main.go b/main.go
index 29af82e..7e615cc 100644
--- a/main.go
+++ b/main.go
@@ -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)
}
}
}