summaryrefslogtreecommitdiff
path: root/main.go
diff options
context:
space:
mode:
Diffstat (limited to 'main.go')
-rw-r--r--main.go316
1 files changed, 316 insertions, 0 deletions
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..7e615cc
--- /dev/null
+++ b/main.go
@@ -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)
+ }
+ }
+}