Compare commits
96 commits
Author | SHA1 | Date | |
---|---|---|---|
|
d6ce76b1ad | ||
|
072a2aed5d | ||
|
a536ac3bde | ||
|
eb675388f0 | ||
|
e4cbab4e43 | ||
|
8923b88e07 | ||
|
94eca2f3fe | ||
|
759300d066 | ||
|
3c69f36748 | ||
|
0b88d11fdf | ||
|
d6b551cc5e | ||
|
5297342603 | ||
|
c3af61791e | ||
|
b8c117c056 | ||
|
3f7cccb511 | ||
|
c9fa7eb1d7 | ||
|
878c4dfe71 | ||
|
27c8542f76 | ||
|
a5667bb828 | ||
|
173c6eb8ec | ||
|
a51644fbb6 | ||
|
f48392d553 | ||
|
85c3c1aff3 | ||
|
abd200177f | ||
|
e7f8662347 | ||
|
54cc672dfc | ||
|
e3756a5466 | ||
|
513d7b863f | ||
|
c9d95300c2 | ||
|
84ba2feda9 | ||
|
65d065dd59 | ||
|
3eb6a76f5d | ||
|
9c65eca128 | ||
|
a4f19380ff | ||
|
d0905266e1 | ||
|
28479be939 | ||
|
914327de85 | ||
|
a10ec1c0f9 | ||
|
d7567d4b2b | ||
|
c1e054d9a3 | ||
|
4318599eb0 | ||
|
a0b67b0367 | ||
|
681c53c23d | ||
|
8cb9754f69 | ||
|
7bdf2de388 | ||
|
b779e0f65b | ||
|
a697b13970 | ||
|
5dee24573a | ||
|
fee0bc6795 | ||
|
fcc7626a76 | ||
|
4d60f35c6a | ||
|
a7feae910b | ||
|
b77cd98484 | ||
|
3394ceeb4a | ||
|
0e3c8c6f1d | ||
|
4a7c807174 | ||
|
78e6751bbf | ||
|
5722d0e4a8 | ||
|
4ed0c12ca5 | ||
|
4123aa75e3 | ||
|
728a967bef | ||
|
e2a301c2a5 | ||
|
fe800b7632 | ||
|
b89d04e581 | ||
|
28d203a188 | ||
|
0608c4ecaa | ||
|
7ffd3943e0 | ||
|
56b17ffcf3 | ||
|
54a21a1b5b | ||
|
d8c5856c33 | ||
|
03d274846e | ||
|
e86d138659 | ||
|
a38ea586fa | ||
|
5c89db1d55 | ||
|
6c79ef7ec3 | ||
|
e1bf01261c | ||
|
8a5517ddd9 | ||
|
2614e0a0ac | ||
|
2630ea675e | ||
|
f87bca2bd5 | ||
|
3dd9f19bc3 | ||
|
76db40fb5b | ||
|
80a4d74032 | ||
|
e44e6f6024 | ||
|
e424c49f97 | ||
|
d61117cd23 | ||
|
dec578e751 | ||
|
aa86892864 | ||
|
03253c21b1 | ||
|
490721bded | ||
|
ba2dbf840b | ||
|
ec15731973 | ||
|
acb1a9ea12 | ||
|
3c96002a62 | ||
|
1dba4b13d4 | ||
|
9dea4b7931 |
6
.forgejo/FUNDING.yml
Normal file
6
.forgejo/FUNDING.yml
Normal file
|
@ -0,0 +1,6 @@
|
|||
# SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
|
||||
#
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
github: wneessen
|
||||
ko_fi: winni
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
45
.forgejo/workflows/sonarqube.yml
Normal file
45
.forgejo/workflows/sonarqube.yml
Normal 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
2
.github/FUNDING.yml
vendored
|
@ -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
2
.gitignore
vendored
|
@ -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
|
||||
|
||||
|
|
|
@ -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
134
CODE_OF_CONDUCT.md
Normal 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
35
CONTRIBUTING.md
Normal 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.
|
67
README.md
67
README.md
|
@ -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-%23go–meteologix-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-%23go–meteologix-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
38
SECURITY.md
Normal 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
1
assets/gopher43.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 104 KiB |
3
assets/gopher43.svg.license
Normal file
3
assets/gopher43.svg.license
Normal 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
228
astroinfo.go
Normal file
|
@ -0,0 +1,228 @@
|
|||
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package meteologix
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// AstronomicalInfo provides astronomical data for the next 14 days.
|
||||
// This includes moon and sun information.
|
||||
type AstronomicalInfo struct {
|
||||
// DailyData holds the different APIAstronomicalDailyData data
|
||||
// points for the next 14 days
|
||||
DailyData []APIAstronomicalDailyData `json:"dailyData"`
|
||||
// Latitude represents the GeoLocation latitude coordinates for the weather data
|
||||
Latitude float64 `json:"lat"`
|
||||
// Longitude represents the GeoLocation longitude coordinates for the weather data
|
||||
Longitude float64 `json:"lon"`
|
||||
// NextFullMoon represent the date and time of the next full moon
|
||||
NextFullMoon time.Time `json:"nextFullMoon"`
|
||||
// NextNewMoon represent the date and time of the next new moon
|
||||
NextNewMoon time.Time `json:"nextNewMoon"`
|
||||
// Run represents when astronomical values have been calculated
|
||||
Run time.Time `json:"run"`
|
||||
// TimeZone is the timezone at the queried location
|
||||
TimeZone string `json:"timeZone"`
|
||||
}
|
||||
|
||||
// APIAstronomicalDailyData holds the API response date for the daily
|
||||
// details in the AstronomicalInfo.
|
||||
type APIAstronomicalDailyData struct {
|
||||
// AstronomicalDawn represents the date and time when civil dawn begins
|
||||
AstronomicalDawn *time.Time `json:"astronomicalDawn,omitempty"`
|
||||
// AstronomicalDusk represents the date and time when civil dusk ends
|
||||
AstronomicalDusk *time.Time `json:"astronomicalDusk,omitempty"`
|
||||
// CivilDawn represents the date and time when civil dawn begins
|
||||
CivilDawn *time.Time `json:"civilDawn,omitempty"`
|
||||
// CivilDusk represents the date and time when civil dusk ends
|
||||
CivilDusk *time.Time `json:"civilDusk,omitempty"`
|
||||
// DateTime represents the date for the forecast values
|
||||
DateTime APIDate `json:"dateTime"`
|
||||
// MoonIllumination represents how much of the moon is illuminated in %
|
||||
MoonIllumination float64 `json:"moonIllumination"`
|
||||
// MoonPhase represents the moon phase in %
|
||||
MoonPhase int `json:"moonPhase"`
|
||||
// MoonRise represents the date and time when the moon rises
|
||||
MoonRise *time.Time `json:"moonRise,omitempty"`
|
||||
// MoonSet represents the date and time when the moon sets
|
||||
MoonSet *time.Time `json:"moonSet,omitempty"`
|
||||
// NauticalDawn represents the date and time when nautical dawn begins
|
||||
NauticalDawn *time.Time `json:"nauticalDawn,omitempty"`
|
||||
// NauticalDusk represents the date and time when nautical dusk ends
|
||||
NauticalDusk *time.Time `json:"nauticalDusk,omitempty"`
|
||||
// Sunrise represents the date and time of the sunrise
|
||||
Sunrise *time.Time `json:"sunrise,omitempty"`
|
||||
// Sunset represents the date and time of the sunset
|
||||
Sunset *time.Time `json:"sunset,omitempty"`
|
||||
// Transit represents the date and time when the sun is at
|
||||
// its zenith
|
||||
Transit *time.Time `json:"transit,omitempty"`
|
||||
}
|
||||
|
||||
// AstronomicalInfoByCoordinates returns the AstronomicalInfo values for
|
||||
// the given coordinates
|
||||
func (c *Client) AstronomicalInfoByCoordinates(la, lo float64) (AstronomicalInfo, error) {
|
||||
var ai AstronomicalInfo
|
||||
lat := strconv.FormatFloat(la, 'f', -1, 64)
|
||||
lon := strconv.FormatFloat(lo, 'f', -1, 64)
|
||||
u := fmt.Sprintf("%s/tools/astronomy/%s/%s", c.config.apiURL, lat, lon)
|
||||
|
||||
r, err := c.httpClient.Get(u)
|
||||
if err != nil {
|
||||
return ai, fmt.Errorf("API request failed: %w", err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(r, &ai); err != nil {
|
||||
return ai, fmt.Errorf("failed to unmarshal API response JSON: %w", err)
|
||||
}
|
||||
|
||||
return ai, nil
|
||||
}
|
||||
|
||||
// AstronomicalInfoByLocation returns the AstronomicalInfo values for
|
||||
// the given location
|
||||
func (c *Client) AstronomicalInfoByLocation(lo string) (AstronomicalInfo, error) {
|
||||
gl, err := c.GetGeoLocationByName(lo)
|
||||
if err != nil {
|
||||
return AstronomicalInfo{}, fmt.Errorf("failed too look up geolocation: %w", err)
|
||||
}
|
||||
return c.AstronomicalInfoByCoordinates(gl.Latitude, gl.Longitude)
|
||||
}
|
||||
|
||||
// SunsetByTime returns the date and time of the sunset on the give
|
||||
// time as DateTime type.
|
||||
// If the data point is not available in the AstronomicalInfo it will
|
||||
// return DateTime in which the "not available" field will be true.
|
||||
//
|
||||
// Please keep in mind that the API only returns 14 days in the future.
|
||||
// Any date given that exceeds that time, wil always return a
|
||||
// "not available" value.
|
||||
func (a *AstronomicalInfo) SunsetByTime(t time.Time) DateTime {
|
||||
if len(a.DailyData) < 1 {
|
||||
return DateTime{na: true}
|
||||
}
|
||||
var cdd APIAstronomicalDailyData
|
||||
for i := range a.DailyData {
|
||||
if a.DailyData[i].DateTime.Format(DateFormat) != t.Format(DateFormat) {
|
||||
continue
|
||||
}
|
||||
cdd = a.DailyData[i]
|
||||
}
|
||||
if cdd.DateTime.IsZero() {
|
||||
return DateTime{na: true}
|
||||
}
|
||||
return DateTime{
|
||||
dt: cdd.DateTime.Time,
|
||||
n: FieldSunset,
|
||||
s: SourceForecast,
|
||||
dv: *cdd.Sunset,
|
||||
}
|
||||
}
|
||||
|
||||
// Sunset returns the date and time of the sunset on the current date
|
||||
// as DateTime type.
|
||||
// If the data point is not available in the AstronomicalInfo it will
|
||||
// return DateTime in which the "not available" field will be true.
|
||||
func (a *AstronomicalInfo) Sunset() DateTime {
|
||||
return a.SunsetByTime(time.Now())
|
||||
}
|
||||
|
||||
// SunsetByDateString returns the date and time of the sunset at a
|
||||
// given date string as DateTime type. Expected format is 2006-01-02.
|
||||
// If the date wasn't able to be parsed or if the data point is not
|
||||
// available in the AstronomicalInfo it will return DateTime in
|
||||
// which the "not available" field will be true.
|
||||
func (a *AstronomicalInfo) SunsetByDateString(ds string) DateTime {
|
||||
t, err := time.Parse(DateFormat, ds)
|
||||
if err != nil {
|
||||
return DateTime{na: true}
|
||||
}
|
||||
return a.SunsetByTime(t)
|
||||
}
|
||||
|
||||
// SunsetAll returns a slice of all sunset data points in the given
|
||||
// AstronomicalInfo instance as DateTime types. If no sunset data
|
||||
// is available it will return an empty slice
|
||||
func (a *AstronomicalInfo) SunsetAll() []DateTime {
|
||||
var sss []DateTime
|
||||
for _, cd := range a.DailyData {
|
||||
if cd.DateTime.IsZero() {
|
||||
continue
|
||||
}
|
||||
sss = append(sss, a.SunsetByTime(cd.DateTime.Time))
|
||||
}
|
||||
|
||||
return sss
|
||||
}
|
||||
|
||||
// SunriseByTime returns the date and time of the sunrise on the give
|
||||
// time as DateTime type.
|
||||
// If the data point is not available in the AstronomicalInfo it will
|
||||
// return DateTime in which the "not available" field will be true.
|
||||
//
|
||||
// Please keep in mind that the API only returns 14 days in the future.
|
||||
// Any date given that exceeds that time, wil always return a
|
||||
// "not available" value.
|
||||
func (a *AstronomicalInfo) SunriseByTime(t time.Time) DateTime {
|
||||
if len(a.DailyData) < 1 {
|
||||
return DateTime{na: true}
|
||||
}
|
||||
var cdd APIAstronomicalDailyData
|
||||
for i := range a.DailyData {
|
||||
if a.DailyData[i].DateTime.Format(DateFormat) != t.Format(DateFormat) {
|
||||
continue
|
||||
}
|
||||
cdd = a.DailyData[i]
|
||||
}
|
||||
if cdd.DateTime.IsZero() {
|
||||
return DateTime{na: true}
|
||||
}
|
||||
return DateTime{
|
||||
dt: cdd.DateTime.Time,
|
||||
n: FieldSunrise,
|
||||
s: SourceForecast,
|
||||
dv: *cdd.Sunrise,
|
||||
}
|
||||
}
|
||||
|
||||
// Sunrise returns the date and time of the sunrise on the current date
|
||||
// as DateTime type.
|
||||
// If the data point is not available in the AstronomicalInfo it will
|
||||
// return DateTime in which the "not available" field will be true.
|
||||
func (a *AstronomicalInfo) Sunrise() DateTime {
|
||||
return a.SunriseByTime(time.Now())
|
||||
}
|
||||
|
||||
// SunriseByDateString returns the date and time of the sunrise at a
|
||||
// given date string as DateTime type. Expected format is 2006-01-02.
|
||||
// If the date wasn't able to be parsed or if the data point is not
|
||||
// available in the AstronomicalInfo it will return DateTime in
|
||||
// which the "not available" field will be true.
|
||||
func (a *AstronomicalInfo) SunriseByDateString(ds string) DateTime {
|
||||
t, err := time.Parse(DateFormat, ds)
|
||||
if err != nil {
|
||||
return DateTime{na: true}
|
||||
}
|
||||
return a.SunriseByTime(t)
|
||||
}
|
||||
|
||||
// SunriseAll returns a slice of all sunrise data points in the given
|
||||
// AstronomicalInfo instance as DateTime types. If no sunrise data
|
||||
// is available it will return an empty slice
|
||||
func (a *AstronomicalInfo) SunriseAll() []DateTime {
|
||||
var sss []DateTime
|
||||
for _, cd := range a.DailyData {
|
||||
if cd.DateTime.IsZero() {
|
||||
continue
|
||||
}
|
||||
sss = append(sss, a.SunriseByTime(cd.DateTime.Time))
|
||||
}
|
||||
|
||||
return sss
|
||||
}
|
221
astroinfo_test.go
Normal file
221
astroinfo_test.go
Normal file
|
@ -0,0 +1,221 @@
|
|||
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
package meteologix
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestClient_AstronomicalInfoByCoordinates(t *testing.T) {
|
||||
la := 52.5067296
|
||||
lo := 13.2599306
|
||||
loc, err := time.LoadLocation("Europe/Berlin")
|
||||
if err != nil {
|
||||
t.Errorf("failed to load time location data for Europe/Berlin: %s", err)
|
||||
return
|
||||
}
|
||||
rt := time.Date(2023, 5, 28, 15, 8, 33, 0, loc)
|
||||
nfmt := time.Date(2023, 6, 4, 5, 43, 56, 0, loc)
|
||||
nnmt := time.Date(2023, 6, 18, 6, 39, 10, 0, loc)
|
||||
c := New(withMockAPI())
|
||||
ai, err := c.AstronomicalInfoByCoordinates(la, lo)
|
||||
if err != nil {
|
||||
t.Errorf("failed to fetch astronomical information: %s", err)
|
||||
return
|
||||
}
|
||||
if ai.Latitude != la {
|
||||
t.Errorf("AstronomicalInfoByCoordinates failed, expected lat: %f, got: %f", la,
|
||||
ai.Latitude)
|
||||
}
|
||||
if ai.Longitude != lo {
|
||||
t.Errorf("AstronomicalInfoByCoordinates failed, expected lon: %f, got: %f", lo,
|
||||
ai.Longitude)
|
||||
}
|
||||
if ai.Run.UnixMilli() != rt.UnixMilli() {
|
||||
t.Errorf("AstronomicalInfoByCoordinates failed, expected run time: %s, got: %s",
|
||||
rt.String(), ai.Run.String())
|
||||
}
|
||||
if ai.TimeZone != loc.String() {
|
||||
t.Errorf("AstronomicalInfoByCoordinates failed, expected time zone: %s, got: %s",
|
||||
loc.String(), ai.TimeZone)
|
||||
}
|
||||
if ai.NextFullMoon.UnixMilli() != nfmt.UnixMilli() {
|
||||
t.Errorf("AstronomicalInfoByCoordinates failed, expected next full moon: %s, got: %s",
|
||||
nfmt.String(), ai.NextFullMoon.String())
|
||||
}
|
||||
if ai.NextNewMoon.UnixMilli() != nnmt.UnixMilli() {
|
||||
t.Errorf("AstronomicalInfoByCoordinates failed, expected next new moon: %s, got: %s",
|
||||
nnmt.String(), ai.NextNewMoon.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_AstronomicalInfoByLocation(t *testing.T) {
|
||||
la := 52.5067296
|
||||
lo := 13.2599306
|
||||
loc, err := time.LoadLocation("Europe/Berlin")
|
||||
if err != nil {
|
||||
t.Errorf("failed to load time location data for Europe/Berlin: %s", err)
|
||||
return
|
||||
}
|
||||
rt := time.Date(2023, 5, 28, 15, 8, 33, 0, loc)
|
||||
nfmt := time.Date(2023, 6, 4, 5, 43, 56, 0, loc)
|
||||
nnmt := time.Date(2023, 6, 18, 6, 39, 10, 0, loc)
|
||||
c := New(withMockAPI())
|
||||
ai, err := c.AstronomicalInfoByLocation("Berlin, Germany")
|
||||
if err != nil {
|
||||
t.Errorf("failed to fetch astronomical information: %s", err)
|
||||
return
|
||||
}
|
||||
if ai.Latitude != la {
|
||||
t.Errorf("AstronomicalInfoByCoordinates failed, expected lat: %f, got: %f", la,
|
||||
ai.Latitude)
|
||||
}
|
||||
if ai.Longitude != lo {
|
||||
t.Errorf("AstronomicalInfoByCoordinates failed, expected lon: %f, got: %f", lo,
|
||||
ai.Longitude)
|
||||
}
|
||||
if ai.Run.UnixMilli() != rt.UnixMilli() {
|
||||
t.Errorf("AstronomicalInfoByCoordinates failed, expected run time: %s, got: %s",
|
||||
rt.String(), ai.Run.String())
|
||||
}
|
||||
if ai.TimeZone != loc.String() {
|
||||
t.Errorf("AstronomicalInfoByCoordinates failed, expected time zone: %s, got: %s",
|
||||
loc.String(), ai.TimeZone)
|
||||
}
|
||||
if ai.NextFullMoon.UnixMilli() != nfmt.UnixMilli() {
|
||||
t.Errorf("AstronomicalInfoByCoordinates failed, expected next full moon: %s, got: %s",
|
||||
nfmt.String(), ai.NextFullMoon.String())
|
||||
}
|
||||
if ai.NextNewMoon.UnixMilli() != nnmt.UnixMilli() {
|
||||
t.Errorf("AstronomicalInfoByCoordinates failed, expected next new moon: %s, got: %s",
|
||||
nnmt.String(), ai.NextNewMoon.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAstronomicalInfo_SunsetByDateString(t *testing.T) {
|
||||
la := 52.5067296
|
||||
lo := 13.2599306
|
||||
loc, err := time.LoadLocation("Europe/Berlin")
|
||||
if err != nil {
|
||||
t.Errorf("failed to load time location data for Europe/Berlin: %s", err)
|
||||
return
|
||||
}
|
||||
ti := time.Date(2023, 5, 28, 21, 16, 37, 0, loc)
|
||||
ddt := time.Date(2023, 5, 28, 0, 0, 0, 0, time.UTC)
|
||||
c := New(withMockAPI())
|
||||
ai, err := c.AstronomicalInfoByCoordinates(la, lo)
|
||||
if err != nil {
|
||||
t.Errorf("failed to fetch astronomical information: %s", err)
|
||||
return
|
||||
}
|
||||
if !ai.SunsetByTime(ti).IsAvailable() {
|
||||
t.Errorf("SunsetByTime failed, expected entry, but got 'not available'")
|
||||
return
|
||||
}
|
||||
if ai.SunsetByTime(ti).Value().UnixMilli() != ti.UnixMilli() {
|
||||
t.Errorf("SunsetByTime failed, expected sunset: %s, got: %s",
|
||||
ti.String(), ai.SunsetByTime(ti).Value().String())
|
||||
}
|
||||
if !ai.SunsetByDateString(ti.Format(DateFormat)).IsAvailable() {
|
||||
t.Errorf("SunsetByDateString failed, expected entry, but got 'not available'")
|
||||
return
|
||||
}
|
||||
if ai.SunsetByTime(ti).String() != ti.Format(time.RFC3339) {
|
||||
t.Errorf("SunsetByTime failed, expected sunset: %s, got: %s",
|
||||
ti.Format(time.RFC3339), ai.SunsetByTime(ti).String())
|
||||
}
|
||||
if ai.SunsetByTime(ti).DateTime().Format(time.RFC3339) != ddt.Format(time.RFC3339) {
|
||||
t.Errorf("SunsetByTime failed, expected sunset: %s, got: %s",
|
||||
ddt.Format(time.RFC3339), ai.SunsetByTime(ti).DateTime().Format(time.RFC3339))
|
||||
}
|
||||
if ai.SunsetByDateString(ti.Format(DateFormat)).Value().UnixMilli() != ti.UnixMilli() {
|
||||
t.Errorf("SunsetByDateString failed, expected sunset: %s, got: %s",
|
||||
ti.String(), ai.SunsetByDateString(ti.Format(DateFormat)).Value().String())
|
||||
}
|
||||
ti = time.Time{}
|
||||
if ai.SunsetByTime(ti).IsAvailable() {
|
||||
t.Errorf("SunsetByTime failed, expected no entry, but got: %s",
|
||||
ai.SunsetByTime(ti).Value().String())
|
||||
}
|
||||
if !ai.SunsetByTime(ti).Value().IsZero() {
|
||||
t.Errorf("SunsetByTime failed, expected no entry, but got: %s",
|
||||
ai.SunsetByTime(ti).Value().String())
|
||||
}
|
||||
if len(ai.SunsetAll()) != 14 {
|
||||
t.Errorf("SunsetByTime failed, expected 14 entired, but got: %d", len(ai.SunsetAll()))
|
||||
return
|
||||
}
|
||||
if ai.SunsetAll()[0].DateTime().Format("2006-01-02") != "2023-05-28" {
|
||||
t.Errorf("SunsetByTime failed, expected first entry to be: %s, got: %s", "2023-05-28",
|
||||
ai.SunsetAll()[0].DateTime().Format("2006-01-02"))
|
||||
}
|
||||
if ai.SunsetAll()[13].DateTime().Format("2006-01-02") != "2023-06-10" {
|
||||
t.Errorf("SunsetByTime failed, expected first entry to be: %s, got: %s", "2023-06-10",
|
||||
ai.SunsetAll()[13].DateTime().Format("2006-01-02"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAstronomicalInfo_SunriseByDateString(t *testing.T) {
|
||||
la := 52.5067296
|
||||
lo := 13.2599306
|
||||
loc, err := time.LoadLocation("Europe/Berlin")
|
||||
if err != nil {
|
||||
t.Errorf("failed to load time location data for Europe/Berlin: %s", err)
|
||||
return
|
||||
}
|
||||
ti := time.Date(2023, 5, 28, 4, 51, 48, 0, loc)
|
||||
ddt := time.Date(2023, 5, 28, 0, 0, 0, 0, time.UTC)
|
||||
c := New(withMockAPI())
|
||||
ai, err := c.AstronomicalInfoByCoordinates(la, lo)
|
||||
if err != nil {
|
||||
t.Errorf("failed to fetch astronomical information: %s", err)
|
||||
return
|
||||
}
|
||||
if !ai.SunriseByTime(ti).IsAvailable() {
|
||||
t.Errorf("SunriseByTime failed, expected entry, but got 'not available'")
|
||||
return
|
||||
}
|
||||
if ai.SunriseByTime(ti).Value().UnixMilli() != ti.UnixMilli() {
|
||||
t.Errorf("SunriseByTime failed, expected sunrise: %s, got: %s",
|
||||
ti.String(), ai.SunriseByTime(ti).Value().String())
|
||||
}
|
||||
if !ai.SunriseByDateString(ti.Format(DateFormat)).IsAvailable() {
|
||||
t.Errorf("SunriseByDateString failed, expected entry, but got 'not available'")
|
||||
return
|
||||
}
|
||||
if ai.SunriseByTime(ti).String() != ti.Format(time.RFC3339) {
|
||||
t.Errorf("SunriseByTime failed, expected sunrise: %s, got: %s",
|
||||
ti.Format(time.RFC3339), ai.SunriseByTime(ti).String())
|
||||
}
|
||||
if ai.SunriseByTime(ti).DateTime().Format(time.RFC3339) != ddt.Format(time.RFC3339) {
|
||||
t.Errorf("SunriseByTime failed, expected sunrise: %s, got: %s",
|
||||
ddt.Format(time.RFC3339), ai.SunriseByTime(ti).DateTime().Format(time.RFC3339))
|
||||
}
|
||||
if ai.SunriseByDateString(ti.Format(DateFormat)).Value().UnixMilli() != ti.UnixMilli() {
|
||||
t.Errorf("SunriseByDateString failed, expected sunrise: %s, got: %s",
|
||||
ti.String(), ai.SunriseByDateString(ti.Format(DateFormat)).Value().String())
|
||||
}
|
||||
ti = time.Time{}
|
||||
if ai.SunriseByTime(ti).IsAvailable() {
|
||||
t.Errorf("SunriseByTime failed, expected no entry, but got: %s",
|
||||
ai.SunriseByTime(ti).Value().String())
|
||||
}
|
||||
if !ai.SunriseByTime(ti).Value().IsZero() {
|
||||
t.Errorf("SunriseByTime failed, expected no entry, but got: %s",
|
||||
ai.SunriseByTime(ti).Value().String())
|
||||
}
|
||||
if len(ai.SunriseAll()) != 14 {
|
||||
t.Errorf("SunriseByTime failed, expected 14 entired, but got: %d", len(ai.SunriseAll()))
|
||||
return
|
||||
}
|
||||
if ai.SunriseAll()[0].DateTime().Format("2006-01-02") != "2023-05-28" {
|
||||
t.Errorf("SunriseByTime failed, expected first entry to be: %s, got: %s", "2023-05-28",
|
||||
ai.SunriseAll()[0].DateTime().Format("2006-01-02"))
|
||||
}
|
||||
if ai.SunriseAll()[13].DateTime().Format("2006-01-02") != "2023-06-10" {
|
||||
t.Errorf("SunriseByTime failed, expected first entry to be: %s, got: %s", "2023-06-10",
|
||||
ai.SunriseAll()[13].DateTime().Format("2006-01-02"))
|
||||
}
|
||||
}
|
|
@ -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
134
condition.go
Normal file
|
@ -0,0 +1,134 @@
|
|||
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package meteologix
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// CondCloudy represents cloudy weather conditions
|
||||
CondCloudy ConditionType = "cloudy"
|
||||
// CondFog represents foggy weather conditions
|
||||
CondFog ConditionType = "fog"
|
||||
// CondFreezingRain represents weather conditions with freezing rain
|
||||
CondFreezingRain ConditionType = "freezingrain"
|
||||
// CondOvercast represents overcast weather conditions
|
||||
CondOvercast ConditionType = "overcast"
|
||||
// CondPartlyCloudy represents partly cloudy weather conditions
|
||||
CondPartlyCloudy ConditionType = "partlycloudy"
|
||||
// CondRain represents rainy weather conditions.
|
||||
// Rain defines as following:
|
||||
// - Falls steadily
|
||||
// - Lasts for hours or days
|
||||
// - Typically widespread throughout your city or town
|
||||
CondRain ConditionType = "rain"
|
||||
// CondRainHeavy represents heavy rain weather conditions
|
||||
CondRainHeavy ConditionType = "rainheavy"
|
||||
// CondShowers represents weather conditions with showers.
|
||||
// Showers define as following:
|
||||
// - Lighter rainfall
|
||||
// - Shorter duration
|
||||
// - Can start and stop over a period of time
|
||||
// - Tends to be more scattered across an area
|
||||
CondShowers ConditionType = "showers"
|
||||
// CondShowersHeavy represents weather conditions with heavy showers
|
||||
CondShowersHeavy ConditionType = "showersheavy"
|
||||
// CondSnow represents snowy weather conditions
|
||||
CondSnow ConditionType = "snow"
|
||||
// CondSnowHeavy represents weather conditions with heavy snow
|
||||
CondSnowHeavy ConditionType = "snowheavy"
|
||||
// CondSnowRain represents weather conditions with snowy rain
|
||||
CondSnowRain ConditionType = "snowrain"
|
||||
// CondSunshine represents clear and sunny weather conditions
|
||||
CondSunshine ConditionType = "sunshine"
|
||||
// CondThunderStorm represents weather conditions with thunderstorms
|
||||
CondThunderStorm ConditionType = "thunderstorm"
|
||||
// CondUnknown represents a unknown weather condition
|
||||
CondUnknown ConditionType = "unknown"
|
||||
)
|
||||
|
||||
// ConditionMap is a map to associate a specific ConditionType to a nicely
|
||||
// formatted, human readable string
|
||||
var ConditionMap = map[ConditionType]string{
|
||||
CondCloudy: "Cloudy",
|
||||
CondFog: "Fog",
|
||||
CondFreezingRain: "Freezing rain",
|
||||
CondOvercast: "Overcast",
|
||||
CondPartlyCloudy: "Partly cloudy",
|
||||
CondRain: "Rain",
|
||||
CondRainHeavy: "Heavy rain",
|
||||
CondShowers: "Showers",
|
||||
CondShowersHeavy: "Heavy showers",
|
||||
CondSnow: "Snow",
|
||||
CondSnowHeavy: "Heavy snow",
|
||||
CondSnowRain: "Sleet",
|
||||
CondSunshine: "Clear sky",
|
||||
CondThunderStorm: "Thunderstorm",
|
||||
CondUnknown: "Unknown",
|
||||
}
|
||||
|
||||
// Condition is a type wrapper of an WeatherData for holding
|
||||
// a specific weather Condition value in the WeatherData
|
||||
type Condition WeatherData
|
||||
|
||||
// ConditionType is a type wrapper for a string type
|
||||
type ConditionType string
|
||||
|
||||
// IsAvailable returns true if a Condition value was available
|
||||
// at time of query
|
||||
func (c Condition) IsAvailable() bool {
|
||||
return !c.na
|
||||
}
|
||||
|
||||
// DateTime returns the timestamp of a Condition value as time.Time
|
||||
func (c Condition) DateTime() time.Time {
|
||||
return c.dt
|
||||
}
|
||||
|
||||
// Value returns the raw value of a Condition as unformatted string
|
||||
// as returned by the API
|
||||
// If the Condition is not available in the WeatherData, Value will
|
||||
// return DataUnavailable instead.
|
||||
func (c Condition) Value() string {
|
||||
if c.na {
|
||||
return DataUnavailable
|
||||
}
|
||||
return c.sv
|
||||
}
|
||||
|
||||
// Condition returns the actual value of that Condition as ConditionType.
|
||||
// If the value is not available or not supported it will return a
|
||||
// CondUnknown
|
||||
func (c Condition) Condition() ConditionType {
|
||||
if c.na {
|
||||
return CondUnknown
|
||||
}
|
||||
if _, ok := ConditionMap[ConditionType(c.sv)]; ok {
|
||||
return ConditionType(c.sv)
|
||||
}
|
||||
return CondUnknown
|
||||
}
|
||||
|
||||
// String returns the formatted, human readable string for a given
|
||||
// Condition type and satisfies the fmt.Stringer interface
|
||||
func (c Condition) String() string {
|
||||
return c.Condition().String()
|
||||
}
|
||||
|
||||
// Source returns the Source of a Condition
|
||||
// If the Source is not available it will return SourceUnknown
|
||||
func (c Condition) Source() Source {
|
||||
return c.s
|
||||
}
|
||||
|
||||
// String returns a human readable, formatted string for a ConditionType and
|
||||
// satisfies the fmt.Stringer interface.
|
||||
func (ct ConditionType) String() string {
|
||||
if cs, ok := ConditionMap[ct]; ok {
|
||||
return cs
|
||||
}
|
||||
return ConditionMap[CondUnknown]
|
||||
}
|
41
condition_test.go
Normal file
41
condition_test.go
Normal file
|
@ -0,0 +1,41 @@
|
|||
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package meteologix
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestCondition_Condition(t *testing.T) {
|
||||
tc := Condition{
|
||||
dt: time.Date(2023, 5, 23, 8, 50, 0, 0, time.UTC),
|
||||
s: SourceAnalysis,
|
||||
sv: "cloudy",
|
||||
}
|
||||
if tc.Condition() != CondCloudy {
|
||||
t.Errorf("Condition failed, expected: %s, got: %s", CondCloudy.String(),
|
||||
tc.Condition().String())
|
||||
}
|
||||
tc = Condition{
|
||||
dt: time.Date(2023, 5, 23, 8, 50, 0, 0, time.UTC),
|
||||
s: SourceAnalysis,
|
||||
sv: "non-existing",
|
||||
}
|
||||
if tc.Condition() != CondUnknown {
|
||||
t.Errorf("Condition failed, expected: %s, got: %s", CondUnknown.String(),
|
||||
tc.Condition().String())
|
||||
}
|
||||
tc = Condition{na: true}
|
||||
if tc.Condition() != CondUnknown {
|
||||
t.Errorf("Condition failed, expected: %s, got: %s", CondUnknown.String(),
|
||||
tc.Condition().String())
|
||||
}
|
||||
ct := ConditionType("foo")
|
||||
if ct.String() != CondUnknown.String() {
|
||||
t.Errorf("Condition.String for unknown type failed, expected: %s, got: %s",
|
||||
CondUnknown.String(), ct.String())
|
||||
}
|
||||
}
|
361
curweather.go
Normal file
361
curweather.go
Normal 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
1029
curweather_test.go
Normal file
File diff suppressed because it is too large
Load diff
155
datatype.go
Normal file
155
datatype.go
Normal 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
37
datatype_test.go
Normal file
|
@ -0,0 +1,37 @@
|
|||
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
package meteologix
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAPIDate_UnmarshalJSON(t *testing.T) {
|
||||
type x struct {
|
||||
Date APIDate `json:"date"`
|
||||
}
|
||||
okd := []byte(`{"date":"2023-05-28"}`)
|
||||
nokd := []byte(`{"date":"2023-05-32"}`)
|
||||
null := []byte(`{"date":null}`)
|
||||
var d x
|
||||
if err := json.Unmarshal(okd, &d); err != nil {
|
||||
t.Errorf("APIDate_UnmarshalJSON failed: %s", err)
|
||||
}
|
||||
if d.Date.Format(DateFormat) != "2023-05-28" {
|
||||
t.Errorf("APIDate_UnmarshalJSON failed, expected: %s, but got: %s",
|
||||
"2023-05-28", d.Date.String())
|
||||
}
|
||||
if err := json.Unmarshal(nokd, &d); err == nil {
|
||||
t.Errorf("APIDate_UnmarshalJSON was supposed to fail, but didn't")
|
||||
}
|
||||
d = x{}
|
||||
if err := json.Unmarshal(null, &d); err != nil {
|
||||
t.Errorf("APIDate_UnmarshalJSON failed: %s", err)
|
||||
}
|
||||
if !d.Date.IsZero() {
|
||||
t.Errorf("APIDate_UnmarshalJSON with null was supposed to be empty, but got: %s",
|
||||
d.Date.String())
|
||||
}
|
||||
}
|
40
datetime.go
Normal file
40
datetime.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package meteologix
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// DateTime is a type wrapper of an WeatherData for holding datetime
|
||||
// values in WeatherData
|
||||
type DateTime WeatherData
|
||||
|
||||
// IsAvailable returns true if an Direction value was
|
||||
// available at time of query
|
||||
func (dt DateTime) IsAvailable() bool {
|
||||
return !dt.na
|
||||
}
|
||||
|
||||
// DateTime returns the timestamp for that specific DateTime type
|
||||
func (dt DateTime) DateTime() time.Time {
|
||||
return dt.dt
|
||||
}
|
||||
|
||||
// Value returns the time.Time value of an DateTime value
|
||||
// If the DateTime is not available in the WeatherData
|
||||
// Value will return time.Time with a zero value instead.
|
||||
func (dt DateTime) Value() time.Time {
|
||||
if dt.na {
|
||||
return time.Time{}
|
||||
}
|
||||
return dt.dv
|
||||
}
|
||||
|
||||
// String satisfies the fmt.Stringer interface for the DateTime type
|
||||
// The date will returned as time.RFC3339 format
|
||||
func (dt DateTime) String() string {
|
||||
return dt.Value().Format(time.RFC3339)
|
||||
}
|
47
density.go
Normal file
47
density.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package meteologix
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Density is a type wrapper of WeatherData for holding density
|
||||
// values in kg/m³ in WeatherData
|
||||
type Density WeatherData
|
||||
|
||||
// IsAvailable returns true if an Density value was
|
||||
// available at time of query
|
||||
func (d Density) IsAvailable() bool {
|
||||
return !d.na
|
||||
}
|
||||
|
||||
// DateTime returns the DateTime of the queried Density value
|
||||
func (d Density) DateTime() time.Time {
|
||||
return d.dt
|
||||
}
|
||||
|
||||
// String satisfies the fmt.Stringer interface for the Density type
|
||||
func (d Density) String() string {
|
||||
return fmt.Sprintf("%.1fkg/m³", d.fv)
|
||||
}
|
||||
|
||||
// Source returns the Source of Density
|
||||
// If the Source is not available it will return SourceUnknown
|
||||
func (d Density) Source() Source {
|
||||
return d.s
|
||||
}
|
||||
|
||||
// Value returns the float64 value of an Density
|
||||
// If the Density is not available in the WeatherData
|
||||
// Vaule will return math.NaN instead.
|
||||
func (d Density) Value() float64 {
|
||||
if d.na {
|
||||
return math.NaN()
|
||||
}
|
||||
return d.fv
|
||||
}
|
134
direction.go
Normal file
134
direction.go
Normal 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
37
direction_test.go
Normal file
|
@ -0,0 +1,37 @@
|
|||
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package meteologix
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFindDirection(t *testing.T) {
|
||||
// Prepare test cases
|
||||
tt := []struct {
|
||||
v float64
|
||||
dm map[float64]string
|
||||
er string
|
||||
}{
|
||||
{15, WindDirAbbrMap, "NbE"},
|
||||
{47, WindDirAbbrMap, "NE"},
|
||||
{200, WindDirAbbrMap, "SSW"},
|
||||
{330, WindDirAbbrMap, "NWbN"},
|
||||
{15, WindDirFullMap, "North by East"},
|
||||
{47, WindDirFullMap, "Northeast"},
|
||||
{200, WindDirFullMap, "South-Southwest"},
|
||||
{330, WindDirFullMap, "Northwest by North"},
|
||||
}
|
||||
|
||||
// Run tests
|
||||
for _, tc := range tt {
|
||||
t.Run("", func(t *testing.T) {
|
||||
r := findDirection(tc.v, tc.dm)
|
||||
if tc.er != r {
|
||||
t.Errorf("findDirection failed, expected: %s, got: %s", tc.er, r)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
4
doc.go
4
doc.go
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
4
go.mod
|
@ -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
85
height.go
Normal file
|
@ -0,0 +1,85 @@
|
|||
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package meteologix
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Height is a type wrapper of an WeatherData for holding height
|
||||
// values in WeatherData (based on meters a default unit)
|
||||
type Height WeatherData
|
||||
|
||||
// IsAvailable returns true if an Height value was
|
||||
// available at time of query
|
||||
func (h Height) IsAvailable() bool {
|
||||
return !h.na
|
||||
}
|
||||
|
||||
// DateTime returns the timestamp associated with the Height value
|
||||
func (h Height) DateTime() time.Time {
|
||||
return h.dt
|
||||
}
|
||||
|
||||
// String satisfies the fmt.Stringer interface for the Height type
|
||||
func (h Height) String() string {
|
||||
return fmt.Sprintf("%.3fm", h.fv)
|
||||
}
|
||||
|
||||
// Source returns the Source of Height
|
||||
// If the Source is not available it will return SourceUnknown
|
||||
func (h Height) Source() Source {
|
||||
return h.s
|
||||
}
|
||||
|
||||
// Value returns the float64 value of an Height
|
||||
// If the Height is not available in the WeatherData
|
||||
// Vaule will return math.NaN instead.
|
||||
func (h Height) Value() float64 {
|
||||
if h.na {
|
||||
return math.NaN()
|
||||
}
|
||||
return h.fv
|
||||
}
|
||||
|
||||
// Meter returns the Height type value as float64 in meters.
|
||||
// This is an alias for the Value() method
|
||||
func (h Height) Meter() float64 {
|
||||
return h.Value()
|
||||
}
|
||||
|
||||
// MeterString returns the Height type as formatted string in meters
|
||||
// This is an alias for the String() method
|
||||
func (h Height) MeterString() string {
|
||||
return h.String()
|
||||
}
|
||||
|
||||
// CentiMeter returns the Height type value as float64 in centimeters.
|
||||
func (h Height) CentiMeter() float64 {
|
||||
if h.na {
|
||||
return math.NaN()
|
||||
}
|
||||
return h.fv * 100
|
||||
}
|
||||
|
||||
// CentiMeterString returns the Height type as formatted string in centimeters
|
||||
func (h Height) CentiMeterString() string {
|
||||
return fmt.Sprintf("%.3fcm", h.CentiMeter())
|
||||
}
|
||||
|
||||
// MilliMeter returns the Height type value as float64 in milliimeters.
|
||||
func (h Height) MilliMeter() float64 {
|
||||
if h.na {
|
||||
return math.NaN()
|
||||
}
|
||||
return h.fv * 1000
|
||||
}
|
||||
|
||||
// MilliMeterString returns the Height type as formatted string in millimeters
|
||||
func (h Height) MilliMeterString() string {
|
||||
return fmt.Sprintf("%.3fmm", h.MilliMeter())
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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
48
humidity.go
Normal 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
|
||||
}
|
|
@ -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 == "" {
|
||||
|
|
|
@ -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))
|
||||
|
|
526
observation.go
526
observation.go
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
47
precipitation.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package meteologix
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 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
47
pressure.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
// SPDX-FileCopyrightText: 2023 Winni Neessen <wn@neessen.dev>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package meteologix
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 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
48
radiation.go
Normal 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
6
sonar-project.properties
Normal 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
58
source.go
Normal 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
56
source_test.go
Normal 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
87
speed.go
Normal 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())
|
||||
}
|
68
station.go
68
station.go
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
72
temperature.go
Normal 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())
|
||||
}
|
Loading…
Reference in a new issue