Compare commits

...

53 commits
v0.0.9 ... main

Author SHA1 Message Date
Winni Neessen d6ce76b1ad
Remove sudo usage for jq installation in sonarqube.yml
All checks were successful
Codecov workflow / run (push) Successful in 1m39s
golangci-lint / lint (push) Successful in 48s
REUSE Compliance Check / test (push) Successful in 4s
SonarQube / Build (push) Successful in 1m40s
The use of sudo keyword for installing 'jq' in the SonarQube workflow file, sonarqube.yml, has been removed. This change is due to the fact that the root permission is not necessary during the installation process, adhering better to the principle of least privilege.
2024-02-14 00:50:25 +01:00
Winni Neessen 072a2aed5d
Remove sudo usage for jq installation in sonarqube.yml
All checks were successful
golangci-lint / lint (push) Successful in 47s
REUSE Compliance Check / test (push) Successful in 4s
SonarQube / Build (push) Successful in 1m37s
The use of sudo keyword for installing 'jq' in the SonarQube workflow file, sonarqube.yml, has been removed. This change is due to the fact that the root permission is not necessary during the installation process, adhering better to the principle of least privilege.
2024-02-14 00:46:07 +01:00
Winni Neessen a536ac3bde
Remove sudo usage for jq installation in sonarqube.yml
Some checks failed
golangci-lint / lint (push) Successful in 47s
REUSE Compliance Check / test (push) Successful in 4s
SonarQube / Build (push) Failing after 1m12s
The use of sudo keyword for installing 'jq' in the SonarQube workflow file, sonarqube.yml, has been removed. This change is due to the fact that the root permission is not necessary during the installation process, adhering better to the principle of least privilege.
2024-02-14 00:39:16 +01:00
Winni Neessen eb675388f0
reports. Simultaneously, the installation of 'jq' has been removed from the sonarqube.yml workflow as it's no longer needed there.
Some checks failed
golangci-lint / lint (push) Successful in 48s
REUSE Compliance Check / test (push) Successful in 4s
SonarQube / Build (push) Failing after 1m12s
2024-02-14 00:36:34 +01:00
Winni Neessen e4cbab4e43
Install jq in the codecov workflow
Some checks failed
golangci-lint / lint (push) Successful in 47s
REUSE Compliance Check / test (push) Successful in 4s
SonarQube / Build (push) Failing after 1m29s
This commit adds a new step to the codecov.yml workflow. This step installs 'jq', a command-line JSON processor, which might be necessary for the processing of test results or coverage
2024-02-14 00:33:51 +01:00
Winni Neessen 8923b88e07
Simplify workflow settings and upgrade setup-go to v4
Some checks failed
REUSE Compliance Check / test (push) Successful in 4s
SonarQube / Build (push) Failing after 1m29s
golangci-lint / lint (push) Failing after 11m20s
This commit simplifies the Forgejo workflow settings by hardcoding the 'docker' and '1.22' options for the 'runs-on' and 'go-version' respectively, instead of using a matrix. The version of setup-go action is also upgraded to v4.
2024-02-13 23:33:37 +01:00
Winni Neessen 94eca2f3fe
Fix Forgejo workflows
Some checks failed
golangci-lint / lint (push) Successful in 47s
REUSE Compliance Check / test (push) Successful in 19s
SonarQube / Build (push) Failing after 1m30s
2024-02-13 22:25:23 +01:00
Winni Neessen 759300d066
Move Github workflows to Forgejo workflows
Some checks failed
REUSE Compliance Check / test (push) Waiting to run
SonarQube / Build (push) Waiting to run
golangci-lint / lint (push) Successful in 47s
Codecov workflow / run (1.20, macos-latest) (push) Has been cancelled
Codecov workflow / run (1.20, ubuntu-latest) (push) Has been cancelled
Codecov workflow / run (1.20, windows-latest) (push) Has been cancelled
2024-02-13 22:21:35 +01:00
Winni Neessen 3c69f36748
Update Go version setup in golangci-lint workflow
All checks were successful
golangci-lint / lint (push) Successful in 48s
The workflow file for golangci-lint has been updated to use the newer version of setup-go action. This change moves from version v3 to v4
2024-02-13 19:51:27 +01:00
Winni Neessen 0b88d11fdf
Update Go version setup in golangci-lint workflow
Some checks failed
golangci-lint / lint (push) Has been cancelled
The workflow file for golangci-lint has been updated to use the newer version of setup-go action. This change moves from version v3 to v4 under
2024-02-13 19:45:53 +01:00
Winni Neessen d6b551cc5e
Correct URL format in Github Actions of golangci-lint workflow
Some checks failed
golangci-lint / lint (push) Has been cancelled
The URLs in the golangci-lint workflow file have been corrected. The extraneous "https://github.com/" part of each URL has been removed in each 'uses' section to ensure the correct working of Github actions. Now, actions/setup-go@v3, actions/checkout@v3, and golangci/g
2024-02-13 19:26:11 +01:00
Winni Neessen 5297342603
Update golangci-lint workflow's 'runs-on' value
Some checks failed
golangci-lint / lint (push) Has been cancelled
The 'runs-on' value of the golangci-lint workflow file has been updated from 'ubuntu-latest' to 'docker'. This change improves the replication of the production environment conditions for linting, ensuring more accurate code quality checks.
2024-02-13 18:58:18 +01:00
Winni Neessen c3af61791e
Update golangci-lint workflow's 'runs-on' value
Some checks failed
golangci-lint / lint (push) Failing after 40s
The 'runs-on' value of the golangci-lint workflow file has been updated from 'ubuntu-latest' to 'docker'. This change improves the replication of the production environment conditions for linting, ensuring more accurate code quality checks.
2024-02-13 18:56:24 +01:00
Winni Neessen b8c117c056
Add golangci-lint workflow
Some checks are pending
golangci-lint / lint (push) Waiting to run
A new Github Actions workflow file named golangci-lint.yml has been added to the workflows directory. This will ensure that the linting tool golangci-lint is run for every push and pull request to this project, thus improving the code quality.
2024-02-13 18:55:16 +01:00
Winni Neessen 3f7cccb511 Merge pull request 'Update URLs to point to new repository domain' (#12) from switch-gh-to-forgejo into main
Reviewed-on: #12
2024-02-11 15:28:32 +01:00
Winni Neessen c9fa7eb1d7
Add mirror repository information to README
A new section called "Mirror" was added to the README.md file indicating that the Github repository is only a mirror of the main repository. The main repository is located at "https://src.neessen.cloud/wneessen/go-meteologix". This change improves project transparency and informs contributors of the primary source of the code.
2024-02-11 15:27:34 +01:00
Winni Neessen 878c4dfe71
Update URLs to point to new repository domain
The URLs in all the files that reference the old domain (github.com) have been updated to reference the new domain (src.neessen.cloud). This includes URLs in badges, links in the README.md, import paths in .go files, and module paths in go.mod and .golangci.toml.
2024-02-11 15:24:46 +01:00
Winni Neessen 27c8542f76
Merge pull request #11 from wneessen/bearer-auth
Add bearer token authentication
2023-08-02 16:13:09 +02:00
Winni Neessen a5667bb828
Add bearer token authentication
In this commit, we've added the capability to authenticate via bearer token to Meteologix's HTTP client. A new method "WithBearerToken" has been implemented, allowing a bearer token to be set in 'meteologix.go'. Additionally, in 'httpclient.go', the token is attached to the "Authorization" header for API requests. Tests asserting token setting functionality have been added in 'meteologix_test.go'. These changes open up an alternative authentication option for our API users. The bearer auth is not public yet, so there is no way for us to test this auth
2023-08-02 16:08:38 +02:00
Winni Neessen 173c6eb8ec
Update Go versions in CI workflow
Removed versions 1.18 and 1.19 from the Go matrix in the codecov.yml file. Now, CI workflow only tests for Go version '1.20'. This change simplifies the CI testing process and removes the change of running into CI failures due to rate limiting.
2023-06-28 10:23:33 +02:00
Winni Neessen a51644fbb6
Remove redundant package comment
The comment referencing bindings to the Meteologix/Kachelmann-Wetter weather API was incorrectly placed in the astroinfo.go file. This comment was irrelevant to the respective package and therefore, has been removed for accuracy and clarity.
2023-06-27 19:37:26 +02:00
Winni Neessen f48392d553
Merge pull request #10 from wneessen/more_curweather_datapoints
More CurrentWeather datapoints and refactoring
2023-06-27 19:31:44 +02:00
Winni Neessen 85c3c1aff3
Update package version in doc.go
The version constant in doc.go was incremented from "0.1.0" to "0.1.1". This reflects recent minor changes or bug fixes made to the meteologix package.
2023-06-27 19:26:02 +02:00
Winni Neessen abd200177f
Update DateTime comment for clarity
The comment explaining the function DateTime in temperature.go was modified. The original comment was inaccurate, suggesting it returned boolean when in fact it returns the exact time the temperature data was obtained. The comment was updated to accurately reflect what the function does.
2023-06-27 19:25:19 +02:00
Winni Neessen e7f8662347
Update precision constants in station.go
Refined precision constants in station.go for better clarity and maintainability. Comment descriptions for each precision level have been expanded for better understanding. Strings have been introduced as constants to represent each precision level, enhancing code readability and preventing inconsistencies. Changes are also done to the UnmarshalJSON() and String() of the Precision type to use these new string constants improving overall code quality.
2023-06-27 19:21:24 +02:00
Winni Neessen 54cc672dfc
Refine comment for DateTime function in speed.go
The comment for the DateTime function in the speed.go file was slightly confusing and inaccurate. It has been updated to more accurately reflect its purpose and functionality. It now clearly states that the function returns the DateTime when the Speed was checked, as originally intended.
2023-06-27 19:12:35 +02:00
Winni Neessen e3756a5466
Update comment for DateTime function in radiation.go
The comment for the DateTime function in radiation.go was updated to more accurately describe its functionality. The previous version of the comment suggested that the function returns a boolean value indicating the availability of a radiation value at the time of a query. However, the function actually returns a time.Time object representing the date and time of the query. The comment was thus updated to reflect this.
2023-06-27 19:01:37 +02:00
Winni Neessen 513d7b863f
Refine DateTime function description in pressure.go
The previous description of the DateTime function in pressure.go was misleading as it implied that the function would return a boolean. The function actually returns the date and time of a Pressure reading. The description has therefore been updated for clarity and accuracy.
2023-06-27 18:59:13 +02:00
Winni Neessen c9d95300c2
Refine Precipitation's DateTime method comment
The Precipitation's DateTime method comment was adjusted to better reflect its functionality. Instead of stating it returns true if the precipitation data was available at the time of the query or not, it was clarified that the method actually returns the DateTime when the Precipitation value was recorded.
2023-06-27 18:57:33 +02:00
Winni Neessen 84ba2feda9
Refine method comments in humidity.go
Comments for DateTime and Value methods in humidity.go file were updated to better describe their functionalities. This clarification should improve understandability for future maintenance/code reading.
2023-06-27 18:49:32 +02:00
Winni Neessen 65d065dd59
Improve error reporting in station_test.go
Modified the error handling sections within the StationSearchByLocation test function to also output the error messages. Changes help in debugging by providing more context whenever the test fails.
2023-06-27 18:31:41 +02:00
Winni Neessen 3eb6a76f5d
Refactor HTTP client for improved error handling and readability
A few changes were made to `httpclient.go`. We swapped the `os` package for `log` to standardize error logging. Instead of having the HTTP transport as a value, it is now a pointer in the HTTP client instantiation function, aligning it with the client itself. We also altered error handling: we now just return an error when our server response is `nil`, and changed `sr.Body.Close()` error reporting to use `log` instead of `fmt`.

To streamline the code, user authentication function `setAuthHeader` was renamed to `setAuthentication`, and the copy of our HTTP response body to buffer now only happens after the status check. We also replaced the `Flush` error to be handled properly.

Successful requests return bytes instead of the buffer itself. As a result, these changes have led to more readable and effective code.
2023-06-27 18:31:13 +02:00
Winni Neessen 9c65eca128
Fix Height conversion and update DateTime comment
- Update the comment for DateTime() to accurately describe its functionality
- Fix the Height conversion (CentiMeter and MilliMeter) calculations by changing the division to multiplication

This commit ensures that the conversion functions work as intended and improves the description for DateTime().
2023-06-27 17:18:15 +02:00
Winni Neessen a4f19380ff
Handle error in GetGeoLocationByName
Return error when fetching GeoLocations fails or the result is empty. This ensures that an error is properly returned and handled when either an error occurs during the API call or no locations are found for the given city name.
2023-06-27 17:15:07 +02:00
Winni Neessen d0905266e1
Fix typo and improve direction calculations
Correct typos related to Angle naming and update calculations for start and end ranges in direction mapping. This ensures accurate and valid directional values are used in the application.
2023-06-27 16:49:22 +02:00
Winni Neessen 28479be939
Add test for findDirection function
Add a new test file, direction_test.go, to test the findDirection function with various input cases. This change helps ensure the reliability and expected functionality of the findDirection function.
2023-06-27 16:48:29 +02:00
Winni Neessen 914327de85
Improve Density type documentation and comments
Update Density type's comments to clarify its purpose and usage, including units and methods, for better code understanding and maintainability.
2023-06-27 16:13:13 +02:00
Winni Neessen a10ec1c0f9
Update DateTime String method to use Value()
Refactor the DateTime String method to use the Value() function instead of accessing the internal field directly, for better encapsulation and readability.
2023-06-27 16:12:59 +02:00
Winni Neessen d7567d4b2b
Remove unused and redundant weather fields
Removed several unused and redundant weather fields like DewPointMean, GlobalRadiation, TemperatureMean, Temperature5cm, etc. from curweather.go to reduce complexity and improve readability.
2023-06-27 16:12:38 +02:00
Winni Neessen c1e054d9a3
Reformat ConditionMap for better readability
Refactor the ConditionMap by aligning the values, which improves code readability and adheres to the code style guidelines.
2023-06-27 16:12:22 +02:00
Winni Neessen 4318599eb0
Replace hardcoded date format with constant
Replace the hardcoded "2006-01-02" date format with the DateFormat constant in both SunsetByDateString and SunriseByDateString functions to ensure consistency and easier future updates.
2023-06-27 16:12:06 +02:00
Winni Neessen a0b67b0367
Add client timeout and improve error handling
- Set HTTPClientTimeout for HTTP client to prevent hanging requests
- Check for http.StatusOK instead of a generic error-range
- Use json.NewDecoder to decode error JSON for better memory usage
2023-06-27 15:20:16 +02:00
Winni Neessen 681c53c23d
Fix humidity check in curweather.go
Check for HumidityRelative instead of Dewpoint in the HumidityRelative() function to return correct "not available" status.
2023-06-27 15:19:41 +02:00
Winni Neessen 8cb9754f69
Added SnowHeight and corrected comments 2023-06-26 12:21:36 +02:00
Winni Neessen 7bdf2de388
Merge pull request #9 from wneessen/more-curweather
v0.1.0: More data points for current weather
2023-06-23 18:37:31 +02:00
Winni Neessen b779e0f65b
Make golangci-lint happy 2023-06-23 18:34:25 +02:00
Winni Neessen a697b13970
Move init multipliers into constants so we can reuse them 2023-06-23 16:42:06 +02:00
Winni Neessen 5dee24573a
Added SnowAmount 2023-06-23 16:38:56 +02:00
Winni Neessen fee0bc6795
Bump version to v0.1.0 2023-06-23 12:36:17 +02:00
Winni Neessen fcc7626a76
Added PressureQFE to current weather 2023-06-23 12:35:52 +02:00
Winni Neessen 4d60f35c6a
Added WindGust to current weather 2023-06-23 12:05:40 +02:00
Winni Neessen a7feae910b
Switch from kn to m/s as default unit for Speed types 2023-06-23 11:52:36 +02:00
Winni Neessen b77cd98484
More current weather data points
- Added `IsDay()`
- Renamed `Windspeed()` to `WindSpeed()`
- Renamed `Winddirection()` to `WindDirection`
2023-06-21 10:29:09 +02:00
33 changed files with 1054 additions and 381 deletions

6
.forgejo/FUNDING.yml Normal file
View file

@ -0,0 +1,6 @@
# SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
#
# SPDX-License-Identifier: CC0-1.0
github: wneessen
ko_fi: winni

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 }}

View file

@ -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"

View file

@ -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-%23gometeologix-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.

View file

@ -2,7 +2,6 @@
//
// SPDX-License-Identifier: MIT
// Package meteologix provides bindings to the Meteologix/Kachelmann-Wetter weather API
package meteologix
import (
@ -140,7 +139,7 @@ func (a *AstronomicalInfo) Sunset() DateTime {
// available in the AstronomicalInfo it will return DateTime in
// which the "not available" field will be true.
func (a *AstronomicalInfo) SunsetByDateString(ds string) DateTime {
t, err := time.Parse("2006-01-02", ds)
t, err := time.Parse(DateFormat, ds)
if err != nil {
return DateTime{na: true}
}
@ -206,7 +205,7 @@ func (a *AstronomicalInfo) Sunrise() DateTime {
// available in the AstronomicalInfo it will return DateTime in
// which the "not available" field will be true.
func (a *AstronomicalInfo) SunriseByDateString(ds string) DateTime {
t, err := time.Parse("2006-01-02", ds)
t, err := time.Parse(DateFormat, ds)
if err != nil {
return DateTime{na: true}
}

View file

@ -53,11 +53,21 @@ const (
// ConditionMap is a map to associate a specific ConditionType to a nicely
// formatted, human readable string
var ConditionMap = map[ConditionType]string{
CondCloudy: "Cloudy", CondFog: "Fog", CondFreezingRain: "Freezing rain",
CondOvercast: "Overcast", CondPartlyCloudy: "Partly cloudy", CondRain: "Rain",
CondRainHeavy: "Heavy rain", CondShowers: "Showers", CondShowersHeavy: "Heavy showers",
CondSnow: "Snow", CondSnowHeavy: "Heavy snow", CondSnowRain: "Sleet",
CondSunshine: "Clear sky", CondThunderStorm: "Thunderstorm", CondUnknown: "Unknown",
CondCloudy: "Cloudy",
CondFog: "Fog",
CondFreezingRain: "Freezing rain",
CondOvercast: "Overcast",
CondPartlyCloudy: "Partly cloudy",
CondRain: "Rain",
CondRainHeavy: "Heavy rain",
CondShowers: "Showers",
CondShowersHeavy: "Heavy showers",
CondSnow: "Snow",
CondSnowHeavy: "Heavy snow",
CondSnowRain: "Sleet",
CondSunshine: "Clear sky",
CondThunderStorm: "Thunderstorm",
CondUnknown: "Unknown",
}
// Condition is a type wrapper of an WeatherData for holding

View file

@ -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,6 +207,82 @@ func (cw CurrentWeather) PressureMSL() Pressure {
return v
}
// PressureQFE returns the pressure at mean sea level data point as Pressure.
// If the data point is not available in the CurrentWeather it will return
// Pressure in which the "not available" field will be true.
func (cw CurrentWeather) PressureQFE() Pressure {
if cw.Data.PressureQFE == nil {
return Pressure{na: true}
}
v := Pressure{
dt: cw.Data.PressureQFE.DateTime,
n: FieldPressureQFE,
s: SourceUnknown,
fv: cw.Data.PressureQFE.Value,
}
if cw.Data.PressureQFE.Source != nil {
v.s = StringToSource(*cw.Data.PressureQFE.Source)
}
return v
}
// SnowAmount returns the 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
@ -255,40 +303,59 @@ func (cw CurrentWeather) WeatherSymbol() Condition {
return v
}
// Winddirection returns the wind direction data point as Direction.
// WindDirection returns the wind direction data point as Direction.
// If the data point is not available in the CurrentWeather it will return
// Direction in which the "not available" field will be true.
func (cw CurrentWeather) Winddirection() Direction {
if cw.Data.Winddirection == nil {
func (cw CurrentWeather) WindDirection() Direction {
if cw.Data.WindDirection == nil {
return Direction{na: true}
}
v := Direction{
dt: cw.Data.Winddirection.DateTime,
n: FieldWinddirection,
dt: cw.Data.WindDirection.DateTime,
n: FieldWindDirection,
s: SourceUnknown,
fv: cw.Data.Winddirection.Value,
fv: cw.Data.WindDirection.Value,
}
if cw.Data.Winddirection.Source != nil {
v.s = StringToSource(*cw.Data.Winddirection.Source)
if cw.Data.WindDirection.Source != nil {
v.s = StringToSource(*cw.Data.WindDirection.Source)
}
return v
}
// Windspeed returns the average wind speed data point as Speed.
// WindGust returns the wind gust data point as Speed.
// If the data point is not available in the CurrentWeather it will return
// Speed in which the "not available" field will be true.
func (cw CurrentWeather) Windspeed() Speed {
if cw.Data.Windspeed == nil {
func (cw CurrentWeather) WindGust() Speed {
if cw.Data.WindGust == nil {
return Speed{na: true}
}
v := Speed{
dt: cw.Data.Windspeed.DateTime,
n: FieldWindspeed,
dt: cw.Data.WindGust.DateTime,
n: FieldWindGust,
s: SourceUnknown,
fv: cw.Data.Windspeed.Value,
fv: cw.Data.WindGust.Value,
}
if cw.Data.Windspeed.Source != nil {
v.s = StringToSource(*cw.Data.Windspeed.Source)
if cw.Data.WindGust.Source != nil {
v.s = StringToSource(*cw.Data.WindGust.Source)
}
return v
}
// WindSpeed returns the average wind speed data point as Speed.
// If the data point is not available in the CurrentWeather it will return
// Speed in which the "not available" field will be true.
func (cw CurrentWeather) WindSpeed() Speed {
if cw.Data.WindSpeed == nil {
return Speed{na: true}
}
v := Speed{
dt: cw.Data.WindSpeed.DateTime,
n: FieldWindSpeed,
s: SourceUnknown,
fv: cw.Data.WindSpeed.Value,
}
if cw.Data.WindSpeed.Source != nil {
v.s = StringToSource(*cw.Data.WindSpeed.Source)
}
return v
}

View file

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

View file

@ -44,6 +44,10 @@ 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
@ -62,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
@ -86,6 +92,14 @@ type APIDate struct {
time.Time
}
// APIBool is the JSON structure of the weather data that is
// returned by the API endpoints in which the value is a boolean
type APIBool struct {
DateTime time.Time `json:"dateTime"`
Source *string `json:"source,omitempty"`
Value bool `json:"value"`
}
// APIFloat is the JSON structure of the weather data that is
// returned by the API endpoints in which the value is a float
type APIFloat struct {
@ -109,6 +123,7 @@ type Timespan int
// Weather) data and can be wrapped into other types to provide type
// specific receiver methods
type WeatherData struct {
// bv bool
dt time.Time
dv time.Time
fv float64

View file

@ -36,5 +36,5 @@ func (dt DateTime) Value() time.Time {
// String satisfies the fmt.Stringer interface for the DateTime type
// The date will returned as time.RFC3339 format
func (dt DateTime) String() string {
return dt.dv.Format(time.RFC3339)
return dt.Value().Format(time.RFC3339)
}

47
density.go Normal file
View 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
}

View file

@ -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
View 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
View file

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

View file

@ -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
View file

@ -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
View 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())
}

View file

@ -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))
}

View file

@ -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()

View file

@ -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 == "" {

View file

@ -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))

View file

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

View file

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

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -10,6 +10,15 @@ import (
"time"
)
const (
// MultiplierKnots is the multiplier for converting the base unit to knots
MultiplierKnots = 1.9438444924
// MultiplierKPH is the multiplier for converting the base unit to kilometers per hour
MultiplierKPH = 3.6
// MultiplierMPH is the multiplier for converting the base unit to miles per hour
MultiplierMPH = 2.236936
)
// Speed is a type wrapper of an WeatherData for holding speed
// values in WeatherData
type Speed WeatherData
@ -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

View file

@ -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
}
}

View file

@ -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"},
}

View file

@ -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 {