Merge pull request #10 from wneessen/more_curweather_datapoints
More CurrentWeather datapoints and refactoring
This commit is contained in:
commit
f48392d553
|
@ -140,7 +140,7 @@ func (a *AstronomicalInfo) Sunset() DateTime {
|
|||
// available in the AstronomicalInfo it will return DateTime in
|
||||
// which the "not available" field will be true.
|
||||
func (a *AstronomicalInfo) SunsetByDateString(ds string) DateTime {
|
||||
t, err := time.Parse("2006-01-02", ds)
|
||||
t, err := time.Parse(DateFormat, ds)
|
||||
if err != nil {
|
||||
return DateTime{na: true}
|
||||
}
|
||||
|
@ -206,7 +206,7 @@ func (a *AstronomicalInfo) Sunrise() DateTime {
|
|||
// available in the AstronomicalInfo it will return DateTime in
|
||||
// which the "not available" field will be true.
|
||||
func (a *AstronomicalInfo) SunriseByDateString(ds string) DateTime {
|
||||
t, err := time.Parse("2006-01-02", ds)
|
||||
t, err := time.Parse(DateFormat, ds)
|
||||
if err != nil {
|
||||
return DateTime{na: true}
|
||||
}
|
||||
|
|
20
condition.go
20
condition.go
|
@ -53,11 +53,21 @@ const (
|
|||
// ConditionMap is a map to associate a specific ConditionType to a nicely
|
||||
// formatted, human readable string
|
||||
var ConditionMap = map[ConditionType]string{
|
||||
CondCloudy: "Cloudy", CondFog: "Fog", CondFreezingRain: "Freezing rain",
|
||||
CondOvercast: "Overcast", CondPartlyCloudy: "Partly cloudy", CondRain: "Rain",
|
||||
CondRainHeavy: "Heavy rain", CondShowers: "Showers", CondShowersHeavy: "Heavy showers",
|
||||
CondSnow: "Snow", CondSnowHeavy: "Heavy snow", CondSnowRain: "Sleet",
|
||||
CondSunshine: "Clear sky", CondThunderStorm: "Thunderstorm", CondUnknown: "Unknown",
|
||||
CondCloudy: "Cloudy",
|
||||
CondFog: "Fog",
|
||||
CondFreezingRain: "Freezing rain",
|
||||
CondOvercast: "Overcast",
|
||||
CondPartlyCloudy: "Partly cloudy",
|
||||
CondRain: "Rain",
|
||||
CondRainHeavy: "Heavy rain",
|
||||
CondShowers: "Showers",
|
||||
CondShowersHeavy: "Heavy showers",
|
||||
CondSnow: "Snow",
|
||||
CondSnowHeavy: "Heavy snow",
|
||||
CondSnowRain: "Sleet",
|
||||
CondSunshine: "Clear sky",
|
||||
CondThunderStorm: "Thunderstorm",
|
||||
CondUnknown: "Unknown",
|
||||
}
|
||||
|
||||
// Condition is a type wrapper of an WeatherData for holding
|
||||
|
|
|
@ -50,6 +50,8 @@ type APICurrentWeatherData struct {
|
|||
PressureQFE *APIFloat `json:"pressure,omitempty"`
|
||||
// SnowAmount represents the the amount of snow in kg/m3
|
||||
SnowAmount *APIFloat `json:"snowAmount,omitempty"`
|
||||
// SnowHeight represents the the height of snow in m
|
||||
SnowHeight *APIFloat `json:"snowHeight,omitempty"`
|
||||
// Temperature represents the temperature in °C
|
||||
Temperature *APIFloat `json:"temp,omitempty"`
|
||||
// WindDirection represents the direction from which the wind
|
||||
|
@ -62,31 +64,6 @@ type APICurrentWeatherData struct {
|
|||
// WeatherSymbol is a text representation of the current weather
|
||||
// conditions
|
||||
WeatherSymbol *APIString `json:"weatherSymbol,omitempty"`
|
||||
/*
|
||||
// DewPointMean represents the mean dewpoint in °C
|
||||
DewpointMean *APIFloat `json:"dewpointMean,omitempty"`
|
||||
// GlobalRadiation10m represents the sum of global radiation over the last
|
||||
// 10 minutes in kJ/m²
|
||||
GlobalRadiation10m *APIFloat `json:"globalRadiation10m,omitempty"`
|
||||
// GlobalRadiation1h represents the sum of global radiation over the last
|
||||
// 1 hour in kJ/m²
|
||||
GlobalRadiation1h *APIFloat `json:"globalRadiation1h,omitempty"`
|
||||
// GlobalRadiation24h represents the sum of global radiation over the last
|
||||
// 24 hour in kJ/m²
|
||||
GlobalRadiation24h *APIFloat `json:"globalRadiation24h,omitempty"`
|
||||
// TemperatureMax represents the maximum temperature in °C
|
||||
TemperatureMax *APIFloat `json:"tempMax,omitempty"`
|
||||
// TemperatureMean represents the mean temperature in °C
|
||||
TemperatureMean *APIFloat `json:"tempMean,omitempty"`
|
||||
// TemperatureMin represents the minimum temperature in °C
|
||||
TemperatureMin *APIFloat `json:"tempMin,omitempty"`
|
||||
// Temperature5cm represents the temperature 5cm above ground in °C
|
||||
Temperature5cm *APIFloat `json:"temp5cm,omitempty"`
|
||||
// Temperature5cm represents the minimum temperature 5cm above
|
||||
// ground in °C
|
||||
Temperature5cmMin *APIFloat `json:"temp5cmMin,omitempty"`
|
||||
|
||||
*/
|
||||
}
|
||||
|
||||
// CurrentWeatherByCoordinates returns the CurrentWeather values for the given coordinates
|
||||
|
@ -146,7 +123,7 @@ func (cw CurrentWeather) Dewpoint() Temperature {
|
|||
// If the data point is not available in the CurrentWeather it will return
|
||||
// Humidity in which the "not available" field will be true.
|
||||
func (cw CurrentWeather) HumidityRelative() Humidity {
|
||||
if cw.Data.Dewpoint == nil {
|
||||
if cw.Data.HumidityRelative == nil {
|
||||
return Humidity{na: true}
|
||||
}
|
||||
v := Humidity{
|
||||
|
@ -249,7 +226,7 @@ func (cw CurrentWeather) PressureQFE() Pressure {
|
|||
return v
|
||||
}
|
||||
|
||||
// SnowAmount returns the temperature data point as Density.
|
||||
// SnowAmount returns the amount of snow data point as Density.
|
||||
// If the data point is not available in the CurrentWeather it will return
|
||||
// Density in which the "not available" field will be true.
|
||||
func (cw CurrentWeather) SnowAmount() Density {
|
||||
|
@ -268,6 +245,25 @@ func (cw CurrentWeather) SnowAmount() Density {
|
|||
return v
|
||||
}
|
||||
|
||||
// SnowHeight returns the snow height data point as Height.
|
||||
// If the data point is not available in the CurrentWeather it will return
|
||||
// Height in which the "not available" field will be true.
|
||||
func (cw CurrentWeather) SnowHeight() Height {
|
||||
if cw.Data.SnowHeight == nil {
|
||||
return Height{na: true}
|
||||
}
|
||||
v := Height{
|
||||
dt: cw.Data.SnowHeight.DateTime,
|
||||
n: FieldSnowHeight,
|
||||
s: SourceUnknown,
|
||||
fv: cw.Data.SnowHeight.Value,
|
||||
}
|
||||
if cw.Data.SnowHeight.Source != nil {
|
||||
v.s = StringToSource(*cw.Data.SnowHeight.Source)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// Temperature returns the temperature data point as Temperature.
|
||||
// If the data point is not available in the CurrentWeather it will return
|
||||
// Temperature in which the "not available" field will be true.
|
||||
|
|
|
@ -626,6 +626,103 @@ func TestClient_CurrentWeatherByLocation_SnowAmount(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestClient_CurrentWeatherByLocation_SnowHeight(t *testing.T) {
|
||||
tt := []struct {
|
||||
// Location name
|
||||
loc string
|
||||
// CurWeather height
|
||||
h *Height
|
||||
}{
|
||||
{"Ehrenfeld, Germany", &Height{
|
||||
dt: time.Date(2023, 5, 23, 6, 0, 0, 0, time.UTC),
|
||||
s: SourceAnalysis,
|
||||
fv: 1.23,
|
||||
}},
|
||||
{"Berlin, Germany", &Height{
|
||||
dt: time.Date(2023, 5, 23, 6, 0, 0, 0, time.UTC),
|
||||
s: SourceAnalysis,
|
||||
fv: 0.003,
|
||||
}},
|
||||
{"Neermoor, Germany", nil},
|
||||
}
|
||||
c := New(withMockAPI())
|
||||
if c == nil {
|
||||
t.Errorf("failed to create new Client, got nil")
|
||||
return
|
||||
}
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.loc, func(t *testing.T) {
|
||||
cw, err := c.CurrentWeatherByLocation(tc.loc)
|
||||
if err != nil {
|
||||
t.Errorf("CurrentWeatherByLocation failed: %s", err)
|
||||
return
|
||||
}
|
||||
if tc.h != nil && tc.h.String() != cw.SnowHeight().String() {
|
||||
t.Errorf("CurrentWeatherByLocation failed, expected snow height "+
|
||||
"string: %s, got: %s", tc.h.String(), cw.SnowHeight())
|
||||
}
|
||||
if tc.h != nil && tc.h.Value() != cw.SnowHeight().Value() {
|
||||
t.Errorf("CurrentWeatherByLocation failed, expected snow height "+
|
||||
"float: %f, got: %f", tc.h.Value(), cw.SnowHeight().Value())
|
||||
}
|
||||
if tc.h != nil && tc.h.MeterString() != cw.SnowHeight().MeterString() {
|
||||
t.Errorf("CurrentWeatherByLocation failed, expected snow height "+
|
||||
"string: %s, got: %s", tc.h.MeterString(), cw.SnowHeight().MeterString())
|
||||
}
|
||||
if tc.h != nil && tc.h.Meter() != cw.SnowHeight().Meter() {
|
||||
t.Errorf("CurrentWeatherByLocation failed, expected snow height "+
|
||||
"float: %f, got: %f", tc.h.Meter(), cw.SnowHeight().Meter())
|
||||
}
|
||||
if tc.h != nil && tc.h.CentiMeterString() != cw.SnowHeight().CentiMeterString() {
|
||||
t.Errorf("CurrentWeatherByLocation failed, expected snow height "+
|
||||
"string: %s, got: %s", tc.h.CentiMeterString(), cw.SnowHeight().CentiMeterString())
|
||||
}
|
||||
if tc.h != nil && tc.h.CentiMeter() != cw.SnowHeight().CentiMeter() {
|
||||
t.Errorf("CurrentWeatherByLocation failed, expected snow height "+
|
||||
"float: %f, got: %f", tc.h.CentiMeter(), cw.SnowHeight().CentiMeter())
|
||||
}
|
||||
if tc.h != nil && tc.h.MilliMeterString() != cw.SnowHeight().MilliMeterString() {
|
||||
t.Errorf("CurrentWeatherByLocation failed, expected snow height "+
|
||||
"string: %s, got: %s", tc.h.MilliMeterString(), cw.SnowHeight().MilliMeterString())
|
||||
}
|
||||
if tc.h != nil && tc.h.MilliMeter() != cw.SnowHeight().MilliMeter() {
|
||||
t.Errorf("CurrentWeatherByLocation failed, expected snow height "+
|
||||
"float: %f, got: %f", tc.h.MilliMeter(), cw.SnowHeight().MilliMeter())
|
||||
}
|
||||
if tc.h != nil && cw.SnowHeight().Source() != tc.h.s {
|
||||
t.Errorf("CurrentWeatherByLocation failed, expected source: %s, but got: %s",
|
||||
tc.h.s, cw.SnowHeight().Source())
|
||||
}
|
||||
if tc.h != nil && tc.h.dt.Unix() != cw.SnowHeight().DateTime().Unix() {
|
||||
t.Errorf("CurrentWeatherByLocation failed, expected datetime: %s, got: %s",
|
||||
tc.h.dt.Format(time.RFC3339), cw.SnowHeight().DateTime().Format(time.RFC3339))
|
||||
}
|
||||
if tc.h == nil {
|
||||
if cw.SnowHeight().IsAvailable() {
|
||||
t.Errorf("CurrentWeatherByLocation failed, expected snow height "+
|
||||
"to have no data, but got: %s", cw.SnowHeight())
|
||||
}
|
||||
if !math.IsNaN(cw.SnowHeight().Value()) {
|
||||
t.Errorf("CurrentWeatherByLocation failed, expected snow height "+
|
||||
"to return NaN, but got: %s", cw.SnowHeight().String())
|
||||
}
|
||||
if !math.IsNaN(cw.SnowHeight().Meter()) {
|
||||
t.Errorf("CurrentWeatherByLocation failed, expected snow height "+
|
||||
"to return NaN, but got: %f", cw.SnowHeight().Meter())
|
||||
}
|
||||
if !math.IsNaN(cw.SnowHeight().CentiMeter()) {
|
||||
t.Errorf("CurrentWeatherByLocation failed, expected snow height "+
|
||||
"to return NaN, but got: %f", cw.SnowHeight().CentiMeter())
|
||||
}
|
||||
if !math.IsNaN(cw.SnowHeight().MilliMeter()) {
|
||||
t.Errorf("CurrentWeatherByLocation failed, expected snow height "+
|
||||
"to return NaN, but got: %f", cw.SnowHeight().MilliMeter())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_CurrentWeatherByLocation_Temperature(t *testing.T) {
|
||||
tt := []struct {
|
||||
// Location name
|
||||
|
|
|
@ -46,6 +46,8 @@ const (
|
|||
FieldPressureQFE
|
||||
// FieldSnowAmount represents the SnowAmount data point
|
||||
FieldSnowAmount
|
||||
// FieldSnowHeight represents the SnowHeight data point
|
||||
FieldSnowHeight
|
||||
// FieldSunrise represents the Sunrise data point
|
||||
FieldSunrise
|
||||
// FieldSunset represents the Sunset data point
|
||||
|
|
|
@ -36,5 +36,5 @@ func (dt DateTime) Value() time.Time {
|
|||
// String satisfies the fmt.Stringer interface for the DateTime type
|
||||
// The date will returned as time.RFC3339 format
|
||||
func (dt DateTime) String() string {
|
||||
return dt.dv.Format(time.RFC3339)
|
||||
return dt.Value().Format(time.RFC3339)
|
||||
}
|
||||
|
|
|
@ -10,8 +10,8 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
// Density is a type wrapper of an WeatherData for holding density
|
||||
// values in WeatherData
|
||||
// Density is a type wrapper of WeatherData for holding density
|
||||
// values in kg/m³ in WeatherData
|
||||
type Density WeatherData
|
||||
|
||||
// IsAvailable returns true if an Density value was
|
||||
|
@ -20,8 +20,7 @@ func (d Density) IsAvailable() bool {
|
|||
return !d.na
|
||||
}
|
||||
|
||||
// DateTime returns true if an Density value was
|
||||
// available at time of query
|
||||
// DateTime returns the DateTime of the queried Density value
|
||||
func (d Density) DateTime() time.Time {
|
||||
return d.dt
|
||||
}
|
||||
|
@ -38,7 +37,7 @@ func (d Density) Source() Source {
|
|||
}
|
||||
|
||||
// Value returns the float64 value of an Density
|
||||
// If the Density is not available in the Observation
|
||||
// If the Density is not available in the WeatherData
|
||||
// Vaule will return math.NaN instead.
|
||||
func (d Density) Value() float64 {
|
||||
if d.na {
|
||||
|
|
18
direction.go
18
direction.go
|
@ -12,10 +12,10 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
// DirectionMinAngel is the minimum angel for a direction
|
||||
DirectionMinAngel = 0
|
||||
// DirectionMaxAngel is the maximum angel for a direction
|
||||
DirectionMaxAngel = 360
|
||||
// DirectionMinAngle is the minimum angel for a direction
|
||||
DirectionMinAngle = 0
|
||||
// DirectionMaxAngle is the maximum angel for a direction
|
||||
DirectionMaxAngle = 360
|
||||
)
|
||||
|
||||
// WindDirAbbrMap is a map to associate a wind direction degree value with
|
||||
|
@ -62,7 +62,7 @@ func (d Direction) DateTime() time.Time {
|
|||
}
|
||||
|
||||
// Value returns the float64 value of an Direction in degrees
|
||||
// If the Direction is not available in the Observation
|
||||
// If the Direction is not available in the WeatherData
|
||||
// Vaule will return math.NaN instead.
|
||||
func (d Direction) Value() float64 {
|
||||
if d.na {
|
||||
|
@ -84,7 +84,7 @@ func (d Direction) Source() Source {
|
|||
|
||||
// Direction returns the abbreviation string for a given Direction type
|
||||
func (d Direction) Direction() string {
|
||||
if d.fv < DirectionMinAngel || d.fv > DirectionMaxAngel {
|
||||
if d.fv < DirectionMinAngle || d.fv > DirectionMaxAngle {
|
||||
return ErrUnsupportedDirection
|
||||
}
|
||||
if ds, ok := WindDirAbbrMap[d.fv]; ok {
|
||||
|
@ -95,7 +95,7 @@ func (d Direction) Direction() string {
|
|||
|
||||
// DirectionFull returns the full string for a given Direction type
|
||||
func (d Direction) DirectionFull() string {
|
||||
if d.fv < DirectionMinAngel || d.fv > DirectionMaxAngel {
|
||||
if d.fv < DirectionMinAngle || d.fv > DirectionMaxAngle {
|
||||
return ErrUnsupportedDirection
|
||||
}
|
||||
if ds, ok := WindDirFullMap[d.fv]; ok {
|
||||
|
@ -125,8 +125,8 @@ func findDirection(v float64, m map[float64]string) string {
|
|||
break
|
||||
}
|
||||
}
|
||||
sr := math.Mod(v, sv)
|
||||
er := math.Mod(ev, v)
|
||||
sr := v - sv
|
||||
er := ev - v
|
||||
if er > sr {
|
||||
return m[sv]
|
||||
}
|
||||
|
|
37
direction_test.go
Normal file
37
direction_test.go
Normal file
|
@ -0,0 +1,37 @@
|
|||
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package meteologix
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFindDirection(t *testing.T) {
|
||||
// Prepare test cases
|
||||
tt := []struct {
|
||||
v float64
|
||||
dm map[float64]string
|
||||
er string
|
||||
}{
|
||||
{15, WindDirAbbrMap, "NbE"},
|
||||
{47, WindDirAbbrMap, "NE"},
|
||||
{200, WindDirAbbrMap, "SSW"},
|
||||
{330, WindDirAbbrMap, "NWbN"},
|
||||
{15, WindDirFullMap, "North by East"},
|
||||
{47, WindDirFullMap, "Northeast"},
|
||||
{200, WindDirFullMap, "South-Southwest"},
|
||||
{330, WindDirFullMap, "Northwest by North"},
|
||||
}
|
||||
|
||||
// Run tests
|
||||
for _, tc := range tt {
|
||||
t.Run("", func(t *testing.T) {
|
||||
r := findDirection(tc.v, tc.dm)
|
||||
if tc.er != r {
|
||||
t.Errorf("findDirection failed, expected: %s, got: %s", tc.er, r)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
2
doc.go
2
doc.go
|
@ -6,4 +6,4 @@
|
|||
package meteologix
|
||||
|
||||
// VERSION represents the current version of the package
|
||||
const VERSION = "0.1.0"
|
||||
const VERSION = "0.1.1"
|
||||
|
|
|
@ -43,7 +43,7 @@ type GeoLocation struct {
|
|||
// This method makes use of the OSM Nominatim API
|
||||
func (c *Client) GetGeoLocationByName(ci string) (GeoLocation, error) {
|
||||
ga, err := c.GetGeoLocationsByName(ci)
|
||||
if len(ga) < 1 {
|
||||
if err != nil || len(ga) < 1 {
|
||||
return GeoLocation{}, err
|
||||
}
|
||||
return ga[0], nil
|
||||
|
|
85
height.go
Normal file
85
height.go
Normal file
|
@ -0,0 +1,85 @@
|
|||
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package meteologix
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Height is a type wrapper of an WeatherData for holding height
|
||||
// values in WeatherData (based on meters a default unit)
|
||||
type Height WeatherData
|
||||
|
||||
// IsAvailable returns true if an Height value was
|
||||
// available at time of query
|
||||
func (h Height) IsAvailable() bool {
|
||||
return !h.na
|
||||
}
|
||||
|
||||
// DateTime returns the timestamp associated with the Height value
|
||||
func (h Height) DateTime() time.Time {
|
||||
return h.dt
|
||||
}
|
||||
|
||||
// String satisfies the fmt.Stringer interface for the Height type
|
||||
func (h Height) String() string {
|
||||
return fmt.Sprintf("%.3fm", h.fv)
|
||||
}
|
||||
|
||||
// Source returns the Source of Height
|
||||
// If the Source is not available it will return SourceUnknown
|
||||
func (h Height) Source() Source {
|
||||
return h.s
|
||||
}
|
||||
|
||||
// Value returns the float64 value of an Height
|
||||
// If the Height is not available in the WeatherData
|
||||
// Vaule will return math.NaN instead.
|
||||
func (h Height) Value() float64 {
|
||||
if h.na {
|
||||
return math.NaN()
|
||||
}
|
||||
return h.fv
|
||||
}
|
||||
|
||||
// Meter returns the Height type value as float64 in meters.
|
||||
// This is an alias for the Value() method
|
||||
func (h Height) Meter() float64 {
|
||||
return h.Value()
|
||||
}
|
||||
|
||||
// MeterString returns the Height type as formatted string in meters
|
||||
// This is an alias for the String() method
|
||||
func (h Height) MeterString() string {
|
||||
return h.String()
|
||||
}
|
||||
|
||||
// CentiMeter returns the Height type value as float64 in centimeters.
|
||||
func (h Height) CentiMeter() float64 {
|
||||
if h.na {
|
||||
return math.NaN()
|
||||
}
|
||||
return h.fv * 100
|
||||
}
|
||||
|
||||
// CentiMeterString returns the Height type as formatted string in centimeters
|
||||
func (h Height) CentiMeterString() string {
|
||||
return fmt.Sprintf("%.3fcm", h.CentiMeter())
|
||||
}
|
||||
|
||||
// MilliMeter returns the Height type value as float64 in milliimeters.
|
||||
func (h Height) MilliMeter() float64 {
|
||||
if h.na {
|
||||
return math.NaN()
|
||||
}
|
||||
return h.fv * 1000
|
||||
}
|
||||
|
||||
// MilliMeterString returns the Height type as formatted string in millimeters
|
||||
func (h Height) MilliMeterString() string {
|
||||
return fmt.Sprintf("%.3fmm", h.MilliMeter())
|
||||
}
|
|
@ -13,9 +13,9 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
@ -49,11 +49,13 @@ type APIError struct {
|
|||
// NewHTTPClient returns a new HTTP client
|
||||
func NewHTTPClient(c *Config) *HTTPClient {
|
||||
tc := &tls.Config{
|
||||
MaxVersion: tls.VersionTLS12,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
ht := http.Transport{TLSClientConfig: tc}
|
||||
hc := &http.Client{Transport: &ht}
|
||||
ht := &http.Transport{TLSClientConfig: tc}
|
||||
hc := &http.Client{
|
||||
Timeout: HTTPClientTimeout,
|
||||
Transport: ht,
|
||||
}
|
||||
return &HTTPClient{c, hc}
|
||||
}
|
||||
|
||||
|
@ -78,31 +80,28 @@ func (hc *HTTPClient) GetWithTimeout(u string, t time.Duration) ([]byte, error)
|
|||
|
||||
// User authentication (only required for Meteologix API calls)
|
||||
if strings.HasPrefix(u, APIBaseURL) {
|
||||
hc.setAuthHeader(hr)
|
||||
hc.setAuthentication(hr)
|
||||
}
|
||||
|
||||
sr, err := hc.Do(hr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if err = sr.Body.Close(); err != nil {
|
||||
_, _ = fmt.Fprintln(os.Stderr, "failed to close HTTP request body", err)
|
||||
if sr == nil {
|
||||
return nil, errors.New("nil response received")
|
||||
}
|
||||
defer func(b io.ReadCloser) {
|
||||
if err = b.Close(); err != nil {
|
||||
log.Printf("failed to close HTTP request body: %s", err)
|
||||
}
|
||||
}()
|
||||
}(sr.Body)
|
||||
|
||||
if !strings.HasPrefix(sr.Header.Get("Content-Type"), MIMETypeJSON) {
|
||||
return nil, ErrNonJSONResponse
|
||||
}
|
||||
buf := &bytes.Buffer{}
|
||||
bw := bufio.NewWriter(buf)
|
||||
_, err = io.Copy(bw, sr.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to copy HTTP response body to buffer: %w", err)
|
||||
}
|
||||
if sr.StatusCode >= 400 {
|
||||
var ae APIError
|
||||
if err = json.Unmarshal(buf.Bytes(), &ae); err != nil {
|
||||
if sr.StatusCode >= http.StatusBadRequest {
|
||||
ae := new(APIError)
|
||||
if err = json.NewDecoder(sr.Body).Decode(ae); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal error JSON: %w", err)
|
||||
}
|
||||
if ae.Code < 1 {
|
||||
|
@ -111,15 +110,25 @@ func (hc *HTTPClient) GetWithTimeout(u string, t time.Duration) ([]byte, error)
|
|||
if ae.Details == "" {
|
||||
ae.Details = sr.Status
|
||||
}
|
||||
return nil, ae
|
||||
return nil, *ae
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
bw := bufio.NewWriter(buf)
|
||||
_, err = io.Copy(bw, sr.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to copy HTTP response body to buffer: %w", err)
|
||||
}
|
||||
if err = bw.Flush(); err != nil {
|
||||
return nil, fmt.Errorf("failed to flush buffer: %w", err)
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// setAuthHeader sets the corresponding user authentication header. If an API Key is set, this
|
||||
// setAuthentication sets the corresponding user authentication header. If an API Key is set, this
|
||||
// will be preferred, alternatively a username/authPass combination for HTTP Basic auth can
|
||||
// be used
|
||||
func (hc *HTTPClient) setAuthHeader(hr *http.Request) {
|
||||
func (hc *HTTPClient) setAuthentication(hr *http.Request) {
|
||||
if hc.apiKey != "" {
|
||||
hr.Header.Set("X-API-Key", hc.Config.apiKey)
|
||||
return
|
||||
|
|
|
@ -20,8 +20,8 @@ func (h Humidity) IsAvailable() bool {
|
|||
return !h.na
|
||||
}
|
||||
|
||||
// DateTime returns true if an Humidity value was
|
||||
// available at time of query
|
||||
// DateTime returns the timestamp of when the humidity
|
||||
// measurement was taken.
|
||||
func (h Humidity) DateTime() time.Time {
|
||||
return h.dt
|
||||
}
|
||||
|
@ -38,8 +38,8 @@ func (h Humidity) Source() Source {
|
|||
}
|
||||
|
||||
// Value returns the float64 value of an Humidity
|
||||
// If the Humidity is not available in the Observation
|
||||
// Vaule will return math.NaN instead.
|
||||
// If the Humidity is not available in the WeatherData
|
||||
// Value will return math.NaN instead.
|
||||
func (h Humidity) Value() float64 {
|
||||
if h.na {
|
||||
return math.NaN()
|
||||
|
|
|
@ -20,8 +20,7 @@ func (p Precipitation) IsAvailable() bool {
|
|||
return !p.na
|
||||
}
|
||||
|
||||
// DateTime returns true if an Precipitation value was
|
||||
// available at time of query
|
||||
// DateTime returns the DateTime when the Precipitation value was recorded
|
||||
func (p Precipitation) DateTime() time.Time {
|
||||
return p.dt
|
||||
}
|
||||
|
@ -38,7 +37,7 @@ func (p Precipitation) Source() Source {
|
|||
}
|
||||
|
||||
// Value returns the float64 value of an Precipitation
|
||||
// If the Precipitation is not available in the Observation
|
||||
// If the Precipitation is not available in the WeatherData
|
||||
// Vaule will return math.NaN instead.
|
||||
func (p Precipitation) Value() float64 {
|
||||
if p.na {
|
||||
|
|
|
@ -20,8 +20,7 @@ func (p Pressure) IsAvailable() bool {
|
|||
return !p.na
|
||||
}
|
||||
|
||||
// DateTime returns true if an Pressure value was
|
||||
// available at time of query
|
||||
// DateTime returns the date and time of the Pressure reading
|
||||
func (p Pressure) DateTime() time.Time {
|
||||
return p.dt
|
||||
}
|
||||
|
@ -38,7 +37,7 @@ func (p Pressure) Source() Source {
|
|||
}
|
||||
|
||||
// Value returns the float64 value of an Pressure
|
||||
// If the Pressure is not available in the Observation
|
||||
// If the Pressure is not available in the WeatherData
|
||||
// Vaule will return math.NaN instead.
|
||||
func (p Pressure) Value() float64 {
|
||||
if p.na {
|
||||
|
|
|
@ -20,14 +20,14 @@ func (r Radiation) IsAvailable() bool {
|
|||
return !r.na
|
||||
}
|
||||
|
||||
// DateTime returns true if an Radiation value was
|
||||
// available at time of query
|
||||
// DateTime returns the time.Time object representing the date and time
|
||||
// at which the Radiation value was queried
|
||||
func (r Radiation) DateTime() time.Time {
|
||||
return r.dt
|
||||
}
|
||||
|
||||
// Value returns the float64 value of an Radiation
|
||||
// If the Radiation is not available in the Observation
|
||||
// If the Radiation is not available in the WeatherData
|
||||
// Vaule will return math.NaN instead.
|
||||
func (r Radiation) Value() float64 {
|
||||
if r.na {
|
||||
|
|
5
speed.go
5
speed.go
|
@ -29,15 +29,14 @@ func (s Speed) IsAvailable() bool {
|
|||
return !s.na
|
||||
}
|
||||
|
||||
// DateTime returns true if an Speed value was
|
||||
// available at time of query
|
||||
// DateTime returns the DateTime when the Speed was checked
|
||||
func (s Speed) DateTime() time.Time {
|
||||
return s.dt
|
||||
}
|
||||
|
||||
// Value returns the float64 value of an Speed in meters
|
||||
// per second.
|
||||
// If the Speed is not available in the Observation
|
||||
// If the Speed is not available in the WeatherData
|
||||
// Vaule will return math.NaN instead.
|
||||
func (s Speed) Value() float64 {
|
||||
if s.na {
|
||||
|
|
66
station.go
66
station.go
|
@ -18,16 +18,42 @@ import (
|
|||
const DefaultRadius int = 10
|
||||
|
||||
const (
|
||||
// PrecisionHigh is a high precision weather station
|
||||
PrecisionHigh Precision = iota
|
||||
// PrecisionMedium is a medium precision weather station
|
||||
PrecisionMedium
|
||||
// PrecisionLow is a low precision weather station
|
||||
PrecisionLow
|
||||
// PrecisionUnknown is weather station of unknown precision
|
||||
// PrecisionSuperHigh represents the precision level of data corresponding
|
||||
// to a resolution of less than or approximately equal to 4 kilometers.
|
||||
// This is the highest level of precision, usually associated with highly
|
||||
// detailed measurements or observations.
|
||||
PrecisionSuperHigh Precision = iota
|
||||
// PrecisionHigh represents the precision level of data corresponding to a
|
||||
// resolution between 4 kilometers and 10 kilometers. This is a high precision
|
||||
// level, suitable for most operational needs that require a balance between
|
||||
// detail and processing requirements.
|
||||
PrecisionHigh
|
||||
// PrecisionStandard represents the precision level of data corresponding to
|
||||
// a resolution of 10 kilometers or more. This is the standard level of
|
||||
// precision, generally used for large-scale analysis and modeling.
|
||||
PrecisionStandard
|
||||
// PrecisionUnknown is used when the precision level of a weather station
|
||||
// is unknown. This constant can be used as a placeholder when the resolution
|
||||
// data is not available.
|
||||
PrecisionUnknown
|
||||
)
|
||||
|
||||
// Precision levels defined as strings to allow for clear, consistent
|
||||
// use throughout the application.
|
||||
const (
|
||||
// PrecisionStringSuperHigh represents the super high precision level string.
|
||||
PrecisionStringSuperHigh = "SUPER_HIGH"
|
||||
|
||||
// PrecisionStringHigh represents the high precision level string.
|
||||
PrecisionStringHigh = "HIGH"
|
||||
|
||||
// PrecisionStringStandard represents the standard precision level string.
|
||||
PrecisionStringStandard = "STANDARD"
|
||||
|
||||
// PrecisionStringUnknown represents an unknown precision level string.
|
||||
PrecisionStringUnknown = "UNKNOWN"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrRadiusTooSmall is returned if a given radius value is too small
|
||||
ErrRadiusTooSmall = errors.New("given radius is too small")
|
||||
|
@ -156,13 +182,13 @@ func (c *Client) StationSearchByCoordinatesWithinRadius(la, lo float64, ra int)
|
|||
func (p *Precision) UnmarshalJSON(s []byte) error {
|
||||
v := string(s)
|
||||
v = strings.ReplaceAll(v, `"`, ``)
|
||||
switch strings.ToLower(v) {
|
||||
case "high":
|
||||
switch strings.ToUpper(v) {
|
||||
case PrecisionStringSuperHigh:
|
||||
*p = PrecisionSuperHigh
|
||||
case PrecisionStringHigh:
|
||||
*p = PrecisionHigh
|
||||
case "medium":
|
||||
*p = PrecisionMedium
|
||||
case "low":
|
||||
*p = PrecisionLow
|
||||
case PrecisionStringStandard:
|
||||
*p = PrecisionStandard
|
||||
default:
|
||||
*p = PrecisionUnknown
|
||||
}
|
||||
|
@ -172,15 +198,15 @@ func (p *Precision) UnmarshalJSON(s []byte) error {
|
|||
// String satisfies the fmt.Stringer interface for the Precision type
|
||||
func (p *Precision) String() string {
|
||||
switch *p {
|
||||
case PrecisionSuperHigh:
|
||||
return PrecisionStringSuperHigh
|
||||
case PrecisionHigh:
|
||||
return "HIGH"
|
||||
case PrecisionMedium:
|
||||
return "MEDIUM"
|
||||
case PrecisionLow:
|
||||
return "LOW"
|
||||
return PrecisionStringHigh
|
||||
case PrecisionStandard:
|
||||
return PrecisionStringStandard
|
||||
case PrecisionUnknown:
|
||||
return "UNKNOWN"
|
||||
return PrecisionStringUnknown
|
||||
default:
|
||||
return "UNKNOWN"
|
||||
return PrecisionStringUnknown
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,7 +46,8 @@ func TestClient_StationSearchByLocation_Fail(t *testing.T) {
|
|||
t.Errorf("StationSearchByLocation was supposed to fail but didn't")
|
||||
}
|
||||
if err != nil && !errors.As(err, &APIError{}) {
|
||||
t.Errorf("StationSearchByLocation was supposed to throw a APIError but didn't")
|
||||
t.Errorf("StationSearchByLocation was supposed to throw a APIError but didn't: %s",
|
||||
err)
|
||||
}
|
||||
c = New(WithAPIKey("invalid"))
|
||||
_, err = c.StationSearchByLocation("Cologne, Germany")
|
||||
|
@ -55,7 +56,8 @@ func TestClient_StationSearchByLocation_Fail(t *testing.T) {
|
|||
return
|
||||
}
|
||||
if err != nil && !errors.As(err, &APIError{}) {
|
||||
t.Errorf("StationSearchByLocation was supposed to throw a APIError but didn't")
|
||||
t.Errorf("StationSearchByLocation was supposed to throw a APIError but didn't: %s",
|
||||
err)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -164,16 +166,16 @@ func TestPrecision_UnmarshalJSON(t *testing.T) {
|
|||
// Should fail
|
||||
sf bool
|
||||
}{
|
||||
{
|
||||
"Super high precision", []byte(`{"precision":"SUPER_HIGH"}`), PrecisionSuperHigh,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"High precision", []byte(`{"precision":"HIGH"}`), PrecisionHigh,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"Medium precision", []byte(`{"precision":"MEDIUM"}`), PrecisionMedium,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"Low precision", []byte(`{"precision":"LOW"}`), PrecisionLow,
|
||||
"Standard precision", []byte(`{"precision":"STANDARD"}`), PrecisionStandard,
|
||||
false,
|
||||
},
|
||||
{
|
||||
|
@ -209,9 +211,9 @@ func TestPrecision_String(t *testing.T) {
|
|||
// Expected string
|
||||
es string
|
||||
}{
|
||||
{"Super high precision", PrecisionSuperHigh, "SUPER_HIGH"},
|
||||
{"High precision", PrecisionHigh, "HIGH"},
|
||||
{"Medium precision", PrecisionMedium, "MEDIUM"},
|
||||
{"Low precision", PrecisionLow, "LOW"},
|
||||
{"Standard precision", PrecisionStandard, "STANDARD"},
|
||||
{"Unknown precision", PrecisionUnknown, "UNKNOWN"},
|
||||
{"Undefined precision", 999, "UNKNOWN"},
|
||||
}
|
||||
|
|
|
@ -20,14 +20,14 @@ func (t Temperature) IsAvailable() bool {
|
|||
return !t.na
|
||||
}
|
||||
|
||||
// DateTime returns true if an Temperature value was
|
||||
// available at time of query
|
||||
// DateTime returns the time at which the temperature data was
|
||||
// generated or fetched
|
||||
func (t Temperature) DateTime() time.Time {
|
||||
return t.dt
|
||||
}
|
||||
|
||||
// Value returns the float64 value of an Temperature
|
||||
// If the Temperature is not available in the Observation
|
||||
// If the Temperature is not available in the WeatherData
|
||||
// Vaule will return math.NaN instead.
|
||||
func (t Temperature) Value() float64 {
|
||||
if t.na {
|
||||
|
|
Loading…
Reference in a new issue