A cmd/data/localObs_Dunedin.json => cmd/data/localObs_Dunedin.json +26 -0
@@ 0,0 1,26 @@
+{
+ "_usage": "This data is restricted and may only be used with explicit permission from MetService NZ. Contact dataenquiries@metservice.com",
+ "id": "loc1036728",
+ "location": "Dunedin",
+ "locationId": "93892",
+ "threeHour": {
+ "clothingLayers": "2",
+ "dateTime": "8:00pm Wednesday 21 Jul 2021",
+ "dateTimeISO": "2021-07-21T20:00:00+12:00",
+ "humidity": "77",
+ "pressure": "n/a",
+ "rainfall": "0.0",
+ "rawTime": 1626854400000,
+ "temp": "9",
+ "windChill": "8",
+ "windDirection": "W",
+ "windProofLayers": "0",
+ "windSpeed": "2"
+ },
+ "twentyFourHour": {
+ "dateTime": "9:00am Wednesday 21 Jul 2021",
+ "maxTemp": 10,
+ "minTemp": 4,
+ "rainfall": "0.0"
+ }
+}
A cmd/observation.go => cmd/observation.go +112 -0
@@ 0,0 1,112 @@
+package cmd
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "math"
+
+ "git.sr.ht/~kota/metservice-go"
+ "github.com/spf13/cobra"
+ "github.com/spf13/viper"
+)
+
+// observationCmd represents the observation command
+var observationCmd = &cobra.Command{
+ Use: "observation",
+ Short: "display current or past weather observations",
+ Run: observation,
+}
+
+func init() {
+ rootCmd.AddCommand(observationCmd)
+ observationCmd.PersistentFlags().StringP("location", "l", "", "Location used for the weather observation")
+ viper.BindPFlag("location", observationCmd.PersistentFlags().Lookup("location"))
+}
+
+// observation fetches and prints an observation based on options provided
+func observation(cmd *cobra.Command, args []string) {
+ client := metservice.NewClient()
+ ctx := context.Background()
+ location := viper.GetString("location")
+ if location == "" {
+ log.Fatal("location is required either using the flag or config")
+ }
+ err := observationThreeHour(client, ctx, location)
+ if err != nil {
+ log.Fatal(err)
+ }
+}
+
+// observationThreeHour fetches and prints an observation of the last three
+// hours
+func observationThreeHour(client *metservice.Client, ctx context.Context, location string) error {
+ o, _, err := client.GetObservation(ctx, location)
+ if err != nil {
+ return fmt.Errorf("getting observation: %v", err)
+ }
+ fmt.Fprintf(out, "%d°C ", *o.ThreeHour.Temp)
+ fmt.Fprintf(out, "%dkm/h ", *o.ThreeHour.WindSpeed)
+ fmt.Fprintf(out, "%s - ", *o.ThreeHour.WindDirection)
+ fmt.Fprintf(out, "%d%%", *o.ThreeHour.Humidity)
+ fmt.Fprintf(out, "(%.2f°C)\n", feelsLike(*o.ThreeHour.Temp, *o.ThreeHour.Humidity, *o.ThreeHour.WindSpeed))
+ return nil
+}
+
+// feelsLike calculates an estimated temperate that considers your ability to
+// heat or cool yourself. We use the same formula described by Metservice here
+// https://blog.metservice.com/FeelsLikeTemp
+// When the measured air temperature is below 10°C the windChill algorithm is
+// used. If it is above 14°C the apparentTemp is calculated and used if it's it
+// higher than the measured air temp. For values between 10-14°C a pragmatic
+// linear roll-off of the wind chill is used. So 12°C at 5km/h wind has a
+// windChill of 12°C.
+// NOTE: The windChill formula listen on the Metservice website is incorrect.
+func feelsLike(temp int, humidity int, speed int) float64 {
+ // return whichever is higher of apparentTemp and temp
+ if temp >= 14 {
+ t := float64(temp)
+ at := apparentTemp(temp, humidity, speed)
+ return math.Max(t, at)
+ }
+ w := windChill(temp, speed)
+ // return windChill
+ if temp < 10 {
+ return w
+ }
+ // linear roll-off between windChill and temp
+ t := float64(temp)
+ f := t - (((t - w) * (14 - t)) / 4)
+ return f
+}
+
+// windChill calculates the wind chill temperature in degrees Celsius based on
+// the following algorithm
+// https://web.archive.org/web/20060415000715/http://www.msc.ec.gc.ca/education/windchill/Science_equations_e.cfm
+// w = 13.12 + 0.6215*t - 11.35*k^0.16 + 0.396*t*k^0.16
+// t = Dry bulb temperature (°C)
+// k = Average wind speed in km/h
+func windChill(temp int, speed int) float64 {
+ t := float64(temp)
+ k := math.Pow(float64(speed), 0.16)
+ w := 13.12 + 0.6215*t - 11.35*k + 0.396*t*k
+ return w
+}
+
+// apparentTemp calculates the apparent temperature in degrees Celsius based on
+// the following algorithm http://www.bom.gov.au/info/thermal_stress/
+// at = t + 0.33e - 0.70m - 4.00
+// t = Dry bulb temperature (°C)
+// e = Water vapour pressure (hPa) [humidity]
+// m = Wind speed (m/s) at an elevation of 10 meters
+// our function takes the values in the following formats and converts them
+// temp = Air temperature (°C) int
+// humidity = Relative humidity percentage int (0-100)
+// speed = Wind speed km/h int
+func apparentTemp(temp int, humidity int, speed int) float64 {
+ t := float64(temp)
+ e := float64(humidity) / 100 * 6.105 * (17.27 * t / (237.7 + t))
+ m := float64(speed) * (5 / 18)
+ at := t + 0.33*e - 0.70*m - 4.00
+ return at
+}
A cmd/observation_test.go => cmd/observation_test.go +65 -0
@@ 0,0 1,65 @@
+package cmd
+
+import (
+ "bytes"
+ "context"
+ "io"
+ "net/http"
+ "os"
+ "testing"
+)
+
+func TestObservationThreeHour(t *testing.T) {
+ client, mux, teardown := setup()
+ defer teardown()
+
+ mux.HandleFunc("/localObs_Dunedin", func(w http.ResponseWriter, r *http.Request) {
+ reader, err := os.Open("data/localObs_Dunedin.json")
+ if err != nil {
+ t.Errorf("metweather.observationThreeHour returned error: %v", err)
+ }
+ io.Copy(w, reader)
+ })
+
+ ctx := context.Background()
+ out = new(bytes.Buffer) // captured output
+ err := observationThreeHour(client, ctx, "Dunedin")
+ if err != nil {
+ t.Errorf("metweather.observationThreeHour returned error: %v", err)
+ }
+ got := out.(*bytes.Buffer).String()
+ want := "9°C 2km/h W - 77%(10.01°C)\n"
+ if got != want {
+ t.Errorf("metweather.observationThreeHour\ngot = %q\nwant = %q", got, want)
+ }
+}
+
+func TestWindChill(t *testing.T) {
+ var tests = []struct {
+ temp int
+ speed int
+ want float64
+ }{
+ {12, 5, 12.042135509662362},
+ {12, 10, 11.04098839261837},
+ {12, 25, 9.535110852166257},
+ {12, 50, 8.239921987968058},
+ {9, 5, 8.640714167661585},
+ {9, 10, 7.459305944972208},
+ {9, 25, 5.682288283565697},
+ {9, 50, 4.153894755731934},
+ {0, 5, -1.5635498583407408},
+ {0, 10, -3.2857413979662784},
+ {0, 25, -5.87617942223598},
+ {0, 50, -8.104186940976438},
+ }
+ for _, test := range tests {
+ if got := windChill(test.temp, test.speed); got != test.want {
+ t.Errorf("metweather.windChill\ntemp = %d\nspeed = %d\nwant = %f\n got = %f\n",
+ test.temp,
+ test.speed,
+ test.want,
+ got)
+ }
+ }
+}
M go.mod => go.mod +4 -1
@@ 3,8 3,11 @@ module git.sr.ht/~kota/metweather
go 1.16
require (
- git.sr.ht/~kota/metservice-go v1.0.0
+ git.sr.ht/~kota/metservice-go v1.0.1
github.com/mitchellh/go-homedir v1.1.0
+ github.com/spf13/cast v1.4.0 // indirect
github.com/spf13/cobra v1.2.1
github.com/spf13/viper v1.8.1
+ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
+ golang.org/x/text v0.3.6 // indirect
)
M go.sum => go.sum +8 -5
@@ 37,8 37,8 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
-git.sr.ht/~kota/metservice-go v1.0.0 h1:PJOtNd//RdqJwitpPQzjMDb9fGTibOSPzo9N3+0mAos=
-git.sr.ht/~kota/metservice-go v1.0.0/go.mod h1:ruGNU5HLsIa4TPYAiB2FHPUTk3Ya4/uRT06/OLCZB4c=
+git.sr.ht/~kota/metservice-go v1.0.1 h1:XjQjIgBywJ3la2RQnpeU82WGU9oEnsJ+8Sch0vb8yGE=
+git.sr.ht/~kota/metservice-go v1.0.1/go.mod h1:ruGNU5HLsIa4TPYAiB2FHPUTk3Ya4/uRT06/OLCZB4c=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
@@ 221,8 221,9 @@ github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIK
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY=
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
-github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cast v1.4.0 h1:WhlbjwB9EGCc8W5Rxdkus+wmH2ASRwwTJk6tgHKwdqQ=
+github.com/spf13/cast v1.4.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw=
github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
@@ 400,8 401,9 @@ golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ 409,8 411,9 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=