Compare commits

...

96 commits
0.0.4 ... 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
Winni Neessen 3394ceeb4a
Merge pull request #8 from wneessen/conditions
Added conditions
2023-06-07 10:15:23 +02:00
Winni Neessen 0e3c8c6f1d
Fixed REUSE 2023-06-07 10:12:38 +02:00
Winni Neessen 4a7c807174
Bumped version to v0.0.9 2023-06-07 10:11:32 +02:00
Winni Neessen 78e6751bbf
Completed condition 2023-06-07 10:10:37 +02:00
Winni Neessen 5722d0e4a8
More work on conditions 2023-06-06 23:28:02 +02:00
Winni Neessen 4ed0c12ca5
Working on conditions 2023-06-06 15:56:34 +02:00
Winni Neessen 4123aa75e3
Merge pull request #7 from wneessen/more-astro-info
Add sunrise data to astronomical info
2023-05-30 17:08:56 +02:00
Winni Neessen 728a967bef
Bump version to v0.0.8 2023-05-30 16:03:20 +02:00
Winni Neessen e2a301c2a5
Added tests for sunrise 2023-05-30 15:57:16 +02:00
Winni Neessen fe800b7632
Added sunrise data 2023-05-30 14:29:31 +02:00
Winni Neessen b89d04e581
Merge pull request #6 from wneessen/astro-info
Astronomical info
2023-05-28 23:46:09 +02:00
Winni Neessen 28d203a188
Fixed DateTime fmt.Stringer method 2023-05-28 23:39:11 +02:00
Winni Neessen 0608c4ecaa
Added tests for astronomical data. Let's merge this for now as 0.0.7 2023-05-28 23:31:53 +02:00
Winni Neessen 7ffd3943e0
More astronomical data 2023-05-27 18:41:05 +02:00
Winni Neessen 56b17ffcf3
Implemented astronomical info endpoint and DateTime type 2023-05-27 17:33:41 +02:00
Winni Neessen 54a21a1b5b
Fix REUSE and bump version to 0.0.6 2023-05-24 22:03:15 +02:00
Winni Neessen d8c5856c33
Merge pull request #5 from wneessen/weather-symbol
Added weather symbol to current weather
2023-05-24 22:02:12 +02:00
Winni Neessen 03d274846e
Added weather symbol to current weather
- Also refactored some types
- Introduced GenericString type
2023-05-24 22:00:47 +02:00
Winni Neessen e86d138659
Merge pull request #4 from wneessen/more-cur-weather
More cur weather
2023-05-24 12:32:59 +02:00
Winni Neessen a38ea586fa
Added wind speed and -direction to current weather
- Also added direction estimation on non-supported direction angels
2023-05-24 12:25:24 +02:00
Winni Neessen 5c89db1d55
Added precipitation to current weather 2023-05-24 11:08:36 +02:00
Winni Neessen 6c79ef7ec3
Added relative humidity to current weather 2023-05-24 10:42:27 +02:00
Winni Neessen e1bf01261c
Merge pull request #3 from wneessen/current-weather
First progress on current weather
2023-05-23 20:47:53 +02:00
Winni Neessen 8a5517ddd9
More testing and Dewpoint added to curweather.go 2023-05-23 20:35:00 +02:00
Winni Neessen 2614e0a0ac
Test for curweather.go 2023-05-23 20:05:14 +02:00
Winni Neessen 2630ea675e
Fixes 2023-05-23 20:00:42 +02:00
Winni Neessen f87bca2bd5
Merge branch 'main' into current-weather 2023-05-23 18:50:56 +02:00
Winni Neessen 3dd9f19bc3
Update Discord link 2023-05-23 18:50:40 +02:00
Winni Neessen 76db40fb5b
Added REUSE header to source.go 2023-05-22 17:15:07 +02:00
Winni Neessen 80a4d74032
Major restructuring in favour of current weather reading
- Renamed types from ObservationXXX to generic type names
- Put generic types into their own files
- Added CurrentWeatherByCoordinates
- Added Source type and made it available for Temperature types
2023-05-22 17:13:36 +02:00
Winni Neessen e44e6f6024
Merge pull request #2 from wneessen/more-wind-fields
Wind directions
2023-05-21 17:33:10 +02:00
Winni Neessen e424c49f97
Added ObservationDirection type and implemented Winddirection observation 2023-05-21 17:28:53 +02:00
Winni Neessen d61117cd23
Added ObservationSpeed type and implemented Windspeed observation 2023-05-21 15:32:40 +02:00
Winni Neessen dec578e751
Added CODE_OF_CONDUCT.md, CONTRIBUTING.md and updated mail address in CopyrightText 2023-05-21 11:46:23 +02:00
Winni Neessen aa86892864
Merge pull request #1 from wneessen/wneessen-patch-1
Create SECURITY.md
2023-05-21 11:42:55 +02:00
Winni Neessen 03253c21b1
Create SECURITY.md 2023-05-21 11:42:35 +02:00
Winni Neessen 490721bded
Added logo by Maria Letta integrated it with README.md 2023-05-21 11:29:05 +02:00
Winni Neessen ba2dbf840b
More test coverage for station.go 2023-05-21 11:13:02 +02:00
Winni Neessen ec15731973
Added example code for ObservationLatestByLocation 2023-05-21 11:07:09 +02:00
Winni Neessen acb1a9ea12
Added test for ObservationLatestByLocation and more test coverage for the existing methods 2023-05-21 11:06:49 +02:00
Winni Neessen 3c96002a62
Bumped version to 0.0.5 2023-05-21 11:06:02 +02:00
Winni Neessen 1dba4b13d4
Added ObservationLatestByLocation that combines geolocation lookup, station lookup and observation lookup. 2023-05-21 11:05:43 +02:00
Winni Neessen 9dea4b7931
Configured sonarqube 2023-05-20 19:40:00 +02:00
49 changed files with 4168 additions and 533 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

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2023 Winni Neessen <winni@neessen.dev>
# SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
#
# SPDX-License-Identifier: CC0-1.0
@ -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

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
# SPDX-FileCopyrightText: 2022 Winni Neessen <wn@neessen.dev>
#
# SPDX-License-Identifier: CC0-1.0
@ -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

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2023 Winni Neessen <winni@neessen.dev>
# SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
#
# SPDX-License-Identifier: CC0-1.0
@ -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

@ -0,0 +1,45 @@
# SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
#
# SPDX-License-Identifier: CC0-1.0
name: SonarQube
on:
push:
branches:
- main # or the name of your main branch
pull_request:
branches:
- main # or the name of your main branch
env:
API_KEY: ${{ secrets.API_KEY }}
jobs:
build:
name: Build
runs-on: docker
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v3
with:
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 }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
- uses: sonarsource/sonarqube-quality-gate-action@master
timeout-minutes: 5
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

2
.github/FUNDING.yml vendored
View file

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2023 Winni Neessen <winni@neessen.dev>
# SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
#
# SPDX-License-Identifier: CC0-1.0

2
.gitignore vendored
View file

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2023 Winni Neessen <winni@neessen.dev>
# SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
#
# SPDX-License-Identifier: CC0-1.0

View file

@ -1,4 +1,4 @@
## SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
## SPDX-FileCopyrightText: 2022 Winni Neessen <wn@neessen.dev>
##
## SPDX-License-Identifier: MIT
@ -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"

134
CODE_OF_CONDUCT.md Normal file
View file

@ -0,0 +1,134 @@
<!--
SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
SPDX-License-Identifier: CC0-1.0
-->
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
github+coc@neessen.net.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

35
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,35 @@
<!--
SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
SPDX-License-Identifier: CC0-1.0
-->
# How to contribute
**Working on your first Pull Request?** You can learn how from this *free*
series [How to Contribute to an Open Source Project on GitHub](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github)
## Guidelines for Pull Requests
How to get your contributions merged smoothly and quickly.
* Create **small PRs** that are narrowly focused on **addressing a single concern**. We often times receive PRs that are
trying to fix several things at a time, but only one fix is considered acceptable, nothing gets merged and both
author's & review's time is wasted. Create more PRs to address different concerns and everyone will be happy.
* For speculative changes, consider opening an issue and discussing it first.
* Provide a good **PR description** as a record of **what** change is being made and **why** it was made. Link to a
github issue if it exists.
* Unless your PR is trivial, you should expect there will be reviewer comments that you'll need to address before
merging. We expect you to be reasonably responsive to those comments, otherwise the PR will be closed after 2-3 weeks
of inactivity.
* Maintain **clean commit history** and use **meaningful commit messages**. PRs with messy commit history are difficult
to review and won't be merged. Use `rebase -i upstream/main` to curate your commit history and/or to bring in latest
changes from main (but avoid rebasing in the middle of a code review).
* Keep your PR up to date with upstream/main (if there are merge conflicts, we can't really merge your change).
* Exceptions to the rules can be made if there's a compelling reason for doing so.

View file

@ -1,19 +1,20 @@
<!--
SPDX-FileCopyrightText: 2023 Winni Neessen <winni@neessen.dev>
SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
SPDX-License-Identifier: CC0-1.0
-->
# go-meteologix
Go bindings to the Meteologix/Kachelmann-Wetter weather API
# 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-meteologix on Discord](https://img.shields.io/badge/Discord-%23gometeologix-blue.svg)](https://discord.gg/CTpX8j4Z)
[![REUSE status](https://api.reuse.software/badge/github.com/wneessen/go-meteologix)](https://api.reuse.software/info/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/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>
## *This package is still WIP*
This Go package provides simple bindings to the
@ -31,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
@ -47,7 +48,7 @@ import (
"fmt"
"os"
"github.com/wneessen/go-meteologix"
"src.neessen.cloud/wneessen/go-meteologix"
)
func main() {
@ -75,7 +76,7 @@ import (
"fmt"
"os"
"github.com/wneessen/go-meteologix"
"src.neessen.cloud/wneessen/go-meteologix"
)
func main() {
@ -105,7 +106,7 @@ import (
"math"
"os"
"github.com/wneessen/go-meteologix"
"src.neessen.cloud/wneessen/go-meteologix"
)
func main() {
@ -124,3 +125,47 @@ func main() {
}
}
```
### Get latest station observation by location
This program takes a location string, searches for the weather station with the shortest distancen and looks up
the station's latest observation data. We then print out the temperature in C and F, as well as the station name
and the time of the measurement (if the data point is available from that station).
```go
package main
import (
"fmt"
"os"
"src.neessen.cloud/wneessen/go-meteologix"
)
func main() {
c := meteologix.New(meteologix.WithAPIKey(os.Getenv("API_KEY")))
o, s, err := c.ObservationLatestByLocation("Ehrenfeld, Germany")
if err != nil {
fmt.Printf("Failed: %s\n", err)
os.Exit(1)
}
if o.Temperature().IsAvailable() {
fmt.Printf("Temperature at %s: %s/%s (time of measurement: %s)\n",
s.Name, o.Temperature(), o.Temperature().FahrenheitString(),
o.Temperature().DateTime().Local().Format("15:04h"))
}
}
```
## Authors/Contributors
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.

38
SECURITY.md Normal file
View file

@ -0,0 +1,38 @@
<!--
SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
SPDX-License-Identifier: CC0-1.0
-->
# Security Policy
## Reporting a Vulnerability
To report (possible) security issues in go-meteologix, please either send a mail to
[wn@neessen.dev](mailto:wn@neessen.dev) or use Github's
[private reporting feature](https://github.com/wneessen/go-meteologix/security/advisories/new).
Reports are always welcome. Even if you are not 100% certain that a specific issue you found
counts as a security issue, we'd love to hear the details, so we can figure out together if
the issue in question needds to be addressed.
Typically, you will receive an answer within a day or even within a few hours.
## Encryption
You can send OpenPGP/GPG encrpyted mails to the [wn@neessen.dev](mailto:wn@neessen.dev) address.
OpenPGP/GPG public key:
```
-----BEGIN PGP PUBLIC KEY BLOCK-----
xjMEY8XedRYJKwYBBAHaRw8BAQdAVPb7jn5V7TPWh7lODBPm9SOgS568Plsk
prDUK/kZWiTNH3duQG5lZXNzZW4uZGV2IDx3bkBuZWVzc2VuLmRldj7CjAQQ
FgoAPgUCY8XedQQLCQcICRC0L3U6o8fYrQMVCAoEFgACAQIZAQIbAwIeARYh
BK6dDe0sVXaVAlOuqrQvdTqjx9itAACfPAEAs1SvBmpVk540On+UEdHCbzP0
aD7bngxm2pUe4+ynzCMBAMt1bZSRaRzItYxiJvXzYH48Z9J6n06eWQbr7wwe
YBEDzjgEY8XedRIKKwYBBAGXVQEFAQEHQGTblfiuHDaOL72GnBpKTl4dJqxs
g0ZfOmD2Sfrmdd89AwEIB8J4BBgWCAAqBQJjxd51CRC0L3U6o8fYrQIbDBYh
BK6dDe0sVXaVAlOuqrQvdTqjx9itAADFrAD8D54IStjrrHlH1cpKCkW60mMB
Rsn++p/UorLoKfhQa3IA/3p3lWhGZ1RYfj35oFGh2bBu1NYDFr5RPYu2dDsO
D10A
=EyfK
-----END PGP PUBLIC KEY BLOCK-----
```

1
assets/gopher43.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 104 KiB

View file

@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2022 Maria Letta (https://github.com/MariaLetta/free-gophers-pack)
SPDX-License-Identifier: CC0-1.0

228
astroinfo.go Normal file
View 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
View 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"))
}
}

View file

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2023 Winni Neessen <winni@neessen.dev>
# SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
#
# SPDX-License-Identifier: CC0-1.0

134
condition.go Normal file
View 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
View 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())
}
}

361
curweather.go Normal file
View file

@ -0,0 +1,361 @@
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
//
// SPDX-License-Identifier: MIT
package meteologix
import (
"encoding/json"
"fmt"
"net/url"
"strconv"
)
// CurrentWeather represents the current weather API response
type CurrentWeather struct {
// Data holds the different APICurrentWeatherData points
Data APICurrentWeatherData `json:"data"`
// 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"`
// UnitSystem is the unit system that is used for the results (we default to metric)
UnitSystem string `json:"systemOfUnits"`
}
// APICurrentWeatherData holds the different data points of the CurrentWeather as
// returned by the current weather API endpoints.
//
// Please keep in mind that different Station types return different values, therefore
// all values are represented as pointer type returning nil if the data point in question
// is not returned for the requested Station.
type APICurrentWeatherData struct {
// Dewpoint represents the dewpoint in °C
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,omitempty"`
// Precipitation10m represents the amount of precipitation over the last 10 minutes
Precipitation10m *APIFloat `json:"prec10m,omitempty"`
// Precipitation1h represents the amount of precipitation over the last hour
Precipitation1h *APIFloat `json:"prec1h,omitempty"`
// Precipitation24h represents the amount of precipitation over the last 24 hours
Precipitation24h *APIFloat `json:"prec24h,omitempty"`
// PressureMSL represents the pressure at mean sea level (MSL) in hPa
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"`
// WindDirection represents the direction from which the wind
// originates in degree (0=N, 90=E, 180=S, 270=W)
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"`
}
// CurrentWeatherByCoordinates returns the CurrentWeather values for the given coordinates
func (c *Client) CurrentWeatherByCoordinates(la, lo float64) (CurrentWeather, error) {
var cw CurrentWeather
lat := strconv.FormatFloat(la, 'f', -1, 64)
lon := strconv.FormatFloat(lo, 'f', -1, 64)
u, err := url.Parse(fmt.Sprintf("%s/current/%s/%s", c.config.apiURL, lat, lon))
if err != nil {
return cw, fmt.Errorf("failed to parse current weather URL: %w", err)
}
uq := u.Query()
uq.Add("units", "metric")
u.RawQuery = uq.Encode()
r, err := c.httpClient.Get(u.String())
if err != nil {
return cw, fmt.Errorf("API request failed: %w", err)
}
if err := json.Unmarshal(r, &cw); err != nil {
return cw, fmt.Errorf("failed to unmarshal API response JSON: %w", err)
}
return cw, nil
}
// CurrentWeatherByLocation returns the CurrentWeather values for the given location
func (c *Client) CurrentWeatherByLocation(lo string) (CurrentWeather, error) {
gl, err := c.GetGeoLocationByName(lo)
if err != nil {
return CurrentWeather{}, fmt.Errorf("failed too look up geolocation: %w", err)
}
return c.CurrentWeatherByCoordinates(gl.Latitude, gl.Longitude)
}
// 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.
func (cw CurrentWeather) Dewpoint() Temperature {
if cw.Data.Dewpoint == nil {
return Temperature{na: true}
}
v := Temperature{
dt: cw.Data.Dewpoint.DateTime,
n: FieldDewpoint,
s: SourceUnknown,
fv: cw.Data.Dewpoint.Value,
}
if cw.Data.Dewpoint.Source != nil {
v.s = StringToSource(*cw.Data.Dewpoint.Source)
}
return v
}
// HumidityRelative returns the relative humidity data point as Humidity.
// 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.HumidityRelative == nil {
return Humidity{na: true}
}
v := Humidity{
dt: cw.Data.HumidityRelative.DateTime,
n: FieldHumidityRelative,
s: SourceUnknown,
fv: cw.Data.HumidityRelative.Value,
}
if cw.Data.HumidityRelative.Source != nil {
v.s = StringToSource(*cw.Data.HumidityRelative.Source)
}
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.
//
// At this point of development, it looks like currently only the 1 Hour value
// is returned by the endpoint, so expect non-availability for any other Timespan
// at this point.
func (cw CurrentWeather) Precipitation(ts Timespan) Precipitation {
var df *APIFloat
var fn Fieldname
switch ts {
case TimespanCurrent:
df = cw.Data.Precipitation
fn = FieldPrecipitation
case Timespan10Min:
df = cw.Data.Precipitation10m
fn = FieldPrecipitation10m
case Timespan1Hour:
df = cw.Data.Precipitation1h
fn = FieldPrecipitation1h
case Timespan24Hours:
df = cw.Data.Precipitation24h
fn = FieldPrecipitation24h
default:
return Precipitation{na: true}
}
if df == nil {
return Precipitation{na: true}
}
v := Precipitation{
dt: df.DateTime,
n: fn,
s: SourceUnknown,
fv: df.Value,
}
if df.Source != nil {
v.s = StringToSource(*df.Source)
}
return v
}
// PressureMSL 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) PressureMSL() Pressure {
if cw.Data.PressureMSL == nil {
return Pressure{na: true}
}
v := Pressure{
dt: cw.Data.PressureMSL.DateTime,
n: FieldPressureMSL,
s: SourceUnknown,
fv: cw.Data.PressureMSL.Value,
}
if cw.Data.PressureMSL.Source != nil {
v.s = StringToSource(*cw.Data.PressureMSL.Source)
}
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
// 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,
sv: cw.Data.WeatherSymbol.Value,
}
if cw.Data.WeatherSymbol.Source != nil {
v.s = StringToSource(*cw.Data.WeatherSymbol.Source)
}
return v
}
// 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 {
return Direction{na: true}
}
v := Direction{
dt: cw.Data.WindDirection.DateTime,
n: FieldWindDirection,
s: SourceUnknown,
fv: cw.Data.WindDirection.Value,
}
if cw.Data.WindDirection.Source != nil {
v.s = StringToSource(*cw.Data.WindDirection.Source)
}
return v
}
// 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) WindGust() Speed {
if cw.Data.WindGust == nil {
return Speed{na: true}
}
v := Speed{
dt: cw.Data.WindGust.DateTime,
n: FieldWindGust,
s: SourceUnknown,
fv: cw.Data.WindGust.Value,
}
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
}

1029
curweather_test.go Normal file

File diff suppressed because it is too large Load diff

155
datatype.go Normal file
View file

@ -0,0 +1,155 @@
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
//
// SPDX-License-Identifier: MIT
package meteologix
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
FieldDewpoint Fieldname = iota
// FieldDewpointMean represents the TemperatureMean data point
FieldDewpointMean
// FieldGlobalRadiation10m represents the GlobalRadiation10m data point
FieldGlobalRadiation10m
// FieldGlobalRadiation1h represents the GlobalRadiation1h data point
FieldGlobalRadiation1h
// FieldGlobalRadiation24h represents the GlobalRadiation24h data point
FieldGlobalRadiation24h
// FieldHumidityRelative represents the HumidityRelative data point
FieldHumidityRelative
// FieldPrecipitation represents the Precipitation data point
FieldPrecipitation
// FieldPrecipitation10m represents the Precipitation10m data point
FieldPrecipitation10m
// FieldPrecipitation1h represents the Precipitation1h data point
FieldPrecipitation1h
// FieldPrecipitation24h represents the Precipitation24h data point
FieldPrecipitation24h
// FieldPressureMSL represents the PressureMSL data point
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
FieldTemperatureAtGround
// FieldTemperatureAtGroundMin represents the TemperatureAtGroundMin data point
FieldTemperatureAtGroundMin
// FieldTemperatureMax represents the TemperatureMax data point
FieldTemperatureMax
// FieldTemperatureMean represents the TemperatureMean data point
FieldTemperatureMean
// FieldTemperatureMin represents the TemperatureMin data point
FieldTemperatureMin
// FieldWeatherSymbol represents the weather symbol data point
FieldWeatherSymbol
// 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
const (
// TimespanCurrent represents the moment of the last observation
TimespanCurrent Timespan = iota
// Timespan10Min represents the last 10 minutes
Timespan10Min
// Timespan1Hour represents the last hour
Timespan1Hour
// Timespan24Hours represents the last 24 hours
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 {
DateTime time.Time `json:"dateTime"`
Source *string `json:"source,omitempty"`
Value float64 `json:"value"`
}
// APIString is the JSON structure of the weather data that is
// returned by the API endpoints in which the value is a string
type APIString struct {
DateTime time.Time `json:"dateTime"`
Source *string `json:"source,omitempty"`
Value string `json:"value"`
}
// Timespan is a type wrapper for an int type
type Timespan int
// WeatherData is a type that holds weather (Observation, Current
// 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
s Source
sv string
}
// 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
View 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
View 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
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
}

134
direction.go Normal file
View file

@ -0,0 +1,134 @@
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
//
// SPDX-License-Identifier: MIT
package meteologix
import (
"fmt"
"math"
"sort"
"time"
)
const (
// 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
// the abbreviated direction string
var WindDirAbbrMap = map[float64]string{
0: "N", 11.25: "NbE", 22.5: "NNE", 33.75: "NEbN", 45: "NE", 56.25: "NEbE",
67.5: "ENE", 78.75: "EbN", 90: "E", 101.25: "EbS", 112.5: "ESE", 123.75: "SEbE",
135: "SE", 146.25: "SEbS", 157.5: "SSE", 168.75: "SbE", 180: "S",
191.25: "SbW", 202.5: "SSW", 213.75: "SWbS", 225: "SW", 236.25: "SWbW",
247.5: "WSW", 258.75: "WbS", 270: "W", 281.25: "WbN", 292.5: "WNW",
303.75: "NWbW", 315: "NW", 326.25: "NWbN", 337.5: "NNW", 348.75: "NbW",
}
// WindDirFullMap is a map to associate a wind direction degree value with
// the full direction string
var WindDirFullMap = map[float64]string{
0: "North", 11.25: "North by East", 22.5: "North-Northeast",
33.75: "Northeast by North", 45: "Northeast", 56.25: "Northeast by East",
67.5: "East-Northeast", 78.75: "East by North", 90: "East",
101.25: "East by South", 112.5: "East-Southeast", 123.75: "Southeast by East",
135: "Southeast", 146.25: "Southeast by South", 157.5: "South-Southeast",
168.75: "South by East", 180: "South", 191.25: "South by West",
202.5: "South-Southwest", 213.75: "Southwest by South", 225: "Southwest",
236.25: "Southwest by West", 247.5: "West-Southwest", 258.75: "West by South",
270: "West", 281.25: "West by North", 292.5: "West-Northwest",
303.75: "Northwest by West", 315: "Northwest", 326.25: "Northwest by North",
337.5: "North-Northwest", 348.75: "North by West",
}
// Direction is a type wrapper of an WeatherData for holding directional
// values in WeatherData
type Direction WeatherData
// IsAvailable returns true if an Direction value was
// available at time of query
func (d Direction) IsAvailable() bool {
return !d.na
}
// DateTime returns true if an Direction value was
// available at time of query
func (d Direction) DateTime() time.Time {
return d.dt
}
// Value returns the float64 value of an Direction in degrees
// If the Direction is not available in the WeatherData
// Vaule will return math.NaN instead.
func (d Direction) Value() float64 {
if d.na {
return math.NaN()
}
return d.fv
}
// String satisfies the fmt.Stringer interface for the Direction type
func (d Direction) String() string {
return fmt.Sprintf("%.0f°", d.fv)
}
// Source returns the Source of a Direction
// If the Source is not available it will return SourceUnknown
func (d Direction) Source() Source {
return d.s
}
// Direction returns the abbreviation string for a given Direction type
func (d Direction) Direction() string {
if d.fv < DirectionMinAngle || d.fv > DirectionMaxAngle {
return ErrUnsupportedDirection
}
if ds, ok := WindDirAbbrMap[d.fv]; ok {
return ds
}
return findDirection(d.fv, WindDirAbbrMap)
}
// DirectionFull returns the full string for a given Direction type
func (d Direction) DirectionFull() string {
if d.fv < DirectionMinAngle || d.fv > DirectionMaxAngle {
return ErrUnsupportedDirection
}
if ds, ok := WindDirFullMap[d.fv]; ok {
return ds
}
return findDirection(d.fv, WindDirFullMap)
}
// findDirection takes a Direction and tries to estimate the nearest
// direction string from a map
func findDirection(v float64, m map[float64]string) string {
ks := make([]float64, 0, len(m))
for k := range m {
ks = append(ks, k)
}
sort.Float64s(ks)
sv := 0.0
ev := 0.0
for i := range ks {
if v > ks[i] {
sv = ks[i]
continue
}
if v < ks[i] {
ev = ks[i]
break
}
}
sr := v - sv
er := ev - v
if er > sr {
return m[sv]
}
return m[ev]
}

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

4
doc.go
View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2023 Winni Neessen <winni@neessen.dev>
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
//
// SPDX-License-Identifier: MIT
@ -6,4 +6,4 @@
package meteologix
// VERSION represents the current version of the package
const VERSION = "0.0.4"
const VERSION = "0.1.1"

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2023 Winni Neessen <winni@neessen.dev>
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
//
// SPDX-License-Identifier: MIT
@ -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

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2023 Winni Neessen <winni@neessen.dev>
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
//
// SPDX-License-Identifier: MIT

4
go.mod
View file

@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2023 Winni Neessen <winni@neessen.dev>
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
//
// 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

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2023 Winni Neessen <winni@neessen.dev>
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
//
// SPDX-License-Identifier: MIT
@ -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

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2023 Winni Neessen <winni@neessen.dev>
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
//
// SPDX-License-Identifier: MIT

48
humidity.go Normal file
View file

@ -0,0 +1,48 @@
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
//
// SPDX-License-Identifier: MIT
package meteologix
import (
"fmt"
"math"
"time"
)
// Humidity is a type wrapper of an WeatherData for holding humidity
// values in WeatherData
type Humidity WeatherData
// IsAvailable returns true if an Humidity value was
// available at time of query
func (h Humidity) IsAvailable() bool {
return !h.na
}
// DateTime returns the timestamp of when the humidity
// measurement was taken.
func (h Humidity) DateTime() time.Time {
return h.dt
}
// String satisfies the fmt.Stringer interface for the Humidity type
func (h Humidity) String() string {
return fmt.Sprintf("%.1f%%", h.fv)
}
// Source returns the Source of Humidity
// If the Source is not available it will return SourceUnknown
func (h Humidity) Source() Source {
return h.s
}
// Value returns the float64 value of an Humidity
// 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()
}
return h.fv
}

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2023 Winni Neessen <winni@neessen.dev>
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
//
// SPDX-License-Identifier: MIT
@ -21,8 +21,8 @@ const (
)
// DefaultUserAgent is the default User-Agent presented by the HTTPClient
var DefaultUserAgent = fmt.Sprintf("go-meteologix/v%s (%s; %s; "+
"+https://github.com/wneessen/go-meteologix)", VERSION, runtime.GOOS,
var DefaultUserAgent = fmt.Sprintf("go-meteologix/fv%s (%s; %s; "+
"+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

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2023 Winni Neessen <winni@neessen.dev>
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
//
// SPDX-License-Identifier: MIT
@ -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

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2023 Winni Neessen <winni@neessen.dev>
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
//
// SPDX-License-Identifier: MIT
@ -7,66 +7,18 @@ package meteologix
import (
"encoding/json"
"fmt"
"math"
"time"
)
const (
// FieldDewpoint represents the Dewpoint data point
FieldDewpoint ObservationFieldName = iota
// FieldTemperature represents the Temperature data point
FieldTemperature
// FieldTemperatureAtGround represents the TemperatureAtGround data point
FieldTemperatureAtGround
// FieldTemperatureMax represents the TemperatureMax data point
FieldTemperatureMax
// FieldTemperatureMin represents the TemperatureMin data point
FieldTemperatureMin
// FieldTemperatureAtGroundMin represents the TemperatureAtGroundMin data point
FieldTemperatureAtGroundMin
// FieldHumidityRelative represents the HumidityRelative data point
FieldHumidityRelative
// FieldPressureMSL represents the PressureMSL data point
FieldPressureMSL
// FieldPressureQFE represents the PressureQFE data point
FieldPressureQFE
// FieldPrecipitation represents the Precipitation data point
FieldPrecipitation
// FieldPrecipitation10m represents the Precipitation10m data point
FieldPrecipitation10m
// FieldPrecipitation1h represents the Precipitation1h data point
FieldPrecipitation1h
// FieldPrecipitation24h represents the Precipitation24h data point
FieldPrecipitation24h
// FieldTemperatureMean represents the TemperatureMean data point
FieldTemperatureMean
// FieldDewpointMean represents the TemperatureMean data point
FieldDewpointMean
// FieldGlobalRadiation10m represents the GlobalRadiation10m data point
FieldGlobalRadiation10m
// FieldGlobalRadiation1h represents the GlobalRadiation1h data point
FieldGlobalRadiation1h
// FieldGlobalRadiation24h represents the GlobalRadiation24h data point
FieldGlobalRadiation24h
)
const (
// TimespanCurrent represents the moment of the last observation
TimespanCurrent Timespan = iota
// Timespan10Min represents the last 10 minutes
Timespan10Min
// Timespan1Hour represents the last hour
Timespan1Hour
// Timespan24Hours represents the last 24 hours
Timespan24Hours
)
// ErrUnsupportedDirection is returned when a direction degree is given,
// that is not resolvable
var ErrUnsupportedDirection = "Unsupported direction"
// Observation represents the observation API response for a Station
type Observation struct {
// Altitude is the altitude of the station providing the Observation
Altitude *int `json:"ele,omitempty"`
// Data holds the different ObservationData points
Data ObservationData `json:"data"`
// Data holds the different APIObservationData points
Data APIObservationData `json:"data"`
// Name is the name of the Station providing the Observation
Name string `json:"name"`
// Latitude represents the GeoLocation latitude coordinates for the Station
@ -77,97 +29,60 @@ type Observation struct {
StationID string `json:"stationId"`
}
// ObservationData holds the different data points of the Observation.
// APIObservationData holds the different data points of the Observation as
// returned by the station observation API endpoints.
//
// Please keep in mind that different Station types return different values, therefore
// all values are represented as pointer type returning nil if the data point in question
// is not returned for the requested Station.
type ObservationData struct {
type APIObservationData struct {
// Dewpoint represents the dewpoint in °C
Dewpoint *ObservationValue `json:"dewpoint,omitempty"`
Dewpoint *APIFloat `json:"dewpoint,omitempty"`
// DewPointMean represents the mean dewpoint in °C
DewpointMean *ObservationValue `json:"dewpointMean,omitempty"`
DewpointMean *APIFloat `json:"dewpointMean,omitempty"`
// GlobalRadiation10m represents the sum of global radiation over the last
// 10 minutes in kJ/m²
GlobalRadiation10m *ObservationValue `json:"globalRadiation10m,omitempty"`
GlobalRadiation10m *APIFloat `json:"globalRadiation10m,omitempty"`
// GlobalRadiation1h represents the sum of global radiation over the last
// 1 hour in kJ/m²
GlobalRadiation1h *ObservationValue `json:"globalRadiation1h,omitempty"`
GlobalRadiation1h *APIFloat `json:"globalRadiation1h,omitempty"`
// GlobalRadiation24h represents the sum of global radiation over the last
// 24 hour in kJ/m²
GlobalRadiation24h *ObservationValue `json:"globalRadiation24h,omitempty"`
GlobalRadiation24h *APIFloat `json:"globalRadiation24h,omitempty"`
// HumidityRelative represents the relative humidity in percent
HumidityRelative *ObservationValue `json:"humidityRelative,omitempty"`
HumidityRelative *APIFloat `json:"humidityRelative,omitempty"`
// Precipitation represents the current amount of precipitation
Precipitation *ObservationValue `json:"prec"`
Precipitation *APIFloat `json:"prec,omitempty"`
// Precipitation10m represents the amount of precipitation over the last 10 minutes
Precipitation10m *ObservationValue `json:"prec10m"`
Precipitation10m *APIFloat `json:"prec10m,omitempty"`
// Precipitation1h represents the amount of precipitation over the last hour
Precipitation1h *ObservationValue `json:"prec1h"`
Precipitation1h *APIFloat `json:"prec1h,omitempty"`
// Precipitation24h represents the amount of precipitation over the last 24 hours
Precipitation24h *ObservationValue `json:"prec24h"`
// PressureMSL represents the pressure at mean sea level (MSL) in hPa
PressureMSL *ObservationValue `json:"pressureMsl"`
// PressureMSL represents the pressure at station level (QFE) in hPa
PressureQFE *ObservationValue `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 *ObservationValue `json:"temp,omitempty"`
Temperature *APIFloat `json:"temp,omitempty"`
// TemperatureMax represents the maximum temperature in °C
TemperatureMax *ObservationValue `json:"tempMax,omitempty"`
TemperatureMax *APIFloat `json:"tempMax,omitempty"`
// TemperatureMean represents the mean temperature in °C
TemperatureMean *ObservationValue `json:"tempMean,omitempty"`
TemperatureMean *APIFloat `json:"tempMean,omitempty"`
// TemperatureMin represents the minimum temperature in °C
TemperatureMin *ObservationValue `json:"tempMin,omitempty"`
TemperatureMin *APIFloat `json:"tempMin,omitempty"`
// Temperature5cm represents the temperature 5cm above ground in °C
Temperature5cm *ObservationValue `json:"temp5cm,omitempty"`
Temperature5cm *APIFloat `json:"temp5cm,omitempty"`
// Temperature5cm represents the minimum temperature 5cm above
// ground in °C
Temperature5cmMin *ObservationValue `json:"temp5cmMin,omitempty"`
Temperature5cmMin *APIFloat `json:"temp5cmMin,omitempty"`
// 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 (soon switched to m/s)
WindSpeed *APIFloat `json:"windSpeed,omitempty"`
}
// ObservationValue is the JSON structure of the Observation data that is
// returned by the API endpoints
type ObservationValue struct {
DateTime time.Time `json:"dateTime"`
Value float64 `json:"value"`
}
// ObservationField is a type that holds Observation data and can be wrapped
// into other types to provide type specific receiver methods
type ObservationField struct {
dt time.Time
n ObservationFieldName
na bool
v float64
}
// ObservationFieldName is a type wrapper for an int for field names
// of an Observation
type ObservationFieldName int
// ObservationHumidity is a type wrapper of an ObservationField for
// holding humidity values
type ObservationHumidity ObservationField
// ObservationPrecipitation is a type wrapper for a precipitation value
// in an Observation
type ObservationPrecipitation ObservationField
// ObservationPressure is a type wrapper for a pressure value
// in an Observation
type ObservationPressure ObservationField
// ObservationRadiation is a type wrapper of an ObservationField for
// holding radiation values
type ObservationRadiation ObservationField
// ObservationTemperature is a type wrapper of an ObservationField for
// holding temperature values
type ObservationTemperature ObservationField
// Timespan is a type wrapper for an int type
type Timespan int
// ObservationLatestByStationID returns the latest Observation values from the
// given Station
func (c *Client) ObservationLatestByStationID(si string) (Observation, error) {
@ -185,185 +100,210 @@ func (c *Client) ObservationLatestByStationID(si string) (Observation, error) {
return o, nil
}
// Dewpoint returns the dewpoint data point as ObservationTemperature
// If the data point is not available in the Observation it will return
// ObservationTemperature in which the "not available" field will be
// true.
func (o Observation) Dewpoint() ObservationTemperature {
if o.Data.Dewpoint == nil {
return ObservationTemperature{na: true}
// ObservationLatestByLocation performs a GeoLocation lookup of the location string, checks for any
// nearby weather stations (25 km radius) and returns the latest Observation values from the
// Stations with the shortest distance. It will also return the Station that was used for the query.
// It will throw an error if no station could be found in that queried location.
func (c *Client) ObservationLatestByLocation(l string) (Observation, Station, error) {
sl, err := c.StationSearchByLocationWithinRadius(l, 25)
if err != nil {
return Observation{}, Station{}, fmt.Errorf("failed search locations at given location: %w", err)
}
return ObservationTemperature{
s := sl[0]
o, err := c.ObservationLatestByStationID(s.ID)
return o, s, err
}
// Dewpoint returns the dewpoint data point as Temperature
// If the data point is not available in the Observation it will return
// Temperature in which the "not available" field will be
// true.
func (o Observation) Dewpoint() Temperature {
if o.Data.Dewpoint == nil {
return Temperature{na: true}
}
return Temperature{
dt: o.Data.Dewpoint.DateTime,
n: FieldDewpoint,
v: o.Data.Dewpoint.Value,
s: SourceObservation,
fv: o.Data.Dewpoint.Value,
}
}
// DewpointMean returns the mean dewpoint data point as ObservationTemperature.
// DewpointMean returns the mean dewpoint data point as Temperature.
// If the data point is not available in the Observation it will return
// ObservationTemperature in which the "not available" field will be
// Temperature in which the "not available" field will be
// true.
func (o Observation) DewpointMean() ObservationTemperature {
func (o Observation) DewpointMean() Temperature {
if o.Data.DewpointMean == nil {
return ObservationTemperature{na: true}
return Temperature{na: true}
}
return ObservationTemperature{
return Temperature{
dt: o.Data.DewpointMean.DateTime,
n: FieldDewpointMean,
v: o.Data.DewpointMean.Value,
s: SourceObservation,
fv: o.Data.DewpointMean.Value,
}
}
// Temperature returns the temperature data point as ObservationTemperature.
// Temperature returns the temperature data point as Temperature.
// If the data point is not available in the Observation it will return
// ObservationTemperature in which the "not available" field will be
// Temperature in which the "not available" field will be
// true.
func (o Observation) Temperature() ObservationTemperature {
func (o Observation) Temperature() Temperature {
if o.Data.Temperature == nil {
return ObservationTemperature{na: true}
return Temperature{na: true}
}
return ObservationTemperature{
return Temperature{
dt: o.Data.Temperature.DateTime,
n: FieldTemperature,
v: o.Data.Temperature.Value,
s: SourceObservation,
fv: o.Data.Temperature.Value,
}
}
// TemperatureAtGround returns the temperature at ground level (5cm)
// data point as ObservationTemperature.
// data point as Temperature.
// If the data point is not available in the Observation it will return
// ObservationTemperature in which the "not available" field will be
// Temperature in which the "not available" field will be
// true.
func (o Observation) TemperatureAtGround() ObservationTemperature {
func (o Observation) TemperatureAtGround() Temperature {
if o.Data.Temperature5cm == nil {
return ObservationTemperature{na: true}
return Temperature{na: true}
}
return ObservationTemperature{
return Temperature{
dt: o.Data.Temperature5cm.DateTime,
n: FieldTemperatureAtGround,
v: o.Data.Temperature5cm.Value,
s: SourceObservation,
fv: o.Data.Temperature5cm.Value,
}
}
// TemperatureMax returns the maximum temperature so far data point as
// ObservationTemperature.
// Temperature.
// If the data point is not available in the Observation it will return
// ObservationTemperature in which the "not available" field will be
// Temperature in which the "not available" field will be
// true.
func (o Observation) TemperatureMax() ObservationTemperature {
func (o Observation) TemperatureMax() Temperature {
if o.Data.TemperatureMax == nil {
return ObservationTemperature{na: true}
return Temperature{na: true}
}
return ObservationTemperature{
return Temperature{
dt: o.Data.TemperatureMax.DateTime,
n: FieldTemperatureMax,
v: o.Data.TemperatureMax.Value,
s: SourceObservation,
fv: o.Data.TemperatureMax.Value,
}
}
// TemperatureMin returns the minimum temperature so far data point as
// ObservationTemperature.
// Temperature.
// If the data point is not available in the Observation it will return
// ObservationTemperature in which the "not available" field will be
// Temperature in which the "not available" field will be
// true.
func (o Observation) TemperatureMin() ObservationTemperature {
func (o Observation) TemperatureMin() Temperature {
if o.Data.TemperatureMin == nil {
return ObservationTemperature{na: true}
return Temperature{na: true}
}
return ObservationTemperature{
return Temperature{
dt: o.Data.TemperatureMin.DateTime,
n: FieldTemperatureMin,
v: o.Data.TemperatureMin.Value,
s: SourceObservation,
fv: o.Data.TemperatureMin.Value,
}
}
// TemperatureAtGroundMin returns the minimum temperature so far
// at ground level (5cm) data point as ObservationTemperature
// at ground level (5cm) data point as Temperature
// If the data point is not available in the Observation it will return
// ObservationTemperature in which the "not available" field will be
// Temperature in which the "not available" field will be
// true.
func (o Observation) TemperatureAtGroundMin() ObservationTemperature {
func (o Observation) TemperatureAtGroundMin() Temperature {
if o.Data.Temperature5cmMin == nil {
return ObservationTemperature{na: true}
return Temperature{na: true}
}
return ObservationTemperature{
return Temperature{
dt: o.Data.Temperature5cmMin.DateTime,
n: FieldTemperatureAtGroundMin,
v: o.Data.Temperature5cmMin.Value,
s: SourceObservation,
fv: o.Data.Temperature5cmMin.Value,
}
}
// TemperatureMean returns the mean temperature data point as ObservationTemperature.
// TemperatureMean returns the mean temperature data point as Temperature.
// If the data point is not available in the Observation it will return
// ObservationTemperature in which the "not available" field will be
// Temperature in which the "not available" field will be
// true.
func (o Observation) TemperatureMean() ObservationTemperature {
func (o Observation) TemperatureMean() Temperature {
if o.Data.TemperatureMean == nil {
return ObservationTemperature{na: true}
return Temperature{na: true}
}
return ObservationTemperature{
return Temperature{
dt: o.Data.TemperatureMean.DateTime,
n: FieldTemperatureMean,
v: o.Data.TemperatureMean.Value,
s: SourceObservation,
fv: o.Data.TemperatureMean.Value,
}
}
// HumidityRelative returns the relative humidity data point as float64.
// If the data point is not available in the Observation it will return
// ObservationHumidity in which the "not available" field will be
// Humidity in which the "not available" field will be
// true.
func (o Observation) HumidityRelative() ObservationHumidity {
func (o Observation) HumidityRelative() Humidity {
if o.Data.HumidityRelative == nil {
return ObservationHumidity{na: true}
return Humidity{na: true}
}
return ObservationHumidity{
return Humidity{
dt: o.Data.HumidityRelative.DateTime,
n: FieldHumidityRelative,
v: o.Data.HumidityRelative.Value,
s: SourceObservation,
fv: o.Data.HumidityRelative.Value,
}
}
// PressureMSL returns the relative pressure at mean seal level data point
// as ObservationPressure.
// as Pressure.
// If the data point is not available in the Observation it will return
// ObservationPressure in which the "not available" field will be
// Pressure in which the "not available" field will be
// true.
func (o Observation) PressureMSL() ObservationPressure {
func (o Observation) PressureMSL() Pressure {
if o.Data.PressureMSL == nil {
return ObservationPressure{na: true}
return Pressure{na: true}
}
return ObservationPressure{
return Pressure{
dt: o.Data.PressureMSL.DateTime,
n: FieldPressureMSL,
v: o.Data.PressureMSL.Value,
s: SourceObservation,
fv: o.Data.PressureMSL.Value,
}
}
// PressureQFE returns the relative pressure at mean seal level data point
// as ObservationPressure.
// as Pressure.
// If the data point is not available in the Observation it will return
// ObservationPressure in which the "not available" field will be
// Pressure in which the "not available" field will be
// true.
func (o Observation) PressureQFE() ObservationPressure {
func (o Observation) PressureQFE() Pressure {
if o.Data.PressureQFE == nil {
return ObservationPressure{na: true}
return Pressure{na: true}
}
return ObservationPressure{
return Pressure{
dt: o.Data.PressureQFE.DateTime,
n: FieldPressureQFE,
v: o.Data.PressureQFE.Value,
s: SourceObservation,
fv: o.Data.PressureQFE.Value,
}
}
// Precipitation returns the current amount of precipitation (mm) as
// ObservationPrecipitation
// Precipitation
// If the data point is not available in the Observation it will return
// ObservationPrecipitation in which the "not available" field will be
// Precipitation in which the "not available" field will be
// true.
func (o Observation) Precipitation(ts Timespan) ObservationPrecipitation {
var df *ObservationValue
var fn ObservationFieldName
func (o Observation) Precipitation(ts Timespan) Precipitation {
var df *APIFloat
var fn Fieldname
switch ts {
case TimespanCurrent:
df = o.Data.Precipitation
@ -378,27 +318,28 @@ func (o Observation) Precipitation(ts Timespan) ObservationPrecipitation {
df = o.Data.Precipitation24h
fn = FieldPrecipitation24h
default:
return ObservationPrecipitation{na: true}
return Precipitation{na: true}
}
if df == nil {
return ObservationPrecipitation{na: true}
return Precipitation{na: true}
}
return ObservationPrecipitation{
return Precipitation{
dt: df.DateTime,
n: fn,
v: df.Value,
s: SourceObservation,
fv: df.Value,
}
}
// GlobalRadiation returns the current amount of global radiation as
// ObservationRadiation
// Radiation
// If the data point is not available in the Observation it will return
// ObservationRadiation in which the "not available" field will be
// Radiation in which the "not available" field will be
// true.
func (o Observation) GlobalRadiation(ts Timespan) ObservationRadiation {
var df *ObservationValue
var fn ObservationFieldName
func (o Observation) GlobalRadiation(ts Timespan) Radiation {
var df *APIFloat
var fn Fieldname
switch ts {
case Timespan10Min:
df = o.Data.GlobalRadiation10m
@ -410,174 +351,47 @@ func (o Observation) GlobalRadiation(ts Timespan) ObservationRadiation {
df = o.Data.GlobalRadiation24h
fn = FieldGlobalRadiation24h
default:
return ObservationRadiation{na: true}
return Radiation{na: true}
}
if df == nil {
return ObservationRadiation{na: true}
return Radiation{na: true}
}
return ObservationRadiation{
return Radiation{
dt: df.DateTime,
n: fn,
v: df.Value,
s: SourceObservation,
fv: df.Value,
}
}
// IsAvailable returns true if an ObservationTemperature value was
// available at time of query
func (t ObservationTemperature) IsAvailable() bool {
return !t.na
}
// DateTime returns true if an ObservationTemperature value was
// available at time of query
func (t ObservationTemperature) DateTime() time.Time {
return t.dt
}
// Value returns the float64 value of an ObservationTemperature
// If the ObservationTemperature is not available in the Observation
// Vaule will return math.NaN instead.
func (t ObservationTemperature) Value() float64 {
if t.na {
return math.NaN()
// 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 {
return Direction{na: true}
}
return t.v
}
// String satisfies the fmt.Stringer interface for the ObservationTemperature type
func (t ObservationTemperature) String() string {
return fmt.Sprintf("%.1f°C", t.v)
}
// Celsius returns the ObservationTemperature value in Celsius
func (t ObservationTemperature) Celsius() float64 {
return t.v
}
// CelsiusString returns the ObservationTemperature value as Celsius
// formated string.
//
// This is an alias for the fmt.Stringer interface
func (t ObservationTemperature) CelsiusString() string {
return t.String()
}
// Fahrenheit returns the ObservationTemperature value in Fahrenheit
func (t ObservationTemperature) Fahrenheit() float64 {
return t.v*9/5 + 32
}
// FahrenheitString returns the ObservationTemperature value as Fahrenheit
// formated string.
func (t ObservationTemperature) FahrenheitString() string {
return fmt.Sprintf("%.1f°F", t.Fahrenheit())
}
// IsAvailable returns true if an ObservationHumidity value was
// available at time of query
func (t ObservationHumidity) IsAvailable() bool {
return !t.na
}
// DateTime returns true if an ObservationHumidity value was
// available at time of query
func (t ObservationHumidity) DateTime() time.Time {
return t.dt
}
// String satisfies the fmt.Stringer interface for the ObservationHumidity type
func (t ObservationHumidity) String() string {
return fmt.Sprintf("%.1f%%", t.v)
}
// Value returns the float64 value of an ObservationHumidity
// If the ObservationHumidity is not available in the Observation
// Vaule will return math.NaN instead.
func (t ObservationHumidity) Value() float64 {
if t.na {
return math.NaN()
return Direction{
dt: o.Data.WindDirection.DateTime,
n: FieldWindDirection,
s: SourceObservation,
fv: o.Data.WindDirection.Value,
}
return t.v
}
// IsAvailable returns true if an ObservationPrecipitation value was
// available at time of query
func (t ObservationPrecipitation) IsAvailable() bool {
return !t.na
}
// DateTime returns true if an ObservationPrecipitation value was
// available at time of query
func (t ObservationPrecipitation) DateTime() time.Time {
return t.dt
}
// String satisfies the fmt.Stringer interface for the ObservationPrecipitation type
func (t ObservationPrecipitation) String() string {
return fmt.Sprintf("%.1fmm", t.v)
}
// Value returns the float64 value of an ObservationPrecipitation
// If the ObservationPrecipitation is not available in the Observation
// Vaule will return math.NaN instead.
func (t ObservationPrecipitation) Value() float64 {
if t.na {
return math.NaN()
// 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 {
return Speed{na: true}
}
return t.v
}
// IsAvailable returns true if an ObservationPressure value was
// available at time of query
func (t ObservationPressure) IsAvailable() bool {
return !t.na
}
// DateTime returns true if an ObservationPressure value was
// available at time of query
func (t ObservationPressure) DateTime() time.Time {
return t.dt
}
// String satisfies the fmt.Stringer interface for the ObservationPressure type
func (t ObservationPressure) String() string {
return fmt.Sprintf("%.1fhPa", t.v)
}
// Value returns the float64 value of an ObservationPressure
// If the ObservationPressure is not available in the Observation
// Vaule will return math.NaN instead.
func (t ObservationPressure) Value() float64 {
if t.na {
return math.NaN()
return Speed{
dt: o.Data.WindSpeed.DateTime,
n: FieldWindSpeed,
s: SourceObservation,
fv: o.Data.WindSpeed.Value * 0.5144444444,
}
return t.v
}
// IsAvailable returns true if an ObservationRadiation value was
// available at time of query
func (t ObservationRadiation) IsAvailable() bool {
return !t.na
}
// DateTime returns true if an ObservationRadiation value was
// available at time of query
func (t ObservationRadiation) DateTime() time.Time {
return t.dt
}
// Value returns the float64 value of an ObservationRadiation
// If the ObservationRadiation is not available in the Observation
// Vaule will return math.NaN instead.
func (t ObservationRadiation) Value() float64 {
if t.na {
return math.NaN()
}
return t.v
}
// String satisfies the fmt.Stringer interface for the ObservationRadiation type
func (t ObservationRadiation) String() string {
return fmt.Sprintf("%.0fkJ/m²", t.v)
}

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2023 Winni Neessen <winni@neessen.dev>
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
//
// SPDX-License-Identifier: MIT
@ -60,6 +60,74 @@ func TestClient_ObservationLatestByStationID_Mock(t *testing.T) {
}
}
func TestClient_ObservationLatestByStationID_MockFail(t *testing.T) {
c := New(withMockAPI())
if c == nil {
t.Errorf("failed to create new Client, got nil")
return
}
_, err := c.ObservationLatestByStationID(" ")
if err == nil {
t.Errorf("ObservationLatestByStationID with non-sense station ID was supposed to fail, but didn't")
}
}
func TestClient_ObservationLatestByLocation(t *testing.T) {
ak := getAPIKeyFromEnv(t)
if ak == "" {
t.Skip("no API_KEY found in environment, skipping test")
}
c := New(WithAPIKey(ak))
if c == nil {
t.Errorf("failed to create new Client, got nil")
return
}
o, s, err := c.ObservationLatestByLocation("Ehrenfeld, Germany")
if err != nil {
t.Errorf("ObservationLatestByLocation failed: %s", err)
return
}
if o.Name != "Koeln-Botanischer Garten" {
t.Errorf("ObservationLatestByLocation failed, expected name: %s, got: %s",
"Koeln-Botanischer Garten", o.Name)
}
if o.StationID != s.ID {
t.Errorf("ObservationLatestByLocation failed, expected ID: %s, got: %s",
"Köln-Botanischer Garten", o.StationID)
}
if o.Altitude != nil && *o.Altitude != s.Altitude {
t.Errorf("ObservationLatestByLocation failed, expected altitude: %d, got: %d",
s.Altitude, *o.Altitude)
}
if o.Altitude == nil {
t.Errorf("ObservationLatestByLocation failed, expected altitude, got nil")
}
if o.Latitude != 50.966700 {
t.Errorf("ObservationLatestByLocation failed, expected latitude: %f, got: %f",
50.966700, o.Latitude)
}
if o.Longitude != 6.966700 {
t.Errorf("ObservationLatestByLocation failed, expected longitude: %f, got: %f",
6.966700, o.Longitude)
}
}
func TestClient_ObservationLatestByLocation_Fail(t *testing.T) {
ak := getAPIKeyFromEnv(t)
if ak == "" {
t.Skip("no API_KEY found in environment, skipping test")
}
c := New(WithAPIKey(ak))
if c == nil {
t.Errorf("failed to create new Client, got nil")
return
}
_, _, err := c.ObservationLatestByLocation("Timbugtu")
if err == nil {
t.Errorf("ObservationLatestByLocation with non-sense location was supposed to fail, but didn't")
}
}
func TestClient_ObservationLatestByStationID_Dewpoint(t *testing.T) {
tt := []struct {
// Test name
@ -67,19 +135,19 @@ func TestClient_ObservationLatestByStationID_Dewpoint(t *testing.T) {
// Station ID
sid string
// Observation dewpoint
dp *ObservationTemperature
dp *Temperature
}{
{"K-Botanischer Garten", "199942", &ObservationTemperature{
{"K-Botanischer Garten", "199942", &Temperature{
dt: time.Date(2023, 0o5, 15, 20, 10, 0, 0, time.UTC),
v: 10.1,
fv: 10.1,
}},
{"K-Stammheim", "H744", &ObservationTemperature{
{"K-Stammheim", "H744", &Temperature{
dt: time.Date(2023, 0o5, 15, 19, 30, 0, 0, time.UTC),
v: 9.7,
fv: 9.7,
}},
{"All data fields", "all", &ObservationTemperature{
{"All data fields", "all", &Temperature{
dt: time.Date(2023, 0o5, 17, 7, 40, 0, 0, time.UTC),
v: 6.5,
fv: 6.5,
}},
{"No data fields", "none", nil},
}
@ -129,11 +197,11 @@ func TestClient_ObservationLatestByStationID_DewpointMean(t *testing.T) {
// Station ID
sid string
// Observation dewpoint
t *ObservationTemperature
t *Temperature
}{
{"K-Botanischer Garten", "199942", nil},
{"K-Stammheim", "H744", nil},
{"All data fields", "all", &ObservationTemperature{v: 8.3}},
{"All data fields", "all", &Temperature{fv: 8.3}},
{"No data fields", "none", nil},
}
c := New(withMockAPI())
@ -177,19 +245,19 @@ func TestClient_ObservationLatestByStationID_HumidityRealtive(t *testing.T) {
// Station ID
sid string
// Observation dewpoint
h *ObservationHumidity
h *Humidity
}{
{"K-Botanischer Garten", "199942", &ObservationHumidity{
{"K-Botanischer Garten", "199942", &Humidity{
dt: time.Date(2023, 0o5, 15, 20, 10, 0, 0, time.UTC),
v: 80,
fv: 80,
}},
{"K-Stammheim", "H744", &ObservationHumidity{
{"K-Stammheim", "H744", &Humidity{
dt: time.Date(2023, 0o5, 15, 19, 30, 0, 0, time.UTC),
v: 73,
fv: 73,
}},
{"All data fields", "all", &ObservationHumidity{
{"All data fields", "all", &Humidity{
dt: time.Date(2023, 0o5, 17, 7, 40, 0, 0, time.UTC),
v: 72,
fv: 72,
}},
{"No data fields", "none", nil},
}
@ -217,6 +285,10 @@ func TestClient_ObservationLatestByStationID_HumidityRealtive(t *testing.T) {
t.Errorf("ObservationLatestByStationID failed, expected datetime: %s, got: %s",
tc.h.dt.Format(time.RFC3339), o.HumidityRelative().DateTime().Format(time.RFC3339))
}
if o.HumidityRelative().Source() != SourceObservation {
t.Errorf("ObservationLatestByStationID failed, expected observation source, but got: %s",
o.HumidityRelative().Source())
}
if tc.h == nil {
if o.HumidityRelative().IsAvailable() {
t.Errorf("ObservationLatestByStationID failed, expected humidity "+
@ -238,19 +310,19 @@ func TestClient_ObservationLatestByStationID_PrecipitationCurrent(t *testing.T)
// Station ID
sid string
// Observation precipitation
p *ObservationPrecipitation
p *Precipitation
}{
{"K-Botanischer Garten", "199942", &ObservationPrecipitation{
{"K-Botanischer Garten", "199942", &Precipitation{
dt: time.Date(2023, 0o5, 15, 18, 0, 0, 0, time.UTC),
v: 0,
fv: 0,
}},
{"K-Stammheim", "H744", &ObservationPrecipitation{
{"K-Stammheim", "H744", &Precipitation{
dt: time.Date(2023, 0o5, 15, 19, 30, 0, 0, time.UTC),
v: 0,
fv: 0,
}},
{"All data fields", "all", &ObservationPrecipitation{
{"All data fields", "all", &Precipitation{
dt: time.Date(2023, 0o5, 17, 7, 30, 0, 0, time.UTC),
v: 0.1,
fv: 0.1,
}},
{"No data fields", "none", nil},
}
@ -280,6 +352,10 @@ func TestClient_ObservationLatestByStationID_PrecipitationCurrent(t *testing.T)
tc.p.dt.Format(time.RFC3339),
o.Precipitation(TimespanCurrent).DateTime().Format(time.RFC3339))
}
if o.Precipitation(TimespanCurrent).Source() != SourceObservation {
t.Errorf("ObservationLatestByStationID failed, expected observation source, but got: %s",
o.Precipitation(TimespanCurrent).Source())
}
if tc.p == nil {
if o.Precipitation(TimespanCurrent).IsAvailable() {
t.Errorf("ObservationLatestByStationID failed, expected precipitation "+
@ -301,11 +377,11 @@ func TestClient_ObservationLatestByStationID_Precipitation10m(t *testing.T) {
// Station ID
sid string
// Observation precipitation
p *ObservationPrecipitation
p *Precipitation
}{
{"K-Botanischer Garten", "199942", &ObservationPrecipitation{v: 0}},
{"K-Stammheim", "H744", &ObservationPrecipitation{v: 0}},
{"All data fields", "all", &ObservationPrecipitation{v: 0.5}},
{"K-Botanischer Garten", "199942", &Precipitation{fv: 0}},
{"K-Stammheim", "H744", &Precipitation{fv: 0}},
{"All data fields", "all", &Precipitation{fv: 0.5}},
{"No data fields", "none", nil},
}
c := New(withMockAPI())
@ -350,11 +426,11 @@ func TestClient_ObservationLatestByStationID_Precipitation1h(t *testing.T) {
// Station ID
sid string
// Observation precipitation
p *ObservationPrecipitation
p *Precipitation
}{
{"K-Botanischer Garten", "199942", &ObservationPrecipitation{v: 0}},
{"K-Stammheim", "H744", &ObservationPrecipitation{v: 0}},
{"All data fields", "all", &ObservationPrecipitation{v: 10.3}},
{"K-Botanischer Garten", "199942", &Precipitation{fv: 0}},
{"K-Stammheim", "H744", &Precipitation{fv: 0}},
{"All data fields", "all", &Precipitation{fv: 10.3}},
{"No data fields", "none", nil},
}
c := New(withMockAPI())
@ -399,11 +475,11 @@ func TestClient_ObservationLatestByStationID_Precipitation24h(t *testing.T) {
// Station ID
sid string
// Observation precipitation
p *ObservationPrecipitation
p *Precipitation
}{
{"K-Botanischer Garten", "199942", &ObservationPrecipitation{v: 0}},
{"K-Stammheim", "H744", &ObservationPrecipitation{v: 0}},
{"All data fields", "all", &ObservationPrecipitation{v: 32.12}},
{"K-Botanischer Garten", "199942", &Precipitation{fv: 0}},
{"K-Stammheim", "H744", &Precipitation{fv: 0}},
{"All data fields", "all", &Precipitation{fv: 32.12}},
{"No data fields", "none", nil},
}
c := New(withMockAPI())
@ -470,11 +546,11 @@ func TestClient_ObservationLatestByStationID_Temperature(t *testing.T) {
// Station ID
sid string
// Observation dewpoint
t *ObservationTemperature
t *Temperature
}{
{"K-Botanischer Garten", "199942", &ObservationTemperature{v: 13.4}},
{"K-Stammheim", "H744", &ObservationTemperature{v: 14.4}},
{"All data fields", "all", &ObservationTemperature{v: 10.8}},
{"K-Botanischer Garten", "199942", &Temperature{fv: 13.4}},
{"K-Stammheim", "H744", &Temperature{fv: 14.4}},
{"All data fields", "all", &Temperature{fv: 10.8}},
{"No data fields", "none", nil},
}
c := New(withMockAPI())
@ -497,6 +573,10 @@ func TestClient_ObservationLatestByStationID_Temperature(t *testing.T) {
t.Errorf("ObservationLatestByStationID failed, expected temperature "+
"float: %f, got: %f", tc.t.Value(), o.Temperature().Value())
}
if o.Temperature().Source() != SourceObservation {
t.Errorf("ObservationLatestByStationID failed, expected observation source, but got: %s",
o.Temperature().Source())
}
if tc.t == nil {
if o.Temperature().IsAvailable() {
t.Errorf("ObservationLatestByStationID failed, expected temperature "+
@ -518,11 +598,11 @@ func TestClient_ObservationLatestByStationID_TemperatureAtGround(t *testing.T) {
// Station ID
sid string
// Observation dewpoint
t *ObservationTemperature
t *Temperature
}{
{"K-Botanischer Garten", "199942", nil},
{"K-Stammheim", "H744", &ObservationTemperature{v: 14.3}},
{"All data fields", "all", &ObservationTemperature{v: 15.4}},
{"K-Stammheim", "H744", &Temperature{fv: 14.3}},
{"All data fields", "all", &Temperature{fv: 15.4}},
{"No data fields", "none", nil},
}
c := New(withMockAPI())
@ -566,11 +646,11 @@ func TestClient_ObservationLatestByStationID_TemperatureMin(t *testing.T) {
// Station ID
sid string
// Observation dewpoint
t *ObservationTemperature
t *Temperature
}{
{"K-Botanischer Garten", "199942", &ObservationTemperature{v: 12.3}},
{"K-Stammheim", "H744", &ObservationTemperature{v: 11.9}},
{"All data fields", "all", &ObservationTemperature{v: 6.2}},
{"K-Botanischer Garten", "199942", &Temperature{fv: 12.3}},
{"K-Stammheim", "H744", &Temperature{fv: 11.9}},
{"All data fields", "all", &Temperature{fv: 6.2}},
{"No data fields", "none", nil},
}
c := New(withMockAPI())
@ -614,11 +694,11 @@ func TestClient_ObservationLatestByStationID_TemperatureMax(t *testing.T) {
// Station ID
sid string
// Observation dewpoint
t *ObservationTemperature
t *Temperature
}{
{"K-Botanischer Garten", "199942", &ObservationTemperature{v: 20.5}},
{"K-Stammheim", "H744", &ObservationTemperature{v: 20.7}},
{"All data fields", "all", &ObservationTemperature{v: 12.4}},
{"K-Botanischer Garten", "199942", &Temperature{fv: 20.5}},
{"K-Stammheim", "H744", &Temperature{fv: 20.7}},
{"All data fields", "all", &Temperature{fv: 12.4}},
{"No data fields", "none", nil},
}
c := New(withMockAPI())
@ -662,11 +742,11 @@ func TestClient_ObservationLatestByStationID_TemperatureAtGroundMin(t *testing.T
// Station ID
sid string
// Observation dewpoint
t *ObservationTemperature
t *Temperature
}{
{"K-Botanischer Garten", "199942", nil},
{"K-Stammheim", "H744", &ObservationTemperature{v: 12.8}},
{"All data fields", "all", &ObservationTemperature{v: 3.7}},
{"K-Stammheim", "H744", &Temperature{fv: 12.8}},
{"All data fields", "all", &Temperature{fv: 3.7}},
{"No data fields", "none", nil},
}
c := New(withMockAPI())
@ -710,11 +790,11 @@ func TestClient_ObservationLatestByStationID_TemperatureMean(t *testing.T) {
// Station ID
sid string
// Observation dewpoint
t *ObservationTemperature
t *Temperature
}{
{"K-Botanischer Garten", "199942", nil},
{"K-Stammheim", "H744", nil},
{"All data fields", "all", &ObservationTemperature{v: 16.3}},
{"All data fields", "all", &Temperature{fv: 16.3}},
{"No data fields", "none", nil},
}
c := New(withMockAPI())
@ -758,16 +838,16 @@ func TestClient_ObservationLatestByStationID_PressureMSL(t *testing.T) {
// Station ID
sid string
// Observation dewpoint
p *ObservationPressure
p *Pressure
}{
{"K-Botanischer Garten", "199942", &ObservationPressure{
{"K-Botanischer Garten", "199942", &Pressure{
dt: time.Date(2023, 0o5, 15, 20, 10, 0, 0, time.UTC),
v: 1015.5,
fv: 1015.5,
}},
{"K-Stammheim", "H744", nil},
{"All data fields", "all", &ObservationPressure{
{"All data fields", "all", &Pressure{
dt: time.Date(2023, 0o5, 17, 7, 40, 0, 0, time.UTC),
v: 1026.3,
fv: 1026.3,
}},
{"No data fields", "none", nil},
}
@ -795,6 +875,10 @@ func TestClient_ObservationLatestByStationID_PressureMSL(t *testing.T) {
t.Errorf("ObservationLatestByStationID failed, expected datetime: %s, got: %s",
tc.p.dt.Format(time.RFC3339), o.PressureMSL().DateTime().Format(time.RFC3339))
}
if o.PressureMSL().Source() != SourceObservation {
t.Errorf("ObservationLatestByStationID failed, expected observation source, but got: %s",
o.PressureMSL().Source())
}
if tc.p == nil {
if o.PressureMSL().IsAvailable() {
t.Errorf("ObservationLatestByStationID failed, expected pressure MSL "+
@ -816,11 +900,11 @@ func TestClient_ObservationLatestByStationID_PressureQFE(t *testing.T) {
// Station ID
sid string
// Observation dewpoint
p *ObservationPressure
p *Pressure
}{
{"K-Botanischer Garten", "199942", &ObservationPressure{v: 1010.2}},
{"K-Botanischer Garten", "199942", &Pressure{fv: 1010.2}},
{"K-Stammheim", "H744", nil},
{"All data fields", "all", &ObservationPressure{v: 1020.9}},
{"All data fields", "all", &Pressure{fv: 1020.9}},
{"No data fields", "none", nil},
}
c := New(withMockAPI())
@ -857,6 +941,28 @@ func TestClient_ObservationLatestByStationID_PressureQFE(t *testing.T) {
}
}
func TestClient_ObservationLatestByStationID_GlobalRadiationCurrent(t *testing.T) {
c := New(withMockAPI())
if c == nil {
t.Errorf("failed to create new Client, got nil")
return
}
o, err := c.ObservationLatestByStationID("199942")
if err != nil {
t.Errorf("ObservationLatestByStationID with station %s "+
"failed: %s", "199942", err)
return
}
if o.GlobalRadiation(TimespanCurrent).IsAvailable() {
t.Errorf("ObservationLatestByStationID failed, expected glob. radiation "+
"to have no data, but got: %s", o.GlobalRadiation(TimespanCurrent))
}
if !math.IsNaN(o.GlobalRadiation(TimespanCurrent).Value()) {
t.Errorf("ObservationLatestByStationID failed, expected glob. radiation "+
"to return NaN, but got: %s", o.GlobalRadiation(TimespanCurrent).String())
}
}
func TestClient_ObservationLatestByStationID_GlobalRadiation10m(t *testing.T) {
tt := []struct {
// Test name
@ -864,11 +970,17 @@ func TestClient_ObservationLatestByStationID_GlobalRadiation10m(t *testing.T) {
// Station ID
sid string
// Observation radiation
p *ObservationRadiation
p *Radiation
}{
{"K-Botanischer Garten", "199942", &ObservationRadiation{v: 0}},
{"K-Botanischer Garten", "199942", &Radiation{
dt: time.Date(2023, 0o5, 15, 20, 10, 0, 0, time.UTC),
fv: 0,
}},
{"K-Stammheim", "H744", nil},
{"All data fields", "all", &ObservationRadiation{v: 62}},
{"All data fields", "all", &Radiation{
dt: time.Date(2023, 0o5, 17, 7, 40, 0, 0, time.UTC),
fv: 62,
}},
{"No data fields", "none", nil},
}
c := New(withMockAPI())
@ -892,6 +1004,14 @@ func TestClient_ObservationLatestByStationID_GlobalRadiation10m(t *testing.T) {
t.Errorf("ObservationLatestByStationID failed, expected glob. radiation "+
"float: %f, got: %f", tc.p.Value(), o.GlobalRadiation(Timespan10Min).Value())
}
if tc.p != nil && tc.p.dt.Unix() != o.GlobalRadiation(Timespan10Min).DateTime().Unix() {
t.Errorf("ObservationLatestByStationID failed, expected datetime: %s, got: %s",
tc.p.dt.Format(time.RFC3339), o.GlobalRadiation(Timespan10Min).DateTime().Format(time.RFC3339))
}
if o.GlobalRadiation(Timespan10Min).Source() != SourceObservation {
t.Errorf("ObservationLatestByStationID failed, expected observation source, but got: %s",
o.GlobalRadiation(Timespan10Min).Source())
}
if tc.p == nil {
if o.GlobalRadiation(Timespan10Min).IsAvailable() {
t.Errorf("ObservationLatestByStationID failed, expected glob. radiation "+
@ -913,11 +1033,11 @@ func TestClient_ObservationLatestByStationID_GlobalRadiation1h(t *testing.T) {
// Station ID
sid string
// Observation radiation
p *ObservationRadiation
p *Radiation
}{
{"K-Botanischer Garten", "199942", &ObservationRadiation{v: 0}},
{"K-Botanischer Garten", "199942", &Radiation{fv: 0}},
{"K-Stammheim", "H744", nil},
{"All data fields", "all", &ObservationRadiation{v: 200}},
{"All data fields", "all", &Radiation{fv: 200}},
{"No data fields", "none", nil},
}
c := New(withMockAPI())
@ -962,11 +1082,11 @@ func TestClient_ObservationLatestByStationID_GlobalRadiation24h(t *testing.T) {
// Station ID
sid string
// Observation radiation
p *ObservationRadiation
p *Radiation
}{
{"K-Botanischer Garten", "199942", &ObservationRadiation{v: 774}},
{"K-Botanischer Garten", "199942", &Radiation{fv: 774}},
{"K-Stammheim", "H744", nil},
{"All data fields", "all", &ObservationRadiation{v: 756}},
{"All data fields", "all", &Radiation{fv: 756}},
{"No data fields", "none", nil},
}
c := New(withMockAPI())
@ -1004,6 +1124,124 @@ func TestClient_ObservationLatestByStationID_GlobalRadiation24h(t *testing.T) {
}
}
func TestClient_ObservationLatestByStationID_WindDirection(t *testing.T) {
tt := []struct {
// Test name
n string
// Station ID
sid string
// Observation dewpoint
p *Direction
}{
{"K-Botanischer Garten", "199942", nil},
{"K-Stammheim", "H744", nil},
{"All data fields", "all", &Direction{
dt: time.Date(2023, 0o5, 21, 11, 30, 0, 0, time.UTC),
fv: 90,
}},
{"No data fields", "none", nil},
}
c := New(withMockAPI())
if c == nil {
t.Errorf("failed to create new Client, got nil")
return
}
for _, tc := range tt {
t.Run(tc.n, func(t *testing.T) {
o, err := c.ObservationLatestByStationID(tc.sid)
if err != nil {
t.Errorf("ObservationLatestByStationID with station %s failed: %s", tc.sid, err)
return
}
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())
}
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())
}
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))
}
if o.WindDirection().Source() != SourceObservation {
t.Errorf("ObservationLatestByStationID failed, expected observation source, but got: %s",
o.WindDirection().Source())
}
if tc.p == nil {
if o.WindDirection().IsAvailable() {
t.Errorf("ObservationLatestByStationID failed, expected wind direction "+
"to have no data, but got: %s", o.WindDirection())
}
if !math.IsNaN(o.WindDirection().Value()) {
t.Errorf("ObservationLatestByStationID failed, expected wind direction "+
"to return NaN, but got: %s", o.WindDirection().String())
}
}
})
}
}
func TestClient_ObservationLatestByStationID_WindSpeed(t *testing.T) {
tt := []struct {
// Test name
n string
// Station ID
sid string
// Observation dewpoint
p *Speed
}{
{"K-Botanischer Garten", "199942", nil},
{"K-Stammheim", "H744", nil},
{"All data fields", "all", &Speed{
dt: time.Date(2023, 0o5, 21, 11, 30, 0, 0, time.UTC),
fv: 7.716666666,
}},
{"No data fields", "none", nil},
}
c := New(withMockAPI())
if c == nil {
t.Errorf("failed to create new Client, got nil")
return
}
for _, tc := range tt {
t.Run(tc.n, func(t *testing.T) {
o, err := c.ObservationLatestByStationID(tc.sid)
if err != nil {
t.Errorf("ObservationLatestByStationID with station %s failed: %s", tc.sid, err)
return
}
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())
}
if tc.p != nil && tc.p.Value() != o.WindSpeed().Value() {
t.Errorf("ObservationLatestByStationID failed, expected windspeed "+
"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() {
t.Errorf("ObservationLatestByStationID failed, expected datetime: %s, got: %s",
tc.p.dt.Format(time.RFC3339), o.WindSpeed().DateTime().Format(time.RFC3339))
}
if o.WindSpeed().Source() != SourceObservation {
t.Errorf("ObservationLatestByStationID failed, expected observation source, but got: %s",
o.WindSpeed().Source())
}
if tc.p == nil {
if o.WindSpeed().IsAvailable() {
t.Errorf("ObservationLatestByStationID failed, expected windspeed "+
"to have no data, but got: %s", o.WindSpeed())
}
if !math.IsNaN(o.WindSpeed().Value()) {
t.Errorf("ObservationLatestByStationID failed, expected windspeed "+
"to return NaN, but got: %s", o.WindSpeed().String())
}
}
})
}
}
func TestObservationTemperature_String(t *testing.T) {
tt := []struct {
// Original celsius value
@ -1047,23 +1285,187 @@ func TestObservationTemperature_String(t *testing.T) {
ff := "%.1f°F"
for _, tc := range tt {
t.Run(fmt.Sprintf("%.2f°C", tc.c), func(t *testing.T) {
ot := ObservationTemperature{v: tc.c}
ot := Temperature{fv: tc.c}
if ot.Celsius() != tc.c {
t.Errorf("ObservationTemperature.Celsius failed, expected: %f, got: %f", tc.c,
t.Errorf("Temperature.Celsius failed, expected: %f, got: %f", tc.c,
ot.Celsius())
}
if ot.CelsiusString() != fmt.Sprintf(cf, tc.c) {
t.Errorf("ObservationTemperature.CelsiusString failed, expected: %s, got: %s",
t.Errorf("Temperature.CelsiusString failed, expected: %s, got: %s",
fmt.Sprintf(cf, tc.c), ot.CelsiusString())
}
if ot.Fahrenheit() != tc.f {
t.Errorf("ObservationTemperature.Fahrenheit failed, expected: %f, got: %f", tc.f,
t.Errorf("Temperature.Fahrenheit failed, expected: %f, got: %f", tc.f,
ot.Fahrenheit())
}
if ot.FahrenheitString() != fmt.Sprintf(ff, tc.f) {
t.Errorf("ObservationTemperature.FahrenheitString failed, expected: %s, got: %s",
t.Errorf("Temperature.FahrenheitString failed, expected: %s, got: %s",
fmt.Sprintf(ff, tc.f), ot.FahrenheitString())
}
})
}
}
func TestObservationSpeed_Conversion(t *testing.T) {
tt := []struct {
// Original m/s value
ms float64
// km/h value
kmh float64
// mi/h value
mph float64
// knots value
kn float64
}{
{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("%.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(msf, tc.ms) {
t.Errorf("Speed.String failed, expected: %s, got: %s",
fmt.Sprintf(msf, tc.ms), os.String())
}
if os.KMH() != tc.kmh {
t.Errorf("Speed.KMH failed, expected: %f, got: %f", tc.kmh,
os.KMH())
}
if os.KMHString() != fmt.Sprintf(kmhf, tc.kmh) {
t.Errorf("Speed.KMHString failed, expected: %s, got: %s",
fmt.Sprintf(kmhf, tc.kmh), os.KMHString())
}
if os.MPH() != tc.mph {
t.Errorf("Speed.MPH failed, expected: %f, got: %f", tc.mph,
os.MPH())
}
if os.MPHString() != fmt.Sprintf(mphf, tc.mph) {
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())
}
})
}
}
func TestObservationDirection_Direction(t *testing.T) {
tt := []struct {
// Original direction in degree
d float64
// Direction string
ds string
}{
{0, "N"},
{11.25, "NbE"},
{22.5, "NNE"},
{33.75, "NEbN"},
{45, "NE"},
{56.25, "NEbE"},
{67.5, "ENE"},
{78.75, "EbN"},
{90, "E"},
{101.25, "EbS"},
{112.5, "ESE"},
{123.75, "SEbE"},
{135, "SE"},
{146.25, "SEbS"},
{157.5, "SSE"},
{168.75, "SbE"},
{180, "S"},
{191.25, "SbW"},
{202.5, "SSW"},
{213.75, "SWbS"},
{225, "SW"},
{236.25, "SWbW"},
{247.5, "WSW"},
{258.75, "WbS"},
{270, "W"},
{281.25, "WbN"},
{292.5, "WNW"},
{303.75, "NWbW"},
{315, "NW"},
{326.25, "NWbN"},
{337.5, "NNW"},
{348.75, "NbW"},
{999, ErrUnsupportedDirection},
}
for _, tc := range tt {
t.Run(fmt.Sprintf("%.2f° => %s", tc.d, tc.ds), func(t *testing.T) {
d := Direction{fv: tc.d}
if d.Direction() != tc.ds {
t.Errorf("Direction.Direction failed, expected: %s, got: %s",
tc.ds, d.Direction())
}
})
}
}
func TestObservationDirection_DirectionFull(t *testing.T) {
tt := []struct {
// Original direction in degree
d float64
// Direction string
ds string
}{
{0, "North"},
{11.25, "North by East"},
{22.5, "North-Northeast"},
{33.75, "Northeast by North"},
{45, "Northeast"},
{56.25, "Northeast by East"},
{67.5, "East-Northeast"},
{78.75, "East by North"},
{90, "East"},
{101.25, "East by South"},
{112.5, "East-Southeast"},
{123.75, "Southeast by East"},
{135, "Southeast"},
{146.25, "Southeast by South"},
{157.5, "South-Southeast"},
{168.75, "South by East"},
{180, "South"},
{191.25, "South by West"},
{202.5, "South-Southwest"},
{213.75, "Southwest by South"},
{225, "Southwest"},
{236.25, "Southwest by West"},
{247.5, "West-Southwest"},
{258.75, "West by South"},
{270, "West"},
{281.25, "West by North"},
{292.5, "West-Northwest"},
{303.75, "Northwest by West"},
{315, "Northwest"},
{326.25, "Northwest by North"},
{337.5, "North-Northwest"},
{348.75, "North by West"},
{999, ErrUnsupportedDirection},
}
for _, tc := range tt {
t.Run(fmt.Sprintf("%.2f° => %s", tc.d, tc.ds), func(t *testing.T) {
d := Direction{fv: tc.d}
if d.DirectionFull() != tc.ds {
t.Errorf("Direction.Direction failed, expected: %s, got: %s",
tc.ds, d.DirectionFull())
}
})
}
}

47
precipitation.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"
)
// Precipitation is a type wrapper of an WeatherData for holding precipitation
// values in WeatherData
type Precipitation WeatherData
// IsAvailable returns true if an Precipitation value was
// available at time of query
func (p Precipitation) IsAvailable() bool {
return !p.na
}
// DateTime returns the DateTime when the Precipitation value was recorded
func (p Precipitation) DateTime() time.Time {
return p.dt
}
// String satisfies the fmt.Stringer interface for the Precipitation type
func (p Precipitation) String() string {
return fmt.Sprintf("%.1fmm", p.fv)
}
// Source returns the Source of Precipitation
// If the Source is not available it will return SourceUnknown
func (p Precipitation) Source() Source {
return p.s
}
// Value returns the float64 value of an Precipitation
// If the Precipitation is not available in the WeatherData
// Vaule will return math.NaN instead.
func (p Precipitation) Value() float64 {
if p.na {
return math.NaN()
}
return p.fv
}

47
pressure.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"
)
// Pressure is a type wrapper of an WeatherData for holding pressure
// values in WeatherData
type Pressure WeatherData
// IsAvailable returns true if an Pressure value was
// available at time of query
func (p Pressure) IsAvailable() bool {
return !p.na
}
// DateTime returns the date and time of the Pressure reading
func (p Pressure) DateTime() time.Time {
return p.dt
}
// String satisfies the fmt.Stringer interface for the Pressure type
func (p Pressure) String() string {
return fmt.Sprintf("%.1fhPa", p.fv)
}
// Source returns the Source of Pressure
// If the Source is not available it will return SourceUnknown
func (p Pressure) Source() Source {
return p.s
}
// Value returns the float64 value of an Pressure
// If the Pressure is not available in the WeatherData
// Vaule will return math.NaN instead.
func (p Pressure) Value() float64 {
if p.na {
return math.NaN()
}
return p.fv
}

48
radiation.go Normal file
View file

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

6
sonar-project.properties Normal file
View file

@ -0,0 +1,6 @@
# SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
#
# SPDX-License-Identifier: CC0-1.0
sonar.projectKey=go-meteologix
sonar.go.coverage.reportPaths=cov.out

58
source.go Normal file
View file

@ -0,0 +1,58 @@
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
//
// SPDX-License-Identifier: MIT
package meteologix
import "strings"
// Enum of different weather data sources
const (
// SourceObservation represent observations from weather stations (high precision)
SourceObservation = iota
// SourceAnalysis represents weather data based on analysis (medium precision)
SourceAnalysis
// SourceForecast represents weather data based on weather forcecasts
SourceForecast
// SourceMixed represents weather data based on mixed sources
SourceMixed
// SourceUnknown represents weather data based on unknown sources
SourceUnknown
)
// Source is a type wrapper for an int type to enum different weather sources
type Source int
// String satisfies the fmt.Stringer interface for the Source type
func (s Source) String() string {
switch s {
case SourceObservation:
return "Observation"
case SourceAnalysis:
return "Analysis"
case SourceForecast:
return "Forecast"
case SourceMixed:
return "Mixed"
case SourceUnknown:
return "Unknown"
default:
return "Unknown"
}
}
// StringToSource converts a given source string to a Source type
func StringToSource(s string) Source {
switch strings.ToLower(s) {
case "observation":
return SourceObservation
case "analysis":
return SourceAnalysis
case "forecast":
return SourceForecast
case "mixed":
return SourceMixed
default:
return SourceUnknown
}
}

56
source_test.go Normal file
View file

@ -0,0 +1,56 @@
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
//
// SPDX-License-Identifier: MIT
package meteologix
import (
"testing"
)
func TestSource_String(t *testing.T) {
tt := []struct {
// Original source
os Source
// Expected string
es string
}{
{SourceObservation, "Observation"},
{SourceAnalysis, "Analysis"},
{SourceForecast, "Forecast"},
{SourceMixed, "Mixed"},
{SourceUnknown, "Unknown"},
{999, "Unknown"},
}
for _, tc := range tt {
t.Run(tc.os.String(), func(t *testing.T) {
if tc.os.String() != tc.es {
t.Errorf("String for Source failed, expected: %s, got: %s",
tc.es, tc.os.String())
}
})
}
}
func TestStringToSource(t *testing.T) {
tt := []struct {
// Original string
os string
// Expected source
es Source
}{
{"Observation", SourceObservation},
{"Analysis", SourceAnalysis},
{"Forecast", SourceForecast},
{"Mixed", SourceMixed},
{"Unknown", SourceUnknown},
}
for _, tc := range tt {
t.Run(tc.es.String(), func(t *testing.T) {
if r := StringToSource(tc.os); r != tc.es {
t.Errorf("StringToSource failed, expected: %s, got: %s",
tc.es.String(), r.String())
}
})
}
}

87
speed.go Normal file
View file

@ -0,0 +1,87 @@
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
//
// SPDX-License-Identifier: MIT
package meteologix
import (
"fmt"
"math"
"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
// IsAvailable returns true if an Speed value was
// available at time of query
func (s Speed) IsAvailable() bool {
return !s.na
}
// DateTime returns the DateTime when the Speed was checked
func (s Speed) DateTime() time.Time {
return s.dt
}
// Value returns the float64 value of an Speed in meters
// per second.
// If the Speed is not available in the WeatherData
// Vaule will return math.NaN instead.
func (s Speed) Value() float64 {
if s.na {
return math.NaN()
}
return s.fv
}
// String satisfies the fmt.Stringer interface for the Speed type
func (s Speed) String() string {
return fmt.Sprintf("%.1fm/s", s.fv)
}
// Source returns the Source of Speed
// If the Source is not available it will return SourceUnknown
func (s Speed) Source() Source {
return s.s
}
// KMH returns the Speed value in km/h
func (s Speed) KMH() float64 {
return s.fv * MultiplierKPH
}
// KMHString returns the Speed value as formatted string in km/h
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 * MultiplierMPH
}
// MPHString returns the Speed value as formatted string in mi/h
func (s Speed) MPHString() string {
return fmt.Sprintf("%.1fmi/h", s.MPH())
}

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2023 Winni Neessen <winni@neessen.dev>
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
//
// SPDX-License-Identifier: MIT
@ -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

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2023 Winni Neessen <winni@neessen.dev>
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
//
// SPDX-License-Identifier: MIT
@ -35,7 +35,7 @@ func TestClient_StationSearchByLocation(t *testing.T) {
}
}
func TestClient_StationSearchByLocation_Failed(t *testing.T) {
func TestClient_StationSearchByLocation_Fail(t *testing.T) {
c := New(WithUsername("invalid"), WithPassword("invalid"))
if c == nil {
t.Errorf("failed to create new Client, got nil")
@ -46,7 +46,8 @@ func TestClient_StationSearchByLocation_Failed(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,31 @@ func TestClient_StationSearchByLocation_Failed(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)
}
}
func TestClient_StationSearchByLocationWithRadius_Fail(t *testing.T) {
ak := getAPIKeyFromEnv(t)
if ak == "" {
t.Skip("no API_KEY found in environment, skipping test")
}
c := New(WithAPIKey(ak))
if c == nil {
t.Errorf("failed to create new Client, got nil")
return
}
_, err := c.StationSearchByLocationWithinRadius("Cologne, Germany", 0)
if err == nil {
t.Errorf("StationSearchByLocationWithRadius was supposed to fail but didn't")
}
if !errors.Is(err, ErrRadiusTooSmall) {
t.Errorf("StationSearchByLocationWithRadius was supposed to return ErrRadiusTooSmall, got: %s", err)
}
_, err = c.StationSearchByLocationWithinRadius("Cologne, Germany", 1000)
if err == nil {
t.Errorf("StationSearchByLocationWithRadius was supposed to fail but didn't")
}
}
@ -141,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,
},
{
@ -186,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"},
}

72
temperature.go Normal file
View file

@ -0,0 +1,72 @@
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
//
// SPDX-License-Identifier: MIT
package meteologix
import (
"fmt"
"math"
"time"
)
// Temperature is a type wrapper of an WeatherData for holding temperature
// values in WeatherData
type Temperature WeatherData
// IsAvailable returns true if an Temperature value was
// available at time of query
func (t Temperature) IsAvailable() bool {
return !t.na
}
// 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 WeatherData
// Vaule will return math.NaN instead.
func (t Temperature) Value() float64 {
if t.na {
return math.NaN()
}
return t.fv
}
// Source returns the Source of an Temperature
// If the Source is not available it will return SourceUnknown
func (t Temperature) Source() Source {
return t.s
}
// String satisfies the fmt.Stringer interface for the Temperature type
func (t Temperature) String() string {
return fmt.Sprintf("%.1f°C", t.fv)
}
// Celsius returns the Temperature value in Celsius
func (t Temperature) Celsius() float64 {
return t.fv
}
// CelsiusString returns the Temperature value as Celsius
// formated string.
//
// This is an alias for the fmt.Stringer interface
func (t Temperature) CelsiusString() string {
return t.String()
}
// Fahrenheit returns the Temperature value in Fahrenheit
func (t Temperature) Fahrenheit() float64 {
return t.fv*9/5 + 32
}
// FahrenheitString returns the Temperature value as Fahrenheit
// formated string.
func (t Temperature) FahrenheitString() string {
return fmt.Sprintf("%.1f°F", t.Fahrenheit())
}