From 03d060ea9570dd5e8a50a43820902173fb4b1259 Mon Sep 17 00:00:00 2001 From: Jakub Jarosz Date: Fri, 25 Oct 2019 21:50:17 +0100 Subject: [PATCH 01/28] Initial commit --- .gitignore | 12 ++++++++++++ LICENSE | 21 +++++++++++++++++++++ README.md | 2 ++ 3 files changed, 35 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f1c181e --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6df38f7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Jakub Jarosz + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..42d725b --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# yrno +Go library for the Norwegian Meteorological Institute and the Norwegian Broadcasting Corporation. From 962f6f64dc598d9afbf45989d15837eb839b38dc Mon Sep 17 00:00:00 2001 From: Jakub Jarosz Date: Tue, 30 Mar 2021 08:21:12 +0300 Subject: [PATCH 02/28] Update README.md --- README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 42d725b..6ca2824 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,7 @@ -# yrno -Go library for the Norwegian Meteorological Institute and the Norwegian Broadcasting Corporation. +# meteo + +Go SDK for the weather and meteorological forecast from Yr. + +Disclaimer: + +Weather forecast from Yr, delivered by the Norwegian Meteorological Institute and NRK From 29e94ec1eed13157da521ab6ef68d36bc4991965 Mon Sep 17 00:00:00 2001 From: Jakub Jarosz Date: Tue, 30 Mar 2021 08:27:06 +0300 Subject: [PATCH 03/28] initial commit --- .gitignore | 2 ++ go.mod | 3 +++ meteo.go | 1 + meteo_test.go | 1 + 4 files changed, 7 insertions(+) create mode 100644 go.mod create mode 100644 meteo.go create mode 100644 meteo_test.go diff --git a/.gitignore b/.gitignore index f1c181e..d57e280 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out +vendor/ + diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..07f2d78 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/qba73/meteo + +go 1.16 diff --git a/meteo.go b/meteo.go new file mode 100644 index 0000000..6ab8e6d --- /dev/null +++ b/meteo.go @@ -0,0 +1 @@ +package meteo \ No newline at end of file diff --git a/meteo_test.go b/meteo_test.go new file mode 100644 index 0000000..6630f7f --- /dev/null +++ b/meteo_test.go @@ -0,0 +1 @@ +package meteo_test From 0344b29bbad060f9f1268d39ee26c529dd3030d7 Mon Sep 17 00:00:00 2001 From: Jakub Jarosz Date: Tue, 30 Mar 2021 08:29:17 +0300 Subject: [PATCH 04/28] Create go.yml --- .github/workflows/go.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/go.yml diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 0000000..872dded --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,22 @@ +name: Go + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.16 + + - name: Test + run: go test -v ./... From 90a789018b9d17015caa7c05debca64233314207 Mon Sep 17 00:00:00 2001 From: Jakub Jarosz Date: Tue, 30 Mar 2021 08:30:53 +0300 Subject: [PATCH 05/28] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 6ca2824..c577868 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +![Go](https://github.com/qba73/meteo/workflows/Go/badge.svg) + # meteo Go SDK for the weather and meteorological forecast from Yr. From 55a0d251f4be925842969c0f071fd8c3681cc003 Mon Sep 17 00:00:00 2001 From: Jakub Jarosz Date: Tue, 30 Mar 2021 08:32:19 +0300 Subject: [PATCH 06/28] gofmt --- meteo.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meteo.go b/meteo.go index 6ab8e6d..ce3d71e 100644 --- a/meteo.go +++ b/meteo.go @@ -1 +1 @@ -package meteo \ No newline at end of file +package meteo From 23d424d9e48d42dac29db3cd04fffdf673b30ace Mon Sep 17 00:00:00 2001 From: Jakub Jarosz Date: Tue, 30 Mar 2021 08:33:22 +0300 Subject: [PATCH 07/28] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c577868..1b0c9a2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ ![Go](https://github.com/qba73/meteo/workflows/Go/badge.svg) +[![Go Report Card](https://goreportcard.com/badge/github.com/qba73/meteo)](https://goreportcard.com/report/github.com/qba73/meteo) # meteo From 538a6f89e62498af26f4c7fee53f4fb7caab9912 Mon Sep 17 00:00:00 2001 From: Jakub Jarosz Date: Wed, 2 Jun 2021 01:08:40 +0100 Subject: [PATCH 08/28] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1b0c9a2..2908222 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ # meteo -Go SDK for the weather and meteorological forecast from Yr. +Go client li brary for the weather and meteorological forecast from [Yr](https://www.yr.no/en). Disclaimer: From 68404db890ea6fda7a23731e7d2a1835043d1754 Mon Sep 17 00:00:00 2001 From: Jakub Jarosz Date: Fri, 3 Sep 2021 00:16:48 +0100 Subject: [PATCH 09/28] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2908222..34753da 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ # meteo -Go client li brary for the weather and meteorological forecast from [Yr](https://www.yr.no/en). +Go client library for the weather and meteorological forecast from [Yr](https://www.yr.no/en). Disclaimer: From fa913ed2a2f56071815681f63bf82d50ebf533a3 Mon Sep 17 00:00:00 2001 From: Jakub Jarosz Date: Fri, 8 Oct 2021 06:42:00 +0100 Subject: [PATCH 10/28] initial lib structure --- .github/workflows/go.yml | 20 +++++----- .gitignore | 2 + Makefile | 83 ++++++++++++++++++++++++++++++++++++++++ examples/basic/main.go | 7 ++++ go.mod | 2 +- meteo.go | 39 +++++++++++++++++++ meteo_test.go | 11 ++++++ 7 files changed, 152 insertions(+), 12 deletions(-) create mode 100644 Makefile create mode 100644 examples/basic/main.go diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 872dded..3d1acd3 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -2,21 +2,19 @@ name: Go on: push: - branches: [ master ] + branches: [main] pull_request: - branches: [ master ] + branches: [main] jobs: - build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - - name: Set up Go - uses: actions/setup-go@v2 - with: - go-version: 1.16 + - name: Checkout main + uses: actions/checkout@v2 - - name: Test - run: go test -v ./... + - name: Run unit tests + run: | + make clean test-ci + env: + CC_TEST_REPORTER_ID: ${{secrets.CC_TEST_REPORTER_ID}} diff --git a/.gitignore b/.gitignore index d57e280..0c517cb 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,8 @@ # Test binary, build with `go test -c` *.test +*.html + # Output of the go coverage tool, specifically when used with LiteIDE *.out diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..349c4a9 --- /dev/null +++ b/Makefile @@ -0,0 +1,83 @@ +.PHONY: help check cover test tidy + +ROOT := $(PWD) +GO_HTML_COV := ./coverage.html +GO_TEST_OUTFILE := ./c.out +GO_DOCKER_IMAGE := golang:1.17 +GO_DOCKER_CONTAINER := meteo-container +CC_TEST_REPORTER_ID := ${CC_TEST_REPORTER_ID} +CC_PREFIX := github.com/qba73/meteo + + +define PRINT_HELP_PYSCRIPT +import re, sys + +for line in sys.stdin: + match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) + if match: + target, help = match.groups() + print("%-20s %s" % (target, help)) +endef +export PRINT_HELP_PYSCRIPT + +default: help + +help: + @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) + +check: ## Run static check analyzer + staticcheck ./... + +cover: ## Run unit tests and generate test coverage report + go test -v ./... -count=1 -covermode=count -coverprofile=coverage.out + go tool cover -html coverage.out + staticcheck ./... + +test: ## Run unit tests locally + go test -v ./... -count=1 + staticcheck ./... + +# MODULES +tidy: ## Run go mod tidy and vendor + go mod tidy + go mod vendor + + +clean: ## Remove docker container if exist + docker rm -f ${GO_DOCKER_CONTAINER} || true + +testdocker: ## Run unittests inside a container + docker run -w /app -v ${ROOT}:/app ${GO_DOCKER_IMAGE} go test ./... -coverprofile=${GO_TEST_OUTFILE} + docker run -w /app -v ${ROOT}:/app ${GO_DOCKER_IMAGE} go tool cover -html=${GO_TEST_OUTFILE} -o ${GO_HTML_COV} + +lint: ## Run linter inside container + docker run --rm -v ${ROOT}:/data cytopia/golint . + +# Custom logic for code climate +_before-cc: + # Download CC test reported + docker run -w /app -v ${ROOT}:/app ${GO_DOCKER_IMAGE} \ + /bin/bash -c \ + "curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter" + + # Make reporter executable + docker run -w /app -v ${ROOT}:/app ${GO_DOCKER_IMAGE} chmod +x ./cc-test-reporter + + # Run before build + docker run -w /app -v ${ROOT}:/app \ + -e CC_TEST_REPORTER_ID=${CC_TEST_REPORTER_ID} ${GO_DOCKER_IMAGE} \ + ./cc-test-reporter before-build + +_after-cc: + # Handle custom prefix + $(eval PREFIX=${CC_PREFIX}) +ifdef prefix + $(eval PREFIX=${prefix}) +endif + # Upload data to CC + docker run -w /app -v ${ROOT}:/app \ + -e CC_TEST_REPORTER_ID=${CC_TEST_REPORTER_ID} \ + ${GO_DOCKER_IMAGE} ./cc-test-reporter after-build --prefix ${PREFIX} + +test-ci: _before-cc testdocker _after-cc + diff --git a/examples/basic/main.go b/examples/basic/main.go new file mode 100644 index 0000000..49f813c --- /dev/null +++ b/examples/basic/main.go @@ -0,0 +1,7 @@ +package main + +import "fmt" + +func main() { + fmt.Println("Example 1") +} diff --git a/go.mod b/go.mod index 07f2d78..12f99f9 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module github.com/qba73/meteo -go 1.16 +go 1.17 diff --git a/meteo.go b/meteo.go index ce3d71e..1900ec7 100644 --- a/meteo.go +++ b/meteo.go @@ -1 +1,40 @@ package meteo + +import ( + "net/http" + "time" +) + +const ( + baseURL = "https://" +) + +// Client represents YR.no weather client. +type Client struct { + apiKey string + BaseURL string + HTTPClient *http.Client +} + +// NewClient knows how to construct a new meteo client. +func NewClient(apikey string) *Client { + return &Client{ + BaseURL: baseURL, + apiKey: apikey, + HTTPClient: &http.Client{ + Timeout: time.Second * 10, + }, + } +} + +/* +type errorResponse struct { + Code int `json:"code"` + Message string `json:"message"` +} + +type successResponse struct { + Code int `json:"code"` + Data interface{} `json:"data"` +} +*/ diff --git a/meteo_test.go b/meteo_test.go index 6630f7f..fad50e0 100644 --- a/meteo_test.go +++ b/meteo_test.go @@ -1 +1,12 @@ package meteo_test + +import ( + "testing" + + "github.com/qba73/meteo" +) + +func TestNewClient(t *testing.T) { + meteo.NewClient("APIKEY") + +} From 67fb5a40beb2fffeb6606426a6c881afd5f35ed8 Mon Sep 17 00:00:00 2001 From: Jakub Jarosz Date: Fri, 8 Oct 2021 06:43:08 +0100 Subject: [PATCH 11/28] update docs --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 34753da..6ce8028 100644 --- a/README.md +++ b/README.md @@ -7,4 +7,5 @@ Go client library for the weather and meteorological forecast from [Yr](https:// Disclaimer: -Weather forecast from Yr, delivered by the Norwegian Meteorological Institute and NRK +Weather forecast from Yr, delivered by the Norwegian Meteorological Institute and NRK. + From 7c2ffc4c7a1cc2ae07fcfa3232bf82bb8655f50e Mon Sep 17 00:00:00 2001 From: Jakub Jarosz Date: Thu, 14 Oct 2021 21:03:09 +0100 Subject: [PATCH 12/28] wip - construct a client --- LICENSE | 2 +- Makefile | 1 - doc.go | 3 +++ meteo.go | 14 +------------- meteo_test.go | 1 - 5 files changed, 5 insertions(+), 16 deletions(-) create mode 100644 doc.go diff --git a/LICENSE b/LICENSE index 6df38f7..b4dc37a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019 Jakub Jarosz +Copyright (c) 2021 Jakub Jarosz Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Makefile b/Makefile index 349c4a9..6e2725c 100644 --- a/Makefile +++ b/Makefile @@ -80,4 +80,3 @@ endif ${GO_DOCKER_IMAGE} ./cc-test-reporter after-build --prefix ${PREFIX} test-ci: _before-cc testdocker _after-cc - diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..fdfc322 --- /dev/null +++ b/doc.go @@ -0,0 +1,3 @@ +// package meteo is a client library for the weather +// API data provided by the Norwegian Meteorological Institute. +package meteo diff --git a/meteo.go b/meteo.go index 1900ec7..b561eaa 100644 --- a/meteo.go +++ b/meteo.go @@ -16,7 +16,7 @@ type Client struct { HTTPClient *http.Client } -// NewClient knows how to construct a new meteo client. +// NewClient knows how to create Meteo service client. func NewClient(apikey string) *Client { return &Client{ BaseURL: baseURL, @@ -26,15 +26,3 @@ func NewClient(apikey string) *Client { }, } } - -/* -type errorResponse struct { - Code int `json:"code"` - Message string `json:"message"` -} - -type successResponse struct { - Code int `json:"code"` - Data interface{} `json:"data"` -} -*/ diff --git a/meteo_test.go b/meteo_test.go index fad50e0..30b7fcb 100644 --- a/meteo_test.go +++ b/meteo_test.go @@ -8,5 +8,4 @@ import ( func TestNewClient(t *testing.T) { meteo.NewClient("APIKEY") - } From adacdc57078a4b5f76ed04e5ee63f30b1d6e3797 Mon Sep 17 00:00:00 2001 From: Jakub Jarosz Date: Fri, 29 Oct 2021 06:31:52 +0100 Subject: [PATCH 13/28] Add test data --- doc.go | 2 +- testdata/response-compact.json | 2871 +++++++++++++++++++++++++ testdata/response-complete.json | 3519 +++++++++++++++++++++++++++++++ 3 files changed, 6391 insertions(+), 1 deletion(-) create mode 100644 testdata/response-compact.json create mode 100644 testdata/response-complete.json diff --git a/doc.go b/doc.go index fdfc322..46db475 100644 --- a/doc.go +++ b/doc.go @@ -1,3 +1,3 @@ -// package meteo is a client library for the weather +// Package meteo is a client library for the weather // API data provided by the Norwegian Meteorological Institute. package meteo diff --git a/testdata/response-compact.json b/testdata/response-compact.json new file mode 100644 index 0000000..ad8a5e4 --- /dev/null +++ b/testdata/response-compact.json @@ -0,0 +1,2871 @@ +{ + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -6.2, + 53.3, + 39 + ] + }, + "properties": { + "meta": { + "updated_at": "2021-10-28T01:28:31Z", + "units": { + "air_pressure_at_sea_level": "hPa", + "air_temperature": "celsius", + "cloud_area_fraction": "%", + "precipitation_amount": "mm", + "relative_humidity": "%", + "wind_from_direction": "degrees", + "wind_speed": "m/s" + } + }, + "timeseries": [ + { + "time": "2021-10-28T05:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 999.7, + "air_temperature": 13.7, + "cloud_area_fraction": 100.0, + "relative_humidity": 94.0, + "wind_from_direction": 180.3, + "wind_speed": 1.5 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "rain" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "rain" + }, + "details": { + "precipitation_amount": 0.6 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "heavyrain" + }, + "details": { + "precipitation_amount": 6.1 + } + } + } + }, + { + "time": "2021-10-28T06:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 999.3, + "air_temperature": 13.7, + "cloud_area_fraction": 100.0, + "relative_humidity": 93.8, + "wind_from_direction": 162.3, + "wind_speed": 2.3 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "lightrain" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "rain" + }, + "details": { + "precipitation_amount": 0.6 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "heavyrain" + }, + "details": { + "precipitation_amount": 6.7 + } + } + } + }, + { + "time": "2021-10-28T07:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 998.6, + "air_temperature": 13.5, + "cloud_area_fraction": 100.0, + "relative_humidity": 92.3, + "wind_from_direction": 164.6, + "wind_speed": 2.6 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "lightrain" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "rain" + }, + "details": { + "precipitation_amount": 0.3 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "heavyrain" + }, + "details": { + "precipitation_amount": 6.1 + } + } + } + }, + { + "time": "2021-10-28T08:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 998.4, + "air_temperature": 13.5, + "cloud_area_fraction": 100.0, + "relative_humidity": 90.8, + "wind_from_direction": 170.7, + "wind_speed": 2.9 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "lightrain" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "rain" + }, + "details": { + "precipitation_amount": 0.3 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "heavyrain" + }, + "details": { + "precipitation_amount": 5.8 + } + } + } + }, + { + "time": "2021-10-28T09:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 997.7, + "air_temperature": 13.7, + "cloud_area_fraction": 100.0, + "relative_humidity": 91.2, + "wind_from_direction": 165.9, + "wind_speed": 3.8 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "lightrain" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "heavyrain" + }, + "details": { + "precipitation_amount": 1.9 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "heavyrain" + }, + "details": { + "precipitation_amount": 5.7 + } + } + } + }, + { + "time": "2021-10-28T10:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 996.8, + "air_temperature": 14.1, + "cloud_area_fraction": 100.0, + "relative_humidity": 90.9, + "wind_from_direction": 184.2, + "wind_speed": 3.4 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "lightrain" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "heavyrain" + }, + "details": { + "precipitation_amount": 2.4 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "rain" + }, + "details": { + "precipitation_amount": 3.9 + } + } + } + }, + { + "time": "2021-10-28T11:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 996.1, + "air_temperature": 13.9, + "cloud_area_fraction": 100.0, + "relative_humidity": 90.7, + "wind_from_direction": 189.4, + "wind_speed": 2.5 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "rain" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "heavyrain" + }, + "details": { + "precipitation_amount": 1.1 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "rain" + }, + "details": { + "precipitation_amount": 1.5 + } + } + } + }, + { + "time": "2021-10-28T12:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 995.4, + "air_temperature": 14.1, + "cloud_area_fraction": 100.0, + "relative_humidity": 89.6, + "wind_from_direction": 221.4, + "wind_speed": 1.4 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "lightrain" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.3 + } + } + } + }, + { + "time": "2021-10-28T13:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 995.5, + "air_temperature": 14.3, + "cloud_area_fraction": 100.0, + "relative_humidity": 89.3, + "wind_from_direction": 249.6, + "wind_speed": 3.0 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "lightrain" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.3 + } + } + } + }, + { + "time": "2021-10-28T14:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 995.3, + "air_temperature": 14.4, + "cloud_area_fraction": 100.0, + "relative_humidity": 87.7, + "wind_from_direction": 220.0, + "wind_speed": 3.2 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "lightrain" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "lightrain" + }, + "details": { + "precipitation_amount": 0.2 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.3 + } + } + } + }, + { + "time": "2021-10-28T15:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 995.3, + "air_temperature": 14.2, + "cloud_area_fraction": 100.0, + "relative_humidity": 85.5, + "wind_from_direction": 225.8, + "wind_speed": 3.1 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "lightrain" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-28T16:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 995.7, + "air_temperature": 13.4, + "cloud_area_fraction": 100.0, + "relative_humidity": 83.7, + "wind_from_direction": 248.3, + "wind_speed": 4.0 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "lightrain" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.1 + } + } + } + }, + { + "time": "2021-10-28T17:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 995.7, + "air_temperature": 12.7, + "cloud_area_fraction": 100.0, + "relative_humidity": 85.8, + "wind_from_direction": 250.6, + "wind_speed": 3.0 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "rain" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "partlycloudy_night" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "rain" + }, + "details": { + "precipitation_amount": 1.2 + } + } + } + }, + { + "time": "2021-10-28T18:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 995.9, + "air_temperature": 12.0, + "cloud_area_fraction": 100.0, + "relative_humidity": 90.0, + "wind_from_direction": 253.5, + "wind_speed": 2.7 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "rain" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "partlycloudy_night" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "rain" + }, + "details": { + "precipitation_amount": 1.6 + } + } + } + }, + { + "time": "2021-10-28T19:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 995.7, + "air_temperature": 11.6, + "cloud_area_fraction": 100.0, + "relative_humidity": 91.4, + "wind_from_direction": 255.6, + "wind_speed": 2.3 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "rain" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "rain" + }, + "details": { + "precipitation_amount": 1.6 + } + } + } + }, + { + "time": "2021-10-28T20:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 995.5, + "air_temperature": 11.3, + "cloud_area_fraction": 100.0, + "relative_humidity": 91.6, + "wind_from_direction": 224.5, + "wind_speed": 1.2 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "rain" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "rain" + }, + "details": { + "precipitation_amount": 1.8 + } + } + } + }, + { + "time": "2021-10-28T21:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 995.4, + "air_temperature": 11.2, + "cloud_area_fraction": 100.0, + "relative_humidity": 92.3, + "wind_from_direction": 189.3, + "wind_speed": 1.4 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "rain" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "lightrain" + }, + "details": { + "precipitation_amount": 0.1 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "rain" + }, + "details": { + "precipitation_amount": 3.0 + } + } + } + }, + { + "time": "2021-10-28T22:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 995.3, + "air_temperature": 10.9, + "cloud_area_fraction": 100.0, + "relative_humidity": 95.2, + "wind_from_direction": 213.3, + "wind_speed": 1.1 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "rain" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "heavyrain" + }, + "details": { + "precipitation_amount": 1.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "rain" + }, + "details": { + "precipitation_amount": 4.4 + } + } + } + }, + { + "time": "2021-10-28T23:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 995.1, + "air_temperature": 10.6, + "cloud_area_fraction": 100.0, + "relative_humidity": 95.5, + "wind_from_direction": 198.3, + "wind_speed": 1.0 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "rain" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "rain" + }, + "details": { + "precipitation_amount": 0.5 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "rain" + }, + "details": { + "precipitation_amount": 4.0 + } + } + } + }, + { + "time": "2021-10-29T00:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 993.8, + "air_temperature": 10.7, + "cloud_area_fraction": 100.0, + "relative_humidity": 96.1, + "wind_from_direction": 353.3, + "wind_speed": 0.8 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "rain" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "rain" + }, + "details": { + "precipitation_amount": 4.7 + } + } + } + }, + { + "time": "2021-10-29T01:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 993.3, + "air_temperature": 10.5, + "cloud_area_fraction": 100.0, + "relative_humidity": 97.0, + "wind_from_direction": 309.1, + "wind_speed": 0.8 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "rainshowers_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "lightrain" + }, + "details": { + "precipitation_amount": 0.2 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "heavyrain" + }, + "details": { + "precipitation_amount": 6.3 + } + } + } + }, + { + "time": "2021-10-29T02:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 993.1, + "air_temperature": 10.4, + "cloud_area_fraction": 100.0, + "relative_humidity": 96.3, + "wind_from_direction": 303.9, + "wind_speed": 0.2 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "lightrainshowers_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "heavyrain" + }, + "details": { + "precipitation_amount": 1.2 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "heavyrain" + }, + "details": { + "precipitation_amount": 7.7 + } + } + } + }, + { + "time": "2021-10-29T03:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 992.3, + "air_temperature": 10.4, + "cloud_area_fraction": 100.0, + "relative_humidity": 94.2, + "wind_from_direction": 216.9, + "wind_speed": 0.7 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "lightrainshowers_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "heavyrain" + }, + "details": { + "precipitation_amount": 1.5 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "heavyrain" + }, + "details": { + "precipitation_amount": 6.7 + } + } + } + }, + { + "time": "2021-10-29T04:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 991.5, + "air_temperature": 10.2, + "cloud_area_fraction": 100.0, + "relative_humidity": 95.7, + "wind_from_direction": 306.6, + "wind_speed": 1.2 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "lightrainshowers_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "rain" + }, + "details": { + "precipitation_amount": 0.6 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "heavyrain" + }, + "details": { + "precipitation_amount": 5.2 + } + } + } + }, + { + "time": "2021-10-29T05:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 991.0, + "air_temperature": 9.8, + "cloud_area_fraction": 100.0, + "relative_humidity": 98.0, + "wind_from_direction": 321.5, + "wind_speed": 1.6 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "lightrainshowers_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "heavyrain" + }, + "details": { + "precipitation_amount": 1.2 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "rainshowers_day" + }, + "details": { + "precipitation_amount": 4.6 + } + } + } + }, + { + "time": "2021-10-29T06:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 991.4, + "air_temperature": 9.5, + "cloud_area_fraction": 100.0, + "relative_humidity": 95.6, + "wind_from_direction": 290.1, + "wind_speed": 3.5 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "lightrainshowers_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "heavyrain" + }, + "details": { + "precipitation_amount": 1.6 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "rainshowers_day" + }, + "details": { + "precipitation_amount": 3.4 + } + } + } + }, + { + "time": "2021-10-29T07:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 991.6, + "air_temperature": 9.7, + "cloud_area_fraction": 100.0, + "relative_humidity": 93.1, + "wind_from_direction": 290.8, + "wind_speed": 4.8 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "lightrainshowers_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "heavyrain" + }, + "details": { + "precipitation_amount": 1.5 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "rainshowers_day" + }, + "details": { + "precipitation_amount": 1.8 + } + } + } + }, + { + "time": "2021-10-29T08:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 992.2, + "air_temperature": 9.6, + "cloud_area_fraction": 100.0, + "relative_humidity": 87.5, + "wind_from_direction": 280.5, + "wind_speed": 3.8 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "lightrain" + }, + "details": { + "precipitation_amount": 0.2 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + }, + "details": { + "precipitation_amount": 0.3 + } + } + } + }, + { + "time": "2021-10-29T09:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 992.7, + "air_temperature": 9.5, + "cloud_area_fraction": 100.0, + "relative_humidity": 88.9, + "wind_from_direction": 266.7, + "wind_speed": 3.6 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "fair_day" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-29T10:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 992.9, + "air_temperature": 9.9, + "cloud_area_fraction": 92.2, + "relative_humidity": 87.8, + "wind_from_direction": 253.5, + "wind_speed": 3.7 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "fair_day" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "fair_day" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-29T11:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 993.2, + "air_temperature": 10.7, + "cloud_area_fraction": 21.9, + "relative_humidity": 81.2, + "wind_from_direction": 244.3, + "wind_speed": 4.2 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "fair_day" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-29T12:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 993.3, + "air_temperature": 11.5, + "cloud_area_fraction": 18.7, + "relative_humidity": 72.1, + "wind_from_direction": 241.2, + "wind_speed": 3.8 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "fair_day" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-29T13:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 993.1, + "air_temperature": 12.2, + "cloud_area_fraction": 14.1, + "relative_humidity": 65.9, + "wind_from_direction": 236.8, + "wind_speed": 3.6 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "fair_day" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-29T14:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 992.7, + "air_temperature": 12.5, + "cloud_area_fraction": 29.7, + "relative_humidity": 63.0, + "wind_from_direction": 226.5, + "wind_speed": 3.5 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "cloudy" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "fair_day" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-29T15:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 992.4, + "air_temperature": 12.5, + "cloud_area_fraction": 26.6, + "relative_humidity": 63.7, + "wind_from_direction": 204.7, + "wind_speed": 2.9 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "cloudy" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-29T16:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 992.3, + "air_temperature": 12.1, + "cloud_area_fraction": 78.1, + "relative_humidity": 66.0, + "wind_from_direction": 186.3, + "wind_speed": 2.7 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "cloudy" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-29T17:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 992.1, + "air_temperature": 10.9, + "cloud_area_fraction": 99.2, + "relative_humidity": 75.7, + "wind_from_direction": 169.1, + "wind_speed": 2.9 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-29T18:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 991.9, + "air_temperature": 10.1, + "cloud_area_fraction": 99.2, + "relative_humidity": 80.8, + "wind_from_direction": 172.5, + "wind_speed": 3.2 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-29T19:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 991.5, + "air_temperature": 10.0, + "cloud_area_fraction": 100.0, + "relative_humidity": 81.8, + "wind_from_direction": 171.6, + "wind_speed": 3.7 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-29T20:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 991.1, + "air_temperature": 9.7, + "cloud_area_fraction": 100.0, + "relative_humidity": 83.9, + "wind_from_direction": 173.0, + "wind_speed": 4.0 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-29T21:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 990.7, + "air_temperature": 9.4, + "cloud_area_fraction": 100.0, + "relative_humidity": 88.5, + "wind_from_direction": 178.9, + "wind_speed": 3.7 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-29T22:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 990.5, + "air_temperature": 9.2, + "cloud_area_fraction": 100.0, + "relative_humidity": 89.5, + "wind_from_direction": 195.1, + "wind_speed": 3.4 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "partlycloudy_night" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-29T23:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 990.2, + "air_temperature": 9.0, + "cloud_area_fraction": 100.0, + "relative_humidity": 89.8, + "wind_from_direction": 217.6, + "wind_speed": 3.1 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "partlycloudy_night" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-30T00:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 989.7, + "air_temperature": 9.0, + "cloud_area_fraction": 100.0, + "relative_humidity": 89.9, + "wind_from_direction": 231.6, + "wind_speed": 3.4 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "partlycloudy_night" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-30T01:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 989.8, + "air_temperature": 8.7, + "cloud_area_fraction": 93.7, + "relative_humidity": 91.6, + "wind_from_direction": 248.2, + "wind_speed": 3.5 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "fair_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "partlycloudy_night" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "partlycloudy_night" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-30T02:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 990.1, + "air_temperature": 8.3, + "cloud_area_fraction": 83.6, + "relative_humidity": 93.6, + "wind_from_direction": 255.3, + "wind_speed": 3.8 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "fair_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "partlycloudy_night" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "fair_night" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-30T03:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 990.4, + "air_temperature": 8.0, + "cloud_area_fraction": 70.3, + "relative_humidity": 93.2, + "wind_from_direction": 254.4, + "wind_speed": 4.2 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "fair_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "partlycloudy_night" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "fair_night" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-30T04:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 990.2, + "air_temperature": 7.8, + "cloud_area_fraction": 46.1, + "relative_humidity": 91.5, + "wind_from_direction": 251.8, + "wind_speed": 4.2 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "fair_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "fair_night" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "fair_day" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-30T05:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 990.8, + "air_temperature": 7.5, + "cloud_area_fraction": 35.9, + "relative_humidity": 91.1, + "wind_from_direction": 247.0, + "wind_speed": 3.9 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "fair_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "fair_night" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "fair_day" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-30T06:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 991.2, + "air_temperature": 7.4, + "cloud_area_fraction": 28.1, + "relative_humidity": 90.8, + "wind_from_direction": 238.9, + "wind_speed": 3.6 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "fair_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "fair_night" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "fair_day" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-30T07:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 991.9, + "air_temperature": 7.1, + "cloud_area_fraction": 18.0, + "relative_humidity": 92.8, + "wind_from_direction": 240.3, + "wind_speed": 3.7 + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "clearsky_day" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "fair_day" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-30T08:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 992.7, + "air_temperature": 7.2, + "cloud_area_fraction": 11.7, + "relative_humidity": 90.8, + "wind_from_direction": 238.5, + "wind_speed": 3.6 + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "clearsky_day" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "fair_day" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-30T09:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 993.3, + "air_temperature": 7.8, + "cloud_area_fraction": 11.7, + "relative_humidity": 86.0, + "wind_from_direction": 242.0, + "wind_speed": 3.4 + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "clearsky_day" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "fair_day" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-30T10:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 993.5, + "air_temperature": 8.9, + "cloud_area_fraction": 4.7, + "relative_humidity": 80.1, + "wind_from_direction": 239.6, + "wind_speed": 3.3 + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "clearsky_day" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-30T11:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 994.0, + "air_temperature": 9.8, + "cloud_area_fraction": 10.2, + "relative_humidity": 74.8, + "wind_from_direction": 243.7, + "wind_speed": 3.7 + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-30T12:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 994.1, + "air_temperature": 10.5, + "cloud_area_fraction": 50.8, + "relative_humidity": 69.6, + "wind_from_direction": 245.2, + "wind_speed": 3.9 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "fair_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-30T13:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 994.2, + "air_temperature": 11.0, + "cloud_area_fraction": 65.6, + "relative_humidity": 65.2, + "wind_from_direction": 241.9, + "wind_speed": 4.2 + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "fair_day" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-30T14:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 994.3, + "air_temperature": 11.1, + "cloud_area_fraction": 32.8, + "relative_humidity": 63.0, + "wind_from_direction": 242.4, + "wind_speed": 4.4 + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "fair_day" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-30T15:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 994.6, + "air_temperature": 11.2, + "cloud_area_fraction": 20.3, + "relative_humidity": 61.6, + "wind_from_direction": 242.6, + "wind_speed": 4.1 + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-30T16:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 995.0, + "air_temperature": 10.7, + "cloud_area_fraction": 49.2, + "relative_humidity": 63.6, + "wind_from_direction": 238.4, + "wind_speed": 2.8 + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-30T17:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 995.4, + "air_temperature": 9.6, + "cloud_area_fraction": 52.3, + "relative_humidity": 74.2, + "wind_from_direction": 204.4, + "wind_speed": 2.0 + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-30T18:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 995.5, + "air_temperature": 8.4, + "cloud_area_fraction": 50.8, + "relative_humidity": 86.9, + "wind_from_direction": 184.0, + "wind_speed": 2.5 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "partlycloudy_night" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "fair_night" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-31T00:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 992.2, + "air_temperature": 10.1, + "cloud_area_fraction": 56.2, + "relative_humidity": 79.6, + "wind_from_direction": 158.7, + "wind_speed": 6.4 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "rainshowers_day" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "heavyrainshowers_night" + }, + "details": { + "precipitation_amount": 7.9 + } + } + } + }, + { + "time": "2021-10-31T06:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 983.7, + "air_temperature": 10.8, + "cloud_area_fraction": 100.0, + "relative_humidity": 90.3, + "wind_from_direction": 143.5, + "wind_speed": 8.9 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "lightrainshowers_day" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "heavyrainshowers_day" + }, + "details": { + "precipitation_amount": 5.9 + } + } + } + }, + { + "time": "2021-10-31T12:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 983.5, + "air_temperature": 10.0, + "cloud_area_fraction": 34.4, + "relative_humidity": 77.6, + "wind_from_direction": 248.8, + "wind_speed": 6.9 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "fair_day" + }, + "details": { + "precipitation_amount": 0.2 + } + } + } + }, + { + "time": "2021-10-31T18:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 984.5, + "air_temperature": 8.3, + "cloud_area_fraction": 2.3, + "relative_humidity": 79.3, + "wind_from_direction": 221.1, + "wind_speed": 3.1 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "cloudy" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "partlycloudy_night" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-11-01T00:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 983.3, + "air_temperature": 5.3, + "cloud_area_fraction": 99.2, + "relative_humidity": 97.3, + "wind_from_direction": 286.3, + "wind_speed": 2.7 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "cloudy" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-11-01T06:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 985.4, + "air_temperature": 5.4, + "cloud_area_fraction": 100.0, + "relative_humidity": 96.3, + "wind_from_direction": 269.9, + "wind_speed": 5.9 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "cloudy" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.3 + } + } + } + }, + { + "time": "2021-11-01T12:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 989.7, + "air_temperature": 9.9, + "cloud_area_fraction": 100.0, + "relative_humidity": 77.8, + "wind_from_direction": 297.2, + "wind_speed": 7.3 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "cloudy" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-11-01T18:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 993.7, + "air_temperature": 9.1, + "cloud_area_fraction": 100.0, + "relative_humidity": 77.6, + "wind_from_direction": 294.1, + "wind_speed": 6.0 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-11-02T00:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 997.0, + "air_temperature": 7.0, + "cloud_area_fraction": 100.0, + "relative_humidity": 91.5, + "wind_from_direction": 280.6, + "wind_speed": 6.0 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "fair_day" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "partlycloudy_night" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-11-02T06:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 999.2, + "air_temperature": 5.8, + "cloud_area_fraction": 25.0, + "relative_humidity": 95.7, + "wind_from_direction": 274.9, + "wind_speed": 5.5 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "fair_day" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "fair_day" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-11-02T12:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 1001.4, + "air_temperature": 8.9, + "cloud_area_fraction": 22.7, + "relative_humidity": 74.1, + "wind_from_direction": 291.2, + "wind_speed": 7.2 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "fair_day" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-11-02T18:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 1002.5, + "air_temperature": 7.9, + "cloud_area_fraction": 51.6, + "relative_humidity": 79.5, + "wind_from_direction": 283.1, + "wind_speed": 7.8 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "partlycloudy_night" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "lightrainshowers_night" + }, + "details": { + "precipitation_amount": 0.9 + } + } + } + }, + { + "time": "2021-11-03T00:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 1005.4, + "air_temperature": 8.0, + "cloud_area_fraction": 86.7, + "relative_humidity": 83.1, + "wind_from_direction": 299.1, + "wind_speed": 7.9 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "fair_day" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "partlycloudy_night" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-11-03T06:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 1010.6, + "air_temperature": 6.7, + "cloud_area_fraction": 7.8, + "relative_humidity": 80.2, + "wind_from_direction": 309.5, + "wind_speed": 7.0 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "fair_day" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-11-03T12:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 1015.2, + "air_temperature": 8.6, + "cloud_area_fraction": 68.0, + "relative_humidity": 69.6, + "wind_from_direction": 326.0, + "wind_speed": 6.8 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "fair_day" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-11-03T18:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 1018.0, + "air_temperature": 6.9, + "cloud_area_fraction": 26.6, + "relative_humidity": 79.4, + "wind_from_direction": 304.5, + "wind_speed": 4.0 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "clearsky_night" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "fair_night" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-11-04T00:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 1020.0, + "air_temperature": 3.9, + "cloud_area_fraction": 0.8, + "relative_humidity": 95.6, + "wind_from_direction": 273.0, + "wind_speed": 3.8 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "clearsky_night" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-11-04T06:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 1018.5, + "air_temperature": 5.2, + "cloud_area_fraction": 8.6, + "relative_humidity": 87.0, + "wind_from_direction": 242.6, + "wind_speed": 4.2 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "cloudy" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-11-04T12:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 1016.9, + "air_temperature": 9.0, + "cloud_area_fraction": 96.1, + "relative_humidity": 82.7, + "wind_from_direction": 248.6, + "wind_speed": 6.6 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "cloudy" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-11-04T18:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 1016.0, + "air_temperature": 10.8, + "cloud_area_fraction": 100.0, + "relative_humidity": 84.4, + "wind_from_direction": 249.6, + "wind_speed": 5.3 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "cloudy" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.1 + } + } + } + }, + { + "time": "2021-11-05T00:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 1016.2, + "air_temperature": 10.5, + "cloud_area_fraction": 100.0, + "relative_humidity": 89.2, + "wind_from_direction": 249.6, + "wind_speed": 4.7 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "cloudy" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.3 + } + } + } + }, + { + "time": "2021-11-05T06:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 1016.1, + "air_temperature": 10.0, + "cloud_area_fraction": 100.0, + "relative_humidity": 90.5, + "wind_from_direction": 242.7, + "wind_speed": 4.3 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "rain" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-11-05T12:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 1013.5, + "air_temperature": 10.4, + "cloud_area_fraction": 100.0, + "relative_humidity": 84.4, + "wind_from_direction": 180.1, + "wind_speed": 3.8 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "rain" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "heavyrain" + }, + "details": { + "precipitation_amount": 9.1 + } + } + } + }, + { + "time": "2021-11-05T18:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 1001.3, + "air_temperature": 11.9, + "cloud_area_fraction": 100.0, + "relative_humidity": 84.6, + "wind_from_direction": 191.8, + "wind_speed": 8.7 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "lightrain" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "rain" + }, + "details": { + "precipitation_amount": 4.7 + } + } + } + }, + { + "time": "2021-11-06T00:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 997.7, + "air_temperature": 14.2, + "cloud_area_fraction": 100.0, + "relative_humidity": 88.2, + "wind_from_direction": 255.3, + "wind_speed": 8.8 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-11-06T06:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 1002.5, + "air_temperature": 10.5, + "cloud_area_fraction": 100.0, + "relative_humidity": 87.9, + "wind_from_direction": 260.1, + "wind_speed": 4.8 + } + } + } + } + ] + } +} \ No newline at end of file diff --git a/testdata/response-complete.json b/testdata/response-complete.json new file mode 100644 index 0000000..3249bbe --- /dev/null +++ b/testdata/response-complete.json @@ -0,0 +1,3519 @@ +{ + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -6.2, + 53.3, + 39 + ] + }, + "properties": { + "meta": { + "updated_at": "2021-10-28T01:24:06Z", + "units": { + "air_pressure_at_sea_level": "hPa", + "air_temperature": "celsius", + "air_temperature_max": "celsius", + "air_temperature_min": "celsius", + "cloud_area_fraction": "%", + "cloud_area_fraction_high": "%", + "cloud_area_fraction_low": "%", + "cloud_area_fraction_medium": "%", + "dew_point_temperature": "celsius", + "fog_area_fraction": "%", + "precipitation_amount": "mm", + "relative_humidity": "%", + "ultraviolet_index_clear_sky": "1", + "wind_from_direction": "degrees", + "wind_speed": "m/s" + } + }, + "timeseries": [ + { + "time": "2021-10-28T05:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 999.7, + "air_temperature": 13.7, + "cloud_area_fraction": 100.0, + "cloud_area_fraction_high": 100.0, + "cloud_area_fraction_low": 100.0, + "cloud_area_fraction_medium": 99.2, + "dew_point_temperature": 12.7, + "fog_area_fraction": 0.0, + "relative_humidity": 94.0, + "ultraviolet_index_clear_sky": 0.0, + "wind_from_direction": 180.3, + "wind_speed": 1.5 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "rain" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "rain" + }, + "details": { + "precipitation_amount": 0.6 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "heavyrain" + }, + "details": { + "air_temperature_max": 14.1, + "air_temperature_min": 13.5, + "precipitation_amount": 6.1 + } + } + } + }, + { + "time": "2021-10-28T06:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 999.3, + "air_temperature": 13.7, + "cloud_area_fraction": 100.0, + "cloud_area_fraction_high": 100.0, + "cloud_area_fraction_low": 39.1, + "cloud_area_fraction_medium": 100.0, + "dew_point_temperature": 12.7, + "fog_area_fraction": 0.0, + "relative_humidity": 93.8, + "ultraviolet_index_clear_sky": 0.0, + "wind_from_direction": 162.3, + "wind_speed": 2.3 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "lightrain" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "rain" + }, + "details": { + "precipitation_amount": 0.6 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "heavyrain" + }, + "details": { + "air_temperature_max": 14.1, + "air_temperature_min": 13.5, + "precipitation_amount": 6.7 + } + } + } + }, + { + "time": "2021-10-28T07:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 998.6, + "air_temperature": 13.5, + "cloud_area_fraction": 100.0, + "cloud_area_fraction_high": 100.0, + "cloud_area_fraction_low": 3.9, + "cloud_area_fraction_medium": 100.0, + "dew_point_temperature": 12.3, + "fog_area_fraction": 0.0, + "relative_humidity": 92.3, + "ultraviolet_index_clear_sky": 0.0, + "wind_from_direction": 164.6, + "wind_speed": 2.6 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "lightrain" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "rain" + }, + "details": { + "precipitation_amount": 0.3 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "heavyrain" + }, + "details": { + "air_temperature_max": 14.3, + "air_temperature_min": 13.5, + "precipitation_amount": 6.1 + } + } + } + }, + { + "time": "2021-10-28T08:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 998.4, + "air_temperature": 13.5, + "cloud_area_fraction": 100.0, + "cloud_area_fraction_high": 100.0, + "cloud_area_fraction_low": 2.3, + "cloud_area_fraction_medium": 100.0, + "dew_point_temperature": 12.0, + "fog_area_fraction": 0.0, + "relative_humidity": 90.8, + "ultraviolet_index_clear_sky": 0.1, + "wind_from_direction": 170.7, + "wind_speed": 2.9 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "lightrain" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "rain" + }, + "details": { + "precipitation_amount": 0.3 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "heavyrain" + }, + "details": { + "air_temperature_max": 14.4, + "air_temperature_min": 13.7, + "precipitation_amount": 5.8 + } + } + } + }, + { + "time": "2021-10-28T09:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 997.7, + "air_temperature": 13.7, + "cloud_area_fraction": 100.0, + "cloud_area_fraction_high": 100.0, + "cloud_area_fraction_low": 14.8, + "cloud_area_fraction_medium": 100.0, + "dew_point_temperature": 12.3, + "fog_area_fraction": 0.0, + "relative_humidity": 91.2, + "ultraviolet_index_clear_sky": 0.4, + "wind_from_direction": 165.9, + "wind_speed": 3.8 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "lightrain" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "heavyrain" + }, + "details": { + "precipitation_amount": 1.9 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "heavyrain" + }, + "details": { + "air_temperature_max": 14.4, + "air_temperature_min": 13.9, + "precipitation_amount": 5.7 + } + } + } + }, + { + "time": "2021-10-28T10:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 996.8, + "air_temperature": 14.1, + "cloud_area_fraction": 100.0, + "cloud_area_fraction_high": 100.0, + "cloud_area_fraction_low": 43.7, + "cloud_area_fraction_medium": 100.0, + "dew_point_temperature": 12.6, + "fog_area_fraction": 0.0, + "relative_humidity": 90.9, + "ultraviolet_index_clear_sky": 0.9, + "wind_from_direction": 184.2, + "wind_speed": 3.4 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "lightrain" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "heavyrain" + }, + "details": { + "precipitation_amount": 2.4 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "rain" + }, + "details": { + "air_temperature_max": 14.4, + "air_temperature_min": 13.4, + "precipitation_amount": 3.9 + } + } + } + }, + { + "time": "2021-10-28T11:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 996.1, + "air_temperature": 13.9, + "cloud_area_fraction": 100.0, + "cloud_area_fraction_high": 100.0, + "cloud_area_fraction_low": 18.0, + "cloud_area_fraction_medium": 100.0, + "dew_point_temperature": 12.4, + "fog_area_fraction": 0.0, + "relative_humidity": 90.7, + "ultraviolet_index_clear_sky": 1.2, + "wind_from_direction": 189.4, + "wind_speed": 2.5 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "rain" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "heavyrain" + }, + "details": { + "precipitation_amount": 1.1 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "rain" + }, + "details": { + "air_temperature_max": 14.4, + "air_temperature_min": 12.7, + "precipitation_amount": 1.5 + } + } + } + }, + { + "time": "2021-10-28T12:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 995.4, + "air_temperature": 14.1, + "cloud_area_fraction": 100.0, + "cloud_area_fraction_high": 99.2, + "cloud_area_fraction_low": 14.8, + "cloud_area_fraction_medium": 100.0, + "dew_point_temperature": 12.3, + "fog_area_fraction": 0.0, + "relative_humidity": 89.6, + "ultraviolet_index_clear_sky": 1.4, + "wind_from_direction": 221.4, + "wind_speed": 1.4 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "lightrain" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "air_temperature_max": 14.4, + "air_temperature_min": 12.0, + "precipitation_amount": 0.3 + } + } + } + }, + { + "time": "2021-10-28T13:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 995.5, + "air_temperature": 14.3, + "cloud_area_fraction": 100.0, + "cloud_area_fraction_high": 100.0, + "cloud_area_fraction_low": 18.0, + "cloud_area_fraction_medium": 10.2, + "dew_point_temperature": 12.5, + "fog_area_fraction": 0.0, + "relative_humidity": 89.3, + "ultraviolet_index_clear_sky": 1.2, + "wind_from_direction": 249.6, + "wind_speed": 3.0 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "lightrain" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "air_temperature_max": 14.4, + "air_temperature_min": 11.6, + "precipitation_amount": 0.3 + } + } + } + }, + { + "time": "2021-10-28T14:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 995.3, + "air_temperature": 14.4, + "cloud_area_fraction": 100.0, + "cloud_area_fraction_high": 100.0, + "cloud_area_fraction_low": 54.7, + "cloud_area_fraction_medium": 17.2, + "dew_point_temperature": 12.3, + "fog_area_fraction": 0.0, + "relative_humidity": 87.7, + "ultraviolet_index_clear_sky": 0.9, + "wind_from_direction": 220.0, + "wind_speed": 3.2 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "lightrain" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "lightrain" + }, + "details": { + "precipitation_amount": 0.2 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "air_temperature_max": 14.2, + "air_temperature_min": 11.3, + "precipitation_amount": 0.3 + } + } + } + }, + { + "time": "2021-10-28T15:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 995.3, + "air_temperature": 14.2, + "cloud_area_fraction": 100.0, + "cloud_area_fraction_high": 100.0, + "cloud_area_fraction_low": 7.0, + "cloud_area_fraction_medium": 13.3, + "dew_point_temperature": 11.8, + "fog_area_fraction": 0.0, + "relative_humidity": 85.5, + "ultraviolet_index_clear_sky": 0.4, + "wind_from_direction": 225.8, + "wind_speed": 3.1 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "lightrain" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "air_temperature_max": 13.4, + "air_temperature_min": 11.2, + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-28T16:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 995.7, + "air_temperature": 13.4, + "cloud_area_fraction": 100.0, + "cloud_area_fraction_high": 100.0, + "cloud_area_fraction_low": 50.8, + "cloud_area_fraction_medium": 34.4, + "dew_point_temperature": 10.7, + "fog_area_fraction": 0.0, + "relative_humidity": 83.7, + "ultraviolet_index_clear_sky": 0.1, + "wind_from_direction": 248.3, + "wind_speed": 4.0 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "lightrain" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "air_temperature_max": 12.7, + "air_temperature_min": 10.9, + "precipitation_amount": 0.1 + } + } + } + }, + { + "time": "2021-10-28T17:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 995.7, + "air_temperature": 12.7, + "cloud_area_fraction": 100.0, + "cloud_area_fraction_high": 100.0, + "cloud_area_fraction_low": 7.8, + "cloud_area_fraction_medium": 7.8, + "dew_point_temperature": 10.5, + "fog_area_fraction": 0.0, + "relative_humidity": 85.8, + "ultraviolet_index_clear_sky": 0.0, + "wind_from_direction": 250.6, + "wind_speed": 3.0 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "rain" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "partlycloudy_night" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "rain" + }, + "details": { + "air_temperature_max": 12.0, + "air_temperature_min": 10.6, + "precipitation_amount": 1.2 + } + } + } + }, + { + "time": "2021-10-28T18:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 995.9, + "air_temperature": 12.0, + "cloud_area_fraction": 100.0, + "cloud_area_fraction_high": 100.0, + "cloud_area_fraction_low": 3.9, + "cloud_area_fraction_medium": 1.6, + "dew_point_temperature": 10.5, + "fog_area_fraction": 0.0, + "relative_humidity": 90.0, + "ultraviolet_index_clear_sky": 0.0, + "wind_from_direction": 253.5, + "wind_speed": 2.7 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "rain" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "partlycloudy_night" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "rain" + }, + "details": { + "air_temperature_max": 11.6, + "air_temperature_min": 10.6, + "precipitation_amount": 1.6 + } + } + } + }, + { + "time": "2021-10-28T19:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 995.7, + "air_temperature": 11.6, + "cloud_area_fraction": 100.0, + "cloud_area_fraction_high": 100.0, + "cloud_area_fraction_low": 1.6, + "cloud_area_fraction_medium": 0.0, + "dew_point_temperature": 10.4, + "fog_area_fraction": 0.0, + "relative_humidity": 91.4, + "ultraviolet_index_clear_sky": 0.0, + "wind_from_direction": 255.6, + "wind_speed": 2.3 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "rain" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "rain" + }, + "details": { + "air_temperature_max": 11.3, + "air_temperature_min": 10.5, + "precipitation_amount": 1.6 + } + } + } + }, + { + "time": "2021-10-28T20:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 995.5, + "air_temperature": 11.3, + "cloud_area_fraction": 100.0, + "cloud_area_fraction_high": 100.0, + "cloud_area_fraction_low": 0.0, + "cloud_area_fraction_medium": 75.8, + "dew_point_temperature": 10.1, + "fog_area_fraction": 0.0, + "relative_humidity": 91.6, + "ultraviolet_index_clear_sky": 0.0, + "wind_from_direction": 224.5, + "wind_speed": 1.2 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "rain" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "rain" + }, + "details": { + "air_temperature_max": 11.2, + "air_temperature_min": 10.4, + "precipitation_amount": 1.8 + } + } + } + }, + { + "time": "2021-10-28T21:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 995.4, + "air_temperature": 11.2, + "cloud_area_fraction": 100.0, + "cloud_area_fraction_high": 100.0, + "cloud_area_fraction_low": 0.0, + "cloud_area_fraction_medium": 100.0, + "dew_point_temperature": 10.1, + "fog_area_fraction": 0.0, + "relative_humidity": 92.3, + "ultraviolet_index_clear_sky": 0.0, + "wind_from_direction": 189.3, + "wind_speed": 1.4 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "rain" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "lightrain" + }, + "details": { + "precipitation_amount": 0.1 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "rain" + }, + "details": { + "air_temperature_max": 10.9, + "air_temperature_min": 10.4, + "precipitation_amount": 3.0 + } + } + } + }, + { + "time": "2021-10-28T22:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 995.3, + "air_temperature": 10.9, + "cloud_area_fraction": 100.0, + "cloud_area_fraction_high": 100.0, + "cloud_area_fraction_low": 26.6, + "cloud_area_fraction_medium": 100.0, + "dew_point_temperature": 10.3, + "fog_area_fraction": 0.0, + "relative_humidity": 95.2, + "ultraviolet_index_clear_sky": 0.0, + "wind_from_direction": 213.3, + "wind_speed": 1.1 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "rain" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "heavyrain" + }, + "details": { + "precipitation_amount": 1.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "rain" + }, + "details": { + "air_temperature_max": 10.7, + "air_temperature_min": 10.2, + "precipitation_amount": 4.4 + } + } + } + }, + { + "time": "2021-10-28T23:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 995.1, + "air_temperature": 10.6, + "cloud_area_fraction": 100.0, + "cloud_area_fraction_high": 100.0, + "cloud_area_fraction_low": 23.4, + "cloud_area_fraction_medium": 100.0, + "dew_point_temperature": 10.0, + "fog_area_fraction": 0.0, + "relative_humidity": 95.5, + "ultraviolet_index_clear_sky": 0.0, + "wind_from_direction": 198.3, + "wind_speed": 1.0 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "rain" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "rain" + }, + "details": { + "precipitation_amount": 0.5 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "rain" + }, + "details": { + "air_temperature_max": 10.7, + "air_temperature_min": 9.8, + "precipitation_amount": 4.0 + } + } + } + }, + { + "time": "2021-10-29T00:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 993.8, + "air_temperature": 10.7, + "cloud_area_fraction": 100.0, + "cloud_area_fraction_high": 100.0, + "cloud_area_fraction_low": 0.8, + "cloud_area_fraction_medium": 100.0, + "dew_point_temperature": 10.2, + "fog_area_fraction": 0.0, + "relative_humidity": 96.1, + "ultraviolet_index_clear_sky": 0.0, + "wind_from_direction": 353.3, + "wind_speed": 0.8 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "rain" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "rain" + }, + "details": { + "air_temperature_max": 10.5, + "air_temperature_min": 9.5, + "precipitation_amount": 4.7 + } + } + } + }, + { + "time": "2021-10-29T01:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 993.3, + "air_temperature": 10.5, + "cloud_area_fraction": 100.0, + "cloud_area_fraction_high": 100.0, + "cloud_area_fraction_low": 3.9, + "cloud_area_fraction_medium": 100.0, + "dew_point_temperature": 10.1, + "fog_area_fraction": 0.0, + "relative_humidity": 97.0, + "ultraviolet_index_clear_sky": 0.0, + "wind_from_direction": 309.1, + "wind_speed": 0.8 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "rainshowers_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "lightrain" + }, + "details": { + "precipitation_amount": 0.2 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "heavyrain" + }, + "details": { + "air_temperature_max": 10.4, + "air_temperature_min": 9.5, + "precipitation_amount": 6.3 + } + } + } + }, + { + "time": "2021-10-29T02:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 993.1, + "air_temperature": 10.4, + "cloud_area_fraction": 100.0, + "cloud_area_fraction_high": 100.0, + "cloud_area_fraction_low": 57.8, + "cloud_area_fraction_medium": 100.0, + "dew_point_temperature": 9.9, + "fog_area_fraction": 25.8, + "relative_humidity": 96.3, + "ultraviolet_index_clear_sky": 0.0, + "wind_from_direction": 303.9, + "wind_speed": 0.2 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "lightrainshowers_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "heavyrain" + }, + "details": { + "precipitation_amount": 1.2 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "heavyrain" + }, + "details": { + "air_temperature_max": 10.4, + "air_temperature_min": 9.5, + "precipitation_amount": 7.7 + } + } + } + }, + { + "time": "2021-10-29T03:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 992.3, + "air_temperature": 10.4, + "cloud_area_fraction": 100.0, + "cloud_area_fraction_high": 100.0, + "cloud_area_fraction_low": 4.7, + "cloud_area_fraction_medium": 100.0, + "dew_point_temperature": 9.5, + "fog_area_fraction": 0.0, + "relative_humidity": 94.2, + "ultraviolet_index_clear_sky": 0.0, + "wind_from_direction": 216.9, + "wind_speed": 0.7 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "lightrainshowers_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "heavyrain" + }, + "details": { + "precipitation_amount": 1.5 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "heavyrain" + }, + "details": { + "air_temperature_max": 10.2, + "air_temperature_min": 9.5, + "precipitation_amount": 6.7 + } + } + } + }, + { + "time": "2021-10-29T04:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 991.5, + "air_temperature": 10.2, + "cloud_area_fraction": 100.0, + "cloud_area_fraction_high": 100.0, + "cloud_area_fraction_low": 45.3, + "cloud_area_fraction_medium": 100.0, + "dew_point_temperature": 9.5, + "fog_area_fraction": 0.0, + "relative_humidity": 95.7, + "ultraviolet_index_clear_sky": 0.0, + "wind_from_direction": 306.6, + "wind_speed": 1.2 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "lightrainshowers_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "rain" + }, + "details": { + "precipitation_amount": 0.6 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "heavyrain" + }, + "details": { + "air_temperature_max": 9.9, + "air_temperature_min": 9.5, + "precipitation_amount": 5.2 + } + } + } + }, + { + "time": "2021-10-29T05:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 991.0, + "air_temperature": 9.8, + "cloud_area_fraction": 100.0, + "cloud_area_fraction_high": 100.0, + "cloud_area_fraction_low": 100.0, + "cloud_area_fraction_medium": 100.0, + "dew_point_temperature": 9.5, + "fog_area_fraction": 0.0, + "relative_humidity": 98.0, + "ultraviolet_index_clear_sky": 0.0, + "wind_from_direction": 321.5, + "wind_speed": 1.6 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "lightrainshowers_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "heavyrain" + }, + "details": { + "precipitation_amount": 1.2 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "rainshowers_day" + }, + "details": { + "air_temperature_max": 10.7, + "air_temperature_min": 9.5, + "precipitation_amount": 4.6 + } + } + } + }, + { + "time": "2021-10-29T06:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 991.4, + "air_temperature": 9.5, + "cloud_area_fraction": 100.0, + "cloud_area_fraction_high": 100.0, + "cloud_area_fraction_low": 94.5, + "cloud_area_fraction_medium": 100.0, + "dew_point_temperature": 8.8, + "fog_area_fraction": 0.0, + "relative_humidity": 95.6, + "ultraviolet_index_clear_sky": 0.0, + "wind_from_direction": 290.1, + "wind_speed": 3.5 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "lightrainshowers_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "heavyrain" + }, + "details": { + "precipitation_amount": 1.6 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "rainshowers_day" + }, + "details": { + "air_temperature_max": 11.5, + "air_temperature_min": 9.5, + "precipitation_amount": 3.4 + } + } + } + }, + { + "time": "2021-10-29T07:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 991.6, + "air_temperature": 9.7, + "cloud_area_fraction": 100.0, + "cloud_area_fraction_high": 100.0, + "cloud_area_fraction_low": 100.0, + "cloud_area_fraction_medium": 100.0, + "dew_point_temperature": 8.6, + "fog_area_fraction": 0.0, + "relative_humidity": 93.1, + "ultraviolet_index_clear_sky": 0.0, + "wind_from_direction": 290.8, + "wind_speed": 4.8 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "lightrainshowers_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "heavyrain" + }, + "details": { + "precipitation_amount": 1.5 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "rainshowers_day" + }, + "details": { + "air_temperature_max": 12.2, + "air_temperature_min": 9.5, + "precipitation_amount": 1.8 + } + } + } + }, + { + "time": "2021-10-29T08:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 992.2, + "air_temperature": 9.6, + "cloud_area_fraction": 100.0, + "cloud_area_fraction_high": 100.0, + "cloud_area_fraction_low": 0.0, + "cloud_area_fraction_medium": 100.0, + "dew_point_temperature": 7.5, + "fog_area_fraction": 0.0, + "relative_humidity": 87.5, + "ultraviolet_index_clear_sky": 0.1, + "wind_from_direction": 280.5, + "wind_speed": 3.8 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "lightrain" + }, + "details": { + "precipitation_amount": 0.2 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + }, + "details": { + "air_temperature_max": 12.5, + "air_temperature_min": 9.5, + "precipitation_amount": 0.3 + } + } + } + }, + { + "time": "2021-10-29T09:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 992.7, + "air_temperature": 9.5, + "cloud_area_fraction": 100.0, + "cloud_area_fraction_high": 100.0, + "cloud_area_fraction_low": 19.5, + "cloud_area_fraction_medium": 100.0, + "dew_point_temperature": 7.7, + "fog_area_fraction": 0.0, + "relative_humidity": 88.9, + "ultraviolet_index_clear_sky": 0.4, + "wind_from_direction": 266.7, + "wind_speed": 3.6 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "fair_day" + }, + "details": { + "air_temperature_max": 12.5, + "air_temperature_min": 9.9, + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-29T10:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 992.9, + "air_temperature": 9.9, + "cloud_area_fraction": 92.2, + "cloud_area_fraction_high": 61.7, + "cloud_area_fraction_low": 11.7, + "cloud_area_fraction_medium": 89.1, + "dew_point_temperature": 7.8, + "fog_area_fraction": 0.0, + "relative_humidity": 87.8, + "ultraviolet_index_clear_sky": 0.7, + "wind_from_direction": 253.5, + "wind_speed": 3.7 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "fair_day" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "fair_day" + }, + "details": { + "air_temperature_max": 12.5, + "air_temperature_min": 10.7, + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-29T11:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 993.2, + "air_temperature": 10.7, + "cloud_area_fraction": 21.9, + "cloud_area_fraction_high": 3.1, + "cloud_area_fraction_low": 18.7, + "cloud_area_fraction_medium": 7.0, + "dew_point_temperature": 7.5, + "fog_area_fraction": 0.0, + "relative_humidity": 81.2, + "ultraviolet_index_clear_sky": 1.1, + "wind_from_direction": 244.3, + "wind_speed": 4.2 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "fair_day" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + }, + "details": { + "air_temperature_max": 12.5, + "air_temperature_min": 10.9, + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-29T12:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 993.3, + "air_temperature": 11.5, + "cloud_area_fraction": 18.7, + "cloud_area_fraction_high": 1.6, + "cloud_area_fraction_low": 15.6, + "cloud_area_fraction_medium": 4.7, + "dew_point_temperature": 6.6, + "fog_area_fraction": 0.0, + "relative_humidity": 72.1, + "ultraviolet_index_clear_sky": 1.2, + "wind_from_direction": 241.2, + "wind_speed": 3.8 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "fair_day" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + }, + "details": { + "air_temperature_max": 12.5, + "air_temperature_min": 10.1, + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-29T13:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 993.1, + "air_temperature": 12.2, + "cloud_area_fraction": 14.1, + "cloud_area_fraction_high": 3.9, + "cloud_area_fraction_low": 7.8, + "cloud_area_fraction_medium": 6.2, + "dew_point_temperature": 6.0, + "fog_area_fraction": 0.0, + "relative_humidity": 65.9, + "ultraviolet_index_clear_sky": 1.1, + "wind_from_direction": 236.8, + "wind_speed": 3.6 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "fair_day" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + }, + "details": { + "air_temperature_max": 12.5, + "air_temperature_min": 10.0, + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-29T14:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 992.7, + "air_temperature": 12.5, + "cloud_area_fraction": 29.7, + "cloud_area_fraction_high": 21.9, + "cloud_area_fraction_low": 7.0, + "cloud_area_fraction_medium": 7.0, + "dew_point_temperature": 5.7, + "fog_area_fraction": 0.0, + "relative_humidity": 63.0, + "ultraviolet_index_clear_sky": 0.7, + "wind_from_direction": 226.5, + "wind_speed": 3.5 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "cloudy" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "fair_day" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + }, + "details": { + "air_temperature_max": 12.5, + "air_temperature_min": 9.7, + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-29T15:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 992.4, + "air_temperature": 12.5, + "cloud_area_fraction": 26.6, + "cloud_area_fraction_high": 25.8, + "cloud_area_fraction_low": 0.8, + "cloud_area_fraction_medium": 0.8, + "dew_point_temperature": 5.9, + "fog_area_fraction": 0.0, + "relative_humidity": 63.7, + "ultraviolet_index_clear_sky": 0.4, + "wind_from_direction": 204.7, + "wind_speed": 2.9 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "cloudy" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "air_temperature_max": 12.1, + "air_temperature_min": 9.4, + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-29T16:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 992.3, + "air_temperature": 12.1, + "cloud_area_fraction": 78.1, + "cloud_area_fraction_high": 77.3, + "cloud_area_fraction_low": 0.8, + "cloud_area_fraction_medium": 0.8, + "dew_point_temperature": 6.0, + "fog_area_fraction": 0.0, + "relative_humidity": 66.0, + "ultraviolet_index_clear_sky": 0.1, + "wind_from_direction": 186.3, + "wind_speed": 2.7 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "cloudy" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "air_temperature_max": 10.9, + "air_temperature_min": 9.2, + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-29T17:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 992.1, + "air_temperature": 10.9, + "cloud_area_fraction": 99.2, + "cloud_area_fraction_high": 99.2, + "cloud_area_fraction_low": 2.3, + "cloud_area_fraction_medium": 1.6, + "dew_point_temperature": 6.8, + "fog_area_fraction": 0.0, + "relative_humidity": 75.7, + "ultraviolet_index_clear_sky": 0.0, + "wind_from_direction": 169.1, + "wind_speed": 2.9 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "air_temperature_max": 10.1, + "air_temperature_min": 9.0, + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-29T18:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 991.9, + "air_temperature": 10.1, + "cloud_area_fraction": 99.2, + "cloud_area_fraction_high": 99.2, + "cloud_area_fraction_low": 7.0, + "cloud_area_fraction_medium": 29.7, + "dew_point_temperature": 6.8, + "fog_area_fraction": 0.0, + "relative_humidity": 80.8, + "ultraviolet_index_clear_sky": 0.0, + "wind_from_direction": 172.5, + "wind_speed": 3.2 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "air_temperature_max": 10.0, + "air_temperature_min": 9.0, + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-29T19:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 991.5, + "air_temperature": 10.0, + "cloud_area_fraction": 100.0, + "cloud_area_fraction_high": 100.0, + "cloud_area_fraction_low": 21.9, + "cloud_area_fraction_medium": 75.8, + "dew_point_temperature": 6.8, + "fog_area_fraction": 0.0, + "relative_humidity": 81.8, + "ultraviolet_index_clear_sky": 0.0, + "wind_from_direction": 171.6, + "wind_speed": 3.7 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "air_temperature_max": 9.7, + "air_temperature_min": 8.7, + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-29T20:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 991.1, + "air_temperature": 9.7, + "cloud_area_fraction": 100.0, + "cloud_area_fraction_high": 100.0, + "cloud_area_fraction_low": 11.7, + "cloud_area_fraction_medium": 99.2, + "dew_point_temperature": 6.9, + "fog_area_fraction": 0.0, + "relative_humidity": 83.9, + "ultraviolet_index_clear_sky": 0.0, + "wind_from_direction": 173.0, + "wind_speed": 4.0 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "air_temperature_max": 9.4, + "air_temperature_min": 8.3, + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-29T21:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 990.7, + "air_temperature": 9.4, + "cloud_area_fraction": 100.0, + "cloud_area_fraction_high": 100.0, + "cloud_area_fraction_low": 7.0, + "cloud_area_fraction_medium": 93.7, + "dew_point_temperature": 7.4, + "fog_area_fraction": 0.0, + "relative_humidity": 88.5, + "ultraviolet_index_clear_sky": 0.0, + "wind_from_direction": 178.9, + "wind_speed": 3.7 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "air_temperature_max": 9.2, + "air_temperature_min": 8.0, + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-29T22:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 990.5, + "air_temperature": 9.2, + "cloud_area_fraction": 100.0, + "cloud_area_fraction_high": 100.0, + "cloud_area_fraction_low": 7.8, + "cloud_area_fraction_medium": 94.5, + "dew_point_temperature": 7.5, + "fog_area_fraction": 0.0, + "relative_humidity": 89.5, + "ultraviolet_index_clear_sky": 0.0, + "wind_from_direction": 195.1, + "wind_speed": 3.4 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "partlycloudy_night" + }, + "details": { + "air_temperature_max": 9.0, + "air_temperature_min": 7.8, + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-29T23:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 990.2, + "air_temperature": 9.0, + "cloud_area_fraction": 100.0, + "cloud_area_fraction_high": 100.0, + "cloud_area_fraction_low": 14.1, + "cloud_area_fraction_medium": 100.0, + "dew_point_temperature": 7.4, + "fog_area_fraction": 0.0, + "relative_humidity": 89.8, + "ultraviolet_index_clear_sky": 0.0, + "wind_from_direction": 217.6, + "wind_speed": 3.1 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "partlycloudy_night" + }, + "details": { + "air_temperature_max": 9.0, + "air_temperature_min": 7.5, + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-30T00:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 989.7, + "air_temperature": 9.0, + "cloud_area_fraction": 100.0, + "cloud_area_fraction_high": 100.0, + "cloud_area_fraction_low": 21.1, + "cloud_area_fraction_medium": 26.6, + "dew_point_temperature": 7.4, + "fog_area_fraction": 0.0, + "relative_humidity": 89.9, + "ultraviolet_index_clear_sky": 0.0, + "wind_from_direction": 231.6, + "wind_speed": 3.4 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "partlycloudy_night" + }, + "details": { + "air_temperature_max": 8.7, + "air_temperature_min": 7.4, + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-30T01:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 989.8, + "air_temperature": 8.7, + "cloud_area_fraction": 93.7, + "cloud_area_fraction_high": 74.2, + "cloud_area_fraction_low": 39.8, + "cloud_area_fraction_medium": 6.2, + "dew_point_temperature": 7.4, + "fog_area_fraction": 1.6, + "relative_humidity": 91.6, + "ultraviolet_index_clear_sky": 0.0, + "wind_from_direction": 248.2, + "wind_speed": 3.5 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "fair_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "partlycloudy_night" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "partlycloudy_night" + }, + "details": { + "air_temperature_max": 8.3, + "air_temperature_min": 7.1, + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-30T02:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 990.1, + "air_temperature": 8.3, + "cloud_area_fraction": 83.6, + "cloud_area_fraction_high": 10.2, + "cloud_area_fraction_low": 82.0, + "cloud_area_fraction_medium": 0.0, + "dew_point_temperature": 7.3, + "fog_area_fraction": 9.4, + "relative_humidity": 93.6, + "ultraviolet_index_clear_sky": 0.0, + "wind_from_direction": 255.3, + "wind_speed": 3.8 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "fair_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "partlycloudy_night" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "fair_night" + }, + "details": { + "air_temperature_max": 8.0, + "air_temperature_min": 7.1, + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-30T03:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 990.4, + "air_temperature": 8.0, + "cloud_area_fraction": 70.3, + "cloud_area_fraction_high": 33.6, + "cloud_area_fraction_low": 54.7, + "cloud_area_fraction_medium": 0.0, + "dew_point_temperature": 7.0, + "fog_area_fraction": 0.0, + "relative_humidity": 93.2, + "ultraviolet_index_clear_sky": 0.0, + "wind_from_direction": 254.4, + "wind_speed": 4.2 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "fair_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "partlycloudy_night" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "fair_night" + }, + "details": { + "air_temperature_max": 7.8, + "air_temperature_min": 7.1, + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-30T04:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 990.2, + "air_temperature": 7.8, + "cloud_area_fraction": 46.1, + "cloud_area_fraction_high": 1.6, + "cloud_area_fraction_low": 45.3, + "cloud_area_fraction_medium": 0.0, + "dew_point_temperature": 6.5, + "fog_area_fraction": 0.0, + "relative_humidity": 91.5, + "ultraviolet_index_clear_sky": 0.0, + "wind_from_direction": 251.8, + "wind_speed": 4.2 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "fair_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "fair_night" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "fair_day" + }, + "details": { + "air_temperature_max": 8.9, + "air_temperature_min": 7.1, + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-30T05:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 990.8, + "air_temperature": 7.5, + "cloud_area_fraction": 35.9, + "cloud_area_fraction_high": 0.0, + "cloud_area_fraction_low": 35.9, + "cloud_area_fraction_medium": 0.0, + "dew_point_temperature": 6.2, + "fog_area_fraction": 4.7, + "relative_humidity": 91.1, + "ultraviolet_index_clear_sky": 0.0, + "wind_from_direction": 247.0, + "wind_speed": 3.9 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "fair_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "fair_night" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "fair_day" + }, + "details": { + "air_temperature_max": 9.8, + "air_temperature_min": 7.1, + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-30T06:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 991.2, + "air_temperature": 7.4, + "cloud_area_fraction": 28.1, + "cloud_area_fraction_high": 5.5, + "cloud_area_fraction_low": 24.2, + "cloud_area_fraction_medium": 0.0, + "dew_point_temperature": 6.0, + "fog_area_fraction": 4.7, + "relative_humidity": 90.8, + "ultraviolet_index_clear_sky": 0.0, + "wind_from_direction": 238.9, + "wind_speed": 3.6 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "fair_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "fair_night" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "fair_day" + }, + "details": { + "air_temperature_max": 10.5, + "air_temperature_min": 7.1, + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-30T07:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 991.9, + "air_temperature": 7.1, + "cloud_area_fraction": 18.0, + "cloud_area_fraction_high": 0.8, + "cloud_area_fraction_low": 18.0, + "cloud_area_fraction_medium": 0.0, + "dew_point_temperature": 6.0, + "fog_area_fraction": 1.6, + "relative_humidity": 92.8, + "ultraviolet_index_clear_sky": 0.0, + "wind_from_direction": 240.3, + "wind_speed": 3.7 + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "clearsky_day" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "fair_day" + }, + "details": { + "air_temperature_max": 11.0, + "air_temperature_min": 7.2, + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-30T08:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 992.7, + "air_temperature": 7.2, + "cloud_area_fraction": 11.7, + "cloud_area_fraction_high": 0.0, + "cloud_area_fraction_low": 11.7, + "cloud_area_fraction_medium": 0.0, + "dew_point_temperature": 5.9, + "fog_area_fraction": 0.8, + "relative_humidity": 90.8, + "ultraviolet_index_clear_sky": 0.1, + "wind_from_direction": 238.5, + "wind_speed": 3.6 + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "clearsky_day" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "fair_day" + }, + "details": { + "air_temperature_max": 11.1, + "air_temperature_min": 7.8, + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-30T09:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 993.3, + "air_temperature": 7.8, + "cloud_area_fraction": 11.7, + "cloud_area_fraction_high": 0.0, + "cloud_area_fraction_low": 11.7, + "cloud_area_fraction_medium": 0.0, + "dew_point_temperature": 5.7, + "fog_area_fraction": 0.0, + "relative_humidity": 86.0, + "ultraviolet_index_clear_sky": 0.4, + "wind_from_direction": 242.0, + "wind_speed": 3.4 + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "clearsky_day" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "fair_day" + }, + "details": { + "air_temperature_max": 11.2, + "air_temperature_min": 8.9, + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-30T10:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 993.5, + "air_temperature": 8.9, + "cloud_area_fraction": 4.7, + "cloud_area_fraction_high": 0.0, + "cloud_area_fraction_low": 4.7, + "cloud_area_fraction_medium": 0.0, + "dew_point_temperature": 5.7, + "fog_area_fraction": 0.0, + "relative_humidity": 80.1, + "ultraviolet_index_clear_sky": 0.7, + "wind_from_direction": 239.6, + "wind_speed": 3.3 + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "clearsky_day" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + }, + "details": { + "air_temperature_max": 11.2, + "air_temperature_min": 9.8, + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-30T11:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 994.0, + "air_temperature": 9.8, + "cloud_area_fraction": 10.2, + "cloud_area_fraction_high": 0.0, + "cloud_area_fraction_low": 10.2, + "cloud_area_fraction_medium": 0.0, + "dew_point_temperature": 5.5, + "fog_area_fraction": 0.0, + "relative_humidity": 74.8, + "ultraviolet_index_clear_sky": 1.0, + "wind_from_direction": 243.7, + "wind_speed": 3.7 + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + }, + "details": { + "air_temperature_max": 11.2, + "air_temperature_min": 9.6, + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-30T12:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 994.1, + "air_temperature": 10.5, + "cloud_area_fraction": 50.8, + "cloud_area_fraction_high": 39.8, + "cloud_area_fraction_low": 18.0, + "cloud_area_fraction_medium": 0.0, + "dew_point_temperature": 5.2, + "fog_area_fraction": 0.0, + "relative_humidity": 69.6, + "ultraviolet_index_clear_sky": 1.2, + "wind_from_direction": 245.2, + "wind_speed": 3.9 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "fair_day" + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + }, + "details": { + "precipitation_amount": 0.0 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + }, + "details": { + "air_temperature_max": 11.2, + "air_temperature_min": 8.4, + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-30T13:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 994.2, + "air_temperature": 11.0, + "cloud_area_fraction": 65.6, + "cloud_area_fraction_high": 53.9, + "cloud_area_fraction_low": 25.0, + "cloud_area_fraction_medium": 3.1, + "dew_point_temperature": 4.8, + "fog_area_fraction": 0.0, + "relative_humidity": 65.2, + "ultraviolet_index_clear_sky": 1.1, + "wind_from_direction": 241.9, + "wind_speed": 4.2 + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "fair_day" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-30T14:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 994.3, + "air_temperature": 11.1, + "cloud_area_fraction": 32.8, + "cloud_area_fraction_high": 14.8, + "cloud_area_fraction_low": 21.1, + "cloud_area_fraction_medium": 3.1, + "dew_point_temperature": 4.4, + "fog_area_fraction": 0.0, + "relative_humidity": 63.0, + "ultraviolet_index_clear_sky": 0.7, + "wind_from_direction": 242.4, + "wind_speed": 4.4 + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "fair_day" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-30T15:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 994.6, + "air_temperature": 11.2, + "cloud_area_fraction": 20.3, + "cloud_area_fraction_high": 7.8, + "cloud_area_fraction_low": 11.7, + "cloud_area_fraction_medium": 3.1, + "dew_point_temperature": 4.1, + "fog_area_fraction": 0.0, + "relative_humidity": 61.6, + "ultraviolet_index_clear_sky": 0.4, + "wind_from_direction": 242.6, + "wind_speed": 4.1 + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-30T16:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 995.0, + "air_temperature": 10.7, + "cloud_area_fraction": 49.2, + "cloud_area_fraction_high": 49.2, + "cloud_area_fraction_low": 0.8, + "cloud_area_fraction_medium": 0.0, + "dew_point_temperature": 4.1, + "fog_area_fraction": 0.0, + "relative_humidity": 63.6, + "ultraviolet_index_clear_sky": 0.1, + "wind_from_direction": 238.4, + "wind_speed": 2.8 + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-30T17:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 995.4, + "air_temperature": 9.6, + "cloud_area_fraction": 52.3, + "cloud_area_fraction_high": 52.3, + "cloud_area_fraction_low": 0.0, + "cloud_area_fraction_medium": 0.0, + "dew_point_temperature": 5.2, + "fog_area_fraction": 0.0, + "relative_humidity": 74.2, + "ultraviolet_index_clear_sky": 0.0, + "wind_from_direction": 204.4, + "wind_speed": 2.0 + } + }, + "next_1_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-30T18:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 995.5, + "air_temperature": 8.4, + "cloud_area_fraction": 50.8, + "cloud_area_fraction_high": 50.8, + "cloud_area_fraction_low": 0.0, + "cloud_area_fraction_medium": 0.0, + "dew_point_temperature": 6.4, + "fog_area_fraction": 0.0, + "relative_humidity": 86.9, + "ultraviolet_index_clear_sky": 0.0, + "wind_from_direction": 184.0, + "wind_speed": 2.5 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "partlycloudy_night" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "fair_night" + }, + "details": { + "air_temperature_max": 10.1, + "air_temperature_min": 8.0, + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-10-31T00:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 992.2, + "air_temperature": 10.1, + "cloud_area_fraction": 56.2, + "cloud_area_fraction_high": 33.6, + "cloud_area_fraction_low": 33.6, + "cloud_area_fraction_medium": 17.2, + "dew_point_temperature": 6.6, + "relative_humidity": 79.6, + "wind_from_direction": 158.7, + "wind_speed": 6.4 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "rainshowers_day" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "heavyrainshowers_night" + }, + "details": { + "air_temperature_max": 10.8, + "air_temperature_min": 9.9, + "precipitation_amount": 7.9 + } + } + } + }, + { + "time": "2021-10-31T06:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 983.7, + "air_temperature": 10.8, + "cloud_area_fraction": 100.0, + "cloud_area_fraction_high": 100.0, + "cloud_area_fraction_low": 100.0, + "cloud_area_fraction_medium": 100.0, + "dew_point_temperature": 9.2, + "relative_humidity": 90.3, + "wind_from_direction": 143.5, + "wind_speed": 8.9 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "lightrainshowers_day" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "heavyrainshowers_day" + }, + "details": { + "air_temperature_max": 10.9, + "air_temperature_min": 9.8, + "precipitation_amount": 5.9 + } + } + } + }, + { + "time": "2021-10-31T12:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 983.5, + "air_temperature": 10.0, + "cloud_area_fraction": 34.4, + "cloud_area_fraction_high": 0.0, + "cloud_area_fraction_low": 29.7, + "cloud_area_fraction_medium": 18.7, + "dew_point_temperature": 6.1, + "relative_humidity": 77.6, + "wind_from_direction": 248.8, + "wind_speed": 6.9 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "fair_day" + }, + "details": { + "air_temperature_max": 10.3, + "air_temperature_min": 8.3, + "precipitation_amount": 0.2 + } + } + } + }, + { + "time": "2021-10-31T18:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 984.5, + "air_temperature": 8.3, + "cloud_area_fraction": 2.3, + "cloud_area_fraction_high": 1.6, + "cloud_area_fraction_low": 1.6, + "cloud_area_fraction_medium": 0.0, + "dew_point_temperature": 5.0, + "relative_humidity": 79.3, + "wind_from_direction": 221.1, + "wind_speed": 3.1 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "cloudy" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "partlycloudy_night" + }, + "details": { + "air_temperature_max": 8.3, + "air_temperature_min": 5.3, + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-11-01T00:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 983.3, + "air_temperature": 5.3, + "cloud_area_fraction": 99.2, + "cloud_area_fraction_high": 97.7, + "cloud_area_fraction_low": 75.8, + "cloud_area_fraction_medium": 82.0, + "dew_point_temperature": 5.0, + "relative_humidity": 97.3, + "wind_from_direction": 286.3, + "wind_speed": 2.7 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "cloudy" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "air_temperature_max": 5.5, + "air_temperature_min": 4.9, + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-11-01T06:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 985.4, + "air_temperature": 5.4, + "cloud_area_fraction": 100.0, + "cloud_area_fraction_high": 100.0, + "cloud_area_fraction_low": 100.0, + "cloud_area_fraction_medium": 33.6, + "dew_point_temperature": 4.9, + "relative_humidity": 96.3, + "wind_from_direction": 269.9, + "wind_speed": 5.9 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "cloudy" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "air_temperature_max": 9.8, + "air_temperature_min": 5.4, + "precipitation_amount": 0.3 + } + } + } + }, + { + "time": "2021-11-01T12:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 989.7, + "air_temperature": 9.9, + "cloud_area_fraction": 100.0, + "cloud_area_fraction_high": 84.4, + "cloud_area_fraction_low": 17.2, + "cloud_area_fraction_medium": 96.9, + "dew_point_temperature": 6.1, + "relative_humidity": 77.8, + "wind_from_direction": 297.2, + "wind_speed": 7.3 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "cloudy" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "air_temperature_max": 10.6, + "air_temperature_min": 9.2, + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-11-01T18:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 993.7, + "air_temperature": 9.1, + "cloud_area_fraction": 100.0, + "cloud_area_fraction_high": 100.0, + "cloud_area_fraction_low": 17.2, + "cloud_area_fraction_medium": 16.4, + "dew_point_temperature": 5.5, + "relative_humidity": 77.6, + "wind_from_direction": 294.1, + "wind_speed": 6.0 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "air_temperature_max": 9.1, + "air_temperature_min": 7.0, + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-11-02T00:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 997.0, + "air_temperature": 7.0, + "cloud_area_fraction": 100.0, + "cloud_area_fraction_high": 68.0, + "cloud_area_fraction_low": 3.1, + "cloud_area_fraction_medium": 100.0, + "dew_point_temperature": 5.8, + "relative_humidity": 91.5, + "wind_from_direction": 280.6, + "wind_speed": 6.0 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "fair_day" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "partlycloudy_night" + }, + "details": { + "air_temperature_max": 7.0, + "air_temperature_min": 5.8, + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-11-02T06:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 999.2, + "air_temperature": 5.8, + "cloud_area_fraction": 25.0, + "cloud_area_fraction_high": 15.6, + "cloud_area_fraction_low": 8.6, + "cloud_area_fraction_medium": 1.6, + "dew_point_temperature": 5.2, + "relative_humidity": 95.7, + "wind_from_direction": 274.9, + "wind_speed": 5.5 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "fair_day" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "fair_day" + }, + "details": { + "air_temperature_max": 8.8, + "air_temperature_min": 5.7, + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-11-02T12:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 1001.4, + "air_temperature": 8.9, + "cloud_area_fraction": 22.7, + "cloud_area_fraction_high": 0.0, + "cloud_area_fraction_low": 22.7, + "cloud_area_fraction_medium": 0.0, + "dew_point_temperature": 4.6, + "relative_humidity": 74.1, + "wind_from_direction": 291.2, + "wind_speed": 7.2 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "fair_day" + }, + "details": { + "air_temperature_max": 9.7, + "air_temperature_min": 7.9, + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-11-02T18:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 1002.5, + "air_temperature": 7.9, + "cloud_area_fraction": 51.6, + "cloud_area_fraction_high": 0.0, + "cloud_area_fraction_low": 50.8, + "cloud_area_fraction_medium": 7.0, + "dew_point_temperature": 4.7, + "relative_humidity": 79.5, + "wind_from_direction": 283.1, + "wind_speed": 7.8 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "partlycloudy_night" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "lightrainshowers_night" + }, + "details": { + "air_temperature_max": 8.0, + "air_temperature_min": 7.4, + "precipitation_amount": 0.9 + } + } + } + }, + { + "time": "2021-11-03T00:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 1005.4, + "air_temperature": 8.0, + "cloud_area_fraction": 86.7, + "cloud_area_fraction_high": 10.2, + "cloud_area_fraction_low": 35.9, + "cloud_area_fraction_medium": 77.3, + "dew_point_temperature": 5.4, + "relative_humidity": 83.1, + "wind_from_direction": 299.1, + "wind_speed": 7.9 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "fair_day" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "partlycloudy_night" + }, + "details": { + "air_temperature_max": 8.0, + "air_temperature_min": 6.7, + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-11-03T06:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 1010.6, + "air_temperature": 6.7, + "cloud_area_fraction": 7.8, + "cloud_area_fraction_high": 0.0, + "cloud_area_fraction_low": 7.8, + "cloud_area_fraction_medium": 0.0, + "dew_point_temperature": 3.5, + "relative_humidity": 80.2, + "wind_from_direction": 309.5, + "wind_speed": 7.0 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "fair_day" + }, + "details": { + "air_temperature_max": 8.6, + "air_temperature_min": 6.3, + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-11-03T12:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 1015.2, + "air_temperature": 8.6, + "cloud_area_fraction": 68.0, + "cloud_area_fraction_high": 56.2, + "cloud_area_fraction_low": 19.5, + "cloud_area_fraction_medium": 0.0, + "dew_point_temperature": 3.3, + "relative_humidity": 69.6, + "wind_from_direction": 326.0, + "wind_speed": 6.8 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "fair_day" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + }, + "details": { + "air_temperature_max": 9.2, + "air_temperature_min": 7.0, + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-11-03T18:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 1018.0, + "air_temperature": 6.9, + "cloud_area_fraction": 26.6, + "cloud_area_fraction_high": 0.0, + "cloud_area_fraction_low": 26.6, + "cloud_area_fraction_medium": 2.3, + "dew_point_temperature": 3.6, + "relative_humidity": 79.4, + "wind_from_direction": 304.5, + "wind_speed": 4.0 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "clearsky_night" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "fair_night" + }, + "details": { + "air_temperature_max": 6.9, + "air_temperature_min": 4.0, + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-11-04T00:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 1020.0, + "air_temperature": 3.9, + "cloud_area_fraction": 0.8, + "cloud_area_fraction_high": 0.0, + "cloud_area_fraction_low": 0.8, + "cloud_area_fraction_medium": 0.0, + "dew_point_temperature": 3.2, + "relative_humidity": 95.6, + "wind_from_direction": 273.0, + "wind_speed": 3.8 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "clearsky_night" + }, + "details": { + "air_temperature_max": 5.1, + "air_temperature_min": 3.8, + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-11-04T06:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 1018.5, + "air_temperature": 5.2, + "cloud_area_fraction": 8.6, + "cloud_area_fraction_high": 3.1, + "cloud_area_fraction_low": 4.7, + "cloud_area_fraction_medium": 2.3, + "dew_point_temperature": 3.1, + "relative_humidity": 87.0, + "wind_from_direction": 242.6, + "wind_speed": 4.2 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "cloudy" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "partlycloudy_day" + }, + "details": { + "air_temperature_max": 9.0, + "air_temperature_min": 5.2, + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-11-04T12:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 1016.9, + "air_temperature": 9.0, + "cloud_area_fraction": 96.1, + "cloud_area_fraction_high": 53.1, + "cloud_area_fraction_low": 29.7, + "cloud_area_fraction_medium": 82.8, + "dew_point_temperature": 6.2, + "relative_humidity": 82.7, + "wind_from_direction": 248.6, + "wind_speed": 6.6 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "cloudy" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "air_temperature_max": 11.0, + "air_temperature_min": 9.0, + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-11-04T18:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 1016.0, + "air_temperature": 10.8, + "cloud_area_fraction": 100.0, + "cloud_area_fraction_high": 0.0, + "cloud_area_fraction_low": 100.0, + "cloud_area_fraction_medium": 98.4, + "dew_point_temperature": 8.2, + "relative_humidity": 84.4, + "wind_from_direction": 249.6, + "wind_speed": 5.3 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "cloudy" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "air_temperature_max": 10.8, + "air_temperature_min": 10.5, + "precipitation_amount": 0.1 + } + } + } + }, + { + "time": "2021-11-05T00:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 1016.2, + "air_temperature": 10.5, + "cloud_area_fraction": 100.0, + "cloud_area_fraction_high": 7.0, + "cloud_area_fraction_low": 100.0, + "cloud_area_fraction_medium": 29.7, + "dew_point_temperature": 8.7, + "relative_humidity": 89.2, + "wind_from_direction": 249.6, + "wind_speed": 4.7 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "cloudy" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "air_temperature_max": 10.5, + "air_temperature_min": 10.0, + "precipitation_amount": 0.3 + } + } + } + }, + { + "time": "2021-11-05T06:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 1016.1, + "air_temperature": 10.0, + "cloud_area_fraction": 100.0, + "cloud_area_fraction_high": 100.0, + "cloud_area_fraction_low": 88.3, + "cloud_area_fraction_medium": 11.7, + "dew_point_temperature": 8.3, + "relative_humidity": 90.5, + "wind_from_direction": 242.7, + "wind_speed": 4.3 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "rain" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "air_temperature_max": 10.4, + "air_temperature_min": 9.5, + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-11-05T12:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 1013.5, + "air_temperature": 10.4, + "cloud_area_fraction": 100.0, + "cloud_area_fraction_high": 100.0, + "cloud_area_fraction_low": 11.7, + "cloud_area_fraction_medium": 100.0, + "dew_point_temperature": 7.7, + "relative_humidity": 84.4, + "wind_from_direction": 180.1, + "wind_speed": 3.8 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "rain" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "heavyrain" + }, + "details": { + "air_temperature_max": 11.9, + "air_temperature_min": 10.4, + "precipitation_amount": 9.1 + } + } + } + }, + { + "time": "2021-11-05T18:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 1001.3, + "air_temperature": 11.9, + "cloud_area_fraction": 100.0, + "cloud_area_fraction_high": 100.0, + "cloud_area_fraction_low": 97.7, + "cloud_area_fraction_medium": 100.0, + "dew_point_temperature": 9.5, + "relative_humidity": 84.6, + "wind_from_direction": 191.8, + "wind_speed": 8.7 + } + }, + "next_12_hours": { + "summary": { + "symbol_code": "lightrain" + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "rain" + }, + "details": { + "air_temperature_max": 14.4, + "air_temperature_min": 11.9, + "precipitation_amount": 4.7 + } + } + } + }, + { + "time": "2021-11-06T00:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 997.7, + "air_temperature": 14.2, + "cloud_area_fraction": 100.0, + "cloud_area_fraction_high": 100.0, + "cloud_area_fraction_low": 99.2, + "cloud_area_fraction_medium": 4.7, + "dew_point_temperature": 12.2, + "relative_humidity": 88.2, + "wind_from_direction": 255.3, + "wind_speed": 8.8 + } + }, + "next_6_hours": { + "summary": { + "symbol_code": "cloudy" + }, + "details": { + "air_temperature_max": 14.2, + "air_temperature_min": 10.5, + "precipitation_amount": 0.0 + } + } + } + }, + { + "time": "2021-11-06T06:00:00Z", + "data": { + "instant": { + "details": { + "air_pressure_at_sea_level": 1002.5, + "air_temperature": 10.5, + "cloud_area_fraction": 100.0, + "cloud_area_fraction_high": 100.0, + "cloud_area_fraction_low": 21.9, + "cloud_area_fraction_medium": 3.1, + "dew_point_temperature": 8.5, + "relative_humidity": 87.9, + "wind_from_direction": 260.1, + "wind_speed": 4.8 + } + } + } + } + ] + } +} \ No newline at end of file From 16cf779c1175a4ce93c40432f837ec5267491b8c Mon Sep 17 00:00:00 2001 From: Jakub Jarosz Date: Fri, 29 Oct 2021 08:12:41 +0100 Subject: [PATCH 14/28] Update dependencies --- go.mod | 2 ++ go.sum | 4 ++++ 2 files changed, 6 insertions(+) create mode 100644 go.sum diff --git a/go.mod b/go.mod index 12f99f9..bb6a61c 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module github.com/qba73/meteo go 1.17 + +require github.com/google/go-cmp v0.5.6 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..03e1a9c --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From ffcb0c84c8104cd7d6a3156138f0c0925a66716c Mon Sep 17 00:00:00 2001 From: Jakub Jarosz Date: Fri, 29 Oct 2021 08:14:23 +0100 Subject: [PATCH 15/28] Get weather for Lat Lon --- cmd/meteo/main.go | 7 +++ examples/default/main.go | 20 ++++++ meteo.go | 37 +++++++----- meteo_test.go | 17 +++++- norway.go | 127 +++++++++++++++++++++++++++++++++++++++ norway_test.go | 48 +++++++++++++++ 6 files changed, 238 insertions(+), 18 deletions(-) create mode 100644 cmd/meteo/main.go create mode 100644 examples/default/main.go create mode 100644 norway.go create mode 100644 norway_test.go diff --git a/cmd/meteo/main.go b/cmd/meteo/main.go new file mode 100644 index 0000000..4c7000e --- /dev/null +++ b/cmd/meteo/main.go @@ -0,0 +1,7 @@ +package main + +import "github.com/qba73/meteo" + +func main() { + meteo.RunCLI() +} diff --git a/examples/default/main.go b/examples/default/main.go new file mode 100644 index 0000000..d46331e --- /dev/null +++ b/examples/default/main.go @@ -0,0 +1,20 @@ +package main + +import ( + "fmt" + "log" + + "github.com/qba73/meteo" +) + +func main() { + // Get weather status for the given lat, lon: + weather, err := meteo.GetWeather(53.2, -6.2) + if err != nil { + log.Println(err) + } + + // Print out weather string. + // Example: Lightrain 8.3°C + fmt.Println(weather) +} diff --git a/meteo.go b/meteo.go index b561eaa..0acde4b 100644 --- a/meteo.go +++ b/meteo.go @@ -1,28 +1,33 @@ package meteo import ( - "net/http" - "time" + "fmt" + "os" + "strings" ) const ( - baseURL = "https://" + userAgent = "Meteo/0.1 https://github.com/qba73/meteo" ) -// Client represents YR.no weather client. -type Client struct { - apiKey string - BaseURL string - HTTPClient *http.Client +// Weather represents weather conditions +// in a geographical region. +type Weather struct { + Summary string + Temp float64 } -// NewClient knows how to create Meteo service client. -func NewClient(apikey string) *Client { - return &Client{ - BaseURL: baseURL, - apiKey: apikey, - HTTPClient: &http.Client{ - Timeout: time.Second * 10, - }, +// String implements stringer interface. +func (w Weather) String() string { + return fmt.Sprintf("%s %.1f°C", strings.Title(w.Summary), w.Temp) +} + +func RunCLI() { + c := NewNorwayClient() + w, err := c.GetForecast(53.2, -6.2) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) } + fmt.Println(w) } diff --git a/meteo_test.go b/meteo_test.go index 30b7fcb..1410fac 100644 --- a/meteo_test.go +++ b/meteo_test.go @@ -1,11 +1,24 @@ package meteo_test import ( + "bytes" + "fmt" "testing" "github.com/qba73/meteo" ) -func TestNewClient(t *testing.T) { - meteo.NewClient("APIKEY") +func TestWeatherStringFormat(t *testing.T) { + t.Parallel() + w := meteo.Weather{ + Summary: "sunny", + Temp: -3.12, + } + out := bytes.Buffer{} + fmt.Fprint(&out, w) + got := out.String() + want := "Sunny -3.1°C" + if want != got { + t.Errorf("want %s, got %s", want, got) + } } diff --git a/norway.go b/norway.go new file mode 100644 index 0000000..7dbc3d9 --- /dev/null +++ b/norway.go @@ -0,0 +1,127 @@ +package meteo + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + "time" +) + +const ( + baseURL = "https://api.met.no" +) + +type norwayForecast struct { + Type string `json:"type"` + Geometry geometry `json:"geometry"` + Properties properties `json:"properties"` +} + +type geometry struct { + Type string `json:"type"` + Coordinates []float64 `json:"coordinates"` + Properties properties `json:"properties"` +} + +type properties struct { + Meta meta `json:"meta"` + Timeseries timeseries `json:"timeseries"` +} + +type meta struct { + UpdatedAt string `json:"updated_at"` + Units map[string]string `json:"units"` +} + +type timeseries []forecastEntry + +type forecastEntry struct { + Time string `json:"time"` + Data data `json:"data"` +} + +type data struct { + Instant struct { + Details struct { + AirTemperature float64 `json:"air_temperature"` + } + } + Next1Hours struct { + Summary struct { + SymbolCode string `json:"symbol_code"` + } + } `json:"next_1_hours"` +} + +// NorwayClient represents a weather client +// for the Norwegian Meteorological Institute. +type NorwayClient struct { + UA string + BaseURL string + HTTPClient *http.Client +} + +func NewNorwayClient() *NorwayClient { + return &NorwayClient{ + BaseURL: baseURL, + UA: userAgent, + HTTPClient: &http.Client{ + Timeout: time.Second * 5, + }, + } +} + +func (c NorwayClient) GetForecast(lat, lon float64) (Weather, error) { + u, err := c.makeURL(lat, lon) + if err != nil { + return Weather{}, err + } + req, err := http.NewRequest(http.MethodGet, u, nil) + if err != nil { + return Weather{}, err + } + req.Header.Add("Content-Type", "application/json") + req.Header.Add("User-Agent", userAgent) + + res, err := c.HTTPClient.Do(req) + if err != nil { + return Weather{}, err + } + defer res.Body.Close() + + var nf norwayForecast + err = json.NewDecoder(res.Body).Decode(&nf) + if err != nil { + return Weather{}, err + } + if len(nf.Properties.Timeseries) < 1 { + return Weather{}, fmt.Errorf("invalid response %+v", nf) + } + + w := Weather{ + Summary: nf.Properties.Timeseries[0].Data.Next1Hours.Summary.SymbolCode, + Temp: nf.Properties.Timeseries[0].Data.Instant.Details.AirTemperature, + } + return w, nil +} + +func (c NorwayClient) makeURL(lat, lon float64) (string, error) { + base, err := url.Parse(c.BaseURL + "/weatherapi/locationforecast/2.0/compact") + if err != nil { + return "", err + } + params := url.Values{} + params.Add("lat", fmt.Sprintf("%.2f", lat)) + params.Add("lon", fmt.Sprintf("%.2f", lon)) + base.RawQuery = params.Encode() + return base.String(), nil +} + +func GetWeather(lat, lon float64) (Weather, error) { + w, err := NewNorwayClient().GetForecast(lat, lon) + if err != nil { + return Weather{}, err + } + return w, nil +} diff --git a/norway_test.go b/norway_test.go new file mode 100644 index 0000000..2ac714b --- /dev/null +++ b/norway_test.go @@ -0,0 +1,48 @@ +package meteo_test + +import ( + "io" + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/qba73/meteo" +) + +func TestCreateNewMeteoClient(t *testing.T) { + t.Parallel() + var c *meteo.NorwayClient + c = meteo.NewNorwayClient() + _ = c +} + +func TestGetForecastCompact(t *testing.T) { + t.Parallel() + ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + f, err := os.Open("testdata/response-compact.json") + if err != nil { + t.Fatal(err) + } + defer f.Close() + io.Copy(rw, f) + })) + defer ts.Close() + + client := meteo.NewNorwayClient() + client.BaseURL = ts.URL + + got, err := client.GetForecast(53.3, -6.2) + if err != nil { + t.Errorf("error getting forecast data, %v", err) + } + want := meteo.Weather{ + Summary: "rain", + Temp: 13.7, + } + + if !cmp.Equal(want, got) { + t.Error(cmp.Diff(want, got)) + } +} From aad152398abef7e96b6e12659d33834c7b376737 Mon Sep 17 00:00:00 2001 From: Jakub Jarosz Date: Mon, 1 Nov 2021 06:28:32 +0000 Subject: [PATCH 16/28] Add Geo coordinates lookup --- examples/geoplaces/main.go | 22 ++++ geoname.go | 144 +++++++++++++++++++++ geoname_test.go | 76 +++++++++++ meteo.go | 6 +- norway.go | 50 +++++-- norway_test.go | 35 ++++- testdata/response-geoname-multiple-02.json | 1 + testdata/response-geoname-multiple.json | 1 + testdata/response-geoname-single.json | 14 ++ 9 files changed, 335 insertions(+), 14 deletions(-) create mode 100644 examples/geoplaces/main.go create mode 100644 geoname.go create mode 100644 geoname_test.go create mode 100644 testdata/response-geoname-multiple-02.json create mode 100644 testdata/response-geoname-multiple.json create mode 100644 testdata/response-geoname-single.json diff --git a/examples/geoplaces/main.go b/examples/geoplaces/main.go new file mode 100644 index 0000000..e9a9390 --- /dev/null +++ b/examples/geoplaces/main.go @@ -0,0 +1,22 @@ +package main + +import ( + "fmt" + "os" + + "github.com/qba73/meteo" +) + +func main() { + // Get coordinates using a default Geo client + user := os.Getenv("GEO_USERNAME") + + coord, err := meteo.GetCoordinates("Castlebar", "IE", user) + if err != nil { + println(err) + } + + fmt.Printf("Lat: %.2f, Lng: %.2f for %s in country %s\n", coord.Lat, coord.Lng, coord.PlaceName, coord.CountryCode) + // It returns: + // Lat: 53.85, Lng: -9.30 for Castlebar in country IE +} diff --git a/geoname.go b/geoname.go new file mode 100644 index 0000000..514bde1 --- /dev/null +++ b/geoname.go @@ -0,0 +1,144 @@ +package meteo + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "time" +) + +const ( + baseURLGeoNames = "http://api.geonames.org" +) + +// PlaceCoordinates represents response from GeoNames web service. +type PlaceCoordinates struct { + Lat float64 + Lng float64 + CountryCode string + PlaceName string +} + +type coordinates struct { + PostalCodes []struct { + Lat float64 `json:"lat"` + Lng float64 `json:"lng"` + CountryCode string `json:"countryCode"` + PlaceName string `json:"placeName"` + } `json:"postalCodes"` +} + +type geoNameOption func(*GeoNamesClient) error + +// WithGeoNamesUserAgent knows how to add custom user agent +// to web requests. +func WithGeoNamesUserAgent(ua string) geoNameOption { + return func(gnc *GeoNamesClient) error { + if ua == "" { + return errors.New("user agent not provided") + } + gnc.UA = ua + return nil + } +} + +// GeoNameClient is a client used for communicating +// with geo name web services. +type GeoNamesClient struct { + UA string + UserName string + BaseURL string + HTTPClient *http.Client +} + +// NewGeoNameClient knows how to create a client for GeoNames Web services. +func NewGeoNamesClient(username string, opts ...geoNameOption) (*GeoNamesClient, error) { + if username == "" { + return nil, errors.New("missing user name") + } + + c := GeoNamesClient{ + BaseURL: baseURLGeoNames, + UA: userAgent, + UserName: username, + HTTPClient: &http.Client{ + Timeout: time.Second * 5, + }, + } + for _, opt := range opts { + if err := opt(&c); err != nil { + return &GeoNamesClient{}, err + } + } + return &c, nil +} + +// GetCoordinates knows how to retrieve lat and long coordinates +// for the given place name and country. +func (g GeoNamesClient) GetCoordinates(placeName, countryCode string) (PlaceCoordinates, error) { + u, err := g.makeURL(placeName, countryCode) + if err != nil { + return PlaceCoordinates{}, err + } + req, err := g.prepareRequest(u) + if err != nil { + return PlaceCoordinates{}, err + } + res, err := g.HTTPClient.Do(req) + if err != nil { + return PlaceCoordinates{}, err + } + defer res.Body.Close() + + var co coordinates + if err := json.NewDecoder(res.Body).Decode(&co); err != nil { + return PlaceCoordinates{}, fmt.Errorf("decoding response %w", err) + } + if len(co.PostalCodes) < 1 { + return PlaceCoordinates{}, fmt.Errorf("place %s in country %s not found", placeName, countryCode) + } + + pc := PlaceCoordinates{ + Lat: co.PostalCodes[0].Lat, + Lng: co.PostalCodes[0].Lng, + PlaceName: co.PostalCodes[0].PlaceName, + CountryCode: co.PostalCodes[0].CountryCode, + } + + return pc, nil +} + +func (g GeoNamesClient) makeURL(placeName, countryCode string) (string, error) { + base, err := url.Parse(g.BaseURL + "/postalCodeSearchJSON") + if err != nil { + return "", fmt.Errorf("making url %w", err) + } + params := url.Values{} + params.Add("placename", placeName) + params.Add("country", countryCode) + params.Add("username", g.UserName) + base.RawQuery = params.Encode() + return base.String(), nil +} + +func (g GeoNamesClient) prepareRequest(u string) (*http.Request, error) { + req, err := http.NewRequest(http.MethodGet, u, nil) + if err != nil { + return nil, fmt.Errorf("preparing request %w", err) + } + req.Header.Add("Content-Type", "application/json") + req.Header.Add("User-Agent", userAgent) + return req, nil +} + +// GetCoordinates knows how to get Lat and Long coordinates for +// the given place and country using default geo client. +func GetCoordinates(placename, countryCode, username string) (PlaceCoordinates, error) { + c, err := NewGeoNamesClient(username) + if err != nil { + return PlaceCoordinates{}, err + } + return c.GetCoordinates(placename, countryCode) +} diff --git a/geoname_test.go b/geoname_test.go new file mode 100644 index 0000000..144501f --- /dev/null +++ b/geoname_test.go @@ -0,0 +1,76 @@ +package meteo_test + +import ( + "io" + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/qba73/meteo" +) + +func TestCreateNewGeoNamesClient(t *testing.T) { + t.Parallel() + var c *meteo.GeoNamesClient + c, err := meteo.NewGeoNamesClient("user") + if err != nil { + t.Fatal(err) + } + _ = c +} + +func TestCreateNewGeoNamesClientWithoutUserName(t *testing.T) { + t.Parallel() + _, err := meteo.NewGeoNamesClient("") + if err == nil { + t.Fatal("create client without user should return err") + } +} + +func TestCreateNewGeoNamesClientWithUser(t *testing.T) { + t.Parallel() + c, err := meteo.NewGeoNamesClient("User") + if err != nil { + t.Fatal(err) + } + want := "User" + if want != c.UserName { + t.Errorf("want %s, got %s", want, c.UserName) + } +} + +func TestGetCoordinatesSingleGeoNames(t *testing.T) { + t.Parallel() + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + f, err := os.Open("testdata/response-geoname-single.json") + if err != nil { + t.Fatal(err) + } + defer f.Close() + io.Copy(w, f) + })) + defer ts.Close() + + c, err := meteo.NewGeoNamesClient("UserName") + if err != nil { + t.Fatal(err) + } + c.BaseURL = ts.URL + + got, err := c.GetCoordinates("Castlebar", "IE") + if err != nil { + t.Fatalf("GetCoordinates(\"Castlebar\", \"IE\") got err %v", err) + } + want := meteo.PlaceCoordinates{ + Lng: -9.3, + Lat: 53.85, + PlaceName: "Castlebar", + CountryCode: "IE", + } + + if !cmp.Equal(want, got) { + t.Errorf("GetCoordinates('Castlebar', 'IE') \n%s", cmp.Diff(want, got)) + } +} diff --git a/meteo.go b/meteo.go index 0acde4b..0f96b12 100644 --- a/meteo.go +++ b/meteo.go @@ -23,7 +23,11 @@ func (w Weather) String() string { } func RunCLI() { - c := NewNorwayClient() + c, err := NewNorwayClient() + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } w, err := c.GetForecast(53.2, -6.2) if err != nil { fmt.Fprintln(os.Stderr, err) diff --git a/norway.go b/norway.go index 7dbc3d9..dae53f9 100644 --- a/norway.go +++ b/norway.go @@ -2,6 +2,7 @@ package meteo import ( "encoding/json" + "errors" "fmt" "net/http" "net/url" @@ -54,6 +55,18 @@ type data struct { } `json:"next_1_hours"` } +type option func(*NorwayClient) error + +func WithUserAgent(ua string) option { + return func(nc *NorwayClient) error { + if ua == "" { + return errors.New("user agent not provided") + } + nc.UA = ua + return nil + } +} + // NorwayClient represents a weather client // for the Norwegian Meteorological Institute. type NorwayClient struct { @@ -62,14 +75,22 @@ type NorwayClient struct { HTTPClient *http.Client } -func NewNorwayClient() *NorwayClient { - return &NorwayClient{ +func NewNorwayClient(opts ...option) (*NorwayClient, error) { + c := NorwayClient{ BaseURL: baseURL, UA: userAgent, HTTPClient: &http.Client{ Timeout: time.Second * 5, }, } + + for _, opt := range opts { + if err := opt(&c); err != nil { + return &NorwayClient{}, err + } + } + return &c, nil + } func (c NorwayClient) GetForecast(lat, lon float64) (Weather, error) { @@ -77,13 +98,10 @@ func (c NorwayClient) GetForecast(lat, lon float64) (Weather, error) { if err != nil { return Weather{}, err } - req, err := http.NewRequest(http.MethodGet, u, nil) + req, err := c.prepareRequest(u) if err != nil { return Weather{}, err } - req.Header.Add("Content-Type", "application/json") - req.Header.Add("User-Agent", userAgent) - res, err := c.HTTPClient.Do(req) if err != nil { return Weather{}, err @@ -91,8 +109,7 @@ func (c NorwayClient) GetForecast(lat, lon float64) (Weather, error) { defer res.Body.Close() var nf norwayForecast - err = json.NewDecoder(res.Body).Decode(&nf) - if err != nil { + if err := json.NewDecoder(res.Body).Decode(&nf); err != nil { return Weather{}, err } if len(nf.Properties.Timeseries) < 1 { @@ -118,10 +135,23 @@ func (c NorwayClient) makeURL(lat, lon float64) (string, error) { return base.String(), nil } +func (c NorwayClient) prepareRequest(u string) (*http.Request, error) { + req, err := http.NewRequest(http.MethodGet, u, nil) + if err != nil { + return nil, err + } + req.Header.Add("Content-Type", "application/json") + req.Header.Add("User-Agent", userAgent) + return req, nil +} + +// GetWeather returns current weather for given +// Lat and Long using default client for the Norwegian +// meteorological Institute. func GetWeather(lat, lon float64) (Weather, error) { - w, err := NewNorwayClient().GetForecast(lat, lon) + c, err := NewNorwayClient() if err != nil { return Weather{}, err } - return w, nil + return c.GetForecast(lat, lon) } diff --git a/norway_test.go b/norway_test.go index 2ac714b..68ba063 100644 --- a/norway_test.go +++ b/norway_test.go @@ -14,11 +14,37 @@ import ( func TestCreateNewMeteoClient(t *testing.T) { t.Parallel() var c *meteo.NorwayClient - c = meteo.NewNorwayClient() + c, err := meteo.NewNorwayClient() + if err != nil { + t.Fatal(err) + } _ = c } -func TestGetForecastCompact(t *testing.T) { +func TestCreateNewMeteoClientWithCustomUserAgent(t *testing.T) { + t.Parallel() + c, err := meteo.NewNorwayClient( + meteo.WithUserAgent("CustomClient/1.0 https://customclient.com"), + ) + if err != nil { + t.Fatalf("creating client with custom agent, %s\n", err) + } + want := "CustomClient/1.0 https://customclient.com" + got := c.UA + if want != got { + t.Errorf("want %q, got %q", want, got) + } +} + +func TestCreateNewNorwayClientWithInvalidUserAgent(t *testing.T) { + t.Parallel() + _, err := meteo.NewNorwayClient(meteo.WithUserAgent("")) + if err == nil { + t.Errorf("invalid user agent string should return error") + } +} + +func TestGetForecast(t *testing.T) { t.Parallel() ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { f, err := os.Open("testdata/response-compact.json") @@ -30,7 +56,10 @@ func TestGetForecastCompact(t *testing.T) { })) defer ts.Close() - client := meteo.NewNorwayClient() + client, err := meteo.NewNorwayClient() + if err != nil { + t.Fatal(err) + } client.BaseURL = ts.URL got, err := client.GetForecast(53.3, -6.2) diff --git a/testdata/response-geoname-multiple-02.json b/testdata/response-geoname-multiple-02.json new file mode 100644 index 0000000..2c12252 --- /dev/null +++ b/testdata/response-geoname-multiple-02.json @@ -0,0 +1 @@ +{"postalCodes":[{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.1210823059082,"countryCode":"IT","postalCode":"38100","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Trento","lat":46.0678714011874},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.04053,"countryCode":"IT","postalCode":"38068","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Rovereto","lat":45.8904},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":10.841166973114014,"countryCode":"IT","postalCode":"38066","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Riva Del Garda","lat":45.88576635570338},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":10.726788814769579,"countryCode":"IT","postalCode":"38079","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Tione Di Trento","lat":46.035497796159675},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":10.89106,"countryCode":"IT","postalCode":"38069","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Torbole","lat":45.87594},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.032761834383843,"countryCode":"IT","postalCode":"38023","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Cles","lat":46.36294231432275},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.599595740840952,"countryCode":"IT","postalCode":"38037","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Predazzo","lat":46.31140072616998},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":10.88672161102295,"countryCode":"IT","postalCode":"38062","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Arco","lat":45.917721261594224},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.004321464107697,"countryCode":"IT","postalCode":"38010","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Andalo","lat":46.16649169333167},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.109313518335805,"countryCode":"IT","postalCode":"38015","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Lavis","lat":46.14130607650874},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":10.963002847091856,"countryCode":"IT","postalCode":"38018","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Molveno","lat":46.142380392892676},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":10.912464512370285,"countryCode":"IT","postalCode":"38027","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Male'","lat":46.353564221896534},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.769288033585699,"countryCode":"IT","postalCode":"38032","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Canazei","lat":46.476274688266884},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.458624916630892,"countryCode":"IT","postalCode":"38033","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Cavalese","lat":46.29047922207462},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.659412112212882,"countryCode":"IT","postalCode":"38035","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Moena","lat":46.376545567146955},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.457564531035661,"countryCode":"IT","postalCode":"38051","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Borgo","lat":46.05118958806449},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.304270447916947,"countryCode":"IT","postalCode":"38056","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Levico Terme","lat":46.01216635399339},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.674181793539258,"countryCode":"IT","postalCode":"38039","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Vigo Di Fassa","lat":46.41897856140535},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.237577456652453,"countryCode":"IT","postalCode":"38057","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Pergine Valsugana","lat":46.064336496441555},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.00458006809729,"countryCode":"IT","postalCode":"38061","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Ala","lat":45.76072450814006},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":10.763762242707573,"countryCode":"IT","postalCode":"38086","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Pinzolo","lat":46.159762392608975},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":10.73937,"countryCode":"IT","postalCode":"38088","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Spiazzo","lat":46.103601},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.168202279895429,"countryCode":"IT","postalCode":"38064","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Folgaria","lat":45.915432005689404},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":10.980516264270328,"countryCode":"IT","postalCode":"38065","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Mori","lat":45.85187056760624},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":10.912014276751814,"countryCode":"IT","postalCode":"38074","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Dro","lat":45.961209053659836},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":10.580220222473145,"countryCode":"IT","postalCode":"38089","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Storo","lat":45.84924956447676},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.137232084095917,"countryCode":"IT","postalCode":"38013","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Fondo","lat":46.438795214182406},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.122531194491703,"countryCode":"IT","postalCode":"38016","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Mezzocorona","lat":46.21158649056654},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":10.800067,"countryCode":"IT","postalCode":"38020","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Mezzana","lat":46.316707},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":10.673533,"countryCode":"IT","postalCode":"38024","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Peio","lat":46.36278},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":10.692817586310149,"countryCode":"IT","postalCode":"38024","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Cogolo","lat":46.352524289697826},{"adminCode2":"TN","adminCode3":"022233","adminName3":"Dimaro Folgarida","adminCode1":"17","adminName2":"Trento","lng":10.873415931132008,"countryCode":"IT","postalCode":"38025","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Dimaro","lat":46.32572860981403},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.741097164916356,"countryCode":"IT","postalCode":"38031","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Campitello Di Fassa","lat":46.475785072579754},{"adminCode2":"TN","adminCode3":"022241","adminName3":"Cembra Lisignago","adminCode1":"17","adminName2":"Trento","lng":11.221738027261605,"countryCode":"IT","postalCode":"38034","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Cembra","lat":46.17489332174378},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.687112318563669,"countryCode":"IT","postalCode":"38036","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Pozza Di Fassa","lat":46.428057588522286},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.509459696180498,"countryCode":"IT","postalCode":"38038","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Tesero","lat":46.291840805477406},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.194592928218619,"countryCode":"IT","postalCode":"38041","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Albiano","lat":46.14450792463351},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.457564531035661,"countryCode":"IT","postalCode":"38051","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Borgo Valsugana","lat":46.05118958806449},{"adminCode2":"TN","adminCode3":"022245","adminName3":"Primiero San Martino Di Castrozza","adminCode1":"17","adminName2":"Trento","lng":11.839394,"countryCode":"IT","postalCode":"38054","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Tonadico","lat":46.181113},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.635633011364993,"countryCode":"IT","postalCode":"38055","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Grigno","lat":46.01568645258471},{"adminCode2":"TN","adminCode3":"022248","adminName3":"Vallelaghi","adminCode1":"17","adminName2":"Trento","lng":10.997345886437923,"countryCode":"IT","postalCode":"38096","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Vezzano","lat":46.07867304099094},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.069501028281511,"countryCode":"IT","postalCode":"38010","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Fai Della Paganella","lat":46.17801447440701},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.09635988132266,"countryCode":"IT","postalCode":"38017","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Mezzolombardo","lat":46.20773584163479},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.246480941772461,"countryCode":"IT","postalCode":"38042","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Baselga Di Pine'","lat":46.13250462971521},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.243683,"countryCode":"IT","postalCode":"38050","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Calceranica Al Lago","lat":46.004603},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.632466131434317,"countryCode":"IT","postalCode":"38053","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Castello Tesino","lat":46.06301996219218},{"adminCode2":"TN","adminCode3":"022245","adminName3":"Primiero San Martino Di Castrozza","adminCode1":"17","adminName2":"Trento","lng":11.832788358411312,"countryCode":"IT","postalCode":"38054","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Transacqua","lat":46.17366774414386},{"adminCode2":"TN","adminCode3":"022245","adminName3":"Primiero San Martino Di Castrozza","adminCode1":"17","adminName2":"Trento","lng":11.828794,"countryCode":"IT","postalCode":"38054","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Fiera Di Primiero","lat":46.176212},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":10.95508,"countryCode":"IT","postalCode":"38060","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Brentonico","lat":45.819096},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.063508582312211,"countryCode":"IT","postalCode":"38060","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Volano","lat":45.91718017944707},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.02388,"countryCode":"IT","postalCode":"38060","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Nogaredo","lat":45.912999},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.109080998805426,"countryCode":"IT","postalCode":"38060","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Besenello","lat":45.94355528399251},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.000588527708171,"countryCode":"IT","postalCode":"38061","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Pilcante","lat":45.77074844463735},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":10.939380422524657,"countryCode":"IT","postalCode":"38063","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Avio","lat":45.733961949145296},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":10.758869,"countryCode":"IT","postalCode":"38080","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Carisolo","lat":46.168803},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":10.76777,"countryCode":"IT","postalCode":"38086","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Giustino","lat":46.151302},{"adminCode2":"TN","adminCode3":"022248","adminName3":"Vallelaghi","adminCode1":"17","adminName2":"Trento","lng":11.04504510440527,"countryCode":"IT","postalCode":"38096","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Terlago","lat":46.09737070187929},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.1550283432007,"countryCode":"IT","postalCode":"38100","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Povo","lat":46.0669781465587},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.177774,"countryCode":"IT","postalCode":"38010","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Ruffre'","lat":46.414813},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.161278,"countryCode":"IT","postalCode":"38010","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Faedo","lat":46.192407},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":10.938567750005088,"countryCode":"IT","postalCode":"38020","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Rabbi","lat":46.38356119424066},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":10.839568,"countryCode":"IT","postalCode":"38020","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Commezzadura","lat":46.321707},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.01857,"countryCode":"IT","postalCode":"38020","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Rumo","lat":46.441412},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.018802662045728,"countryCode":"IT","postalCode":"38020","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Marcena","lat":46.441207195930666},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":10.737566,"countryCode":"IT","postalCode":"38026","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Ossana","lat":46.306506},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":10.731683425861183,"countryCode":"IT","postalCode":"38026","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Fucine","lat":46.311398969856015},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":10.925869,"countryCode":"IT","postalCode":"38027","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Terzolas","lat":46.361109},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.4172,"countryCode":"IT","postalCode":"38030","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Molina","lat":46.27208},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.458283,"countryCode":"IT","postalCode":"38030","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Varena","lat":46.306713},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.439782,"countryCode":"IT","postalCode":"38033","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Carano","lat":46.291512},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.112151953475646,"countryCode":"IT","postalCode":"38040","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Ravina","lat":46.03944036017364},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.207513809204102,"countryCode":"IT","postalCode":"38040","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Fornace","lat":46.11804907826783},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.25988,"countryCode":"IT","postalCode":"38047","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Segonzano","lat":46.190208},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.264283,"countryCode":"IT","postalCode":"38050","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Tenna","lat":46.015703},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.408238356117364,"countryCode":"IT","postalCode":"38050","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Roncegno","lat":46.049335813618335},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.505688,"countryCode":"IT","postalCode":"38050","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Scurelle","lat":46.064507},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.789193,"countryCode":"IT","postalCode":"38050","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Imer","lat":46.149311},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":10.96564824166577,"countryCode":"IT","postalCode":"38060","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Ronzo","lat":45.88212540554558},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.00918,"countryCode":"IT","postalCode":"38060","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Isera","lat":45.887598},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.117884,"countryCode":"IT","postalCode":"38060","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Vallarsa","lat":45.782796},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":10.89106,"countryCode":"IT","postalCode":"38069","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Nago","lat":45.87594},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":10.89106,"countryCode":"IT","postalCode":"38069","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Nago Torbole","lat":45.87594},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":10.731239318847656,"countryCode":"IT","postalCode":"38060","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Pieve Di Ledro","lat":45.88848473149638},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.092756984434942,"countryCode":"IT","postalCode":"38060","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Aldeno","lat":45.97758446593058},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.071976558615326,"countryCode":"IT","postalCode":"38060","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Nomi","lat":45.929232486290694},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":10.84225058555603,"countryCode":"IT","postalCode":"38075","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Fiave'","lat":46.00457089896917},{"adminCode2":"TN","adminCode3":"022238","adminName3":"Borgo Chiese","adminCode1":"17","adminName2":"Trento","lng":10.593659983393366,"countryCode":"IT","postalCode":"38083","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Condino","lat":45.88166625058836},{"adminCode2":"TN","adminCode3":"022246","adminName3":"Sella Giudicarie","adminCode1":"17","adminName2":"Trento","lng":10.668171,"countryCode":"IT","postalCode":"38087","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Roncone","lat":45.982697},{"adminCode2":"TN","adminCode3":"022248","adminName3":"Vallelaghi","adminCode1":"17","adminName2":"Trento","lng":10.984781329841269,"countryCode":"IT","postalCode":"38096","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Padergnone","lat":46.059815889953576},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.119197938209247,"countryCode":"IT","postalCode":"38010","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Romeno","lat":46.39451704006989},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.049125013450833,"countryCode":"IT","postalCode":"38010","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Denno","lat":46.27424112753419},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.03277345221915,"countryCode":"IT","postalCode":"38010","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Cavedago","lat":46.18485837619542},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.152749217616556,"countryCode":"IT","postalCode":"38010","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Ronzone","lat":46.424369337920005},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.048051112877742,"countryCode":"IT","postalCode":"38010","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Spormaggiore","lat":46.21852625382254},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.139338066839956,"countryCode":"IT","postalCode":"38011","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Cavareno","lat":46.40780298527367},{"adminCode2":"TN","adminCode3":"022230","adminName3":"Predaia","adminCode1":"17","adminName2":"Trento","lng":11.109774,"countryCode":"IT","postalCode":"38012","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Smarano","lat":46.34311},{"adminCode2":"TN","adminCode3":"022230","adminName3":"Predaia","adminCode1":"17","adminName2":"Trento","lng":11.0900115966797,"countryCode":"IT","postalCode":"38012","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Coredo","lat":46.34906049944},{"adminCode2":"TN","adminCode3":"022249","adminName3":"Ville d’Anaunia","adminCode1":"17","adminName2":"Trento","lng":11.023063659667969,"countryCode":"IT","postalCode":"38019","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Tuenno","lat":46.3284390873061},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":10.757899033072906,"countryCode":"IT","postalCode":"38020","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Pellizzano","lat":46.30979856892243},{"adminCode2":"TN","adminCode1":"17","adminName2":"Trento","lng":11.105782562087922,"countryCode":"IT","postalCode":"38021","adminName1":"Trentino-Alto Adige","ISO3166-2":"32","placeName":"Brez","lat":46.43220436526391}]} \ No newline at end of file diff --git a/testdata/response-geoname-multiple.json b/testdata/response-geoname-multiple.json new file mode 100644 index 0000000..2fa72a0 --- /dev/null +++ b/testdata/response-geoname-multiple.json @@ -0,0 +1 @@ +{"postalCodes":[{"adminCode1":"L","lng":-6.254537,"countryCode":"IE","postalCode":"D01","adminName1":"Leinster","ISO3166-2":"L","placeName":"Dublin 1","lat":53.353976},{"adminCode1":"L","lng":-6.254295,"countryCode":"IE","postalCode":"D02","adminName1":"Leinster","ISO3166-2":"L","placeName":"Dublin 2","lat":53.339971},{"adminCode1":"L","lng":-6.23776,"countryCode":"IE","postalCode":"D03","adminName1":"Leinster","ISO3166-2":"L","placeName":"Dublin 3","lat":53.364465},{"adminCode1":"L","lng":-6.233526,"countryCode":"IE","postalCode":"D04","adminName1":"Leinster","ISO3166-2":"L","placeName":"Dublin 4","lat":53.333435},{"adminCode1":"L","lng":-6.192128,"countryCode":"IE","postalCode":"D05","adminName1":"Leinster","ISO3166-2":"L","placeName":"Dublin 5","lat":53.384222},{"adminCode1":"L","lng":-6.263126,"countryCode":"IE","postalCode":"D06","adminName1":"Leinster","ISO3166-2":"L","placeName":"Dublin 6","lat":53.308787},{"adminCode1":"L","lng":-6.291792,"countryCode":"IE","postalCode":"D07","adminName1":"Leinster","ISO3166-2":"L","placeName":"Dublin 7","lat":53.361507},{"adminCode1":"L","lng":-6.273257,"countryCode":"IE","postalCode":"D08","adminName1":"Leinster","ISO3166-2":"L","placeName":"Dublin 8","lat":53.33455},{"adminCode1":"L","lng":-6.246501,"countryCode":"IE","postalCode":"D09","adminName1":"Leinster","ISO3166-2":"L","placeName":"Dublin 9","lat":53.381763},{"adminCode1":"L","lng":-6.354476,"countryCode":"IE","postalCode":"D10","adminName1":"Leinster","ISO3166-2":"L","placeName":"Dublin 10","lat":53.340906},{"adminCode1":"L","lng":-6.292976,"countryCode":"IE","postalCode":"D11","adminName1":"Leinster","ISO3166-2":"L","placeName":"Dublin 11","lat":53.389903},{"adminCode1":"L","lng":-6.316477,"countryCode":"IE","postalCode":"D12","adminName1":"Leinster","ISO3166-2":"L","placeName":"Dublin 12","lat":53.32203},{"adminCode1":"L","lng":-6.1495,"countryCode":"IE","postalCode":"D13","adminName1":"Leinster","ISO3166-2":"L","placeName":"Dublin 13","lat":53.394577},{"adminCode1":"L","lng":-6.259331,"countryCode":"IE","postalCode":"D14","adminName1":"Leinster","ISO3166-2":"L","placeName":"Dublin 14","lat":53.295987},{"adminCode1":"L","lng":-6.416518,"countryCode":"IE","postalCode":"D15","adminName1":"Leinster","ISO3166-2":"L","placeName":"Dublin 15","lat":53.383156},{"adminCode1":"L","lng":-6.278967,"countryCode":"IE","postalCode":"D16","adminName1":"Leinster","ISO3166-2":"L","placeName":"Dublin 16","lat":53.341884},{"adminCode1":"L","lng":-6.205763,"countryCode":"IE","postalCode":"D17","adminName1":"Leinster","ISO3166-2":"L","placeName":"Dublin 17","lat":53.400646},{"adminCode1":"L","lng":-6.177386,"countryCode":"IE","postalCode":"D18","adminName1":"Leinster","ISO3166-2":"L","placeName":"Dublin 18","lat":53.246902},{"adminCode1":"L","lng":-6.369332,"countryCode":"IE","postalCode":"D20","adminName1":"Leinster","ISO3166-2":"L","placeName":"Dublin 20","lat":53.35177},{"adminCode1":"L","lng":-6.400591,"countryCode":"IE","postalCode":"D22","adminName1":"Leinster","ISO3166-2":"L","placeName":"Dublin 22","lat":53.32751},{"adminCode1":"L","lng":-6.371327,"countryCode":"IE","postalCode":"D24","adminName1":"Leinster","ISO3166-2":"L","placeName":"Dublin 24","lat":53.285119},{"adminCode1":"L","lng":-6.30119,"countryCode":"IE","postalCode":"D6W","adminName1":"Leinster","ISO3166-2":"L","placeName":"Dublin 6W","lat":53.308651}]} \ No newline at end of file diff --git a/testdata/response-geoname-single.json b/testdata/response-geoname-single.json new file mode 100644 index 0000000..e7623dc --- /dev/null +++ b/testdata/response-geoname-single.json @@ -0,0 +1,14 @@ +{ + "postalCodes": [ + { + "adminCode1": "C", + "lng": -9.3, + "countryCode": "IE", + "postalCode": "F23", + "adminName1": "Connacht", + "ISO3166-2": "C", + "placeName": "Castlebar", + "lat": 53.85 + } + ] +} \ No newline at end of file From 11ea4923ce4db37ad492610909bb0210d945bd6a Mon Sep 17 00:00:00 2001 From: Jakub Jarosz Date: Mon, 1 Nov 2021 22:03:42 +0000 Subject: [PATCH 17/28] Implement geolookup interface --- examples/default/main.go | 4 +- meteo.go | 12 ++- norway.go | 30 ++++-- norway_test.go | 42 ++++++-- geoname.go => postal.go | 22 ++-- geoname_test.go => postal_test.go | 2 +- testdata/response-geoname-wikipedia.json | 125 +++++++++++++++++++++++ wikipedia.go | 120 ++++++++++++++++++++++ wikipedia_test.go | 77 ++++++++++++++ 9 files changed, 404 insertions(+), 30 deletions(-) rename geoname.go => postal.go (86%) rename geoname_test.go => postal_test.go (97%) create mode 100644 testdata/response-geoname-wikipedia.json create mode 100644 wikipedia.go create mode 100644 wikipedia_test.go diff --git a/examples/default/main.go b/examples/default/main.go index d46331e..42cb5b8 100644 --- a/examples/default/main.go +++ b/examples/default/main.go @@ -8,8 +8,8 @@ import ( ) func main() { - // Get weather status for the given lat, lon: - weather, err := meteo.GetWeather(53.2, -6.2) + // Get weather status for city Castlebar in Ireland: + weather, err := meteo.GetWeather("Castlebar", "IE") if err != nil { log.Println(err) } diff --git a/meteo.go b/meteo.go index 0f96b12..0723260 100644 --- a/meteo.go +++ b/meteo.go @@ -22,13 +22,21 @@ func (w Weather) String() string { return fmt.Sprintf("%s %.1f°C", strings.Title(w.Summary), w.Temp) } +type NameResolver interface { + GetCoordinates(placeName, country string) (Place, error) +} + func RunCLI() { - c, err := NewNorwayClient() + resolver, err := NewWikipediaClient(os.Getenv("GEO_USERNAME")) + if err != nil { + fmt.Fprintln(os.Stderr, err) + } + c, err := NewNorwayClient(resolver) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } - w, err := c.GetForecast(53.2, -6.2) + w, err := c.GetForecast("Castlebar", "IE") if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) diff --git a/norway.go b/norway.go index dae53f9..bc37bda 100644 --- a/norway.go +++ b/norway.go @@ -6,6 +6,7 @@ import ( "fmt" "net/http" "net/url" + "os" "time" ) @@ -73,15 +74,17 @@ type NorwayClient struct { UA string BaseURL string HTTPClient *http.Client + Resolver NameResolver } -func NewNorwayClient(opts ...option) (*NorwayClient, error) { +func NewNorwayClient(resolver NameResolver, opts ...option) (*NorwayClient, error) { c := NorwayClient{ BaseURL: baseURL, UA: userAgent, HTTPClient: &http.Client{ Timeout: time.Second * 5, }, + Resolver: resolver, } for _, opt := range opts { @@ -90,15 +93,22 @@ func NewNorwayClient(opts ...option) (*NorwayClient, error) { } } return &c, nil +} +func (c NorwayClient) GetForecast(place, country string) (Weather, error) { + p, err := c.Resolver.GetCoordinates(place, country) + if err != nil { + return Weather{}, err + } + return c.getForecast(p.Lat, p.Lng) } -func (c NorwayClient) GetForecast(lat, lon float64) (Weather, error) { +func (c NorwayClient) getForecast(lat, lon float64) (Weather, error) { u, err := c.makeURL(lat, lon) if err != nil { return Weather{}, err } - req, err := c.prepareRequest(u) + req, err := prepareRequest(u) if err != nil { return Weather{}, err } @@ -135,7 +145,7 @@ func (c NorwayClient) makeURL(lat, lon float64) (string, error) { return base.String(), nil } -func (c NorwayClient) prepareRequest(u string) (*http.Request, error) { +func prepareRequest(u string) (*http.Request, error) { req, err := http.NewRequest(http.MethodGet, u, nil) if err != nil { return nil, err @@ -146,12 +156,16 @@ func (c NorwayClient) prepareRequest(u string) (*http.Request, error) { } // GetWeather returns current weather for given -// Lat and Long using default client for the Norwegian +// place and country using default client for the Norwegian // meteorological Institute. -func GetWeather(lat, lon float64) (Weather, error) { - c, err := NewNorwayClient() +func GetWeather(place, country string) (Weather, error) { + resolver, err := NewWikipediaClient(os.Getenv("GEO_USERNAME")) + if err != nil { + return Weather{}, err + } + c, err := NewNorwayClient(resolver) if err != nil { return Weather{}, err } - return c.GetForecast(lat, lon) + return c.GetForecast(place, country) } diff --git a/norway_test.go b/norway_test.go index 68ba063..072364e 100644 --- a/norway_test.go +++ b/norway_test.go @@ -14,7 +14,11 @@ import ( func TestCreateNewMeteoClient(t *testing.T) { t.Parallel() var c *meteo.NorwayClient - c, err := meteo.NewNorwayClient() + resolver, err := meteo.NewWikipediaClient("User") + if err != nil { + t.Fatal(err) + } + c, err = meteo.NewNorwayClient(resolver) if err != nil { t.Fatal(err) } @@ -23,7 +27,12 @@ func TestCreateNewMeteoClient(t *testing.T) { func TestCreateNewMeteoClientWithCustomUserAgent(t *testing.T) { t.Parallel() + resolver, err := meteo.NewWikipediaClient("User") + if err != nil { + t.Fatal(err) + } c, err := meteo.NewNorwayClient( + resolver, meteo.WithUserAgent("CustomClient/1.0 https://customclient.com"), ) if err != nil { @@ -38,7 +47,11 @@ func TestCreateNewMeteoClientWithCustomUserAgent(t *testing.T) { func TestCreateNewNorwayClientWithInvalidUserAgent(t *testing.T) { t.Parallel() - _, err := meteo.NewNorwayClient(meteo.WithUserAgent("")) + resolver, err := meteo.NewWikipediaClient("User") + if err != nil { + t.Fatal(err) + } + _, err = meteo.NewNorwayClient(resolver, meteo.WithUserAgent("")) if err == nil { t.Errorf("invalid user agent string should return error") } @@ -46,23 +59,40 @@ func TestCreateNewNorwayClientWithInvalidUserAgent(t *testing.T) { func TestGetForecast(t *testing.T) { t.Parallel() - ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + mux := http.NewServeMux() + + mux.HandleFunc("/weatherapi/locationforecast/2.0/compact", func(rw http.ResponseWriter, r *http.Request) { f, err := os.Open("testdata/response-compact.json") if err != nil { t.Fatal(err) } defer f.Close() io.Copy(rw, f) - })) + }) + mux.HandleFunc("/wikipediaSearchJSON", func(rw http.ResponseWriter, r *http.Request) { + f, err := os.Open("testdata/response-geoname-wikipedia.json") + if err != nil { + t.Fatal(err) + } + defer f.Close() + io.Copy(rw, f) + }) + ts := httptest.NewServer(mux) defer ts.Close() - client, err := meteo.NewNorwayClient() + resolver, err := meteo.NewWikipediaClient("UserName") + if err != nil { + t.Fatal(err) + } + resolver.BaseURL = ts.URL + + client, err := meteo.NewNorwayClient(resolver) if err != nil { t.Fatal(err) } client.BaseURL = ts.URL - got, err := client.GetForecast(53.3, -6.2) + got, err := client.GetForecast("Castlebar", "IE") if err != nil { t.Errorf("error getting forecast data, %v", err) } diff --git a/geoname.go b/postal.go similarity index 86% rename from geoname.go rename to postal.go index 514bde1..55f3351 100644 --- a/geoname.go +++ b/postal.go @@ -13,8 +13,8 @@ const ( baseURLGeoNames = "http://api.geonames.org" ) -// PlaceCoordinates represents response from GeoNames web service. -type PlaceCoordinates struct { +// Place represents response from GeoNames web service. +type Place struct { Lat float64 Lng float64 CountryCode string @@ -77,30 +77,30 @@ func NewGeoNamesClient(username string, opts ...geoNameOption) (*GeoNamesClient, // GetCoordinates knows how to retrieve lat and long coordinates // for the given place name and country. -func (g GeoNamesClient) GetCoordinates(placeName, countryCode string) (PlaceCoordinates, error) { +func (g GeoNamesClient) GetCoordinates(placeName, countryCode string) (Place, error) { u, err := g.makeURL(placeName, countryCode) if err != nil { - return PlaceCoordinates{}, err + return Place{}, err } req, err := g.prepareRequest(u) if err != nil { - return PlaceCoordinates{}, err + return Place{}, err } res, err := g.HTTPClient.Do(req) if err != nil { - return PlaceCoordinates{}, err + return Place{}, err } defer res.Body.Close() var co coordinates if err := json.NewDecoder(res.Body).Decode(&co); err != nil { - return PlaceCoordinates{}, fmt.Errorf("decoding response %w", err) + return Place{}, fmt.Errorf("decoding response %w", err) } if len(co.PostalCodes) < 1 { - return PlaceCoordinates{}, fmt.Errorf("place %s in country %s not found", placeName, countryCode) + return Place{}, fmt.Errorf("place %s in country %s not found", placeName, countryCode) } - pc := PlaceCoordinates{ + pc := Place{ Lat: co.PostalCodes[0].Lat, Lng: co.PostalCodes[0].Lng, PlaceName: co.PostalCodes[0].PlaceName, @@ -135,10 +135,10 @@ func (g GeoNamesClient) prepareRequest(u string) (*http.Request, error) { // GetCoordinates knows how to get Lat and Long coordinates for // the given place and country using default geo client. -func GetCoordinates(placename, countryCode, username string) (PlaceCoordinates, error) { +func GetCoordinates(placename, countryCode, username string) (Place, error) { c, err := NewGeoNamesClient(username) if err != nil { - return PlaceCoordinates{}, err + return Place{}, err } return c.GetCoordinates(placename, countryCode) } diff --git a/geoname_test.go b/postal_test.go similarity index 97% rename from geoname_test.go rename to postal_test.go index 144501f..6c2a55f 100644 --- a/geoname_test.go +++ b/postal_test.go @@ -63,7 +63,7 @@ func TestGetCoordinatesSingleGeoNames(t *testing.T) { if err != nil { t.Fatalf("GetCoordinates(\"Castlebar\", \"IE\") got err %v", err) } - want := meteo.PlaceCoordinates{ + want := meteo.Place{ Lng: -9.3, Lat: 53.85, PlaceName: "Castlebar", diff --git a/testdata/response-geoname-wikipedia.json b/testdata/response-geoname-wikipedia.json new file mode 100644 index 0000000..31cef77 --- /dev/null +++ b/testdata/response-geoname-wikipedia.json @@ -0,0 +1,125 @@ +{ + "geonames": [ + { + "summary": "Castlebar is the county town of County Mayo, Ireland. It is in the middle of the county and is its largest town by population. A campus of Galway-Mayo Institute of Technology and the Country Life section of the National Museum of Ireland are two important local amenities (...)", + "elevation": 41, + "geoNameId": 2965654, + "lng": -9.2988, + "countryCode": "IE", + "rank": 100, + "lang": "en", + "title": "Castlebar", + "lat": 53.8608, + "wikipediaUrl": "en.wikipedia.org/wiki/Castlebar" + }, + { + "summary": "County Mayo (meaning \"Plain of the yew trees\") is a county in Ireland. In the West of Ireland, it is part of the province of Connacht and is named after the village of Mayo, now generally known as Mayo Abbey. Mayo County Council is the local authority for the county (...)", + "elevation": 294, + "feature": "adm1st", + "lng": -9.36, + "countryCode": "IE", + "rank": 100, + "lang": "en", + "title": "County Mayo", + "lat": 53.92, + "wikipediaUrl": "en.wikipedia.org/wiki/County_Mayo" + }, + { + "summary": "Westport (historically anglicised as Cahernamart) (see archival records) is a town in County Mayo in Ireland. It is at the south-east corner of Clew Bay, an inlet of the Atlantic Ocean on the west coast of Ireland (...)", + "elevation": 19, + "geoNameId": 2960970, + "feature": "city", + "lng": -9.5225, + "countryCode": "IE", + "rank": 98, + "thumbnailImg": "http://www.geonames.org/img/wikipedia/92000/thumb-91512-100.jpg", + "lang": "en", + "title": "Westport, County Mayo", + "lat": 53.799166666666665, + "wikipediaUrl": "en.wikipedia.org/wiki/Westport%2C_County_Mayo" + }, + { + "summary": "Clew Bay is a natural ocean bay in County Mayo, Republic of Ireland. It contains Ireland's best example of sunken drumlins. The bay is overlooked by Croagh Patrick to the south and the Nephin Range mountains of North Mayo. Clare Island guards the entrance of the bay (...)", + "geoNameId": 2965450, + "feature": "waterbody", + "lng": -9.8, + "rank": 93, + "thumbnailImg": "http://www.geonames.org/img/wikipedia/52000/thumb-51626-100.jpg", + "lang": "en", + "title": "Clew Bay", + "lat": 53.833333333333336, + "wikipediaUrl": "en.wikipedia.org/wiki/Clew_Bay" + }, + { + "summary": "The Battle of Castlebar occurred on 27 August 1798 near the town of Castlebar, County Mayo, during the Irish Rebellion of that year. A combined force of 2,000 French troops and Irish rebels routed a force of 6,000 British militia in what would later become known as the \"Castlebar Races\" or \"Races of (...)", + "elevation": 41, + "lng": -9.2989, + "countryCode": "IE", + "rank": 82, + "lang": "en", + "title": "Battle of Castlebar", + "lat": 53.8608, + "wikipediaUrl": "en.wikipedia.org/wiki/Battle_of_Castlebar" + }, + { + "summary": "Ealing is a major suburban district of west London, England and the administrative centre of the London Borough of Ealing. It is located west of Charing Cross and around from the City of London. It is one of the major metropolitan centres identified in the London Plan (...)", + "elevation": 35, + "geoNameId": 2650567, + "feature": "city", + "lng": -0.3058, + "countryCode": "GB", + "rank": 100, + "lang": "en", + "title": "Ealing", + "lat": 51.5111, + "wikipediaUrl": "en.wikipedia.org/wiki/Ealing" + }, + { + "summary": "Connacht or Connaught (or Cúige Chonnacht) is one of the Provinces of Ireland situated in the west of the country. Up to the 9th century it consisted of several independent major kingdoms (Lúighne, Uí Maine, Iarthar Connacht) (...)", + "elevation": 66, + "feature": "adm1st", + "lng": -9.05, + "countryCode": "IE", + "rank": 100, + "lang": "en", + "title": "Connacht", + "lat": 53.78, + "wikipediaUrl": "en.wikipedia.org/wiki/Connacht" + }, + { + "summary": "Ballina is a town in north County Mayo, Ireland. It lies at the mouth of the River Moy near Killala Bay, in the Moy valley and Parish of Kilmoremoy, with the Ox Mountains to the east and the Nephin Beg mountains to the west (...)", + "elevation": 32, + "geoNameId": 2966778, + "lng": -9.1667, + "countryCode": "IE", + "rank": 98, + "lang": "en", + "title": "Ballina, County Mayo", + "lat": 54.1167, + "wikipediaUrl": "en.wikipedia.org/wiki/Ballina%2C_County_Mayo" + }, + { + "summary": "Longford is the county town of County Longford in Ireland. It has a population of 9,601 according to the 2011 census. It is the biggest town in the county and about one third of the county's population lives there (...)", + "elevation": 53, + "geoNameId": 2962840, + "lng": -7.7998, + "countryCode": "IE", + "rank": 100, + "lang": "en", + "title": "Longford", + "lat": 53.727, + "wikipediaUrl": "en.wikipedia.org/wiki/Longford" + }, + { + "summary": "Castlebar railway station serves the town of Castlebar in County Mayo, Ireland. The station is on the Dublin to Westport Rail service. Passengers to or from Galway travel to Athlone and change trains. Passengers to or from Ballina and Foxford travel to Manulla Junction and change trains. (...)", + "elevation": 42, + "feature": "railwaystation", + "lng": -9.288214, + "rank": 75, + "lang": "en", + "title": "Castlebar railway station", + "lat": 53.8473045, + "wikipediaUrl": "en.wikipedia.org/wiki/Castlebar_railway_station" + } + ] +} \ No newline at end of file diff --git a/wikipedia.go b/wikipedia.go new file mode 100644 index 0000000..49e62e5 --- /dev/null +++ b/wikipedia.go @@ -0,0 +1,120 @@ +package meteo + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "strconv" + "time" +) + +const ( + maxRawResults = 10 +) + +type wikipediaPlaces struct { + Geonames []struct { + Lat float64 `json:"lat"` + Lng float64 `json:"lng"` + CountryCode string `json:"countryCode"` + Title string `json:"title"` + } `json:"geonames"` +} + +type wikipediaOption func(wk *WikipediaClient) error + +func WikiWithUserAgent(ua string) wikipediaOption { + return func(wk *WikipediaClient) error { + wk.UA = ua + return nil + } +} + +func WikiWithHTTPClient(hc *http.Client) wikipediaOption { + return func(wk *WikipediaClient) error { + wk.HTTPClient = hc + return nil + } +} + +// WikipediaClient implements NameResolver interface. +type WikipediaClient struct { + UA string + UserName string + BaseURL string + HTTPClient *http.Client +} + +// NewWikipediaClient knows how to create a new client. +// The client impements NameResolver interface. +func NewWikipediaClient(username string) (*WikipediaClient, error) { + if username == "" { + return nil, errors.New("nil username") + } + c := WikipediaClient{ + UA: userAgent, + UserName: username, + BaseURL: baseURLGeoNames, + HTTPClient: &http.Client{ + Timeout: 5 * time.Second, + }, + } + return &c, nil +} + +// GetCoordinates knows how to retrive geo coordinates for +// the given place name and country code. +func (w WikipediaClient) GetCoordinates(placeName, country string) (Place, error) { + u, err := w.makeURL(placeName) + if err != nil { + return Place{}, err + } + req, err := prepareRequest(u) + if err != nil { + return Place{}, err + } + res, err := w.HTTPClient.Do(req) + if err != nil { + return Place{}, err + } + defer res.Body.Close() + + var wp wikipediaPlaces + if err := json.NewDecoder(res.Body).Decode(&wp); err != nil { + return Place{}, fmt.Errorf("decoding response %w", err) + + } + if len(wp.Geonames) < 1 { + return Place{}, fmt.Errorf("place %s in country %s not found", placeName, country) + } + return lookupPlace(wp, placeName, country), nil +} + +func (w WikipediaClient) makeURL(placeName string) (string, error) { + base, err := url.Parse(w.BaseURL + "/wikipediaSearchJSON") + if err != nil { + return "", err + } + params := url.Values{} + params.Add("q", placeName) + params.Add("maxRows", strconv.Itoa(maxRawResults)) + params.Add("username", w.UserName) + base.RawQuery = params.Encode() + return base.String(), nil +} + +func lookupPlace(places wikipediaPlaces, placeName, country string) Place { + for _, pl := range places.Geonames { + if pl.Title == placeName && pl.CountryCode == country { + return Place{ + Lat: pl.Lat, + Lng: pl.Lng, + CountryCode: pl.CountryCode, + PlaceName: pl.Title, + } + } + } + return Place{} +} diff --git a/wikipedia_test.go b/wikipedia_test.go new file mode 100644 index 0000000..05f6d47 --- /dev/null +++ b/wikipedia_test.go @@ -0,0 +1,77 @@ +package meteo_test + +import ( + "io" + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/qba73/meteo" +) + +func TestNewWikipediaClient(t *testing.T) { + t.Parallel() + var err error + var c *meteo.WikipediaClient + c, err = meteo.NewWikipediaClient("UserName") + if err != nil { + t.Fatal(err) + } + _ = c +} + +func TestNewWikipediaClientWithoutUserName(t *testing.T) { + t.Parallel() + _, err := meteo.NewWikipediaClient("") + if err == nil { + t.Fatal(err) + } +} + +func TestNewWikipediaClientWithUserName(t *testing.T) { + t.Parallel() + got, err := meteo.NewWikipediaClient("UserName") + if err != nil { + t.Fatal(err) + } + want := "UserName" + if want != got.UserName { + t.Errorf("want %s, got %s", want, got.UserName) + } +} + +func TestGetCoordinatesSingleGeoName(t *testing.T) { + t.Parallel() + ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + f, err := os.Open("testdata/response-geoname-wikipedia.json") + if err != nil { + t.Fatal(err) + } + defer f.Close() + io.Copy(rw, f) + })) + defer ts.Close() + + c, err := meteo.NewWikipediaClient("UserName") + if err != nil { + t.Fatal(err) + } + c.BaseURL = ts.URL + + got, err := c.GetCoordinates("Castlebar", "IE") + if err != nil { + t.Fatalf("GetCoordinates('Castlebar', 'IE') got err %v", err) + } + want := meteo.Place{ + Lng: -9.2988, + Lat: 53.8608, + PlaceName: "Castlebar", + CountryCode: "IE", + } + + if !cmp.Equal(want, got) { + t.Errorf("GetCoordinates('Castlebar', 'IE') \n%s", cmp.Diff(want, got)) + } +} From a0f37add198861ed3e5bd5b63604e7d643d57e82 Mon Sep 17 00:00:00 2001 From: Jakub Jarosz Date: Mon, 1 Nov 2021 22:31:47 +0000 Subject: [PATCH 18/28] Added initial docs --- README.md | 41 +++++++++++++++++++++++++++++++++++++++++ meteo.go | 3 ++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6ce8028..83a4c86 100644 --- a/README.md +++ b/README.md @@ -9,3 +9,44 @@ Disclaimer: Weather forecast from Yr, delivered by the Norwegian Meteorological Institute and NRK. +# Usage + +## Preconditions + +You must register your user agent string in the [YR.NO service](https://developer.yr.no/doc/TermsOfService/) and your user name in the [GeoNames service](https://www.geonames.org/login) to use the package. + + +## Installation +``` +$ go get git@github.com:qba73/meteo.git +``` + +## Default + +Export ```GEO_USERNAME``` env var that you registered with GeoNames.org. + +Example: +``` +$ export GEO_USERNAME=Jane123 +``` +Use the ```meteo``` package in your application: +```go +$ package main + +import ( + "fmt" + "log" + "github.com/qba73/meteo" +) + +func main() { + // Get weather status for Castlebar in Ireland: + weather, err := meteo.GetWeather("Castlebar", "IE") + if err != nil { + log.Println(err) + } + // Print out weather string. + // Example: Lightrain 8.3°C + fmt.Println(weather) +} +``` diff --git a/meteo.go b/meteo.go index 0723260..b7185f6 100644 --- a/meteo.go +++ b/meteo.go @@ -27,7 +27,8 @@ type NameResolver interface { } func RunCLI() { - resolver, err := NewWikipediaClient(os.Getenv("GEO_USERNAME")) + uname := os.Getenv("GEO_USERNAME") + resolver, err := NewWikipediaClient(uname) if err != nil { fmt.Fprintln(os.Stderr, err) } From d9492b3d54cf49a6459368cb8207d29f7cc409fc Mon Sep 17 00:00:00 2001 From: Jakub Jarosz Date: Tue, 2 Nov 2021 05:38:45 +0000 Subject: [PATCH 19/28] Update go.yml --- .github/workflows/go.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 3d1acd3..014b793 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -2,9 +2,9 @@ name: Go on: push: - branches: [main] + branches: [master] pull_request: - branches: [main] + branches: [master] jobs: build: From 5c49400701d66bb1ee41b367dd72964946fe3c1f Mon Sep 17 00:00:00 2001 From: Jakub Jarosz Date: Tue, 2 Nov 2021 05:45:30 +0000 Subject: [PATCH 20/28] Update README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 83a4c86..b89fcb7 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ ![Go](https://github.com/qba73/meteo/workflows/Go/badge.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/qba73/meteo)](https://goreportcard.com/report/github.com/qba73/meteo) +[![Maintainability](https://api.codeclimate.com/v1/badges/4afc34a390da95ed9327/maintainability)](https://codeclimate.com/github/qba73/meteo/maintainability) +[![Test Coverage](https://api.codeclimate.com/v1/badges/4afc34a390da95ed9327/test_coverage)](https://codeclimate.com/github/qba73/meteo/test_coverage) + # meteo From c6e68ef6ed2a9837359762048feea087b04656c6 Mon Sep 17 00:00:00 2001 From: Jakub Jarosz Date: Tue, 2 Nov 2021 06:31:10 +0000 Subject: [PATCH 21/28] Update documentation --- .github/workflows/go.yml | 2 +- go.mod | 2 ++ go.sum | 3 ++- meteo.go | 6 ++++++ norway.go | 6 ++++++ postal.go | 18 ++++-------------- wikipedia.go | 11 ++++++++--- 7 files changed, 29 insertions(+), 19 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 014b793..35e16b6 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -15,6 +15,6 @@ jobs: - name: Run unit tests run: | - make clean test-ci + make test-ci env: CC_TEST_REPORTER_ID: ${{secrets.CC_TEST_REPORTER_ID}} diff --git a/go.mod b/go.mod index bb6a61c..4d51eac 100644 --- a/go.mod +++ b/go.mod @@ -3,3 +3,5 @@ module github.com/qba73/meteo go 1.17 require github.com/google/go-cmp v0.5.6 + +require golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect diff --git a/go.sum b/go.sum index 03e1a9c..58b75a4 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,5 @@ github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/meteo.go b/meteo.go index b7185f6..f00e8ea 100644 --- a/meteo.go +++ b/meteo.go @@ -22,10 +22,16 @@ func (w Weather) String() string { return fmt.Sprintf("%s %.1f°C", strings.Title(w.Summary), w.Temp) } +// NameResolver interface is used by an Meteo Client +// to obtain geo coordinates for given place located in +// a country identified by country id. type NameResolver interface { + // GetCoordinates takes place and country code + // and returns geo information like lat and lng. GetCoordinates(placeName, country string) (Place, error) } +// RunCLI is a main function that runs the cli machinery. func RunCLI() { uname := os.Getenv("GEO_USERNAME") resolver, err := NewWikipediaClient(uname) diff --git a/norway.go b/norway.go index bc37bda..c3a17fd 100644 --- a/norway.go +++ b/norway.go @@ -58,6 +58,9 @@ type data struct { type option func(*NorwayClient) error +// WithUserAgent is a func option and allows to +// customise User-Agent header used internally +// by the NorwayClient. func WithUserAgent(ua string) option { return func(nc *NorwayClient) error { if ua == "" { @@ -77,6 +80,7 @@ type NorwayClient struct { Resolver NameResolver } +// NewNorwayClient knows how to construct a new client. func NewNorwayClient(resolver NameResolver, opts ...option) (*NorwayClient, error) { c := NorwayClient{ BaseURL: baseURL, @@ -95,6 +99,8 @@ func NewNorwayClient(resolver NameResolver, opts ...option) (*NorwayClient, erro return &c, nil } +// GetForecast takes place and country code and +// returns weather summary and air temperature. func (c NorwayClient) GetForecast(place, country string) (Weather, error) { p, err := c.Resolver.GetCoordinates(place, country) if err != nil { diff --git a/postal.go b/postal.go index 55f3351..b103ecd 100644 --- a/postal.go +++ b/postal.go @@ -37,14 +37,14 @@ type geoNameOption func(*GeoNamesClient) error func WithGeoNamesUserAgent(ua string) geoNameOption { return func(gnc *GeoNamesClient) error { if ua == "" { - return errors.New("user agent not provided") + return errors.New("nil user agent") } gnc.UA = ua return nil } } -// GeoNameClient is a client used for communicating +// GeoNamesClient is a client used for communicating // with geo name web services. type GeoNamesClient struct { UA string @@ -53,7 +53,7 @@ type GeoNamesClient struct { HTTPClient *http.Client } -// NewGeoNameClient knows how to create a client for GeoNames Web services. +// NewGeoNamesClient knows how to create a client for GeoNames Web services. func NewGeoNamesClient(username string, opts ...geoNameOption) (*GeoNamesClient, error) { if username == "" { return nil, errors.New("missing user name") @@ -82,7 +82,7 @@ func (g GeoNamesClient) GetCoordinates(placeName, countryCode string) (Place, er if err != nil { return Place{}, err } - req, err := g.prepareRequest(u) + req, err := prepareRequest(u) if err != nil { return Place{}, err } @@ -123,16 +123,6 @@ func (g GeoNamesClient) makeURL(placeName, countryCode string) (string, error) { return base.String(), nil } -func (g GeoNamesClient) prepareRequest(u string) (*http.Request, error) { - req, err := http.NewRequest(http.MethodGet, u, nil) - if err != nil { - return nil, fmt.Errorf("preparing request %w", err) - } - req.Header.Add("Content-Type", "application/json") - req.Header.Add("User-Agent", userAgent) - return req, nil -} - // GetCoordinates knows how to get Lat and Long coordinates for // the given place and country using default geo client. func GetCoordinates(placename, countryCode, username string) (Place, error) { diff --git a/wikipedia.go b/wikipedia.go index 49e62e5..368fb96 100644 --- a/wikipedia.go +++ b/wikipedia.go @@ -25,21 +25,26 @@ type wikipediaPlaces struct { type wikipediaOption func(wk *WikipediaClient) error -func WikiWithUserAgent(ua string) wikipediaOption { +// WithWikiUserAgent returns a func option for +// setting up a custom User-Agent used in +// http requests. +func WithWikiUserAgent(ua string) wikipediaOption { return func(wk *WikipediaClient) error { wk.UA = ua return nil } } -func WikiWithHTTPClient(hc *http.Client) wikipediaOption { +// WithWikiHTTPClient returns a func option for +// configuring a custom http client. +func WithWikiHTTPClient(hc *http.Client) wikipediaOption { return func(wk *WikipediaClient) error { wk.HTTPClient = hc return nil } } -// WikipediaClient implements NameResolver interface. +// WikipediaClient implements Nameresolver interface. type WikipediaClient struct { UA string UserName string From 6621d7ae391af1ba5bfb2d82232a6769a9be8e01 Mon Sep 17 00:00:00 2001 From: Jakub Jarosz Date: Tue, 2 Nov 2021 06:38:49 +0000 Subject: [PATCH 22/28] Update docs --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b89fcb7..fc521a5 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ # meteo -Go client library for the weather and meteorological forecast from [Yr](https://www.yr.no/en). +Meteo is a Go client library for the weather and meteorological forecast from [Yr](https://www.yr.no/en). Disclaimer: @@ -53,3 +53,4 @@ func main() { fmt.Println(weather) } ``` + From 03a65d2519a4f96b885972e5929345aecf7d86dc Mon Sep 17 00:00:00 2001 From: Jakub Jarosz Date: Tue, 2 Nov 2021 06:40:52 +0000 Subject: [PATCH 23/28] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fc521a5..fd24f96 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ $ export GEO_USERNAME=Jane123 ``` Use the ```meteo``` package in your application: ```go -$ package main +package main import ( "fmt" From bf677f8d99a4fab340cd7c4dc12d674c61b1a177 Mon Sep 17 00:00:00 2001 From: Jakub Jarosz Date: Wed, 3 Nov 2021 05:11:14 +0000 Subject: [PATCH 24/28] Make tests more robust --- norway_test.go | 31 +++++++++++++++++++++++++++---- postal_test.go | 11 ++++++++++- wikipedia_test.go | 10 +++++++++- 3 files changed, 46 insertions(+), 6 deletions(-) diff --git a/norway_test.go b/norway_test.go index 072364e..046b1ea 100644 --- a/norway_test.go +++ b/norway_test.go @@ -62,20 +62,43 @@ func TestGetForecast(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc("/weatherapi/locationforecast/2.0/compact", func(rw http.ResponseWriter, r *http.Request) { - f, err := os.Open("testdata/response-compact.json") + testFile := "testdata/response-compact.json" + wantReqURL := "/weatherapi/locationforecast/2.0/compact?lat=53.86&lon=-9.30" + gotReqURL := r.RequestURI + if wantReqURL != gotReqURL { + t.Errorf("want %q for location compact forecast, got %q", wantReqURL, gotReqURL) + } + + f, err := os.Open(testFile) if err != nil { t.Fatal(err) } defer f.Close() - io.Copy(rw, f) + + _, err = io.Copy(rw, f) + if err != nil { + t.Fatalf("copying data from file %s to test HTTP server: %v", testFile, err) + } }) + mux.HandleFunc("/wikipediaSearchJSON", func(rw http.ResponseWriter, r *http.Request) { - f, err := os.Open("testdata/response-geoname-wikipedia.json") + testFile := "testdata/response-geoname-wikipedia.json" + wantReqURL := "/wikipediaSearchJSON?maxRows=10&q=Castlebar&username=UserName" + gotReqURL := r.RequestURI + if wantReqURL != gotReqURL { + t.Errorf("want %q for wikipedia search JSON, got %q", wantReqURL, gotReqURL) + } + + f, err := os.Open(testFile) if err != nil { t.Fatal(err) } defer f.Close() - io.Copy(rw, f) + + _, err = io.Copy(rw, f) + if err != nil { + t.Fatalf("copying data from file %s to test HTTP server: %v", testFile, err) + } }) ts := httptest.NewServer(mux) defer ts.Close() diff --git a/postal_test.go b/postal_test.go index 6c2a55f..745677d 100644 --- a/postal_test.go +++ b/postal_test.go @@ -43,8 +43,17 @@ func TestCreateNewGeoNamesClientWithUser(t *testing.T) { func TestGetCoordinatesSingleGeoNames(t *testing.T) { t.Parallel() + + testFile := "testdata/response-geoname-single.json" + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - f, err := os.Open("testdata/response-geoname-single.json") + wantReqURL := "/postalCodeSearchJSON?country=IE&placename=Castlebar&username=UserName" + gotReqURL := r.RequestURI + if wantReqURL != gotReqURL { + t.Errorf("want %q for wikipedia URL, got %q", wantReqURL, gotReqURL) + } + + f, err := os.Open(testFile) if err != nil { t.Fatal(err) } diff --git a/wikipedia_test.go b/wikipedia_test.go index 05f6d47..fc4ee9e 100644 --- a/wikipedia_test.go +++ b/wikipedia_test.go @@ -44,8 +44,16 @@ func TestNewWikipediaClientWithUserName(t *testing.T) { func TestGetCoordinatesSingleGeoName(t *testing.T) { t.Parallel() + + testFile := "testdata/response-geoname-wikipedia.json" + ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - f, err := os.Open("testdata/response-geoname-wikipedia.json") + wantReqURL := "/wikipediaSearchJSON?maxRows=10&q=Castlebar&username=UserName" + gotReqURL := r.RequestURI + if wantReqURL != gotReqURL { + t.Errorf("want %q for wikipedia URL, got %q", wantReqURL, gotReqURL) + } + f, err := os.Open(testFile) if err != nil { t.Fatal(err) } From 6637f6b46d66c680a2a436406f11411a7a8d926e Mon Sep 17 00:00:00 2001 From: Jakub Jarosz Date: Thu, 11 Nov 2021 04:35:53 +0000 Subject: [PATCH 25/28] Update meteo.go Co-authored-by: John Arundel --- meteo.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/meteo.go b/meteo.go index f00e8ea..ae92eee 100644 --- a/meteo.go +++ b/meteo.go @@ -34,7 +34,9 @@ type NameResolver interface { // RunCLI is a main function that runs the cli machinery. func RunCLI() { uname := os.Getenv("GEO_USERNAME") - resolver, err := NewWikipediaClient(uname) + c, err := NewYrWeatherClient( + WithWikipediaGeoResolver(), + ) if err != nil { fmt.Fprintln(os.Stderr, err) } From 6c01d6abb4e7ce10ff51157783595c537c39797f Mon Sep 17 00:00:00 2001 From: Jakub Jarosz Date: Thu, 11 Nov 2021 04:44:01 +0000 Subject: [PATCH 26/28] Update meteo.go Co-authored-by: John Arundel --- meteo.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meteo.go b/meteo.go index ae92eee..7751f99 100644 --- a/meteo.go +++ b/meteo.go @@ -45,7 +45,7 @@ func RunCLI() { fmt.Fprintln(os.Stderr, err) os.Exit(1) } - w, err := c.GetForecast("Castlebar", "IE") + forecast, err := c.GetForecast("Castlebar", "IE") if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) From 5ddc66e2d0b5781570907b0e6ae603469fc2d5a8 Mon Sep 17 00:00:00 2001 From: Jakub Jarosz Date: Thu, 11 Nov 2021 04:44:22 +0000 Subject: [PATCH 27/28] Update norway_test.go Co-authored-by: John Arundel --- norway_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/norway_test.go b/norway_test.go index 046b1ea..a957080 100644 --- a/norway_test.go +++ b/norway_test.go @@ -14,7 +14,7 @@ import ( func TestCreateNewMeteoClient(t *testing.T) { t.Parallel() var c *meteo.NorwayClient - resolver, err := meteo.NewWikipediaClient("User") + resolver, err := meteo.NewWikipediaClient("DummyUsername") if err != nil { t.Fatal(err) } From 6c68270d82282d2228dd7860cf710f3b4cfa5986 Mon Sep 17 00:00:00 2001 From: Jakub Jarosz Date: Thu, 11 Nov 2021 04:45:31 +0000 Subject: [PATCH 28/28] Update norway_test.go Co-authored-by: John Arundel --- norway_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/norway_test.go b/norway_test.go index a957080..d69168f 100644 --- a/norway_test.go +++ b/norway_test.go @@ -19,6 +19,10 @@ func TestCreateNewMeteoClient(t *testing.T) { t.Fatal(err) } c, err = meteo.NewNorwayClient(resolver) + want := meteo.NorwayClient{ + // maybe other fields here, + Resolver: resolver, + } if err != nil { t.Fatal(err) }