Merge pull request #9 from wneessen/more-curweather

v0.1.0: More data points for current weather
This commit is contained in:
Winni Neessen 2023-06-23 18:37:31 +02:00 committed by GitHub
commit 7bdf2de388
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 603 additions and 239 deletions

View file

@ -34,23 +34,31 @@ type APICurrentWeatherData struct {
Dewpoint *APIFloat `json:"dewpoint,omitempty"`
// HumidityRelative represents the relative humidity in percent
HumidityRelative *APIFloat `json:"humidityRelative,omitempty"`
// IsDay is true when it is currently daytime
IsDay *APIBool `json:"isDay"`
// Precipitation represents the current amount of precipitation
Precipitation *APIFloat `json:"prec"`
Precipitation *APIFloat `json:"prec,omitempty"`
// Precipitation10m represents the amount of precipitation over the last 10 minutes
Precipitation10m *APIFloat `json:"prec10m"`
Precipitation10m *APIFloat `json:"prec10m,omitempty"`
// Precipitation1h represents the amount of precipitation over the last hour
Precipitation1h *APIFloat `json:"prec1h"`
Precipitation1h *APIFloat `json:"prec1h,omitempty"`
// Precipitation24h represents the amount of precipitation over the last 24 hours
Precipitation24h *APIFloat `json:"prec24h"`
Precipitation24h *APIFloat `json:"prec24h,omitempty"`
// PressureMSL represents the pressure at mean sea level (MSL) in hPa
PressureMSL *APIFloat `json:"pressureMsl"`
PressureMSL *APIFloat `json:"pressureMsl,omitempty"`
// PressureQFE represents the pressure at station level (QFE) in hPa
PressureQFE *APIFloat `json:"pressure,omitempty"`
// SnowAmount represents the the amount of snow in kg/m3
SnowAmount *APIFloat `json:"snowAmount,omitempty"`
// Temperature represents the temperature in °C
Temperature *APIFloat `json:"temp,omitempty"`
// Windspeed represents the wind speed in knots
Windspeed *APIFloat `json:"windSpeed,omitempty"`
// Winddirection represents the direction from which the wind
// WindDirection represents the direction from which the wind
// originates in degree (0=N, 90=E, 180=S, 270=W)
Winddirection *APIFloat `json:"windDirection,omitempty"`
WindDirection *APIFloat `json:"windDirection,omitempty"`
// WindGust represents the wind gust speed in m/s
WindGust *APIFloat `json:"windGust,omitempty"`
// WindSpeed represents the wind speed in m/s
WindSpeed *APIFloat `json:"windSpeed,omitempty"`
// WeatherSymbol is a text representation of the current weather
// conditions
WeatherSymbol *APIString `json:"weatherSymbol,omitempty"`
@ -66,8 +74,6 @@ type APICurrentWeatherData struct {
// GlobalRadiation24h represents the sum of global radiation over the last
// 24 hour in kJ/m²
GlobalRadiation24h *APIFloat `json:"globalRadiation24h,omitempty"`
// PressureMSL represents the pressure at station level (QFE) in hPa
PressureQFE *APIFloat `json:"pressure"`
// TemperatureMax represents the maximum temperature in °C
TemperatureMax *APIFloat `json:"tempMax,omitempty"`
// TemperatureMean represents the mean temperature in °C
@ -117,25 +123,6 @@ func (c *Client) CurrentWeatherByLocation(lo string) (CurrentWeather, error) {
return c.CurrentWeatherByCoordinates(gl.Latitude, gl.Longitude)
}
// 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.
func (cw CurrentWeather) Temperature() Temperature {
if cw.Data.Temperature == nil {
return Temperature{na: true}
}
v := Temperature{
dt: cw.Data.Temperature.DateTime,
n: FieldTemperature,
s: SourceUnknown,
fv: cw.Data.Temperature.Value,
}
if cw.Data.Temperature.Source != nil {
v.s = StringToSource(*cw.Data.Temperature.Source)
}
return v
}
// Dewpoint returns the dewpoint 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.
@ -174,6 +161,14 @@ func (cw CurrentWeather) HumidityRelative() Humidity {
return v
}
// IsDay returns true if it is currently day at queried location
func (cw CurrentWeather) IsDay() bool {
if cw.Data.IsDay == nil {
return false
}
return cw.Data.IsDay.Value
}
// Precipitation returns the current amount of precipitation (mm) as Precipitation
// If the data point is not available in the CurrentWeather it will return
// Precipitation in which the "not available" field will be true.
@ -235,6 +230,63 @@ func (cw CurrentWeather) PressureMSL() Pressure {
return v
}
// PressureQFE returns the pressure at mean sea level data point as Pressure.
// If the data point is not available in the CurrentWeather it will return
// Pressure in which the "not available" field will be true.
func (cw CurrentWeather) PressureQFE() Pressure {
if cw.Data.PressureQFE == nil {
return Pressure{na: true}
}
v := Pressure{
dt: cw.Data.PressureQFE.DateTime,
n: FieldPressureQFE,
s: SourceUnknown,
fv: cw.Data.PressureQFE.Value,
}
if cw.Data.PressureQFE.Source != nil {
v.s = StringToSource(*cw.Data.PressureQFE.Source)
}
return v
}
// SnowAmount returns the temperature 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 {
if cw.Data.SnowAmount == nil {
return Density{na: true}
}
v := Density{
dt: cw.Data.SnowAmount.DateTime,
n: FieldSnowAmount,
s: SourceUnknown,
fv: cw.Data.SnowAmount.Value,
}
if cw.Data.SnowAmount.Source != nil {
v.s = StringToSource(*cw.Data.SnowAmount.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.
func (cw CurrentWeather) Temperature() Temperature {
if cw.Data.Temperature == nil {
return Temperature{na: true}
}
v := Temperature{
dt: cw.Data.Temperature.DateTime,
n: FieldTemperature,
s: SourceUnknown,
fv: cw.Data.Temperature.Value,
}
if cw.Data.Temperature.Source != nil {
v.s = StringToSource(*cw.Data.Temperature.Source)
}
return v
}
// WeatherSymbol returns a text representation of the current weather
// as Condition.
// If the data point is not available in the CurrentWeather it will return
@ -255,40 +307,59 @@ func (cw CurrentWeather) WeatherSymbol() Condition {
return v
}
// Winddirection returns the wind direction data point as Direction.
// WindDirection returns the wind direction data point as Direction.
// If the data point is not available in the CurrentWeather it will return
// Direction in which the "not available" field will be true.
func (cw CurrentWeather) Winddirection() Direction {
if cw.Data.Winddirection == nil {
func (cw CurrentWeather) WindDirection() Direction {
if cw.Data.WindDirection == nil {
return Direction{na: true}
}
v := Direction{
dt: cw.Data.Winddirection.DateTime,
n: FieldWinddirection,
dt: cw.Data.WindDirection.DateTime,
n: FieldWindDirection,
s: SourceUnknown,
fv: cw.Data.Winddirection.Value,
fv: cw.Data.WindDirection.Value,
}
if cw.Data.Winddirection.Source != nil {
v.s = StringToSource(*cw.Data.Winddirection.Source)
if cw.Data.WindDirection.Source != nil {
v.s = StringToSource(*cw.Data.WindDirection.Source)
}
return v
}
// Windspeed returns the average wind speed data point as Speed.
// WindGust returns the wind gust data point as Speed.
// If the data point is not available in the CurrentWeather it will return
// Speed in which the "not available" field will be true.
func (cw CurrentWeather) Windspeed() Speed {
if cw.Data.Windspeed == nil {
func (cw CurrentWeather) WindGust() Speed {
if cw.Data.WindGust == nil {
return Speed{na: true}
}
v := Speed{
dt: cw.Data.Windspeed.DateTime,
n: FieldWindspeed,
dt: cw.Data.WindGust.DateTime,
n: FieldWindGust,
s: SourceUnknown,
fv: cw.Data.Windspeed.Value,
fv: cw.Data.WindGust.Value,
}
if cw.Data.Windspeed.Source != nil {
v.s = StringToSource(*cw.Data.Windspeed.Source)
if cw.Data.WindGust.Source != nil {
v.s = StringToSource(*cw.Data.WindGust.Source)
}
return v
}
// WindSpeed returns the average wind speed data point as Speed.
// If the data point is not available in the CurrentWeather it will return
// Speed in which the "not available" field will be true.
func (cw CurrentWeather) WindSpeed() Speed {
if cw.Data.WindSpeed == nil {
return Speed{na: true}
}
v := Speed{
dt: cw.Data.WindSpeed.DateTime,
n: FieldWindSpeed,
s: SourceUnknown,
fv: cw.Data.WindSpeed.Value,
}
if cw.Data.WindSpeed.Source != nil {
v.s = StringToSource(*cw.Data.WindSpeed.Source)
}
return v
}

View file

@ -107,63 +107,6 @@ func TestClient_CurrentWeatherByLocation_Fail(t *testing.T) {
}
}
func TestClient_CurrentWeatherByLocation_Temperature(t *testing.T) {
tt := []struct {
// Location name
loc string
// CurWeather temperature
t *Temperature
}{
{"Ehrenfeld, Germany", &Temperature{
dt: time.Date(2023, 5, 23, 7, 0, 0, 0, time.Local),
s: SourceObservation,
fv: 14.6,
}},
{"Berlin, Germany", &Temperature{
dt: time.Date(2023, 5, 23, 7, 0, 0, 0, time.Local),
s: SourceAnalysis,
fv: 17.8,
}},
{"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.t != nil && tc.t.String() != cw.Temperature().String() {
t.Errorf("CurrentWeatherByLocation failed, expected temperature "+
"string: %s, got: %s", tc.t.String(), cw.Temperature())
}
if tc.t != nil && tc.t.Value() != cw.Temperature().Value() {
t.Errorf("CurrentWeatherByLocation failed, expected temperature "+
"float: %f, got: %f", tc.t.Value(), cw.Temperature().Value())
}
if tc.t != nil && cw.Temperature().Source() != tc.t.s {
t.Errorf("CurrentWeatherByLocation failed, expected source: %s, but got: %s",
tc.t.s, cw.Temperature().Source())
}
if tc.t == nil {
if cw.Temperature().IsAvailable() {
t.Errorf("CurrentWeatherByLocation failed, expected temperature "+
"to have no data, but got: %s", cw.Temperature())
}
if !math.IsNaN(cw.Temperature().Value()) {
t.Errorf("CurrentWeatherByLocation failed, expected temperature "+
"to return NaN, but got: %s", cw.Temperature().String())
}
}
})
}
}
func TestClient_CurrentWeatherByLocation_Dewpoint(t *testing.T) {
tt := []struct {
// Location name
@ -278,6 +221,36 @@ func TestClient_CurrentWeatherByLocation_HumidityRelative(t *testing.T) {
}
}
func TestClient_CurrentWeatherByLocation_IsDay(t *testing.T) {
tt := []struct {
// Location name
loc string
// CurWeather IsDay
d bool
}{
{"Ehrenfeld, Germany", false},
{"Berlin, Germany", true},
{"Neermoor, Germany", false},
}
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 cw.IsDay() != tc.d {
t.Errorf("CurrentWeather IsDay failed, expected: %t, got: %t", cw.IsDay(), tc.d)
}
})
}
}
func TestClient_CurrentWeatherByLocation_PrecipitationCurrent(t *testing.T) {
tt := []struct {
// Location name
@ -539,28 +512,20 @@ func TestClient_CurrentWeatherByLocation_PressureMSL(t *testing.T) {
}
}
func TestClient_CurrentWeatherByLocation_Winddirection(t *testing.T) {
func TestClient_CurrentWeatherByLocation_PressureQFE(t *testing.T) {
tt := []struct {
// Location name
loc string
// CurWeather direction
d *Direction
// Direction abbr. string
da string
// Direction full string
df string
// CurWeather pressure
p *Pressure
}{
{"Ehrenfeld, Germany", &Direction{
{"Ehrenfeld, Germany", &Pressure{
dt: time.Date(2023, 5, 23, 7, 0, 0, 0, time.Local),
s: SourceAnalysis,
fv: 302,
}, "NWbW", "Northwest by West"},
{"Berlin, Germany", &Direction{
dt: time.Date(2023, 5, 23, 7, 0, 0, 0, time.Local),
s: SourceAnalysis,
fv: 286,
}, "WbN", "West by North"},
{"Neermoor, Germany", nil, "", ""},
fv: 1011.7,
}},
{"Berlin, Germany", nil},
{"Neermoor, Germany", nil},
}
c := New(withMockAPI())
if c == nil {
@ -574,56 +539,48 @@ func TestClient_CurrentWeatherByLocation_Winddirection(t *testing.T) {
t.Errorf("CurrentWeatherByLocation failed: %s", err)
return
}
if tc.d != nil && tc.d.String() != cw.Winddirection().String() {
t.Errorf("CurrentWeatherByLocation failed, expected wind direction "+
"string: %s, got: %s", tc.d.String(), cw.Winddirection())
if tc.p != nil && tc.p.String() != cw.PressureQFE().String() {
t.Errorf("CurrentWeatherByLocation failed, expected pressure "+
"string: %s, got: %s", tc.p.String(), cw.PressureQFE())
}
if tc.d != nil && tc.d.Value() != cw.Winddirection().Value() {
t.Errorf("CurrentWeatherByLocation failed, expected wind direction "+
"float: %f, got: %f", tc.d.Value(), cw.Winddirection().Value())
if tc.p != nil && tc.p.Value() != cw.PressureQFE().Value() {
t.Errorf("CurrentWeatherByLocation failed, expected pressure "+
"float: %f, got: %f", tc.p.Value(), cw.PressureQFE().Value())
}
if tc.d != nil && cw.Winddirection().Source() != tc.d.s {
if tc.p != nil && cw.PressureQFE().Source() != tc.p.s {
t.Errorf("CurrentWeatherByLocation failed, expected source: %s, but got: %s",
tc.d.s, cw.Winddirection().Source())
tc.p.s, cw.PressureQFE().Source())
}
if tc.d != nil && cw.Winddirection().Direction() != tc.da {
t.Errorf("CurrentWeatherByLocation failed, expected direction abbr.: %s, but got: %s",
tc.da, cw.Winddirection().Direction())
}
if tc.d != nil && cw.Winddirection().DirectionFull() != tc.df {
t.Errorf("CurrentWeatherByLocation failed, expected direction full: %s, but got: %s",
tc.df, cw.Winddirection().DirectionFull())
}
if tc.d == nil {
if cw.Winddirection().IsAvailable() {
t.Errorf("CurrentWeatherByLocation failed, expected wind direction "+
"to have no data, but got: %s", cw.Winddirection())
if tc.p == nil {
if cw.PressureQFE().IsAvailable() {
t.Errorf("CurrentWeatherByLocation failed, expected pressure "+
"to have no data, but got: %s", cw.PressureQFE())
}
if !math.IsNaN(cw.Winddirection().Value()) {
t.Errorf("CurrentWeatherByLocation failed, expected wind direction "+
"to return NaN, but got: %s", cw.Windspeed().String())
if !math.IsNaN(cw.PressureQFE().Value()) {
t.Errorf("CurrentWeatherByLocation failed, expected pressure "+
"to return NaN, but got: %s", cw.PressureQFE().String())
}
}
})
}
}
func TestClient_CurrentWeatherByLocation_Windspeed(t *testing.T) {
func TestClient_CurrentWeatherByLocation_SnowAmount(t *testing.T) {
tt := []struct {
// Location name
loc string
// CurWeather speed
s *Speed
// CurWeather pressure
d *Density
}{
{"Ehrenfeld, Germany", &Speed{
dt: time.Date(2023, 5, 23, 7, 0, 0, 0, time.Local),
{"Ehrenfeld, Germany", &Density{
dt: time.Date(2023, 5, 23, 6, 0, 0, 0, time.UTC),
s: SourceAnalysis,
fv: 3.94,
fv: 0,
}},
{"Berlin, Germany", &Speed{
dt: time.Date(2023, 5, 23, 7, 0, 0, 0, time.Local),
{"Berlin, Germany", &Density{
dt: time.Date(2023, 5, 23, 8, 0, 0, 0, time.UTC),
s: SourceAnalysis,
fv: 3.19,
fv: 21.1,
}},
{"Neermoor, Germany", nil},
}
@ -639,26 +596,87 @@ func TestClient_CurrentWeatherByLocation_Windspeed(t *testing.T) {
t.Errorf("CurrentWeatherByLocation failed: %s", err)
return
}
if tc.s != nil && tc.s.String() != cw.Windspeed().String() {
t.Errorf("CurrentWeatherByLocation failed, expected wind speed "+
"string: %s, got: %s", tc.s.String(), cw.Windspeed())
if tc.d != nil && tc.d.String() != cw.SnowAmount().String() {
t.Errorf("CurrentWeatherByLocation failed, expected snow amount "+
"string: %s, got: %s", tc.d.String(), cw.SnowAmount())
}
if tc.s != nil && tc.s.Value() != cw.Windspeed().Value() {
t.Errorf("CurrentWeatherByLocation failed, expected wind speed "+
"float: %f, got: %f", tc.s.Value(), cw.Windspeed().Value())
if tc.d != nil && tc.d.Value() != cw.SnowAmount().Value() {
t.Errorf("CurrentWeatherByLocation failed, expected snow amount "+
"float: %f, got: %f", tc.d.Value(), cw.SnowAmount().Value())
}
if tc.s != nil && cw.Windspeed().Source() != tc.s.s {
if tc.d != nil && cw.SnowAmount().Source() != tc.d.s {
t.Errorf("CurrentWeatherByLocation failed, expected source: %s, but got: %s",
tc.s.s, cw.Windspeed().Source())
tc.d.s, cw.SnowAmount().Source())
}
if tc.s == nil {
if cw.Windspeed().IsAvailable() {
t.Errorf("CurrentWeatherByLocation failed, expected wind speed "+
"to have no data, but got: %s", cw.Windspeed())
if tc.d != nil && tc.d.dt.Unix() != cw.SnowAmount().DateTime().Unix() {
t.Errorf("CurrentWeatherByLocation failed, expected datetime: %s, got: %s",
tc.d.dt.Format(time.RFC3339), cw.SnowAmount().DateTime().Format(time.RFC3339))
}
if tc.d == nil {
if cw.SnowAmount().IsAvailable() {
t.Errorf("CurrentWeatherByLocation failed, expected snow amount "+
"to have no data, but got: %s", cw.SnowAmount())
}
if !math.IsNaN(cw.Windspeed().Value()) {
t.Errorf("CurrentWeatherByLocation failed, expected wind speed "+
"to return NaN, but got: %s", cw.Windspeed().String())
if !math.IsNaN(cw.SnowAmount().Value()) {
t.Errorf("CurrentWeatherByLocation failed, expected snow amount "+
"to return NaN, but got: %s", cw.SnowAmount().String())
}
}
})
}
}
func TestClient_CurrentWeatherByLocation_Temperature(t *testing.T) {
tt := []struct {
// Location name
loc string
// CurWeather temperature
t *Temperature
}{
{"Ehrenfeld, Germany", &Temperature{
dt: time.Date(2023, 5, 23, 7, 0, 0, 0, time.Local),
s: SourceObservation,
fv: 14.6,
}},
{"Berlin, Germany", &Temperature{
dt: time.Date(2023, 5, 23, 7, 0, 0, 0, time.Local),
s: SourceAnalysis,
fv: 17.8,
}},
{"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.t != nil && tc.t.String() != cw.Temperature().String() {
t.Errorf("CurrentWeatherByLocation failed, expected temperature "+
"string: %s, got: %s", tc.t.String(), cw.Temperature())
}
if tc.t != nil && tc.t.Value() != cw.Temperature().Value() {
t.Errorf("CurrentWeatherByLocation failed, expected temperature "+
"float: %f, got: %f", tc.t.Value(), cw.Temperature().Value())
}
if tc.t != nil && cw.Temperature().Source() != tc.t.s {
t.Errorf("CurrentWeatherByLocation failed, expected source: %s, but got: %s",
tc.t.s, cw.Temperature().Source())
}
if tc.t == nil {
if cw.Temperature().IsAvailable() {
t.Errorf("CurrentWeatherByLocation failed, expected temperature "+
"to have no data, but got: %s", cw.Temperature())
}
if !math.IsNaN(cw.Temperature().Value()) {
t.Errorf("CurrentWeatherByLocation failed, expected temperature "+
"to return NaN, but got: %s", cw.Temperature().String())
}
}
})
@ -729,3 +747,186 @@ func TestClient_CurrentWeatherByLocation_WeatherSymbol(t *testing.T) {
})
}
}
func TestClient_CurrentWeatherByLocation_WindDirection(t *testing.T) {
tt := []struct {
// Location name
loc string
// CurWeather direction
d *Direction
// Direction abbr. string
da string
// Direction full string
df string
}{
{"Ehrenfeld, Germany", &Direction{
dt: time.Date(2023, 5, 23, 7, 0, 0, 0, time.Local),
s: SourceAnalysis,
fv: 302,
}, "NWbW", "Northwest by West"},
{"Berlin, Germany", &Direction{
dt: time.Date(2023, 5, 23, 7, 0, 0, 0, time.Local),
s: SourceAnalysis,
fv: 286,
}, "WbN", "West by North"},
{"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.d != nil && tc.d.String() != cw.WindDirection().String() {
t.Errorf("CurrentWeatherByLocation failed, expected wind direction "+
"string: %s, got: %s", tc.d.String(), cw.WindDirection())
}
if tc.d != nil && tc.d.Value() != cw.WindDirection().Value() {
t.Errorf("CurrentWeatherByLocation failed, expected wind direction "+
"float: %f, got: %f", tc.d.Value(), cw.WindDirection().Value())
}
if tc.d != nil && cw.WindDirection().Source() != tc.d.s {
t.Errorf("CurrentWeatherByLocation failed, expected source: %s, but got: %s",
tc.d.s, cw.WindDirection().Source())
}
if tc.d != nil && cw.WindDirection().Direction() != tc.da {
t.Errorf("CurrentWeatherByLocation failed, expected direction abbr.: %s, but got: %s",
tc.da, cw.WindDirection().Direction())
}
if tc.d != nil && cw.WindDirection().DirectionFull() != tc.df {
t.Errorf("CurrentWeatherByLocation failed, expected direction full: %s, but got: %s",
tc.df, cw.WindDirection().DirectionFull())
}
if tc.d == nil {
if cw.WindDirection().IsAvailable() {
t.Errorf("CurrentWeatherByLocation failed, expected wind direction "+
"to have no data, but got: %s", cw.WindDirection())
}
if !math.IsNaN(cw.WindDirection().Value()) {
t.Errorf("CurrentWeatherByLocation failed, expected wind direction "+
"to return NaN, but got: %s", cw.WindSpeed().String())
}
}
})
}
}
func TestClient_CurrentWeatherByLocation_WindGust(t *testing.T) {
tt := []struct {
// Location name
loc string
// CurWeather speed
s *Speed
}{
{"Ehrenfeld, Germany", &Speed{
dt: time.Date(2023, 5, 23, 7, 0, 0, 0, time.Local),
s: SourceAnalysis,
fv: 7.770000,
}},
{"Berlin, Germany", &Speed{
dt: time.Date(2023, 5, 23, 7, 0, 0, 0, time.Local),
s: SourceAnalysis,
fv: 5.570000,
}},
{"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.s != nil && tc.s.String() != cw.WindGust().String() {
t.Errorf("CurrentWeatherByLocation failed, expected wind gust "+
"string: %s, got: %s", tc.s.String(), cw.WindGust())
}
if tc.s != nil && tc.s.Value() != cw.WindGust().Value() {
t.Errorf("CurrentWeatherByLocation failed, expected wind gust "+
"float: %f, got: %f", tc.s.Value(), cw.WindGust().Value())
}
if tc.s != nil && cw.WindGust().Source() != tc.s.s {
t.Errorf("CurrentWeatherByLocation failed, expected source: %s, but got: %s",
tc.s.s, cw.WindGust().Source())
}
if tc.s == nil {
if cw.WindGust().IsAvailable() {
t.Errorf("CurrentWeatherByLocation failed, expected wind gust "+
"to have no data, but got: %s", cw.WindGust())
}
if !math.IsNaN(cw.WindGust().Value()) {
t.Errorf("CurrentWeatherByLocation failed, expected wind gust "+
"to return NaN, but got: %s", cw.WindGust().String())
}
}
})
}
}
func TestClient_CurrentWeatherByLocation_WindSpeed(t *testing.T) {
tt := []struct {
// Location name
loc string
// CurWeather speed
s *Speed
}{
{"Ehrenfeld, Germany", &Speed{
dt: time.Date(2023, 5, 23, 7, 0, 0, 0, time.Local),
s: SourceAnalysis,
fv: 3.94,
}},
{"Berlin, Germany", &Speed{
dt: time.Date(2023, 5, 23, 7, 0, 0, 0, time.Local),
s: SourceAnalysis,
fv: 3.19,
}},
{"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.s != nil && tc.s.String() != cw.WindSpeed().String() {
t.Errorf("CurrentWeatherByLocation failed, expected wind speed "+
"string: %s, got: %s", tc.s.String(), cw.WindSpeed())
}
if tc.s != nil && tc.s.Value() != cw.WindSpeed().Value() {
t.Errorf("CurrentWeatherByLocation failed, expected wind speed "+
"float: %f, got: %f", tc.s.Value(), cw.WindSpeed().Value())
}
if tc.s != nil && cw.WindSpeed().Source() != tc.s.s {
t.Errorf("CurrentWeatherByLocation failed, expected source: %s, but got: %s",
tc.s.s, cw.WindSpeed().Source())
}
if tc.s == nil {
if cw.WindSpeed().IsAvailable() {
t.Errorf("CurrentWeatherByLocation failed, expected wind speed "+
"to have no data, but got: %s", cw.WindSpeed())
}
if !math.IsNaN(cw.WindSpeed().Value()) {
t.Errorf("CurrentWeatherByLocation failed, expected wind speed "+
"to return NaN, but got: %s", cw.WindSpeed().String())
}
}
})
}
}

View file

@ -44,6 +44,8 @@ const (
FieldPressureMSL
// FieldPressureQFE represents the PressureQFE data point
FieldPressureQFE
// FieldSnowAmount represents the SnowAmount data point
FieldSnowAmount
// FieldSunrise represents the Sunrise data point
FieldSunrise
// FieldSunset represents the Sunset data point
@ -62,10 +64,12 @@ const (
FieldTemperatureMin
// FieldWeatherSymbol represents the weather symbol data point
FieldWeatherSymbol
// FieldWinddirection represents the Winddirection data point
FieldWinddirection
// FieldWindspeed represents the Windspeed data point
FieldWindspeed
// FieldWindDirection represents the WindDirection data point
FieldWindDirection
// FieldWindGust represents the WindGust data point
FieldWindGust
// FieldWindSpeed represents the WindSpeed data point
FieldWindSpeed
)
// Enum for different Timespan values
@ -86,6 +90,14 @@ type APIDate struct {
time.Time
}
// APIBool is the JSON structure of the weather data that is
// returned by the API endpoints in which the value is a boolean
type APIBool struct {
DateTime time.Time `json:"dateTime"`
Source *string `json:"source,omitempty"`
Value bool `json:"value"`
}
// APIFloat is the JSON structure of the weather data that is
// returned by the API endpoints in which the value is a float
type APIFloat struct {
@ -109,6 +121,7 @@ type Timespan int
// Weather) data and can be wrapped into other types to provide type
// specific receiver methods
type WeatherData struct {
// bv bool
dt time.Time
dv time.Time
fv float64

48
density.go Normal file
View file

@ -0,0 +1,48 @@
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
//
// SPDX-License-Identifier: MIT
package meteologix
import (
"fmt"
"math"
"time"
)
// Density is a type wrapper of an WeatherData for holding density
// values in WeatherData
type Density WeatherData
// IsAvailable returns true if an Density value was
// available at time of query
func (d Density) IsAvailable() bool {
return !d.na
}
// DateTime returns true if an Density value was
// available at time of query
func (d Density) DateTime() time.Time {
return d.dt
}
// String satisfies the fmt.Stringer interface for the Density type
func (d Density) String() string {
return fmt.Sprintf("%.1fkg/m³", d.fv)
}
// Source returns the Source of Density
// If the Source is not available it will return SourceUnknown
func (d Density) Source() Source {
return d.s
}
// Value returns the float64 value of an Density
// If the Density is not available in the Observation
// Vaule will return math.NaN instead.
func (d Density) Value() float64 {
if d.na {
return math.NaN()
}
return d.fv
}

2
doc.go
View file

@ -6,4 +6,4 @@
package meteologix
// VERSION represents the current version of the package
const VERSION = "0.0.9"
const VERSION = "0.1.0"

View file

@ -52,17 +52,17 @@ type APIObservationData struct {
// HumidityRelative represents the relative humidity in percent
HumidityRelative *APIFloat `json:"humidityRelative,omitempty"`
// Precipitation represents the current amount of precipitation
Precipitation *APIFloat `json:"prec"`
Precipitation *APIFloat `json:"prec,omitempty"`
// Precipitation10m represents the amount of precipitation over the last 10 minutes
Precipitation10m *APIFloat `json:"prec10m"`
Precipitation10m *APIFloat `json:"prec10m,omitempty"`
// Precipitation1h represents the amount of precipitation over the last hour
Precipitation1h *APIFloat `json:"prec1h"`
Precipitation1h *APIFloat `json:"prec1h,omitempty"`
// Precipitation24h represents the amount of precipitation over the last 24 hours
Precipitation24h *APIFloat `json:"prec24h"`
// PressureMSL represents the pressure at mean sea level (MSL) in hPa
PressureMSL *APIFloat `json:"pressureMsl"`
// PressureMSL represents the pressure at station level (QFE) in hPa
PressureQFE *APIFloat `json:"pressure"`
Precipitation24h *APIFloat `json:"prec24h,omitempty"`
// PressureMSL represents the air pressure at MSL / temperature adjusted (QFF) in hPa
PressureMSL *APIFloat `json:"pressureMsl,omitempty"`
// PressureQFE represents the pressure at station level (QFE) in hPa
PressureQFE *APIFloat `json:"pressure,omitempty"`
// Temperature represents the temperature in °C
Temperature *APIFloat `json:"temp,omitempty"`
// TemperatureMax represents the maximum temperature in °C
@ -76,11 +76,11 @@ type APIObservationData struct {
// Temperature5cm represents the minimum temperature 5cm above
// ground in °C
Temperature5cmMin *APIFloat `json:"temp5cmMin,omitempty"`
// Winddirection represents the direction from which the wind
// WindDirection represents the direction from which the wind
// originates in degree (0=N, 90=E, 180=S, 270=W)
Winddirection *APIFloat `json:"windDirection,omitempty"`
// Windspeed represents the wind speed in knots
Windspeed *APIFloat `json:"windSpeed,omitempty"`
WindDirection *APIFloat `json:"windDirection,omitempty"`
// WindSpeed represents the wind speed in knots (soon switched to m/s)
WindSpeed *APIFloat `json:"windSpeed,omitempty"`
}
// ObservationLatestByStationID returns the latest Observation values from the
@ -365,33 +365,33 @@ func (o Observation) GlobalRadiation(ts Timespan) Radiation {
}
}
// Winddirection returns the current direction from which the wind
// WindDirection returns the current direction from which the wind
// originates in degree (0=N, 90=E, 180=S, 270=W) as Direction.
// If the data point is not available in the Observation it will return
// Direction in which the "not available" field will be true.
func (o Observation) Winddirection() Direction {
if o.Data.Winddirection == nil {
func (o Observation) WindDirection() Direction {
if o.Data.WindDirection == nil {
return Direction{na: true}
}
return Direction{
dt: o.Data.Winddirection.DateTime,
n: FieldWinddirection,
dt: o.Data.WindDirection.DateTime,
n: FieldWindDirection,
s: SourceObservation,
fv: o.Data.Winddirection.Value,
fv: o.Data.WindDirection.Value,
}
}
// Windspeed returns the current windspeed data point as Speed.
// WindSpeed returns the current windspeed data point as Speed.
// If the data point is not available in the Observation it will return
// Speed in which the "not available" field will be true.
func (o Observation) Windspeed() Speed {
if o.Data.Windspeed == nil {
func (o Observation) WindSpeed() Speed {
if o.Data.WindSpeed == nil {
return Speed{na: true}
}
return Speed{
dt: o.Data.Windspeed.DateTime,
n: FieldWindspeed,
dt: o.Data.WindSpeed.DateTime,
n: FieldWindSpeed,
s: SourceObservation,
fv: o.Data.Windspeed.Value,
fv: o.Data.WindSpeed.Value * 0.5144444444,
}
}

View file

@ -1124,7 +1124,7 @@ func TestClient_ObservationLatestByStationID_GlobalRadiation24h(t *testing.T) {
}
}
func TestClient_ObservationLatestByStationID_Winddirection(t *testing.T) {
func TestClient_ObservationLatestByStationID_WindDirection(t *testing.T) {
tt := []struct {
// Test name
n string
@ -1153,37 +1153,37 @@ func TestClient_ObservationLatestByStationID_Winddirection(t *testing.T) {
t.Errorf("ObservationLatestByStationID with station %s failed: %s", tc.sid, err)
return
}
if tc.p != nil && tc.p.String() != o.Winddirection().String() {
if tc.p != nil && tc.p.String() != o.WindDirection().String() {
t.Errorf("ObservationLatestByStationID failed, expected wind direction "+
"string: %s, got: %s", tc.p.String(), o.Winddirection())
"string: %s, got: %s", tc.p.String(), o.WindDirection())
}
if tc.p != nil && tc.p.Value() != o.Winddirection().Value() {
if tc.p != nil && tc.p.Value() != o.WindDirection().Value() {
t.Errorf("ObservationLatestByStationID failed, expected wind direction "+
"float: %f, got: %f", tc.p.Value(), o.Winddirection().Value())
"float: %f, got: %f", tc.p.Value(), o.WindDirection().Value())
}
if tc.p != nil && tc.p.dt.Unix() != o.Winddirection().DateTime().Unix() {
if tc.p != nil && tc.p.dt.Unix() != o.WindDirection().DateTime().Unix() {
t.Errorf("ObservationLatestByStationID failed, expected datetime: %s, got: %s",
tc.p.dt.Format(time.RFC3339), o.Winddirection().DateTime().Format(time.RFC3339))
tc.p.dt.Format(time.RFC3339), o.WindDirection().DateTime().Format(time.RFC3339))
}
if o.Winddirection().Source() != SourceObservation {
if o.WindDirection().Source() != SourceObservation {
t.Errorf("ObservationLatestByStationID failed, expected observation source, but got: %s",
o.Winddirection().Source())
o.WindDirection().Source())
}
if tc.p == nil {
if o.Winddirection().IsAvailable() {
if o.WindDirection().IsAvailable() {
t.Errorf("ObservationLatestByStationID failed, expected wind direction "+
"to have no data, but got: %s", o.Winddirection())
"to have no data, but got: %s", o.WindDirection())
}
if !math.IsNaN(o.Winddirection().Value()) {
if !math.IsNaN(o.WindDirection().Value()) {
t.Errorf("ObservationLatestByStationID failed, expected wind direction "+
"to return NaN, but got: %s", o.Winddirection().String())
"to return NaN, but got: %s", o.WindDirection().String())
}
}
})
}
}
func TestClient_ObservationLatestByStationID_Windspeed(t *testing.T) {
func TestClient_ObservationLatestByStationID_WindSpeed(t *testing.T) {
tt := []struct {
// Test name
n string
@ -1196,7 +1196,7 @@ func TestClient_ObservationLatestByStationID_Windspeed(t *testing.T) {
{"K-Stammheim", "H744", nil},
{"All data fields", "all", &Speed{
dt: time.Date(2023, 0o5, 21, 11, 30, 0, 0, time.UTC),
fv: 15,
fv: 7.716666666,
}},
{"No data fields", "none", nil},
}
@ -1212,30 +1212,30 @@ func TestClient_ObservationLatestByStationID_Windspeed(t *testing.T) {
t.Errorf("ObservationLatestByStationID with station %s failed: %s", tc.sid, err)
return
}
if tc.p != nil && tc.p.String() != o.Windspeed().String() {
if tc.p != nil && tc.p.String() != o.WindSpeed().String() {
t.Errorf("ObservationLatestByStationID failed, expected windspeed "+
"string: %s, got: %s", tc.p.String(), o.Windspeed())
"string: %s, got: %s", tc.p.String(), o.WindSpeed())
}
if tc.p != nil && tc.p.Value() != o.Windspeed().Value() {
if tc.p != nil && tc.p.Value() != o.WindSpeed().Value() {
t.Errorf("ObservationLatestByStationID failed, expected windspeed "+
"float: %f, got: %f", tc.p.Value(), o.Windspeed().Value())
"float: %f, got: %f, %+v", tc.p.Value(), o.WindSpeed().Value(), o.Data.WindSpeed)
}
if tc.p != nil && tc.p.dt.Unix() != o.Windspeed().DateTime().Unix() {
if tc.p != nil && tc.p.dt.Unix() != o.WindSpeed().DateTime().Unix() {
t.Errorf("ObservationLatestByStationID failed, expected datetime: %s, got: %s",
tc.p.dt.Format(time.RFC3339), o.Windspeed().DateTime().Format(time.RFC3339))
tc.p.dt.Format(time.RFC3339), o.WindSpeed().DateTime().Format(time.RFC3339))
}
if o.Windspeed().Source() != SourceObservation {
if o.WindSpeed().Source() != SourceObservation {
t.Errorf("ObservationLatestByStationID failed, expected observation source, but got: %s",
o.Windspeed().Source())
o.WindSpeed().Source())
}
if tc.p == nil {
if o.Windspeed().IsAvailable() {
if o.WindSpeed().IsAvailable() {
t.Errorf("ObservationLatestByStationID failed, expected windspeed "+
"to have no data, but got: %s", o.Windspeed())
"to have no data, but got: %s", o.WindSpeed())
}
if !math.IsNaN(o.Windspeed().Value()) {
if !math.IsNaN(o.WindSpeed().Value()) {
t.Errorf("ObservationLatestByStationID failed, expected windspeed "+
"to return NaN, but got: %s", o.Windspeed().String())
"to return NaN, but got: %s", o.WindSpeed().String())
}
}
})
@ -1308,32 +1308,35 @@ func TestObservationTemperature_String(t *testing.T) {
func TestObservationSpeed_Conversion(t *testing.T) {
tt := []struct {
// Original knots value
kn float64
// Original m/s value
ms float64
// km/h value
kmh float64
// mi/h value
mph float64
// knots value
kn float64
}{
{0, 0, 0},
{1, 1.852, 1.151},
{10, 18.52, 11.51},
{15, 27.78, 17.265},
{30, 55.56, 34.53},
{0, 0, 0, 0},
{1, 3.6, 2.236936, 1.9438444924},
{10, 36, 22.369360, 19.438444924},
{15, 54, 33.554040, 29.157667386},
{30, 108, 67.108080, 58.315334772},
}
msf := "%.1fm/s"
knf := "%.0fkn"
kmhf := "%.1fkm/h"
mphf := "%.1fmi/h"
for _, tc := range tt {
t.Run(fmt.Sprintf("%.0fkn", tc.kn), func(t *testing.T) {
os := Speed{fv: tc.kn}
if os.Value() != tc.kn {
t.Errorf("Speed.Value failed, expected: %f, got: %f", tc.kn,
t.Run(fmt.Sprintf("%.0fm/s", tc.ms), func(t *testing.T) {
os := Speed{fv: tc.ms}
if os.Value() != tc.ms {
t.Errorf("Speed.Value failed, expected: %f, got: %f", tc.ms,
os.Value())
}
if os.String() != fmt.Sprintf(knf, tc.kn) {
if os.String() != fmt.Sprintf(msf, tc.ms) {
t.Errorf("Speed.String failed, expected: %s, got: %s",
fmt.Sprintf(knf, tc.kn), os.String())
fmt.Sprintf(msf, tc.ms), os.String())
}
if os.KMH() != tc.kmh {
t.Errorf("Speed.KMH failed, expected: %f, got: %f", tc.kmh,
@ -1351,6 +1354,14 @@ func TestObservationSpeed_Conversion(t *testing.T) {
t.Errorf("Speed.MPHString failed, expected: %s, got: %s",
fmt.Sprintf(mphf, tc.mph), os.MPHString())
}
if os.Knots() != tc.kn {
t.Errorf("Speed.Knots failed, expected: %f, got: %f", tc.kn,
os.Knots())
}
if os.KnotsString() != fmt.Sprintf(knf, tc.kn) {
t.Errorf("Speed.KnotsString failed, expected: %s, got: %s",
fmt.Sprintf(knf, tc.kn), os.KnotsString())
}
})
}
}

View file

@ -10,6 +10,15 @@ import (
"time"
)
const (
// MultiplierKnots is the multiplier for converting the base unit to knots
MultiplierKnots = 1.9438444924
// MultiplierKPH is the multiplier for converting the base unit to kilometers per hour
MultiplierKPH = 3.6
// MultiplierMPH is the multiplier for converting the base unit to miles per hour
MultiplierMPH = 2.236936
)
// Speed is a type wrapper of an WeatherData for holding speed
// values in WeatherData
type Speed WeatherData
@ -26,7 +35,8 @@ func (s Speed) DateTime() time.Time {
return s.dt
}
// Value returns the float64 value of an Speed in knots
// Value returns the float64 value of an Speed in meters
// per second.
// If the Speed is not available in the Observation
// Vaule will return math.NaN instead.
func (s Speed) Value() float64 {
@ -38,7 +48,7 @@ func (s Speed) Value() float64 {
// String satisfies the fmt.Stringer interface for the Speed type
func (s Speed) String() string {
return fmt.Sprintf("%.0fkn", s.fv)
return fmt.Sprintf("%.1fm/s", s.fv)
}
// Source returns the Source of Speed
@ -49,7 +59,7 @@ func (s Speed) Source() Source {
// KMH returns the Speed value in km/h
func (s Speed) KMH() float64 {
return s.fv * 1.852
return s.fv * MultiplierKPH
}
// KMHString returns the Speed value as formatted string in km/h
@ -57,9 +67,19 @@ func (s Speed) KMHString() string {
return fmt.Sprintf("%.1fkm/h", s.KMH())
}
// Knots returns the Speed value in kn
func (s Speed) Knots() float64 {
return s.fv * MultiplierKnots
}
// KnotsString returns the Speed value as formatted string in kn
func (s Speed) KnotsString() string {
return fmt.Sprintf("%.0fkn", s.Knots())
}
// MPH returns the Speed value in mi/h
func (s Speed) MPH() float64 {
return s.fv * 1.151
return s.fv * MultiplierMPH
}
// MPHString returns the Speed value as formatted string in mi/h