Compare commits
68 commits
Author | SHA1 | Date | |
---|---|---|---|
|
d6ce76b1ad | ||
|
072a2aed5d | ||
|
a536ac3bde | ||
|
eb675388f0 | ||
|
e4cbab4e43 | ||
|
8923b88e07 | ||
|
94eca2f3fe | ||
|
759300d066 | ||
|
3c69f36748 | ||
|
0b88d11fdf | ||
|
d6b551cc5e | ||
|
5297342603 | ||
|
c3af61791e | ||
|
b8c117c056 | ||
|
3f7cccb511 | ||
|
c9fa7eb1d7 | ||
|
878c4dfe71 | ||
|
27c8542f76 | ||
|
a5667bb828 | ||
|
173c6eb8ec | ||
|
a51644fbb6 | ||
|
f48392d553 | ||
|
85c3c1aff3 | ||
|
abd200177f | ||
|
e7f8662347 | ||
|
54cc672dfc | ||
|
e3756a5466 | ||
|
513d7b863f | ||
|
c9d95300c2 | ||
|
84ba2feda9 | ||
|
65d065dd59 | ||
|
3eb6a76f5d | ||
|
9c65eca128 | ||
|
a4f19380ff | ||
|
d0905266e1 | ||
|
28479be939 | ||
|
914327de85 | ||
|
a10ec1c0f9 | ||
|
d7567d4b2b | ||
|
c1e054d9a3 | ||
|
4318599eb0 | ||
|
a0b67b0367 | ||
|
681c53c23d | ||
|
8cb9754f69 | ||
|
7bdf2de388 | ||
|
b779e0f65b | ||
|
a697b13970 | ||
|
5dee24573a | ||
|
fee0bc6795 | ||
|
fcc7626a76 | ||
|
4d60f35c6a | ||
|
a7feae910b | ||
|
b77cd98484 | ||
|
3394ceeb4a | ||
|
0e3c8c6f1d | ||
|
4a7c807174 | ||
|
78e6751bbf | ||
|
5722d0e4a8 | ||
|
4ed0c12ca5 | ||
|
4123aa75e3 | ||
|
728a967bef | ||
|
e2a301c2a5 | ||
|
fe800b7632 | ||
|
b89d04e581 | ||
|
28d203a188 | ||
|
0608c4ecaa | ||
|
7ffd3943e0 | ||
|
56b17ffcf3 |
6
.forgejo/FUNDING.yml
Normal file
6
.forgejo/FUNDING.yml
Normal file
|
@ -0,0 +1,6 @@
|
|||
# SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
|
||||
#
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
github: wneessen
|
||||
ko_fi: winni
|
|
@ -10,7 +10,7 @@ on:
|
|||
paths:
|
||||
- '**.go'
|
||||
- 'go.*'
|
||||
- '.github/**'
|
||||
- '.forgejo/**'
|
||||
- 'codecov.yml'
|
||||
pull_request:
|
||||
branches:
|
||||
|
@ -18,29 +18,24 @@ on:
|
|||
paths:
|
||||
- '**.go'
|
||||
- 'go.*'
|
||||
- '.github/**'
|
||||
- '.forgejo/**'
|
||||
- 'codecov.yml'
|
||||
env:
|
||||
API_KEY: ${{ secrets.API_KEY }}
|
||||
jobs:
|
||||
run:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
go: [1.18, 1.19, '1.20']
|
||||
runs-on: docker
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@master
|
||||
- name: Setup go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
go-version: '1.22'
|
||||
- name: Run Tests
|
||||
run: |
|
||||
go test -v -shuffle=on -race --coverprofile=coverage.coverprofile --covermode=atomic ./...
|
||||
- name: Upload coverage to Codecov
|
||||
if: success() && matrix.go == '1.20' && matrix.os == 'ubuntu-latest'
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
|
|
@ -17,11 +17,11 @@ permissions:
|
|||
jobs:
|
||||
golangci:
|
||||
name: lint
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: docker
|
||||
steps:
|
||||
- uses: actions/setup-go@v3
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.20'
|
||||
go-version: '1.21'
|
||||
- uses: actions/checkout@v3
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v3
|
|
@ -8,7 +8,7 @@ on: [push, pull_request]
|
|||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: docker
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: REUSE Compliance Check
|
|
@ -15,7 +15,7 @@ env:
|
|||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: docker
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
|
@ -24,12 +24,16 @@ jobs:
|
|||
- name: Setup Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.20.x'
|
||||
go-version: '1.22'
|
||||
|
||||
- name: Run unit Tests
|
||||
run: |
|
||||
go test -v -shuffle=on -race --coverprofile=./cov.out ./...
|
||||
|
||||
- name: Install jq
|
||||
run: |
|
||||
apt-get update; apt-get -y install jq; which jq
|
||||
|
||||
- uses: sonarsource/sonarqube-scan-action@master
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
|
@ -11,5 +11,5 @@ enable = ["stylecheck", "whitespace", "containedctx", "contextcheck", "decorder"
|
|||
"errname", "errorlint", "gofmt", "gofumpt", "goimports"]
|
||||
|
||||
[linters-settings.goimports]
|
||||
local-prefixes = "github.com/wneessen/go-meteologix"
|
||||
local-prefixes = "src.neessen.cloud/wneessen/go-meteologix"
|
||||
|
||||
|
|
24
README.md
24
README.md
|
@ -6,11 +6,11 @@ SPDX-License-Identifier: CC0-1.0
|
|||
|
||||
# go-meteologix - Go packages for accessing Meteologix/Kachelmann Wetter/WeatherUS data
|
||||
|
||||
[![GoDoc](https://godoc.org/github.com/wneessen/go-mail?status.svg)](https://pkg.go.dev/github.com/wneessen/go-meteologix)
|
||||
[![GoDoc](https://godoc.org/src.neessen.cloud/wneessen/go-mail?status.svg)](https://pkg.go.dev/src.neessen.cloud/wneessen/go-meteologix)
|
||||
[![codecov](https://codecov.io/gh/wneessen/go-meteologix/branch/main/graph/badge.svg?token=W4QI1RMR4L)](https://codecov.io/gh/wneessen/go-meteologix)
|
||||
[![Go Report Card](https://goreportcard.com/badge/github.com/wneessen/go-meteologix)](https://goreportcard.com/report/github.com/wneessen/go-meteologix)
|
||||
[![Go Report Card](https://goreportcard.com/badge/src.neessen.cloud/wneessen/go-meteologix)](https://goreportcard.com/report/src.neessen.cloud/wneessen/go-meteologix)
|
||||
[![#go-meteologix on Discord](https://img.shields.io/badge/Discord-%23go–meteologix-blue.svg)](https://discord.gg/TvNMuDh4pK)
|
||||
[![REUSE status](https://api.reuse.software/badge/github.com/wneessen/go-meteologix)](https://api.reuse.software/info/github.com/wneessen/go-meteologix)
|
||||
[![REUSE status](https://api.reuse.software/badge/src.neessen.cloud/wneessen/go-meteologix)](https://api.reuse.software/info/src.neessen.cloud/wneessen/go-meteologix)
|
||||
<a href="https://ko-fi.com/D1D24V9IX"><img src="https://uploads-ssl.webflow.com/5c14e387dab576fe667689cf/5cbed8a4ae2b88347c06c923_BuyMeACoffee_blue.png" height="20" alt="buy ma a coffee"></a>
|
||||
|
||||
<p align="center"><img src="./assets/gopher43.svg" width="250" alt="go-meteologx logo"/></p>
|
||||
|
@ -32,7 +32,7 @@ For Geolocation lookups, the package makes use of the
|
|||
## Usage
|
||||
|
||||
The library is fully documented using the execellent GoDoc functionality. Check out
|
||||
the [full reference on pkg.go.dev](https://pkg.go.dev/github.com/wneessen/go-hibp) for
|
||||
the [full reference on pkg.go.dev](https://pkg.go.dev/src.neessen.cloud/wneessen/go-hibp) for
|
||||
details.
|
||||
|
||||
## Examples
|
||||
|
@ -48,7 +48,7 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/wneessen/go-meteologix"
|
||||
"src.neessen.cloud/wneessen/go-meteologix"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
@ -76,7 +76,7 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/wneessen/go-meteologix"
|
||||
"src.neessen.cloud/wneessen/go-meteologix"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
@ -106,7 +106,7 @@ import (
|
|||
"math"
|
||||
"os"
|
||||
|
||||
"github.com/wneessen/go-meteologix"
|
||||
"src.neessen.cloud/wneessen/go-meteologix"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
@ -139,7 +139,7 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/wneessen/go-meteologix"
|
||||
"src.neessen.cloud/wneessen/go-meteologix"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
@ -158,8 +158,14 @@ func main() {
|
|||
```
|
||||
|
||||
## Authors/Contributors
|
||||
go-meteologix was authored and developed by [Winni Neessen](https://github.com/wneessen/).
|
||||
go-meteologix was authored and developed by [Winni Neessen](https://src.neessen.cloud/wneessen/).
|
||||
|
||||
Big thanks to the following people, for contributing to the go-meteologix project
|
||||
(either in form of code, reviewing code, writing documenation or contributing in any other form):
|
||||
* [Maria Letta](https://github.com/MariaLetta) (designed the go-meteologix logo)
|
||||
|
||||
## Mirror
|
||||
|
||||
Please note that the repository on Github is just a mirror of
|
||||
[https://src.neessen.cloud/wneessen/go-meteologix](https://src.neessen.cloud/wneessen/go-meteologix)
|
||||
for ease of access and reachability.
|
228
astroinfo.go
Normal file
228
astroinfo.go
Normal file
|
@ -0,0 +1,228 @@
|
|||
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package meteologix
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// AstronomicalInfo provides astronomical data for the next 14 days.
|
||||
// This includes moon and sun information.
|
||||
type AstronomicalInfo struct {
|
||||
// DailyData holds the different APIAstronomicalDailyData data
|
||||
// points for the next 14 days
|
||||
DailyData []APIAstronomicalDailyData `json:"dailyData"`
|
||||
// Latitude represents the GeoLocation latitude coordinates for the weather data
|
||||
Latitude float64 `json:"lat"`
|
||||
// Longitude represents the GeoLocation longitude coordinates for the weather data
|
||||
Longitude float64 `json:"lon"`
|
||||
// NextFullMoon represent the date and time of the next full moon
|
||||
NextFullMoon time.Time `json:"nextFullMoon"`
|
||||
// NextNewMoon represent the date and time of the next new moon
|
||||
NextNewMoon time.Time `json:"nextNewMoon"`
|
||||
// Run represents when astronomical values have been calculated
|
||||
Run time.Time `json:"run"`
|
||||
// TimeZone is the timezone at the queried location
|
||||
TimeZone string `json:"timeZone"`
|
||||
}
|
||||
|
||||
// APIAstronomicalDailyData holds the API response date for the daily
|
||||
// details in the AstronomicalInfo.
|
||||
type APIAstronomicalDailyData struct {
|
||||
// AstronomicalDawn represents the date and time when civil dawn begins
|
||||
AstronomicalDawn *time.Time `json:"astronomicalDawn,omitempty"`
|
||||
// AstronomicalDusk represents the date and time when civil dusk ends
|
||||
AstronomicalDusk *time.Time `json:"astronomicalDusk,omitempty"`
|
||||
// CivilDawn represents the date and time when civil dawn begins
|
||||
CivilDawn *time.Time `json:"civilDawn,omitempty"`
|
||||
// CivilDusk represents the date and time when civil dusk ends
|
||||
CivilDusk *time.Time `json:"civilDusk,omitempty"`
|
||||
// DateTime represents the date for the forecast values
|
||||
DateTime APIDate `json:"dateTime"`
|
||||
// MoonIllumination represents how much of the moon is illuminated in %
|
||||
MoonIllumination float64 `json:"moonIllumination"`
|
||||
// MoonPhase represents the moon phase in %
|
||||
MoonPhase int `json:"moonPhase"`
|
||||
// MoonRise represents the date and time when the moon rises
|
||||
MoonRise *time.Time `json:"moonRise,omitempty"`
|
||||
// MoonSet represents the date and time when the moon sets
|
||||
MoonSet *time.Time `json:"moonSet,omitempty"`
|
||||
// NauticalDawn represents the date and time when nautical dawn begins
|
||||
NauticalDawn *time.Time `json:"nauticalDawn,omitempty"`
|
||||
// NauticalDusk represents the date and time when nautical dusk ends
|
||||
NauticalDusk *time.Time `json:"nauticalDusk,omitempty"`
|
||||
// Sunrise represents the date and time of the sunrise
|
||||
Sunrise *time.Time `json:"sunrise,omitempty"`
|
||||
// Sunset represents the date and time of the sunset
|
||||
Sunset *time.Time `json:"sunset,omitempty"`
|
||||
// Transit represents the date and time when the sun is at
|
||||
// its zenith
|
||||
Transit *time.Time `json:"transit,omitempty"`
|
||||
}
|
||||
|
||||
// AstronomicalInfoByCoordinates returns the AstronomicalInfo values for
|
||||
// the given coordinates
|
||||
func (c *Client) AstronomicalInfoByCoordinates(la, lo float64) (AstronomicalInfo, error) {
|
||||
var ai AstronomicalInfo
|
||||
lat := strconv.FormatFloat(la, 'f', -1, 64)
|
||||
lon := strconv.FormatFloat(lo, 'f', -1, 64)
|
||||
u := fmt.Sprintf("%s/tools/astronomy/%s/%s", c.config.apiURL, lat, lon)
|
||||
|
||||
r, err := c.httpClient.Get(u)
|
||||
if err != nil {
|
||||
return ai, fmt.Errorf("API request failed: %w", err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(r, &ai); err != nil {
|
||||
return ai, fmt.Errorf("failed to unmarshal API response JSON: %w", err)
|
||||
}
|
||||
|
||||
return ai, nil
|
||||
}
|
||||
|
||||
// AstronomicalInfoByLocation returns the AstronomicalInfo values for
|
||||
// the given location
|
||||
func (c *Client) AstronomicalInfoByLocation(lo string) (AstronomicalInfo, error) {
|
||||
gl, err := c.GetGeoLocationByName(lo)
|
||||
if err != nil {
|
||||
return AstronomicalInfo{}, fmt.Errorf("failed too look up geolocation: %w", err)
|
||||
}
|
||||
return c.AstronomicalInfoByCoordinates(gl.Latitude, gl.Longitude)
|
||||
}
|
||||
|
||||
// SunsetByTime returns the date and time of the sunset on the give
|
||||
// time as DateTime type.
|
||||
// If the data point is not available in the AstronomicalInfo it will
|
||||
// return DateTime in which the "not available" field will be true.
|
||||
//
|
||||
// Please keep in mind that the API only returns 14 days in the future.
|
||||
// Any date given that exceeds that time, wil always return a
|
||||
// "not available" value.
|
||||
func (a *AstronomicalInfo) SunsetByTime(t time.Time) DateTime {
|
||||
if len(a.DailyData) < 1 {
|
||||
return DateTime{na: true}
|
||||
}
|
||||
var cdd APIAstronomicalDailyData
|
||||
for i := range a.DailyData {
|
||||
if a.DailyData[i].DateTime.Format(DateFormat) != t.Format(DateFormat) {
|
||||
continue
|
||||
}
|
||||
cdd = a.DailyData[i]
|
||||
}
|
||||
if cdd.DateTime.IsZero() {
|
||||
return DateTime{na: true}
|
||||
}
|
||||
return DateTime{
|
||||
dt: cdd.DateTime.Time,
|
||||
n: FieldSunset,
|
||||
s: SourceForecast,
|
||||
dv: *cdd.Sunset,
|
||||
}
|
||||
}
|
||||
|
||||
// Sunset returns the date and time of the sunset on the current date
|
||||
// as DateTime type.
|
||||
// If the data point is not available in the AstronomicalInfo it will
|
||||
// return DateTime in which the "not available" field will be true.
|
||||
func (a *AstronomicalInfo) Sunset() DateTime {
|
||||
return a.SunsetByTime(time.Now())
|
||||
}
|
||||
|
||||
// SunsetByDateString returns the date and time of the sunset at a
|
||||
// given date string as DateTime type. Expected format is 2006-01-02.
|
||||
// If the date wasn't able to be parsed or if the data point is not
|
||||
// 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(DateFormat, ds)
|
||||
if err != nil {
|
||||
return DateTime{na: true}
|
||||
}
|
||||
return a.SunsetByTime(t)
|
||||
}
|
||||
|
||||
// SunsetAll returns a slice of all sunset data points in the given
|
||||
// AstronomicalInfo instance as DateTime types. If no sunset data
|
||||
// is available it will return an empty slice
|
||||
func (a *AstronomicalInfo) SunsetAll() []DateTime {
|
||||
var sss []DateTime
|
||||
for _, cd := range a.DailyData {
|
||||
if cd.DateTime.IsZero() {
|
||||
continue
|
||||
}
|
||||
sss = append(sss, a.SunsetByTime(cd.DateTime.Time))
|
||||
}
|
||||
|
||||
return sss
|
||||
}
|
||||
|
||||
// SunriseByTime returns the date and time of the sunrise on the give
|
||||
// time as DateTime type.
|
||||
// If the data point is not available in the AstronomicalInfo it will
|
||||
// return DateTime in which the "not available" field will be true.
|
||||
//
|
||||
// Please keep in mind that the API only returns 14 days in the future.
|
||||
// Any date given that exceeds that time, wil always return a
|
||||
// "not available" value.
|
||||
func (a *AstronomicalInfo) SunriseByTime(t time.Time) DateTime {
|
||||
if len(a.DailyData) < 1 {
|
||||
return DateTime{na: true}
|
||||
}
|
||||
var cdd APIAstronomicalDailyData
|
||||
for i := range a.DailyData {
|
||||
if a.DailyData[i].DateTime.Format(DateFormat) != t.Format(DateFormat) {
|
||||
continue
|
||||
}
|
||||
cdd = a.DailyData[i]
|
||||
}
|
||||
if cdd.DateTime.IsZero() {
|
||||
return DateTime{na: true}
|
||||
}
|
||||
return DateTime{
|
||||
dt: cdd.DateTime.Time,
|
||||
n: FieldSunrise,
|
||||
s: SourceForecast,
|
||||
dv: *cdd.Sunrise,
|
||||
}
|
||||
}
|
||||
|
||||
// Sunrise returns the date and time of the sunrise on the current date
|
||||
// as DateTime type.
|
||||
// If the data point is not available in the AstronomicalInfo it will
|
||||
// return DateTime in which the "not available" field will be true.
|
||||
func (a *AstronomicalInfo) Sunrise() DateTime {
|
||||
return a.SunriseByTime(time.Now())
|
||||
}
|
||||
|
||||
// SunriseByDateString returns the date and time of the sunrise at a
|
||||
// given date string as DateTime type. Expected format is 2006-01-02.
|
||||
// If the date wasn't able to be parsed or if the data point is not
|
||||
// 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(DateFormat, ds)
|
||||
if err != nil {
|
||||
return DateTime{na: true}
|
||||
}
|
||||
return a.SunriseByTime(t)
|
||||
}
|
||||
|
||||
// SunriseAll returns a slice of all sunrise data points in the given
|
||||
// AstronomicalInfo instance as DateTime types. If no sunrise data
|
||||
// is available it will return an empty slice
|
||||
func (a *AstronomicalInfo) SunriseAll() []DateTime {
|
||||
var sss []DateTime
|
||||
for _, cd := range a.DailyData {
|
||||
if cd.DateTime.IsZero() {
|
||||
continue
|
||||
}
|
||||
sss = append(sss, a.SunriseByTime(cd.DateTime.Time))
|
||||
}
|
||||
|
||||
return sss
|
||||
}
|
221
astroinfo_test.go
Normal file
221
astroinfo_test.go
Normal file
|
@ -0,0 +1,221 @@
|
|||
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
package meteologix
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestClient_AstronomicalInfoByCoordinates(t *testing.T) {
|
||||
la := 52.5067296
|
||||
lo := 13.2599306
|
||||
loc, err := time.LoadLocation("Europe/Berlin")
|
||||
if err != nil {
|
||||
t.Errorf("failed to load time location data for Europe/Berlin: %s", err)
|
||||
return
|
||||
}
|
||||
rt := time.Date(2023, 5, 28, 15, 8, 33, 0, loc)
|
||||
nfmt := time.Date(2023, 6, 4, 5, 43, 56, 0, loc)
|
||||
nnmt := time.Date(2023, 6, 18, 6, 39, 10, 0, loc)
|
||||
c := New(withMockAPI())
|
||||
ai, err := c.AstronomicalInfoByCoordinates(la, lo)
|
||||
if err != nil {
|
||||
t.Errorf("failed to fetch astronomical information: %s", err)
|
||||
return
|
||||
}
|
||||
if ai.Latitude != la {
|
||||
t.Errorf("AstronomicalInfoByCoordinates failed, expected lat: %f, got: %f", la,
|
||||
ai.Latitude)
|
||||
}
|
||||
if ai.Longitude != lo {
|
||||
t.Errorf("AstronomicalInfoByCoordinates failed, expected lon: %f, got: %f", lo,
|
||||
ai.Longitude)
|
||||
}
|
||||
if ai.Run.UnixMilli() != rt.UnixMilli() {
|
||||
t.Errorf("AstronomicalInfoByCoordinates failed, expected run time: %s, got: %s",
|
||||
rt.String(), ai.Run.String())
|
||||
}
|
||||
if ai.TimeZone != loc.String() {
|
||||
t.Errorf("AstronomicalInfoByCoordinates failed, expected time zone: %s, got: %s",
|
||||
loc.String(), ai.TimeZone)
|
||||
}
|
||||
if ai.NextFullMoon.UnixMilli() != nfmt.UnixMilli() {
|
||||
t.Errorf("AstronomicalInfoByCoordinates failed, expected next full moon: %s, got: %s",
|
||||
nfmt.String(), ai.NextFullMoon.String())
|
||||
}
|
||||
if ai.NextNewMoon.UnixMilli() != nnmt.UnixMilli() {
|
||||
t.Errorf("AstronomicalInfoByCoordinates failed, expected next new moon: %s, got: %s",
|
||||
nnmt.String(), ai.NextNewMoon.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_AstronomicalInfoByLocation(t *testing.T) {
|
||||
la := 52.5067296
|
||||
lo := 13.2599306
|
||||
loc, err := time.LoadLocation("Europe/Berlin")
|
||||
if err != nil {
|
||||
t.Errorf("failed to load time location data for Europe/Berlin: %s", err)
|
||||
return
|
||||
}
|
||||
rt := time.Date(2023, 5, 28, 15, 8, 33, 0, loc)
|
||||
nfmt := time.Date(2023, 6, 4, 5, 43, 56, 0, loc)
|
||||
nnmt := time.Date(2023, 6, 18, 6, 39, 10, 0, loc)
|
||||
c := New(withMockAPI())
|
||||
ai, err := c.AstronomicalInfoByLocation("Berlin, Germany")
|
||||
if err != nil {
|
||||
t.Errorf("failed to fetch astronomical information: %s", err)
|
||||
return
|
||||
}
|
||||
if ai.Latitude != la {
|
||||
t.Errorf("AstronomicalInfoByCoordinates failed, expected lat: %f, got: %f", la,
|
||||
ai.Latitude)
|
||||
}
|
||||
if ai.Longitude != lo {
|
||||
t.Errorf("AstronomicalInfoByCoordinates failed, expected lon: %f, got: %f", lo,
|
||||
ai.Longitude)
|
||||
}
|
||||
if ai.Run.UnixMilli() != rt.UnixMilli() {
|
||||
t.Errorf("AstronomicalInfoByCoordinates failed, expected run time: %s, got: %s",
|
||||
rt.String(), ai.Run.String())
|
||||
}
|
||||
if ai.TimeZone != loc.String() {
|
||||
t.Errorf("AstronomicalInfoByCoordinates failed, expected time zone: %s, got: %s",
|
||||
loc.String(), ai.TimeZone)
|
||||
}
|
||||
if ai.NextFullMoon.UnixMilli() != nfmt.UnixMilli() {
|
||||
t.Errorf("AstronomicalInfoByCoordinates failed, expected next full moon: %s, got: %s",
|
||||
nfmt.String(), ai.NextFullMoon.String())
|
||||
}
|
||||
if ai.NextNewMoon.UnixMilli() != nnmt.UnixMilli() {
|
||||
t.Errorf("AstronomicalInfoByCoordinates failed, expected next new moon: %s, got: %s",
|
||||
nnmt.String(), ai.NextNewMoon.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAstronomicalInfo_SunsetByDateString(t *testing.T) {
|
||||
la := 52.5067296
|
||||
lo := 13.2599306
|
||||
loc, err := time.LoadLocation("Europe/Berlin")
|
||||
if err != nil {
|
||||
t.Errorf("failed to load time location data for Europe/Berlin: %s", err)
|
||||
return
|
||||
}
|
||||
ti := time.Date(2023, 5, 28, 21, 16, 37, 0, loc)
|
||||
ddt := time.Date(2023, 5, 28, 0, 0, 0, 0, time.UTC)
|
||||
c := New(withMockAPI())
|
||||
ai, err := c.AstronomicalInfoByCoordinates(la, lo)
|
||||
if err != nil {
|
||||
t.Errorf("failed to fetch astronomical information: %s", err)
|
||||
return
|
||||
}
|
||||
if !ai.SunsetByTime(ti).IsAvailable() {
|
||||
t.Errorf("SunsetByTime failed, expected entry, but got 'not available'")
|
||||
return
|
||||
}
|
||||
if ai.SunsetByTime(ti).Value().UnixMilli() != ti.UnixMilli() {
|
||||
t.Errorf("SunsetByTime failed, expected sunset: %s, got: %s",
|
||||
ti.String(), ai.SunsetByTime(ti).Value().String())
|
||||
}
|
||||
if !ai.SunsetByDateString(ti.Format(DateFormat)).IsAvailable() {
|
||||
t.Errorf("SunsetByDateString failed, expected entry, but got 'not available'")
|
||||
return
|
||||
}
|
||||
if ai.SunsetByTime(ti).String() != ti.Format(time.RFC3339) {
|
||||
t.Errorf("SunsetByTime failed, expected sunset: %s, got: %s",
|
||||
ti.Format(time.RFC3339), ai.SunsetByTime(ti).String())
|
||||
}
|
||||
if ai.SunsetByTime(ti).DateTime().Format(time.RFC3339) != ddt.Format(time.RFC3339) {
|
||||
t.Errorf("SunsetByTime failed, expected sunset: %s, got: %s",
|
||||
ddt.Format(time.RFC3339), ai.SunsetByTime(ti).DateTime().Format(time.RFC3339))
|
||||
}
|
||||
if ai.SunsetByDateString(ti.Format(DateFormat)).Value().UnixMilli() != ti.UnixMilli() {
|
||||
t.Errorf("SunsetByDateString failed, expected sunset: %s, got: %s",
|
||||
ti.String(), ai.SunsetByDateString(ti.Format(DateFormat)).Value().String())
|
||||
}
|
||||
ti = time.Time{}
|
||||
if ai.SunsetByTime(ti).IsAvailable() {
|
||||
t.Errorf("SunsetByTime failed, expected no entry, but got: %s",
|
||||
ai.SunsetByTime(ti).Value().String())
|
||||
}
|
||||
if !ai.SunsetByTime(ti).Value().IsZero() {
|
||||
t.Errorf("SunsetByTime failed, expected no entry, but got: %s",
|
||||
ai.SunsetByTime(ti).Value().String())
|
||||
}
|
||||
if len(ai.SunsetAll()) != 14 {
|
||||
t.Errorf("SunsetByTime failed, expected 14 entired, but got: %d", len(ai.SunsetAll()))
|
||||
return
|
||||
}
|
||||
if ai.SunsetAll()[0].DateTime().Format("2006-01-02") != "2023-05-28" {
|
||||
t.Errorf("SunsetByTime failed, expected first entry to be: %s, got: %s", "2023-05-28",
|
||||
ai.SunsetAll()[0].DateTime().Format("2006-01-02"))
|
||||
}
|
||||
if ai.SunsetAll()[13].DateTime().Format("2006-01-02") != "2023-06-10" {
|
||||
t.Errorf("SunsetByTime failed, expected first entry to be: %s, got: %s", "2023-06-10",
|
||||
ai.SunsetAll()[13].DateTime().Format("2006-01-02"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAstronomicalInfo_SunriseByDateString(t *testing.T) {
|
||||
la := 52.5067296
|
||||
lo := 13.2599306
|
||||
loc, err := time.LoadLocation("Europe/Berlin")
|
||||
if err != nil {
|
||||
t.Errorf("failed to load time location data for Europe/Berlin: %s", err)
|
||||
return
|
||||
}
|
||||
ti := time.Date(2023, 5, 28, 4, 51, 48, 0, loc)
|
||||
ddt := time.Date(2023, 5, 28, 0, 0, 0, 0, time.UTC)
|
||||
c := New(withMockAPI())
|
||||
ai, err := c.AstronomicalInfoByCoordinates(la, lo)
|
||||
if err != nil {
|
||||
t.Errorf("failed to fetch astronomical information: %s", err)
|
||||
return
|
||||
}
|
||||
if !ai.SunriseByTime(ti).IsAvailable() {
|
||||
t.Errorf("SunriseByTime failed, expected entry, but got 'not available'")
|
||||
return
|
||||
}
|
||||
if ai.SunriseByTime(ti).Value().UnixMilli() != ti.UnixMilli() {
|
||||
t.Errorf("SunriseByTime failed, expected sunrise: %s, got: %s",
|
||||
ti.String(), ai.SunriseByTime(ti).Value().String())
|
||||
}
|
||||
if !ai.SunriseByDateString(ti.Format(DateFormat)).IsAvailable() {
|
||||
t.Errorf("SunriseByDateString failed, expected entry, but got 'not available'")
|
||||
return
|
||||
}
|
||||
if ai.SunriseByTime(ti).String() != ti.Format(time.RFC3339) {
|
||||
t.Errorf("SunriseByTime failed, expected sunrise: %s, got: %s",
|
||||
ti.Format(time.RFC3339), ai.SunriseByTime(ti).String())
|
||||
}
|
||||
if ai.SunriseByTime(ti).DateTime().Format(time.RFC3339) != ddt.Format(time.RFC3339) {
|
||||
t.Errorf("SunriseByTime failed, expected sunrise: %s, got: %s",
|
||||
ddt.Format(time.RFC3339), ai.SunriseByTime(ti).DateTime().Format(time.RFC3339))
|
||||
}
|
||||
if ai.SunriseByDateString(ti.Format(DateFormat)).Value().UnixMilli() != ti.UnixMilli() {
|
||||
t.Errorf("SunriseByDateString failed, expected sunrise: %s, got: %s",
|
||||
ti.String(), ai.SunriseByDateString(ti.Format(DateFormat)).Value().String())
|
||||
}
|
||||
ti = time.Time{}
|
||||
if ai.SunriseByTime(ti).IsAvailable() {
|
||||
t.Errorf("SunriseByTime failed, expected no entry, but got: %s",
|
||||
ai.SunriseByTime(ti).Value().String())
|
||||
}
|
||||
if !ai.SunriseByTime(ti).Value().IsZero() {
|
||||
t.Errorf("SunriseByTime failed, expected no entry, but got: %s",
|
||||
ai.SunriseByTime(ti).Value().String())
|
||||
}
|
||||
if len(ai.SunriseAll()) != 14 {
|
||||
t.Errorf("SunriseByTime failed, expected 14 entired, but got: %d", len(ai.SunriseAll()))
|
||||
return
|
||||
}
|
||||
if ai.SunriseAll()[0].DateTime().Format("2006-01-02") != "2023-05-28" {
|
||||
t.Errorf("SunriseByTime failed, expected first entry to be: %s, got: %s", "2023-05-28",
|
||||
ai.SunriseAll()[0].DateTime().Format("2006-01-02"))
|
||||
}
|
||||
if ai.SunriseAll()[13].DateTime().Format("2006-01-02") != "2023-06-10" {
|
||||
t.Errorf("SunriseByTime failed, expected first entry to be: %s, got: %s", "2023-06-10",
|
||||
ai.SunriseAll()[13].DateTime().Format("2006-01-02"))
|
||||
}
|
||||
}
|
134
condition.go
Normal file
134
condition.go
Normal file
|
@ -0,0 +1,134 @@
|
|||
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package meteologix
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// CondCloudy represents cloudy weather conditions
|
||||
CondCloudy ConditionType = "cloudy"
|
||||
// CondFog represents foggy weather conditions
|
||||
CondFog ConditionType = "fog"
|
||||
// CondFreezingRain represents weather conditions with freezing rain
|
||||
CondFreezingRain ConditionType = "freezingrain"
|
||||
// CondOvercast represents overcast weather conditions
|
||||
CondOvercast ConditionType = "overcast"
|
||||
// CondPartlyCloudy represents partly cloudy weather conditions
|
||||
CondPartlyCloudy ConditionType = "partlycloudy"
|
||||
// CondRain represents rainy weather conditions.
|
||||
// Rain defines as following:
|
||||
// - Falls steadily
|
||||
// - Lasts for hours or days
|
||||
// - Typically widespread throughout your city or town
|
||||
CondRain ConditionType = "rain"
|
||||
// CondRainHeavy represents heavy rain weather conditions
|
||||
CondRainHeavy ConditionType = "rainheavy"
|
||||
// CondShowers represents weather conditions with showers.
|
||||
// Showers define as following:
|
||||
// - Lighter rainfall
|
||||
// - Shorter duration
|
||||
// - Can start and stop over a period of time
|
||||
// - Tends to be more scattered across an area
|
||||
CondShowers ConditionType = "showers"
|
||||
// CondShowersHeavy represents weather conditions with heavy showers
|
||||
CondShowersHeavy ConditionType = "showersheavy"
|
||||
// CondSnow represents snowy weather conditions
|
||||
CondSnow ConditionType = "snow"
|
||||
// CondSnowHeavy represents weather conditions with heavy snow
|
||||
CondSnowHeavy ConditionType = "snowheavy"
|
||||
// CondSnowRain represents weather conditions with snowy rain
|
||||
CondSnowRain ConditionType = "snowrain"
|
||||
// CondSunshine represents clear and sunny weather conditions
|
||||
CondSunshine ConditionType = "sunshine"
|
||||
// CondThunderStorm represents weather conditions with thunderstorms
|
||||
CondThunderStorm ConditionType = "thunderstorm"
|
||||
// CondUnknown represents a unknown weather condition
|
||||
CondUnknown ConditionType = "unknown"
|
||||
)
|
||||
|
||||
// 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",
|
||||
}
|
||||
|
||||
// Condition is a type wrapper of an WeatherData for holding
|
||||
// a specific weather Condition value in the WeatherData
|
||||
type Condition WeatherData
|
||||
|
||||
// ConditionType is a type wrapper for a string type
|
||||
type ConditionType string
|
||||
|
||||
// IsAvailable returns true if a Condition value was available
|
||||
// at time of query
|
||||
func (c Condition) IsAvailable() bool {
|
||||
return !c.na
|
||||
}
|
||||
|
||||
// DateTime returns the timestamp of a Condition value as time.Time
|
||||
func (c Condition) DateTime() time.Time {
|
||||
return c.dt
|
||||
}
|
||||
|
||||
// Value returns the raw value of a Condition as unformatted string
|
||||
// as returned by the API
|
||||
// If the Condition is not available in the WeatherData, Value will
|
||||
// return DataUnavailable instead.
|
||||
func (c Condition) Value() string {
|
||||
if c.na {
|
||||
return DataUnavailable
|
||||
}
|
||||
return c.sv
|
||||
}
|
||||
|
||||
// Condition returns the actual value of that Condition as ConditionType.
|
||||
// If the value is not available or not supported it will return a
|
||||
// CondUnknown
|
||||
func (c Condition) Condition() ConditionType {
|
||||
if c.na {
|
||||
return CondUnknown
|
||||
}
|
||||
if _, ok := ConditionMap[ConditionType(c.sv)]; ok {
|
||||
return ConditionType(c.sv)
|
||||
}
|
||||
return CondUnknown
|
||||
}
|
||||
|
||||
// String returns the formatted, human readable string for a given
|
||||
// Condition type and satisfies the fmt.Stringer interface
|
||||
func (c Condition) String() string {
|
||||
return c.Condition().String()
|
||||
}
|
||||
|
||||
// Source returns the Source of a Condition
|
||||
// If the Source is not available it will return SourceUnknown
|
||||
func (c Condition) Source() Source {
|
||||
return c.s
|
||||
}
|
||||
|
||||
// String returns a human readable, formatted string for a ConditionType and
|
||||
// satisfies the fmt.Stringer interface.
|
||||
func (ct ConditionType) String() string {
|
||||
if cs, ok := ConditionMap[ct]; ok {
|
||||
return cs
|
||||
}
|
||||
return ConditionMap[CondUnknown]
|
||||
}
|
41
condition_test.go
Normal file
41
condition_test.go
Normal file
|
@ -0,0 +1,41 @@
|
|||
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package meteologix
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestCondition_Condition(t *testing.T) {
|
||||
tc := Condition{
|
||||
dt: time.Date(2023, 5, 23, 8, 50, 0, 0, time.UTC),
|
||||
s: SourceAnalysis,
|
||||
sv: "cloudy",
|
||||
}
|
||||
if tc.Condition() != CondCloudy {
|
||||
t.Errorf("Condition failed, expected: %s, got: %s", CondCloudy.String(),
|
||||
tc.Condition().String())
|
||||
}
|
||||
tc = Condition{
|
||||
dt: time.Date(2023, 5, 23, 8, 50, 0, 0, time.UTC),
|
||||
s: SourceAnalysis,
|
||||
sv: "non-existing",
|
||||
}
|
||||
if tc.Condition() != CondUnknown {
|
||||
t.Errorf("Condition failed, expected: %s, got: %s", CondUnknown.String(),
|
||||
tc.Condition().String())
|
||||
}
|
||||
tc = Condition{na: true}
|
||||
if tc.Condition() != CondUnknown {
|
||||
t.Errorf("Condition failed, expected: %s, got: %s", CondUnknown.String(),
|
||||
tc.Condition().String())
|
||||
}
|
||||
ct := ConditionType("foo")
|
||||
if ct.String() != CondUnknown.String() {
|
||||
t.Errorf("Condition.String for unknown type failed, expected: %s, got: %s",
|
||||
CondUnknown.String(), ct.String())
|
||||
}
|
||||
}
|
225
curweather.go
225
curweather.go
|
@ -34,53 +34,36 @@ 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"`
|
||||
// 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"`
|
||||
// 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"`
|
||||
/*
|
||||
// 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"`
|
||||
// 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
|
||||
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
|
||||
|
@ -117,25 +100,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.
|
||||
|
@ -159,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{
|
||||
|
@ -174,6 +138,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,15 +207,91 @@ func (cw CurrentWeather) PressureMSL() Pressure {
|
|||
return v
|
||||
}
|
||||
|
||||
// WeatherSymbol returns a text representation of the current weather
|
||||
// as GenericString.
|
||||
// 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
|
||||
// GenericString in which the "not available" field will be true.
|
||||
func (cw CurrentWeather) WeatherSymbol() GenericString {
|
||||
if cw.Data.WeatherSymbol == nil {
|
||||
return GenericString{na: true}
|
||||
// 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 := GenericString{
|
||||
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 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 {
|
||||
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
|
||||
}
|
||||
|
||||
// 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.
|
||||
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
|
||||
// Condition in which the "not available" field will be true.
|
||||
func (cw CurrentWeather) WeatherSymbol() Condition {
|
||||
if cw.Data.WeatherSymbol == nil {
|
||||
return Condition{na: true}
|
||||
}
|
||||
v := Condition{
|
||||
dt: cw.Data.WeatherSymbol.DateTime,
|
||||
n: FieldWeatherSymbol,
|
||||
s: SourceUnknown,
|
||||
|
@ -255,40 +303,59 @@ func (cw CurrentWeather) WeatherSymbol() GenericString {
|
|||
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
|
||||
}
|
||||
|
|
|
@ -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,7 +512,340 @@ 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 pressure
|
||||
p *Pressure
|
||||
}{
|
||||
{"Ehrenfeld, Germany", &Pressure{
|
||||
dt: time.Date(2023, 5, 23, 7, 0, 0, 0, time.Local),
|
||||
s: SourceAnalysis,
|
||||
fv: 1011.7,
|
||||
}},
|
||||
{"Berlin, Germany", nil},
|
||||
{"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.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.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.p != nil && cw.PressureQFE().Source() != tc.p.s {
|
||||
t.Errorf("CurrentWeatherByLocation failed, expected source: %s, but got: %s",
|
||||
tc.p.s, cw.PressureQFE().Source())
|
||||
}
|
||||
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.PressureQFE().Value()) {
|
||||
t.Errorf("CurrentWeatherByLocation failed, expected pressure "+
|
||||
"to return NaN, but got: %s", cw.PressureQFE().String())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_CurrentWeatherByLocation_SnowAmount(t *testing.T) {
|
||||
tt := []struct {
|
||||
// Location name
|
||||
loc string
|
||||
// CurWeather pressure
|
||||
d *Density
|
||||
}{
|
||||
{"Ehrenfeld, Germany", &Density{
|
||||
dt: time.Date(2023, 5, 23, 6, 0, 0, 0, time.UTC),
|
||||
s: SourceAnalysis,
|
||||
fv: 0,
|
||||
}},
|
||||
{"Berlin, Germany", &Density{
|
||||
dt: time.Date(2023, 5, 23, 8, 0, 0, 0, time.UTC),
|
||||
s: SourceAnalysis,
|
||||
fv: 21.1,
|
||||
}},
|
||||
{"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.SnowAmount().String() {
|
||||
t.Errorf("CurrentWeatherByLocation failed, expected snow amount "+
|
||||
"string: %s, got: %s", tc.d.String(), cw.SnowAmount())
|
||||
}
|
||||
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.d != nil && cw.SnowAmount().Source() != tc.d.s {
|
||||
t.Errorf("CurrentWeatherByLocation failed, expected source: %s, but got: %s",
|
||||
tc.d.s, cw.SnowAmount().Source())
|
||||
}
|
||||
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.SnowAmount().Value()) {
|
||||
t.Errorf("CurrentWeatherByLocation failed, expected snow amount "+
|
||||
"to return NaN, but got: %s", cw.SnowAmount().String())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
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_WeatherSymbol(t *testing.T) {
|
||||
tt := []struct {
|
||||
// Location name
|
||||
loc string
|
||||
// CurWeather generic string
|
||||
gs *Condition
|
||||
}{
|
||||
{"Ehrenfeld, Germany", &Condition{
|
||||
dt: time.Date(2023, 5, 23, 7, 30, 0, 0, time.UTC),
|
||||
s: SourceAnalysis,
|
||||
sv: "overcast",
|
||||
}},
|
||||
{"Berlin, Germany", &Condition{
|
||||
dt: time.Date(2023, 5, 23, 8, 50, 0, 0, time.UTC),
|
||||
s: SourceAnalysis,
|
||||
sv: "cloudy",
|
||||
}},
|
||||
{"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.gs != nil && tc.gs.String() != cw.WeatherSymbol().String() {
|
||||
t.Errorf("CurrentWeatherByLocation failed, expected weathersymbol "+
|
||||
"string: %s, got: %s", tc.gs.String(), cw.WeatherSymbol())
|
||||
}
|
||||
if tc.gs != nil && tc.gs.Condition() != cw.WeatherSymbol().Condition() {
|
||||
t.Errorf("CurrentWeatherByLocation failed, expected condition "+
|
||||
"string: %s, got: %s", tc.gs.Condition(), cw.WeatherSymbol().Condition())
|
||||
}
|
||||
if tc.gs != nil && tc.gs.Value() != cw.WeatherSymbol().Value() {
|
||||
t.Errorf("CurrentWeatherByLocation failed, expected weathersymbol "+
|
||||
"string: %s, got: %s", tc.gs.Value(), cw.WeatherSymbol().Value())
|
||||
}
|
||||
if tc.gs != nil && cw.WeatherSymbol().Source() != tc.gs.s {
|
||||
t.Errorf("CurrentWeatherByLocation failed, expected source: %s, but got: %s",
|
||||
tc.gs.s, cw.WeatherSymbol().Source())
|
||||
}
|
||||
if tc.gs != nil && tc.gs.dt.Unix() != cw.WeatherSymbol().DateTime().Unix() {
|
||||
t.Errorf("CurrentWeatherByLocation failed, expected datetime: %s, got: %s",
|
||||
tc.gs.dt.Format(time.RFC3339), cw.WeatherSymbol().DateTime().Format(time.RFC3339))
|
||||
}
|
||||
if tc.gs == nil {
|
||||
if cw.WeatherSymbol().IsAvailable() {
|
||||
t.Errorf("CurrentWeatherByLocation failed, expected weathersymbol "+
|
||||
"to have no data, but got: %s", cw.WeatherSymbol())
|
||||
}
|
||||
if cw.WeatherSymbol().Value() != DataUnavailable {
|
||||
t.Errorf("CurrentWeatherByLocation failed, expected weathersymbol "+
|
||||
"to return DataUnavailable, but got: %s", cw.WeatherSymbol().String())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_CurrentWeatherByLocation_WindDirection(t *testing.T) {
|
||||
tt := []struct {
|
||||
// Location name
|
||||
loc string
|
||||
|
@ -574,41 +880,98 @@ func TestClient_CurrentWeatherByLocation_Winddirection(t *testing.T) {
|
|||
t.Errorf("CurrentWeatherByLocation failed: %s", err)
|
||||
return
|
||||
}
|
||||
if tc.d != nil && tc.d.String() != cw.Winddirection().String() {
|
||||
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())
|
||||
"string: %s, got: %s", tc.d.String(), cw.WindDirection())
|
||||
}
|
||||
if tc.d != nil && tc.d.Value() != cw.Winddirection().Value() {
|
||||
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())
|
||||
"float: %f, got: %f", tc.d.Value(), cw.WindDirection().Value())
|
||||
}
|
||||
if tc.d != nil && cw.Winddirection().Source() != tc.d.s {
|
||||
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())
|
||||
tc.d.s, cw.WindDirection().Source())
|
||||
}
|
||||
if tc.d != nil && cw.Winddirection().Direction() != tc.da {
|
||||
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())
|
||||
tc.da, cw.WindDirection().Direction())
|
||||
}
|
||||
if tc.d != nil && cw.Winddirection().DirectionFull() != tc.df {
|
||||
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())
|
||||
tc.df, cw.WindDirection().DirectionFull())
|
||||
}
|
||||
if tc.d == nil {
|
||||
if cw.Winddirection().IsAvailable() {
|
||||
if cw.WindDirection().IsAvailable() {
|
||||
t.Errorf("CurrentWeatherByLocation failed, expected wind direction "+
|
||||
"to have no data, but got: %s", cw.Winddirection())
|
||||
"to have no data, but got: %s", cw.WindDirection())
|
||||
}
|
||||
if !math.IsNaN(cw.Winddirection().Value()) {
|
||||
if !math.IsNaN(cw.WindDirection().Value()) {
|
||||
t.Errorf("CurrentWeatherByLocation failed, expected wind direction "+
|
||||
"to return NaN, but got: %s", cw.Windspeed().String())
|
||||
"to return NaN, but got: %s", cw.WindSpeed().String())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_CurrentWeatherByLocation_Windspeed(t *testing.T) {
|
||||
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
|
||||
|
@ -639,87 +1002,26 @@ func TestClient_CurrentWeatherByLocation_Windspeed(t *testing.T) {
|
|||
t.Errorf("CurrentWeatherByLocation failed: %s", err)
|
||||
return
|
||||
}
|
||||
if tc.s != nil && tc.s.String() != cw.Windspeed().String() {
|
||||
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())
|
||||
"string: %s, got: %s", tc.s.String(), cw.WindSpeed())
|
||||
}
|
||||
if tc.s != nil && tc.s.Value() != cw.Windspeed().Value() {
|
||||
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())
|
||||
"float: %f, got: %f", tc.s.Value(), cw.WindSpeed().Value())
|
||||
}
|
||||
if tc.s != nil && cw.Windspeed().Source() != tc.s.s {
|
||||
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())
|
||||
tc.s.s, cw.WindSpeed().Source())
|
||||
}
|
||||
if tc.s == nil {
|
||||
if cw.Windspeed().IsAvailable() {
|
||||
if cw.WindSpeed().IsAvailable() {
|
||||
t.Errorf("CurrentWeatherByLocation failed, expected wind speed "+
|
||||
"to have no data, but got: %s", cw.Windspeed())
|
||||
"to have no data, but got: %s", cw.WindSpeed())
|
||||
}
|
||||
if !math.IsNaN(cw.Windspeed().Value()) {
|
||||
if !math.IsNaN(cw.WindSpeed().Value()) {
|
||||
t.Errorf("CurrentWeatherByLocation failed, expected wind speed "+
|
||||
"to return NaN, but got: %s", cw.Windspeed().String())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_CurrentWeatherByLocation_WeatherSymbol(t *testing.T) {
|
||||
tt := []struct {
|
||||
// Location name
|
||||
loc string
|
||||
// CurWeather generic string
|
||||
gs *GenericString
|
||||
}{
|
||||
{"Ehrenfeld, Germany", &GenericString{
|
||||
dt: time.Date(2023, 5, 23, 7, 30, 0, 0, time.UTC),
|
||||
s: SourceAnalysis,
|
||||
sv: "overcast",
|
||||
}},
|
||||
{"Berlin, Germany", &GenericString{
|
||||
dt: time.Date(2023, 5, 23, 8, 50, 0, 0, time.UTC),
|
||||
s: SourceAnalysis,
|
||||
sv: "cloudy",
|
||||
}},
|
||||
{"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.gs != nil && tc.gs.String() != cw.WeatherSymbol().String() {
|
||||
t.Errorf("CurrentWeatherByLocation failed, expected weathersymbol "+
|
||||
"string: %s, got: %s", tc.gs.String(), cw.WeatherSymbol())
|
||||
}
|
||||
if tc.gs != nil && tc.gs.Value() != cw.WeatherSymbol().Value() {
|
||||
t.Errorf("CurrentWeatherByLocation failed, expected weathersymbol "+
|
||||
"string: %s, got: %s", tc.gs.Value(), cw.WeatherSymbol().Value())
|
||||
}
|
||||
if tc.gs != nil && cw.WeatherSymbol().Source() != tc.gs.s {
|
||||
t.Errorf("CurrentWeatherByLocation failed, expected source: %s, but got: %s",
|
||||
tc.gs.s, cw.WeatherSymbol().Source())
|
||||
}
|
||||
if tc.gs != nil && tc.gs.dt.Unix() != cw.WeatherSymbol().DateTime().Unix() {
|
||||
t.Errorf("CurrentWeatherByLocation failed, expected datetime: %s, got: %s",
|
||||
tc.gs.dt.Format(time.RFC3339), cw.WeatherSymbol().DateTime().Format(time.RFC3339))
|
||||
}
|
||||
if tc.gs == nil {
|
||||
if cw.WeatherSymbol().IsAvailable() {
|
||||
t.Errorf("CurrentWeatherByLocation failed, expected weathersymbol "+
|
||||
"to have no data, but got: %s", cw.WeatherSymbol())
|
||||
}
|
||||
if cw.WeatherSymbol().Value() != DataUnavailable {
|
||||
t.Errorf("CurrentWeatherByLocation failed, expected weathersymbol "+
|
||||
"to return DataUnavailable, but got: %s", cw.WeatherSymbol().String())
|
||||
"to return NaN, but got: %s", cw.WindSpeed().String())
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
61
datatype.go
61
datatype.go
|
@ -4,12 +4,20 @@
|
|||
|
||||
package meteologix
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DataUnavailable is a constant string that is returned if a
|
||||
// data point is not available
|
||||
const DataUnavailable = "Data unavailable"
|
||||
|
||||
// DateFormat is the parsing format that is used for datetime strings
|
||||
// that only hold the date but no time
|
||||
const DateFormat = "2006-01-02"
|
||||
|
||||
// Enum for different Fieldname values
|
||||
const (
|
||||
// FieldDewpoint represents the Dewpoint data point
|
||||
|
@ -36,6 +44,14 @@ const (
|
|||
FieldPressureMSL
|
||||
// FieldPressureQFE represents the PressureQFE data point
|
||||
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
|
||||
FieldSunset
|
||||
// FieldTemperature represents the Temperature data point
|
||||
FieldTemperature
|
||||
// FieldTemperatureAtGround represents the TemperatureAtGround data point
|
||||
|
@ -50,10 +66,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
|
||||
|
@ -68,6 +86,20 @@ const (
|
|||
Timespan24Hours
|
||||
)
|
||||
|
||||
// APIDate is type wrapper for datestamp (without time) returned by
|
||||
// the API endpoints
|
||||
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 {
|
||||
|
@ -91,7 +123,9 @@ 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
|
||||
n Fieldname
|
||||
na bool
|
||||
|
@ -102,3 +136,20 @@ type WeatherData struct {
|
|||
// Fieldname is a type wrapper for an int for field names
|
||||
// of an Observation
|
||||
type Fieldname int
|
||||
|
||||
// UnmarshalJSON interprets the API datestamp and converts it into a
|
||||
// time.Time type
|
||||
func (a *APIDate) UnmarshalJSON(s []byte) error {
|
||||
d := string(s)
|
||||
d = strings.ReplaceAll(d, `"`, ``)
|
||||
if d == "null" {
|
||||
return nil
|
||||
}
|
||||
|
||||
pd, err := time.Parse(DateFormat, d)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse JSON string as APIDate string: %w", err)
|
||||
}
|
||||
a.Time = pd
|
||||
return nil
|
||||
}
|
||||
|
|
37
datatype_test.go
Normal file
37
datatype_test.go
Normal file
|
@ -0,0 +1,37 @@
|
|||
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
package meteologix
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAPIDate_UnmarshalJSON(t *testing.T) {
|
||||
type x struct {
|
||||
Date APIDate `json:"date"`
|
||||
}
|
||||
okd := []byte(`{"date":"2023-05-28"}`)
|
||||
nokd := []byte(`{"date":"2023-05-32"}`)
|
||||
null := []byte(`{"date":null}`)
|
||||
var d x
|
||||
if err := json.Unmarshal(okd, &d); err != nil {
|
||||
t.Errorf("APIDate_UnmarshalJSON failed: %s", err)
|
||||
}
|
||||
if d.Date.Format(DateFormat) != "2023-05-28" {
|
||||
t.Errorf("APIDate_UnmarshalJSON failed, expected: %s, but got: %s",
|
||||
"2023-05-28", d.Date.String())
|
||||
}
|
||||
if err := json.Unmarshal(nokd, &d); err == nil {
|
||||
t.Errorf("APIDate_UnmarshalJSON was supposed to fail, but didn't")
|
||||
}
|
||||
d = x{}
|
||||
if err := json.Unmarshal(null, &d); err != nil {
|
||||
t.Errorf("APIDate_UnmarshalJSON failed: %s", err)
|
||||
}
|
||||
if !d.Date.IsZero() {
|
||||
t.Errorf("APIDate_UnmarshalJSON with null was supposed to be empty, but got: %s",
|
||||
d.Date.String())
|
||||
}
|
||||
}
|
40
datetime.go
Normal file
40
datetime.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package meteologix
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// DateTime is a type wrapper of an WeatherData for holding datetime
|
||||
// values in WeatherData
|
||||
type DateTime WeatherData
|
||||
|
||||
// IsAvailable returns true if an Direction value was
|
||||
// available at time of query
|
||||
func (dt DateTime) IsAvailable() bool {
|
||||
return !dt.na
|
||||
}
|
||||
|
||||
// DateTime returns the timestamp for that specific DateTime type
|
||||
func (dt DateTime) DateTime() time.Time {
|
||||
return dt.dt
|
||||
}
|
||||
|
||||
// Value returns the time.Time value of an DateTime value
|
||||
// If the DateTime is not available in the WeatherData
|
||||
// Value will return time.Time with a zero value instead.
|
||||
func (dt DateTime) Value() time.Time {
|
||||
if dt.na {
|
||||
return time.Time{}
|
||||
}
|
||||
return dt.dv
|
||||
}
|
||||
|
||||
// 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.Value().Format(time.RFC3339)
|
||||
}
|
47
density.go
Normal file
47
density.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package meteologix
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 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
|
||||
// available at time of query
|
||||
func (d Density) IsAvailable() bool {
|
||||
return !d.na
|
||||
}
|
||||
|
||||
// DateTime returns the DateTime of the queried Density value
|
||||
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 WeatherData
|
||||
// Vaule will return math.NaN instead.
|
||||
func (d Density) Value() float64 {
|
||||
if d.na {
|
||||
return math.NaN()
|
||||
}
|
||||
return d.fv
|
||||
}
|
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.0.6"
|
||||
const VERSION = "0.1.1"
|
||||
|
|
46
generic.go
46
generic.go
|
@ -1,46 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package meteologix
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// GenericString is a type wrapper of an WeatherData for holding
|
||||
// a generic string value in the WeatherData
|
||||
type GenericString WeatherData
|
||||
|
||||
// IsAvailable returns true if a GenericString value was available
|
||||
// at time of query
|
||||
func (gs GenericString) IsAvailable() bool {
|
||||
return !gs.na
|
||||
}
|
||||
|
||||
// DateTime returns the timestamp of a GenericString value as time.Time
|
||||
func (gs GenericString) DateTime() time.Time {
|
||||
return gs.dt
|
||||
}
|
||||
|
||||
// Value returns the string value of a GenericString as simple
|
||||
// unformatted string
|
||||
// If the GenericSString is not available in the WeatherData
|
||||
// Value will return DataUnavailable instead.
|
||||
func (gs GenericString) Value() string {
|
||||
if gs.na {
|
||||
return DataUnavailable
|
||||
}
|
||||
return gs.sv
|
||||
}
|
||||
|
||||
// String satisfies the fmt.Stringer interface for the GenericString type
|
||||
func (gs GenericString) String() string {
|
||||
return gs.Value()
|
||||
}
|
||||
|
||||
// Source returns the Source of a GenericString
|
||||
// If the Source is not available it will return SourceUnknown
|
||||
func (gs GenericString) Source() Source {
|
||||
return gs.s
|
||||
}
|
|
@ -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
|
||||
|
|
2
go.mod
2
go.mod
|
@ -2,6 +2,6 @@
|
|||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
module github.com/wneessen/go-meteologix
|
||||
module src.neessen.cloud/wneessen/go-meteologix
|
||||
|
||||
go 1.20
|
||||
|
|
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,19 +110,33 @@ 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
|
||||
}
|
||||
if hc.bearerToken != "" {
|
||||
hr.Header.Set("Authorization", "Bearer"+hc.bearerToken)
|
||||
return
|
||||
}
|
||||
if hc.authUser != "" && hc.authPass != "" {
|
||||
hr.SetBasicAuth(url.QueryEscape(hc.authUser), url.QueryEscape(hc.authPass))
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -22,7 +22,7 @@ const (
|
|||
|
||||
// DefaultUserAgent is the default User-Agent presented by the HTTPClient
|
||||
var DefaultUserAgent = fmt.Sprintf("go-meteologix/fv%s (%s; %s; "+
|
||||
"+https://github.com/wneessen/go-meteologix)", VERSION, runtime.GOOS,
|
||||
"+https://src.neessen.cloud/wneessen/go-meteologix)", VERSION, runtime.GOOS,
|
||||
runtime.Version())
|
||||
|
||||
// Client represents the Meteologix API Client
|
||||
|
@ -46,6 +46,8 @@ type Config struct {
|
|||
authPass string
|
||||
// authUser holds the (optional) username for the API user authentication
|
||||
authUser string
|
||||
// bearerToken holds the (optional) bearer token for the API authentication
|
||||
bearerToken string
|
||||
// userAgent represents an alternative User-Agent HTTP header string
|
||||
userAgent string
|
||||
}
|
||||
|
@ -97,6 +99,17 @@ func WithAPIKey(k string) Option {
|
|||
}
|
||||
}
|
||||
|
||||
// WithBearerToken uses a bearer token for the client authentication of the
|
||||
// HTTP client
|
||||
func WithBearerToken(t string) Option {
|
||||
if t == "" {
|
||||
return nil
|
||||
}
|
||||
return func(co *Config) {
|
||||
co.bearerToken = t
|
||||
}
|
||||
}
|
||||
|
||||
// WithPassword sets the HTTP Basic auth authPass for the HTTP client
|
||||
func WithPassword(p string) Option {
|
||||
if p == "" {
|
||||
|
|
|
@ -66,6 +66,28 @@ func TestNew_WithAPIKey(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestNew_WithBearerToken(t *testing.T) {
|
||||
e := "BEARER-TOKEN"
|
||||
c := New(WithBearerToken(e))
|
||||
if c == nil {
|
||||
t.Errorf("NewWithBearerToken failed, expected Client, got nil")
|
||||
return
|
||||
}
|
||||
if c.config.bearerToken != e {
|
||||
t.Errorf("NewWithBearerToken failed, expected token value: %s, got: %s", e,
|
||||
c.config.bearerToken)
|
||||
}
|
||||
c = New(WithBearerToken(""))
|
||||
if c == nil {
|
||||
t.Errorf("NewWithBearerToken failed, expected Client, got nil")
|
||||
return
|
||||
}
|
||||
if c.config.bearerToken != "" {
|
||||
t.Errorf("NewWithBearerToken failed, expected empty token, got: %s",
|
||||
c.config.bearerToken)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew_WithUsername(t *testing.T) {
|
||||
e := "username"
|
||||
c := New(WithUsername(e))
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
33
speed.go
33
speed.go
|
@ -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
|
||||
|
@ -20,14 +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 knots
|
||||
// If the Speed is not available in the Observation
|
||||
// Value returns the float64 value of an Speed in meters
|
||||
// per second.
|
||||
// If the Speed is not available in the WeatherData
|
||||
// Vaule will return math.NaN instead.
|
||||
func (s Speed) Value() float64 {
|
||||
if s.na {
|
||||
|
@ -38,7 +47,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 +58,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 +66,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
|
||||
|
|
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