├── .github └── workflows │ └── laravel.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── composer.json ├── config └── weather.php ├── phpunit.xml.dist ├── src ├── Exceptions │ └── WeatherException.php ├── Objects │ ├── DataBlock.php │ ├── DataPoint.php │ ├── Precipitation.php │ └── Temperature.php ├── Providers │ ├── Darksky.php │ ├── Provider.php │ ├── WeatherKit.php │ └── Weatherstack.php ├── Request.php ├── Response.php ├── WeatherProvider.php ├── WeatherServiceProvider.php └── helpers.php └── tests ├── Providers ├── DarkskyTest.php ├── ProviderTest.php ├── WeatherKitTest.php └── WeatherstackTest.php ├── TestCase.php ├── WeatherTest.php └── stub ├── darksky ├── forecast.json ├── historical_1.json └── historical_2.json ├── geocoder.json ├── response.json ├── weatherkit ├── forecast.json ├── historical-1.json └── historical-2.json └── weatherstack ├── forecast.json └── historical.json /.github/workflows/laravel.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | laravel-tests: 9 | runs-on: ubuntu-latest 10 | 11 | strategy: 12 | fail-fast: true 13 | matrix: 14 | php: [8.0, '8.2'] 15 | stability: [prefer-lowest, prefer-stable] 16 | 17 | name: PHP ${{ matrix.php }} - ${{ matrix.stability }} 18 | 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@v2 22 | 23 | - name: Setup PHP 24 | uses: shivammathur/setup-php@v2 25 | with: 26 | php-version: ${{ matrix.php }} 27 | tools: composer:v2 28 | coverage: none 29 | 30 | - name: Install dependencies 31 | run: composer update --${{ matrix.stability }} --prefer-dist --no-interaction --no-progress 32 | 33 | - name: Execute tests 34 | run: vendor/bin/phpunit --verbose 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | .idea 3 | composer.lock 4 | .phpunit.result.cache -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Vemcogroup 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Weather 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/vemcogroup/laravel-weather.svg?style=flat-square)](https://packagist.org/packages/vemcogroup/laravel-weather) 4 | [![Total Downloads](https://img.shields.io/packagist/dt/vemcogroup/laravel-weather.svg?style=flat-square)](https://packagist.org/packages/vemcogroup/laravel-weather) 5 | ![tests](https://github.com/vemcogroup/laravel-weather/workflows/tests/badge.svg) 6 | 7 | ## Description 8 | 9 | This package allows you to fetch weather data from different weather providers 10 | 11 | 12 | ## Installation 13 | 14 | You can install the package via composer: 15 | 16 | ```bash 17 | composer require vemcogroup/laravel-weather 18 | ``` 19 | 20 | The package will automatically register its service provider. 21 | 22 | To publish the config file to `config/weather.php` run: 23 | 24 | ```bash 25 | php artisan vendor:publish --provider="Vemcogroup\Weather\WeatherServiceProvider" 26 | ``` 27 | 28 | The default configuration can be seen [here](https://github.com/vemcogroup/laravel-weather/blob/master/config/weather.php) 29 | 30 | 31 | ## Usage 32 | 33 | At the moment this package support the following weather services, you can update `WEATHER_PROVIDER` to one of the following 34 | 35 | | Service | Provider name | Website | Geocoding | Remarks | 36 | |:-------------|:--------------|:----------------------------------------|:---------:|:--------------------------------------------------------------------------------------------------------------------------| 37 | | Dark Sky | darksky | https://darksky.net | Manual | Deprecated, not able to acquire api key https://blog.darksky.net. Will continue to function until March 31st, 2023. | 38 | | Weatherstack | weatherstack | https://weatherstack.com | Auto | For historical data a minimum Standard license is required. For forecast data a minimum Professional license is required. | 39 | | WeatherKit | weatherkit | https://developer.apple.com/weatherkit/ | Auto | Needs an apple developer account. | 40 | 41 | For other weather services fill free to create an issue or make a Pull Request. 42 | 43 | For `Manual` geocoding you need a Google geocode api key. 44 | Acquire it here https://developers.google.com/maps/documentation/geocoding/start and insert it into your .env file. 45 | 46 | ```php 47 | GOOGLE_MAPS_GEOCODING_API_KEY= 48 | ``` 49 | 50 | ### Request 51 | 52 | Start by setting up your request 53 | 54 | ```php 55 | $request = (new Vemcogroup\Weather\Request('1 Infinite Loop, Cupertino, CA 95014, USA')); 56 | ``` 57 | 58 | By default, it caches the weather response for 24hrs (86.400sec), this can be changed by setting a second parameter to cache timeout (in seconds) 59 | 60 | ```php 61 | $request = (new Vemcogroup\Weather\Request('1 Infinite Loop, Cupertino, CA 95014, USA', 600)); 62 | ``` 63 | 64 | 65 | *Units* 66 | There two available unit types, default is Metric: 67 | 68 | Metric (m): `Vemcogroup\Weather\Providers\Provider::WEATHER_UNITS_METRIC` 69 | Fahrenheit (f): `Vemcogroup\Weather\Providers\Provider::WEATHER_UNITS_FAHRENHEIT` 70 | 71 | To change the response units you can do the following: 72 | 73 | ```php 74 | $request->withUnits(Vemcogroup\Weather\Providers\Provider::WEATHER_UNITS_FAHRENHEIT); 75 | ``` 76 | 77 | *Locale* 78 | To change the locale for descriptions, summaries and other texts in the response, do the following: 79 | ```php 80 | $request->withLocale('nl'); 81 | ``` 82 | Locale need to be an 2-letter ISO Code of your preferred language. 83 | 84 | *Dates* 85 | If you need to select the dates to get weather data for E.g for historical data, set the dates like this: 86 | 87 | ```php 88 | $request->withDates([$date, ...]); 89 | ``` 90 | All dates in the array need to `Carbon` objects. 91 | 92 | *Options* 93 | If you need to set any extra options based in your selected weather provider you can do the following: 94 | 95 | ```php 96 | $request->withOption('name', 'value'); 97 | ``` 98 | 99 | ### Current weather and forecast 100 | 101 | To get current weather and forecast response you can do this: 102 | 103 | ```php 104 | $weather = weather()->getForecast($request); 105 | ``` 106 | 107 | Weather response will always be a `Collection` of responses. 108 | Forecast days depends on weather service provider. 109 | 110 | To get current weather data: 111 | 112 | ```php 113 | $weather->first()->getCurrently(); // DataPoint 114 | ``` 115 | 116 | To get forecast you can take first element of response and get the forecast like this: 117 | 118 | ```php 119 | $weather->first()->getDaily()->getData(); // array 120 | ``` 121 | Afterward run through the array with represent each day of the forecast on a `DataPoint` object. 122 | 123 | ### Historical 124 | 125 | To get historical data you can do this: 126 | 127 | ```php 128 | $weather = weather()->getHistorical($request); 129 | ``` 130 | 131 | Remember to set dates on request. 132 | Response will be a collection with keys representing the dates for historical data. 133 | 134 | ### Response 135 | To see what response data is available look into source code `/src/Objects` 136 | 137 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vemcogroup/laravel-weather", 3 | "description": "Weather package for Laravel to use different providers to get weather info", 4 | "keywords": [ 5 | "darksy", 6 | "laravel", 7 | "weather", 8 | "weatherstack" 9 | ], 10 | "homepage": "https://github.com/vemcogroup/laravel-weather", 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "Henrik B Hansen", 15 | "email": "hbh@vemcount.com", 16 | "homepage": "https://www.vemcogroup.com", 17 | "role": "Developer" 18 | } 19 | ], 20 | "require": { 21 | "php": "^8.0", 22 | "ext-json": "*", 23 | "firebase/php-jwt": "^6.4", 24 | "laravel/framework": "^9.0|^10.0|^11.0|^12.0", 25 | "spatie/geocoder": "^3.1", 26 | "ext-openssl": "*" 27 | }, 28 | "require-dev": { 29 | "orchestra/testbench": "^7.0|^9.0", 30 | "phpunit/phpunit": "^9.0|^10.5" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "Vemcogroup\\Weather\\": "src/" 35 | }, 36 | "files": [ 37 | "src/helpers.php" 38 | ] 39 | }, 40 | "autoload-dev": { 41 | "psr-4": { 42 | "Vemcogroup\\Weather\\Tests\\": "tests" 43 | } 44 | }, 45 | "scripts": { 46 | "test": "vendor/bin/phpunit" 47 | }, 48 | "config": { 49 | "sort-packages": true 50 | }, 51 | "extra": { 52 | "laravel": { 53 | "providers": [ 54 | "Vemcogroup\\Weather\\WeatherServiceProvider" 55 | ] 56 | } 57 | }, 58 | "minimum-stability": "dev", 59 | "prefer-stable": true 60 | } 61 | -------------------------------------------------------------------------------- /config/weather.php: -------------------------------------------------------------------------------- 1 | env('WEATHER_API_KEY'), 15 | 16 | /* 17 | |-------------------------------------------------------------------------- 18 | | Weather provider 19 | |-------------------------------------------------------------------------- 20 | | 21 | | Here you define provider you want to get weather information from. 22 | | 23 | */ 24 | 25 | 'provider' => env('WEATHER_PROVIDER'), 26 | 27 | /* 28 | |-------------------------------------------------------------------------- 29 | | Midday 30 | |-------------------------------------------------------------------------- 31 | | 32 | | Here you define what time is midday. 33 | | 34 | */ 35 | 'midday' => [ 36 | 'hour' => '13', 37 | 'minute' => '59', 38 | ], 39 | 40 | /* 41 | |-------------------------------------------------------------------------- 42 | | Providers 43 | |-------------------------------------------------------------------------- 44 | | 45 | | Here you define the provider-specific settings 46 | | 47 | */ 48 | 'providers' => [ 49 | 50 | 'weatherkit' => [ 51 | 52 | /* 53 | |-------------------------------------------------------------------------- 54 | | Private key 55 | |-------------------------------------------------------------------------- 56 | | 57 | | Private key issued from your Apple Developer Account 58 | | 59 | */ 60 | 'private-key' => base64_decode(env('WEATHER_KIT_PRIVATE_KEY')), 61 | 62 | /* 63 | |-------------------------------------------------------------------------- 64 | | Algorithm 65 | |-------------------------------------------------------------------------- 66 | | 67 | | The algorithm with which to sign the token. Weatherkit only supports ES256. 68 | | 69 | */ 70 | 'alg' => env('WEATHER_KIT_ALGORITHM', 'ES256'), 71 | 72 | /* 73 | |-------------------------------------------------------------------------- 74 | | Key ID 75 | |-------------------------------------------------------------------------- 76 | | 77 | | Key identifier obtained from your Apple Developer Account 78 | | 79 | */ 80 | 'kid' => env('WEATHER_KIT_KID'), 81 | 82 | /* 83 | |-------------------------------------------------------------------------- 84 | | ID 85 | |-------------------------------------------------------------------------- 86 | | 87 | | An identifier that consists of your 10-character Team ID and Service ID, separated by a period. 88 | | 89 | */ 90 | 'id' => env('WEATHER_KIT_ID'), 91 | 92 | /* 93 | |-------------------------------------------------------------------------- 94 | | Issuer Claim Key 95 | |-------------------------------------------------------------------------- 96 | | 97 | | The issuer claim key. This value is your 10-character Team ID from your developer account. 98 | | 99 | */ 100 | 'iss' => env('WEATHER_KIT_ISS'), 101 | 102 | /* 103 | |-------------------------------------------------------------------------- 104 | | Subject Public Claim Key 105 | |-------------------------------------------------------------------------- 106 | | 107 | | The subject public claim key. This value is your registered Service ID. 108 | | 109 | */ 110 | 'sub' => env('WEATHER_KIT_SUB'), 111 | ], 112 | 113 | 'weatherstack' => [ 114 | 115 | /* 116 | |-------------------------------------------------------------------------- 117 | | Intervals 118 | |-------------------------------------------------------------------------- 119 | | 120 | | Here you define the intervals for forecast and historical data. 121 | | 122 | */ 123 | 'intervals' => [ 124 | 'forecast' => env('WEATHER_FORECAST_INTERVAL', 24), 125 | 'historical' => env('WEATHER_HISTORICAL_INTERVAL', 1), 126 | ], 127 | ] 128 | ], 129 | ]; 130 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | tests 15 | 16 | 17 | 18 | 19 | src/ 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/Exceptions/WeatherException.php: -------------------------------------------------------------------------------- 1 | summary)) { 18 | $this->summary = $data->summary; 19 | } 20 | if (isset($data->icon)) { 21 | $this->icon = $data->icon; 22 | } 23 | if (isset($data->data)) { 24 | foreach ($data->data as $dataPoint) { 25 | $this->data[] = new DataPoint($dataPoint); 26 | } 27 | } 28 | } 29 | 30 | public function getSummary(): ?string 31 | { 32 | return $this->summary; 33 | } 34 | 35 | public function getIcon(): ?string 36 | { 37 | return $this->icon; 38 | } 39 | 40 | public function getData(): ?array 41 | { 42 | return $this->data; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Objects/DataPoint.php: -------------------------------------------------------------------------------- 1 | data = $data; 37 | 38 | if (isset($data->time)) { 39 | $this->time = Carbon::parse($data->time); 40 | } 41 | if (isset($data->summary)) { 42 | $this->summary = $data->summary; 43 | } 44 | if (isset($data->icon)) { 45 | $this->icon = $data->icon; 46 | } 47 | if (isset($data->sunriseTime)) { 48 | $this->sunriseTime = Carbon::parse($data->sunriseTime); 49 | } 50 | if (isset($data->sunsetTime)) { 51 | $this->sunsetTime = Carbon::parse($data->sunsetTime); 52 | } 53 | if (isset($data->moonPhase)) { 54 | $this->moonPhase = $data->moonPhase; 55 | } 56 | if (isset($data->nearestStormDistance)) { 57 | $this->nearestStormDistance = $data->nearestStormDistance; 58 | } 59 | if (isset($data->nearestStormBearing)) { 60 | $this->nearestStormBearing = $data->nearestStormBearing; 61 | } 62 | 63 | $this->precipitation = new Precipitation( 64 | ($data->precipIntensity ?? null), 65 | ($data->precipIntensityMax ?? null), 66 | ($data->precipIntensityMaxTime ?? null), 67 | ($data->precipProbability ?? null), 68 | ($data->precipType ?? null), 69 | ($data->precipAccumulation ?? null) 70 | ); 71 | $this->temperature = new Temperature( 72 | ($data->temperature ?? null), 73 | ($data->temperatureMin ?? null), 74 | ($data->temperatureMinTime ?? null), 75 | ($data->temperatureMax ?? null), 76 | ($data->temperatureMaxTime ?? null) 77 | ); 78 | $this->apparentTemperature = new Temperature( 79 | ($data->apparentTemperature ?? null), 80 | ($data->apparentTemperatureMin ?? null), 81 | ($data->apparentTemperatureMinTime ?? null), 82 | ($data->apparentTemperatureMax ?? null), 83 | ($data->apparentTemperatureMaxTime ?? null) 84 | ); 85 | 86 | if (isset($data->dewPoint)) { 87 | $this->dewPoint = $data->dewPoint; 88 | } 89 | if (isset($data->windSpeed)) { 90 | $this->windSpeed = $data->windSpeed; 91 | } 92 | if (isset($data->windBearing)) { 93 | $this->windBearing = $data->windBearing; 94 | } 95 | if (isset($data->cloudCover)) { 96 | $this->cloudCover = $data->cloudCover; 97 | } 98 | if (isset($data->humidity)) { 99 | $this->humidity = $data->humidity; 100 | } 101 | if (isset($data->pressure)) { 102 | $this->pressure = $data->pressure; 103 | } 104 | if (isset($data->visibility)) { 105 | $this->visibility = $data->visibility; 106 | } 107 | if (isset($data->ozone)) { 108 | $this->ozone = $data->ozone; 109 | } 110 | } 111 | 112 | public function getTime(): ?Carbon 113 | { 114 | return $this->time; 115 | } 116 | 117 | public function getSummary(): ?string 118 | { 119 | return $this->summary; 120 | } 121 | 122 | public function getIcon(): ?string 123 | { 124 | return $this->icon; 125 | } 126 | 127 | public function getSunriseTime(): ?Carbon 128 | { 129 | return $this->sunriseTime; 130 | } 131 | 132 | public function getSunsetTime(): ?Carbon 133 | { 134 | return $this->sunsetTime; 135 | } 136 | 137 | public function getMoonPhase(): ?float 138 | { 139 | return $this->moonPhase; 140 | } 141 | 142 | public function getNearestStormDistance(): ?int 143 | { 144 | return $this->nearestStormDistance; 145 | } 146 | 147 | public function getNearestStormBearing(): ?int 148 | { 149 | return $this->nearestStormBearing; 150 | } 151 | 152 | public function getPrecipitation(): Precipitation 153 | { 154 | return $this->precipitation; 155 | } 156 | 157 | public function getTemperature(): Temperature 158 | { 159 | return $this->temperature; 160 | } 161 | 162 | public function getApparentTemperature(): Temperature 163 | { 164 | return $this->apparentTemperature; 165 | } 166 | 167 | public function getDewPoint(): ?float 168 | { 169 | return $this->dewPoint; 170 | } 171 | 172 | public function getWindSpeed(): ?float 173 | { 174 | return $this->windSpeed; 175 | } 176 | 177 | public function getWindBearing(): ?int 178 | { 179 | return $this->windBearing; 180 | } 181 | 182 | public function getCloudCover(): ?float 183 | { 184 | return $this->cloudCover; 185 | } 186 | 187 | public function getHumidity(): ?float 188 | { 189 | return $this->humidity; 190 | } 191 | 192 | public function getPressure(): ?float 193 | { 194 | return $this->pressure; 195 | } 196 | 197 | public function getVisibility(): ?float 198 | { 199 | return $this->visibility; 200 | } 201 | 202 | public function getOzone(): ?float 203 | { 204 | return $this->ozone; 205 | } 206 | 207 | public function getData() 208 | { 209 | return $this->data; 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/Objects/Precipitation.php: -------------------------------------------------------------------------------- 1 | intensity = $intensity; 19 | $this->maxIntensity = $maxIntensity; 20 | $this->maxIntensityTime = $maxIntensityTime ? Carbon::parse($maxIntensityTime) : null; 21 | $this->probability = $probability; 22 | $this->type = $type; 23 | $this->accumulation = $accumulation; 24 | } 25 | 26 | public function getIntensity(): float 27 | { 28 | return $this->intensity; 29 | } 30 | 31 | public function getMaxIntensity(): float 32 | { 33 | return $this->maxIntensity; 34 | } 35 | 36 | public function getMaxIntensityTime(): ?Carbon 37 | { 38 | return $this->maxIntensityTime; 39 | } 40 | 41 | public function getProbability(): float 42 | { 43 | return $this->probability; 44 | } 45 | 46 | public function getType(): string 47 | { 48 | return $this->type; 49 | } 50 | 51 | public function getAccumulation(): float 52 | { 53 | return $this->accumulation; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Objects/Temperature.php: -------------------------------------------------------------------------------- 1 | current = $current; 18 | $this->min = $min; 19 | $this->minTime = $minTime ? Carbon::parse($minTime) : null; 20 | $this->max = $max; 21 | $this->maxTime = $maxTime ? Carbon::parse($maxTime) : null; 22 | } 23 | 24 | public function getCurrent(): float 25 | { 26 | return $this->current; 27 | } 28 | 29 | public function getMin(): float 30 | { 31 | return $this->min; 32 | } 33 | 34 | public function getMinTime(): ?Carbon 35 | { 36 | return $this->minTime; 37 | } 38 | 39 | public function getMax(): float 40 | { 41 | return $this->max; 42 | } 43 | 44 | public function getMaxTime(): ?Carbon 45 | { 46 | return $this->maxTime; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Providers/Darksky.php: -------------------------------------------------------------------------------- 1 | getData($requests, self::WEATHER_TYPE_HISTORICAL); 15 | } 16 | 17 | public function getForecast($requests): Collection 18 | { 19 | return $this->getData($requests, self::WEATHER_TYPE_FORECAST); 20 | } 21 | 22 | private function getData($requests, $type): Collection 23 | { 24 | $responses = collect(); 25 | $this->setupRequests($requests); 26 | $this->buildRequest($type); 27 | $this->processRequests(); 28 | 29 | /** @var Request $request */ 30 | foreach ($this->requests as $request) { 31 | $responses->put($request->getKey(), $request->getForecast()); 32 | } 33 | 34 | return $responses; 35 | } 36 | 37 | private function buildRequest($type = self::WEATHER_TYPE_FORECAST): void 38 | { 39 | $requests = []; 40 | 41 | /** @var Request $request **/ 42 | foreach ($this->requests as $request) { 43 | $request->lookupGeocode(); 44 | $latitude = $request->getLatitude(); 45 | $longitude = $request->getLongitude(); 46 | 47 | // convert units 48 | if ($request->getUnits() === self::WEATHER_UNITS_METRIC) { 49 | $request->withUnits('si'); 50 | } 51 | 52 | if ($request->getUnits() === self::WEATHER_UNITS_FAHRENHEIT) { 53 | $request->withUnits('us'); 54 | } 55 | 56 | $options = $request->getHttpQuery(); 57 | 58 | if ($type === self::WEATHER_TYPE_FORECAST) { 59 | $dateRequest = clone $request; 60 | $url = $this->url . $this->apiKey 61 | . "/$latitude,$longitude" 62 | . "?lang=" . $request->getLocale() 63 | . "&units=" . $request->getUnits() 64 | . ($options ? "&$options" : ''); 65 | $dateRequest->setUrl($url); 66 | $requests[] = $dateRequest; 67 | } 68 | 69 | if($type === self::WEATHER_TYPE_HISTORICAL) { 70 | foreach($request->getDates() as $date) { 71 | $dateRequest = clone $request; 72 | $url = $this->url . $this->apiKey 73 | . "/$latitude,$longitude" 74 | . ",$date->timestamp" 75 | . "?lang=" . $request->getLocale() 76 | . "&units=" . $request->getUnits() 77 | . ($options ? "&$options" : ''); 78 | $dateRequest->setKey($date->format('Y-m-d H:i')); 79 | $dateRequest->setUrl($url); 80 | $requests[] = $dateRequest; 81 | } 82 | } 83 | } 84 | 85 | if(count($requests)) { 86 | $this->requests = $requests; 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Providers/Provider.php: -------------------------------------------------------------------------------- 1 | requests = []; 46 | 47 | if (!($this->apiKey = config('weather.api_key'))) { 48 | throw WeatherException::noApiKey(); 49 | } 50 | 51 | $this->client = app(Client::class); 52 | } 53 | 54 | abstract public function getForecast($requests): Collection; 55 | abstract public function getHistorical($requests): Collection; 56 | 57 | protected function setupRequests($requests): void 58 | { 59 | $this->requests = is_array($requests) ? $requests : [$requests]; 60 | } 61 | 62 | protected function processRequests(): void 63 | { 64 | $promises = (function() { 65 | /** @var Request $request */ 66 | foreach ($this->requests as $request) { 67 | if ($cachedResponse = $request->getCacheResponse()) { 68 | $request->setResponse($cachedResponse); 69 | yield new FulfilledPromise($cachedResponse); 70 | continue; 71 | } 72 | 73 | yield $this->client->getAsync($request->getUrl())->then(function (GuzzleResponse $response) use ($request) { 74 | $content = json_decode($response->getBody()); 75 | 76 | if (!method_exists($this, 'verifyResponse') || (method_exists($this, 'verifyResponse') && $this->verifyResponse($content))) { 77 | Cache::put(md5('laravel-weather-' . $request->getUrl()), $content, $request->getCacheTimeout()); 78 | } 79 | 80 | $request->setResponse($content); 81 | }); 82 | } 83 | })(); 84 | 85 | $eachPromise = new EachPromise($promises, [ 86 | 'concurrency' => $this->threads, 87 | 'fulfilled' => function ($profile) {}, 88 | 'rejected' => function ($reason) { 89 | //throw WeatherException::communicationError($reason); 90 | } 91 | ]); 92 | $eachPromise->promise()->wait(); 93 | } 94 | 95 | protected function celsiusToFahrenheit(float $celsius): float 96 | { 97 | return ($celsius * 1.8) + 32; 98 | } 99 | 100 | protected function kmToMiles(float $km): float 101 | { 102 | return $km * 0.62137119; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Providers/WeatherKit.php: -------------------------------------------------------------------------------- 1 | getCode() !== WeatherException::NO_API_KEY_ERROR_CODE) { 32 | throw $exception; 33 | } 34 | } 35 | 36 | if ( 37 | config('weather.providers.weatherkit.alg') === null 38 | || config('weather.providers.weatherkit.kid') === null 39 | || config('weather.providers.weatherkit.id') === null 40 | || config('weather.providers.weatherkit.iss') === null 41 | || config('weather.providers.weatherkit.sub') === null 42 | ) { 43 | throw new WeatherException('Missing or misconfigured WeatherKit settings, please add/edit it in .env', 1001); 44 | } 45 | 46 | try { 47 | $this->getToken(); 48 | } catch (Exception $e) { 49 | throw new WeatherException('Missing or misconfigured WeatherKit settings, please add/edit it in .env', 1001); 50 | } 51 | 52 | if (!app()->environment('testing')) { 53 | $this->client = app(Client::class, [ 54 | 'config' => [ 55 | 'headers' => [ 56 | 'Authorization' => 'Bearer ' . $this->getToken(), 57 | ], 58 | ] 59 | ]); 60 | } 61 | } 62 | 63 | public function getForecast($requests): Collection 64 | { 65 | $this->setupRequests($requests); 66 | $this->buildRequests(); 67 | $this->processRequests(); 68 | 69 | return collect($this->formatResponse()); 70 | } 71 | 72 | public function getHistorical($requests): Collection 73 | { 74 | $this->setupRequests($requests); 75 | $this->buildRequests(self::WEATHER_TYPE_HISTORICAL); 76 | $this->processRequests(); 77 | 78 | return collect($this->formatResponse()); 79 | } 80 | 81 | private function buildRequests($type = self::WEATHER_TYPE_FORECAST): void 82 | { 83 | $requests = []; 84 | 85 | foreach ($this->requests as $request) { 86 | $request->lookupGeocode(); 87 | $latitude = $request->getLatitude(); 88 | $longitude = $request->getLongitude(); 89 | $locale = $request->getLocale(); 90 | 91 | if ($request->getUnits() === self::WEATHER_UNITS_METRIC) { 92 | $request->withUnits('si'); 93 | } 94 | 95 | if ($request->getUnits() === self::WEATHER_UNITS_FAHRENHEIT) { 96 | $request->withUnits('us'); 97 | } 98 | 99 | if ($type === self::WEATHER_TYPE_FORECAST) { 100 | $dateRequest = clone $request; 101 | $url = $this->url . "$locale/$latitude/$longitude?dataSets=forecastDaily&timezone=" . $request->getTimezone() . '¤tAsOf=' . now()->setMinute(0)->setSecond(0)->toIso8601ZuluString(); 102 | $dateRequest->setUrl($url); 103 | $requests[] = $dateRequest; 104 | } 105 | 106 | if ($type === self::WEATHER_TYPE_HISTORICAL) { 107 | foreach ($request->getDates() as $date) { 108 | $dateRequest = clone $request; 109 | $url = $this->url . "$locale/$latitude/$longitude?dataSets=currentWeather&timezone=" . $request->getTimezone() . '¤tAsOf=' . $date->toIso8601ZuluString(); 110 | $dateRequest->setKey($date->format('Y-m-d H:i')); 111 | $dateRequest->setUrl($url); 112 | $requests[] = $dateRequest; 113 | } 114 | } 115 | } 116 | 117 | if (count($requests)) { 118 | $this->requests = $requests; 119 | } 120 | } 121 | 122 | protected function verifyResponse(stdClass $response): bool 123 | { 124 | return isset($response->currentWeather) || isset($response->forecastDaily); 125 | } 126 | 127 | private function formatResponse(): array 128 | { 129 | $result = []; 130 | 131 | /** @var Request $request */ 132 | foreach ($this->requests as $request) { 133 | $response = $request->getResponse(); 134 | 135 | if (!isset($response->currentWeather) && !isset($response->forecastDaily)) { 136 | continue; 137 | } 138 | 139 | $result[$request->getKey()] = $this->formatSingleResponse($response); 140 | } 141 | 142 | return $result; 143 | } 144 | 145 | private function formatSingleResponse($response): Response 146 | { 147 | /** @var Request $request */ 148 | $request = Arr::first($this->requests); 149 | 150 | $data = []; 151 | 152 | if (isset($response->currentWeather)) { 153 | $data = $this->getCurrentWeatherDataFromResponse($response->currentWeather, $request); 154 | } elseif (isset($response->forecastDaily)) { 155 | $data = $this->getForecastWeatherDataFromResponse($response->forecastDaily, $request); 156 | } 157 | 158 | return new Response($data); 159 | } 160 | 161 | protected function convertIcon(string $conditionCode): string 162 | { 163 | $map = [ 164 | 'Clear' => self::WEATHER_ICON_CLEAR_DAY, 165 | 'Breezy' => self::WEATHER_ICON_WIND, 166 | 'Cloudy' => self::WEATHER_ICON_CLOUDY, 167 | 'Drizzle' => self::WEATHER_ICON_RAIN, 168 | 'MostlyClear' => self::WEATHER_ICON_PARTLY_CLOUDY_DAY, 169 | 'MostlyCloudy' => self::WEATHER_ICON_CLOUDY, 170 | 'PartlyCloudy' => self::WEATHER_ICON_PARTLY_CLOUDY_DAY, 171 | "Dust" => self::WEATHER_ICON_FOG, 172 | "Fog" => self::WEATHER_ICON_FOG, 173 | "Haze" => self::WEATHER_ICON_FOG, 174 | "ScatteredThunderstorms" => self::WEATHER_ICON_WIND, 175 | "Smoke" => self::WEATHER_ICON_FOG, 176 | "Windy" => self::WEATHER_ICON_WIND, 177 | "HeavyRain" => self::WEATHER_ICON_RAIN, 178 | "Rain" => self::WEATHER_ICON_RAIN, 179 | "Showers" => self::WEATHER_ICON_RAIN, 180 | "Flurries" => self::WEATHER_ICON_SLEET, 181 | "HeavySnow" => self::WEATHER_ICON_SNOW, 182 | "MixedRainAndSleet" => self::WEATHER_ICON_SLEET, 183 | "MixedRainAndSnow" => self::WEATHER_ICON_SNOW, 184 | "MixedRainfall" => self::WEATHER_ICON_RAIN, 185 | "MixedSnowAndSleet" => self::WEATHER_ICON_SLEET, 186 | "ScatteredShowers" => self::WEATHER_ICON_RAIN, 187 | "ScatteredSnowShowers" => self::WEATHER_ICON_SNOW, 188 | "Sleet" => self::WEATHER_ICON_SLEET, 189 | "Snow" => self::WEATHER_ICON_SNOW, 190 | "SnowShowers" => self::WEATHER_ICON_SNOW, 191 | "Blizzard" => self::WEATHER_ICON_SNOW, 192 | "BlowingSnow" => self::WEATHER_ICON_SNOW, 193 | "FreezingDrizzle" => self::WEATHER_ICON_RAIN, 194 | "FreezingRain" => self::WEATHER_ICON_RAIN, 195 | "Frigid" => self::WEATHER_ICON_SLEET, 196 | "Hail" => self::WEATHER_ICON_SNOW, 197 | "Hot" => self::WEATHER_ICON_CLEAR_DAY, 198 | "Hurricane" => self::WEATHER_ICON_WIND, 199 | "IsolatedThunderstorms" => self::WEATHER_ICON_WIND, 200 | "SevereThunderstorm" => self::WEATHER_ICON_WIND, 201 | "Thunderstorm" => self::WEATHER_ICON_WIND, 202 | "Tornado" => self::WEATHER_ICON_WIND, 203 | "TropicalStorm" => self::WEATHER_ICON_WIND 204 | ]; 205 | 206 | return $map[$conditionCode] ?? self::WEATHER_ICON_NA; 207 | } 208 | 209 | private function getToken(): string 210 | { 211 | $privateKey = openssl_pkey_get_private(config('weather.providers.weatherkit.private-key')); 212 | 213 | $header = [ 214 | 'alg' => config('weather.providers.weatherkit.alg'), 215 | 'kid' => config('weather.providers.weatherkit.kid'), 216 | 'id' => config('weather.providers.weatherkit.id'), 217 | ]; 218 | 219 | $payload = [ 220 | 'iss' => config('weather.providers.weatherkit.iss'), 221 | 'sub' => config('weather.providers.weatherkit.sub'), 222 | 'iat' => now('UTC')->timestamp, 223 | 'exp' => now('UTC')->addMinute()->timestamp, 224 | ]; 225 | 226 | return JWT::encode($payload, $privateKey, config('weather.providers.weatherkit.alg'), null, $header); 227 | } 228 | 229 | protected function getCurrentWeatherDataFromResponse($weatherData, Request $request): array 230 | { 231 | return [ 232 | 'latitude' => (float) $weatherData->metadata->latitude, 233 | 'longitude' => (float) $weatherData->metadata->longitude, 234 | 'timezone' => $request->getTimezone(), 235 | 'currently' => [ 236 | 'time' => Carbon::parse($weatherData->asOf), 237 | 'summary' => $weatherData->conditionCode, 238 | 'icon' => $this->convertIcon($weatherData->conditionCode), 239 | 'precipIntensity' => $weatherData->precipitationIntensity, 240 | 'precipProbability' => 0, 241 | 'temperature' => $request->getUnits() === 'si' ? $weatherData->temperature : $this->celsiusToFahrenheit($weatherData->temperature), 242 | 'apparentTemperature' => $request->getUnits() === 'si' ? $weatherData->temperatureApparent : $this->celsiusToFahrenheit($weatherData->temperatureApparent), 243 | 'dewPoint' => $request->getUnits() === 'si' ? $weatherData->temperatureDewPoint : $this->celsiusToFahrenheit($weatherData->temperatureDewPoint), 244 | 'humidity' => $weatherData->humidity, 245 | 'pressure' => $weatherData->pressure, 246 | 'windSpeed' => $request->getUnits() === 'si' ? $weatherData->windSpeed : $this->kmToMiles($weatherData->windSpeed), 247 | 'windGust' => $request->getUnits() === 'si' ? $weatherData->windGust : $this->kmToMiles($weatherData->windGust), 248 | 'windBearing' => $weatherData->windDirection, 249 | 'cloudCover' => $weatherData->cloudCover, 250 | 'uvIndex' => $weatherData->uvIndex, 251 | 'visibility' => $weatherData->visibility, 252 | 'ozone' => 0, 253 | ], 254 | 'offset' => 0, 255 | 'daily' => [ 256 | 'icon' => $this->convertIcon($weatherData->conditionCode), 257 | 'summary' => $weatherData->conditionCode, 258 | ], 259 | ]; 260 | } 261 | 262 | protected function getForecastWeatherDataFromResponse($weatherData, Request $request): array 263 | { 264 | $data = [ 265 | 'latitude' => (float) $weatherData->metadata->latitude, 266 | 'longitude' => (float) $weatherData->metadata->longitude, 267 | 'timezone' => $request->getTimezone(), 268 | 'currently' => [ 269 | 'time' => Carbon::parse($weatherData->days[0]->forecastStart), 270 | 'summary' => $weatherData->days[0]->conditionCode, 271 | 'icon' => $this->convertIcon($weatherData->days[0]->conditionCode), 272 | 'precipIntensity' => $weatherData->days[0]->precipitationAmount, 273 | 'precipProbability' => $weatherData->days[0]->precipitationChance, 274 | 'temperature' => $request->getUnits() === 'si' ? $weatherData->days[0]->temperatureMax : $this->celsiusToFahrenheit($weatherData->days[0]->temperatureMax), 275 | 'apparentTemperature' => 0, 276 | 'dewPoint' => 0, 277 | 'humidity' => $weatherData->days[0]->daytimeForecast->humidity, 278 | 'pressure' => 0, 279 | 'windSpeed' => $request->getUnits() === 'si' ? $weatherData->days[0]->daytimeForecast->windSpeed : $this->kmToMiles($weatherData->days[0]->daytimeForecast->windSpeed), 280 | 'windGust' => 0, 281 | 'windBearing' => $weatherData->days[0]->daytimeForecast->windDirection, 282 | 'cloudCover' => $weatherData->days[0]->daytimeForecast->cloudCover, 283 | 'uvIndex' => $weatherData->days[0]->maxUvIndex, 284 | 'visibility' => 0, 285 | 'ozone' => 0, 286 | ], 287 | 'offset' => 0, 288 | 'daily' => [ 289 | 'icon' => $this->convertIcon($weatherData->days[0]->conditionCode), 290 | 'summary' => $weatherData->days[0]->conditionCode, 291 | ], 292 | ]; 293 | 294 | foreach ($weatherData->days as $day) { 295 | $data['daily']['data'][] = [ 296 | 'time' => $day->forecastStart ?? null, 297 | 'icon' => $this->convertIcon($day->conditionCode) ?? self::WEATHER_ICON_NA, 298 | 'summary' => $day->conditionCode ?? null, 299 | 'temperatureMin' => $day->temperatureMin ?? null, 300 | 'temperatureMax' => $day->temperatureMax ?? null, 301 | ]; 302 | } 303 | 304 | return $data; 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /src/Providers/Weatherstack.php: -------------------------------------------------------------------------------- 1 | setupRequests($requests); 17 | $this->buildRequests(self::WEATHER_TYPE_HISTORICAL); 18 | $this->processRequests(); 19 | 20 | return collect($this->formatResponse()); 21 | } 22 | 23 | public function getForecast($requests): Collection 24 | { 25 | $this->setupRequests($requests); 26 | $this->buildRequests(self::WEATHER_TYPE_FORECAST); 27 | $this->processRequests(); 28 | 29 | return collect($this->formatResponse()); 30 | } 31 | 32 | private function buildRequests($type = self::WEATHER_TYPE_FORECAST): void 33 | { 34 | /** @var Request $request */ 35 | foreach ($this->requests as $request) { 36 | $url = $this->url; 37 | $request->withOption('access_key', $this->apiKey) 38 | ->withOption('query', $request->getAddress()); 39 | 40 | if($request->getLocale() === 'en') { 41 | $request->withLocale(null); 42 | } 43 | 44 | if ($type === self::WEATHER_TYPE_FORECAST) { 45 | $url .= 'forecast'; 46 | $request->withOption('forecast_days', 7) 47 | ->withOption('hourly', 1) 48 | ->withOption('interval', config('weather.providers.weatherstack.intervals.forecast')); 49 | } 50 | 51 | if ($type === self::WEATHER_TYPE_HISTORICAL) { 52 | $url .= 'historical'; 53 | $request->withOption('hourly', 1)->withOption('interval', config('weather.providers.weatherstack.intervals.historical')); 54 | $dates = []; 55 | /** @var Carbon $date */ 56 | foreach ($request->getDates() as $date) { 57 | $dates[] = $date->format('Y-m-d'); 58 | } 59 | 60 | $request->withOption('historical_date', implode(';', $dates)); 61 | } 62 | 63 | $options = $request->getHttpQuery(); 64 | 65 | $url .= "?units=" . $request->getUnits() 66 | . ($request->getLocale() ? "&language=" . $request->getLocale() : '') 67 | . ($options ? "&$options" : ''); 68 | 69 | $request->setUrl($url); 70 | } 71 | } 72 | 73 | private function formatResponse(): array 74 | { 75 | $result = []; 76 | 77 | /** @var Request $request */ 78 | foreach ($this->requests as $request) { 79 | $response = $request->getResponse(); 80 | 81 | if (isset($response->historical)) { 82 | /** @var Carbon $date */ 83 | foreach ($request->getDates() as $date) { 84 | $key = $date->format('Y-m-d'); 85 | if (property_exists($response->historical, $key)) { 86 | $responseData = $response->historical->$key; 87 | $result[$date->format('Y-m-d H:i')] = $this->formatSingleResponse($response, $responseData->hourly[$date->hour]); 88 | } 89 | } 90 | } else { 91 | $result[$request->getKey()] = $this->formatSingleResponse($response, $response->current); 92 | } 93 | } 94 | 95 | return $result; 96 | } 97 | 98 | private function formatSingleResponse($response, $data): Response 99 | { 100 | $data = [ 101 | 'latitude' => (float) $response->location->lat, 102 | 'longitude' => (float) $response->location->lon, 103 | 'timezone' => $response->location->timezone_id, 104 | 'currently' => [ 105 | 'time' => isset($data->observation_time) ? Carbon::parse(strtotime($data->observation_time)) : Carbon::parse(strtotime(now()->format('Y-m-d') . ' ' . $data->time)), 106 | 'summary' => $data->weather_descriptions[0], 107 | 'icon' => $this->convertIcon($data->weather_code), 108 | 'precipIntensity' => $data->precip, 109 | 'precipProbability' => 0, // Not available 110 | 'temperature' => $data->temperature, 111 | 'apparentTemperature' => $data->feelslike, 112 | 'dewPoint' => 0, // Not available 113 | 'humidity' => $data->humidity, 114 | 'pressure' => $data->pressure, 115 | 'windSpeed' => $data->wind_speed, 116 | 'windGust' => 0, // Not available 117 | 'windBearing' => $data->wind_degree, 118 | 'cloudCover' => $data->cloudcover, 119 | 'uvIndex' => $data->uv_index, 120 | 'visibility' => $data->visibility, 121 | 'ozone' => 0, // Not available 122 | ], 123 | 'offset' => $response->location->utc_offset, 124 | 'daily' => [ 125 | 'icon' => $this->convertIcon($response->current->weather_code), 126 | 'summary' => $response->current->weather_descriptions[0] ?? null, 127 | ], 128 | ]; 129 | 130 | if (isset($response->forecast)) { 131 | foreach ($response->forecast as $day) { 132 | $data['daily']['data'][] = [ 133 | 'time' => $day->date_epoch ?? null, 134 | 'icon' => $this->convertIcon($day->hourly[0]->weather_code) ?? self::WEATHER_ICON_NA, 135 | 'summary' => $day->hourly[0]->weather_descriptions[0] ?? null, 136 | 'temperatureMin' => $day->mintemp ?? null, 137 | 'temperatureMax' => $day->maxtemp ?? null, 138 | ]; 139 | } 140 | } 141 | 142 | return new Response($data); 143 | } 144 | 145 | private function convertIcon($code) 146 | { 147 | $map = [ 148 | 113 => self::WEATHER_ICON_CLEAR_DAY, 149 | 116 => self::WEATHER_ICON_PARTLY_CLOUDY_DAY, 150 | 119 => self::WEATHER_ICON_CLOUDY, 151 | 122 => self::WEATHER_ICON_CLOUDY, 152 | 143 => self::WEATHER_ICON_CLOUDY, 153 | 176 => self::WEATHER_ICON_RAIN, 154 | 179 => self::WEATHER_ICON_SLEET, 155 | 182 => self::WEATHER_ICON_SLEET, 156 | 185 => self::WEATHER_ICON_SLEET, 157 | 200 => self::WEATHER_ICON_RAIN, 158 | 227 => self::WEATHER_ICON_SNOW, 159 | 230 => self::WEATHER_ICON_SNOW, 160 | 248 => self::WEATHER_ICON_FOG, 161 | 260 => self::WEATHER_ICON_FOG, 162 | 263 => self::WEATHER_ICON_RAIN, 163 | 266 => self::WEATHER_ICON_RAIN, 164 | 281 => self::WEATHER_ICON_SLEET, 165 | 284 => self::WEATHER_ICON_SLEET, 166 | 293 => self::WEATHER_ICON_RAIN, 167 | 296 => self::WEATHER_ICON_RAIN, 168 | 299 => self::WEATHER_ICON_RAIN, 169 | 302 => self::WEATHER_ICON_RAIN, 170 | 305 => self::WEATHER_ICON_RAIN, 171 | 308 => self::WEATHER_ICON_RAIN, 172 | 311 => self::WEATHER_ICON_SLEET, 173 | 314 => self::WEATHER_ICON_SLEET, 174 | 317 => self::WEATHER_ICON_SLEET, 175 | 320 => self::WEATHER_ICON_SLEET, 176 | 323 => self::WEATHER_ICON_SNOW, 177 | 326 => self::WEATHER_ICON_SNOW, 178 | 329 => self::WEATHER_ICON_SNOW, 179 | 332 => self::WEATHER_ICON_SNOW, 180 | 335 => self::WEATHER_ICON_SNOW, 181 | 338 => self::WEATHER_ICON_SNOW, 182 | 350 => self::WEATHER_ICON_SLEET, 183 | 353 => self::WEATHER_ICON_RAIN, 184 | 356 => self::WEATHER_ICON_RAIN, 185 | 359 => self::WEATHER_ICON_RAIN, 186 | 362 => self::WEATHER_ICON_SLEET, 187 | 365 => self::WEATHER_ICON_SLEET, 188 | 368 => self::WEATHER_ICON_SNOW, 189 | 371 => self::WEATHER_ICON_SNOW, 190 | 374 => self::WEATHER_ICON_SLEET, 191 | 377 => self::WEATHER_ICON_SLEET, 192 | 386 => self::WEATHER_ICON_RAIN, 193 | 389 => self::WEATHER_ICON_RAIN, 194 | 392 => self::WEATHER_ICON_SNOW, 195 | 395 => self::WEATHER_ICON_SNOW, 196 | ]; 197 | 198 | return $map[$code] ?: self::WEATHER_ICON_NA; 199 | 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/Request.php: -------------------------------------------------------------------------------- 1 | dates = []; 29 | $this->options = []; 30 | $this->address = $address; 31 | $this->cacheTimeout = $cacheTimeout; 32 | 33 | $this->units = 's'; 34 | $this->locale = 'en'; 35 | } 36 | 37 | public function lookupGeocode(): void 38 | { 39 | $cacheKey = md5('laravel-weather-geocode-' . $this->address); 40 | try { 41 | if (!($this->geocode = Cache::get($cacheKey))) { 42 | $response = (new Geocoder(app(Client::class))) 43 | ->setApiKey(config('geocoder.key', '')) 44 | ->getCoordinatesForAddress($this->address); 45 | if ($response['lat'] === 0 && $response['lng'] === 0) { 46 | throw WeatherException::invalidAddress($this->address, $response['formatted_address']); 47 | } 48 | Cache::put($cacheKey, $response, now()->addDays(10)); 49 | $this->geocode = $response; 50 | } 51 | } catch (\Exception $e) { 52 | throw WeatherException::invalidAddress($this->address, $e->getMessage()); 53 | } 54 | } 55 | 56 | protected function getMidday(): Carbon 57 | { 58 | return now()->setHour(config('weather.midday.hour'))->setMinute(config('weather.midday.minute')); 59 | } 60 | 61 | public function getHttpQuery($type = 'GET'): string 62 | { 63 | if ($type === 'GET') { 64 | return http_build_query($this->options); 65 | } 66 | 67 | return 'JSON_STRING'; 68 | } 69 | 70 | public function getCacheResponse(): ?object 71 | { 72 | return Cache::get(md5('laravel-weather-' . $this->url)); 73 | } 74 | 75 | public function getCacheTimeout() 76 | { 77 | return $this->cacheTimeout; 78 | } 79 | 80 | public function getAddress(): string 81 | { 82 | return $this->address; 83 | } 84 | 85 | public function getLongitude() 86 | { 87 | return $this->geocode['lng'] ?? null; 88 | } 89 | 90 | public function getLatitude() 91 | { 92 | return $this->geocode['lat'] ?? null; 93 | } 94 | 95 | public function getDates(): array 96 | { 97 | return count($this->dates) ? $this->dates : [$this->getMidday()]; 98 | } 99 | 100 | public function getKey(): ?string 101 | { 102 | return $this->key ?: $this->getMidday()->format('Y-m-d H:i'); 103 | } 104 | 105 | public function withOption(string $name, $value = null): Request 106 | { 107 | $this->options[$name] = $value; 108 | 109 | return $this; 110 | } 111 | 112 | public function withUnits(string $units): Request 113 | { 114 | $this->units = $units; 115 | 116 | return $this; 117 | } 118 | 119 | public function getUnits(): string 120 | { 121 | return $this->units; 122 | } 123 | 124 | public function withLocale(string $locale = null): Request 125 | { 126 | $this->locale = $locale; 127 | 128 | return $this; 129 | } 130 | 131 | public function getLocale(): ?string 132 | { 133 | return $this->locale; 134 | } 135 | 136 | public function atDates(array $dates): Request 137 | { 138 | $this->dates = $dates; 139 | 140 | return $this; 141 | } 142 | 143 | public function setKey(string $key): Request 144 | { 145 | $this->key = $key; 146 | 147 | return $this; 148 | } 149 | 150 | public function setResponse($response): void 151 | { 152 | $this->response = $response; 153 | } 154 | 155 | public function getResponse($asType = null) 156 | { 157 | if($asType === 'array') { 158 | return (array) $this->response; 159 | } 160 | 161 | if ($asType === 'string') { 162 | return json_encode((array) $this->response); 163 | } 164 | 165 | return $this->response; 166 | } 167 | 168 | public function getForecast(): Response 169 | { 170 | return new Response((object) $this->response); 171 | } 172 | 173 | public function setUrl(string $url): Request 174 | { 175 | $this->url = $url; 176 | 177 | return $this; 178 | } 179 | 180 | public function getUrl() 181 | { 182 | if (! $this->url) { 183 | throw WeatherException::noUrl(); 184 | } 185 | 186 | return $this->url; 187 | } 188 | 189 | public function getTimezone() 190 | { 191 | return $this->timezone; 192 | } 193 | 194 | public function setTimezone($timezone): Request 195 | { 196 | $this->timezone = $timezone; 197 | 198 | return $this; 199 | } 200 | } -------------------------------------------------------------------------------- /src/Response.php: -------------------------------------------------------------------------------- 1 | latitude)) { 27 | $this->latitude = $data->latitude; 28 | } 29 | if (isset($data->longitude)) { 30 | $this->longitude = $data->longitude; 31 | } 32 | if (isset($data->timezone)) { 33 | $this->timezone = new CarbonTimeZone($data->timezone); 34 | } 35 | if (isset($data->offset)) { 36 | $this->offset = $data->offset; 37 | } 38 | if (isset($data->currently)) { 39 | $this->currently = new DataPoint($data->currently); 40 | } 41 | if (isset($data->minutely)) { 42 | $this->minutely = new DataBlock($data->minutely); 43 | } 44 | if (isset($data->hourly)) { 45 | $this->hourly = new DataBlock($data->hourly); 46 | } 47 | if (isset($data->daily)) { 48 | $this->daily = new DataBlock($data->daily); 49 | } 50 | } 51 | 52 | public function getLatitude(): float 53 | { 54 | return $this->latitude; 55 | } 56 | 57 | public function getLongitude(): float 58 | { 59 | return $this->longitude; 60 | } 61 | 62 | public function getTimezone(): ?CarbonTimeZone 63 | { 64 | return $this->timezone; 65 | } 66 | 67 | public function getOffset(): int 68 | { 69 | return $this->offset; 70 | } 71 | 72 | public function getCurrently(): ?DataPoint 73 | { 74 | return $this->currently; 75 | } 76 | 77 | public function getMinutely(): ?DataBlock 78 | { 79 | return $this->minutely; 80 | } 81 | 82 | public function getHourly(): ?DataBlock 83 | { 84 | return $this->hourly; 85 | } 86 | 87 | public function getDaily(): ?DataBlock 88 | { 89 | return $this->daily; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/WeatherProvider.php: -------------------------------------------------------------------------------- 1 | Darksky::class, 22 | 'weatherstack' => Weatherstack::class, 23 | 'weatherkit' => WeatherKit::class, 24 | ]; 25 | 26 | public function __construct() 27 | { 28 | if (!$name = config('weather.provider')) { 29 | throw WeatherException::noProvider(); 30 | } 31 | 32 | if (!isset(self::$providers[$name])) { 33 | throw WeatherException::wrongProvider(); 34 | } 35 | 36 | $this->provider = new self::$providers[$name](); 37 | } 38 | 39 | public function getForecast($requests): Collection 40 | { 41 | return $this->provider->getForecast($requests); 42 | } 43 | 44 | public function getHistorical($requests): Collection 45 | { 46 | return $this->provider->getHistorical($requests); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/WeatherServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 12 | __DIR__ . '/../config/weather.php' => config_path('weather.php'), 13 | ], 'config'); 14 | } 15 | 16 | public function register(): void 17 | { 18 | $this->mergeConfigFrom( 19 | __DIR__ . '/../config/weather.php', 'weather' 20 | ); 21 | 22 | $this->app->bind(WeatherProvider::class); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/helpers.php: -------------------------------------------------------------------------------- 1 | addMockHandler(200, $this->getFile('geocoder.json')); 18 | $this->addMockHandler(200, $this->getFile('darksky/historical_1.json')); 19 | $this->addMockHandler(200, $this->getFile('darksky/historical_2.json')); 20 | 21 | $request = (new Request('1 Infinite Loop, Cupertino, CA 95014, USA')) 22 | ->atDates([Carbon::parse('2020-01-01 13:59'), Carbon::parse('2020-01-02 13:59')]) 23 | ->withUnits( 'm') 24 | ->withLocale('en'); 25 | 26 | $responses = (new Darksky)->getHistorical($request); 27 | $this->checkWeatherResponse($responses); 28 | } 29 | 30 | /** 31 | * @test 32 | */ 33 | public function itShouldReturnCorrectForecastData(): void 34 | { 35 | $this->addMockHandler(200, $this->getFile('geocoder.json')); 36 | $this->addMockHandler(200, $this->getFile('darksky/forecast.json')); 37 | 38 | $request = (new Request('1 Infinite Loop, Cupertino, CA 95014, USA')) 39 | ->withUnits( 'm') 40 | ->withLocale('en'); 41 | 42 | $responses = (new Darksky)->getForecast($request); 43 | $this->checkWeatherResponse($responses); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/Providers/ProviderTest.php: -------------------------------------------------------------------------------- 1 | assertArrayHasKey('2020-01-01 13:59', $responses); 23 | 24 | /** @var Response $response */ 25 | foreach ($responses as $response) { 26 | $this->assertEquals(55.5779099, $response->getLatitude()); 27 | $this->assertEquals(9.6559581, $response->getLongitude()); 28 | $this->assertEquals(18.83, $response->getCurrently()->getTemperature()->getCurrent()); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/Providers/WeatherKitTest.php: -------------------------------------------------------------------------------- 1 | set('weather.provider', 'weatherkit'); 18 | 19 | config()->set('weather.providers.weatherkit.private-key', '-----BEGIN EC PRIVATE KEY----- 20 | MHcCAQEEIMDqs1qze4ofGHHyujZ+uM0s2E+cr6koXxp47bxtl+OPoAoGCCqGSM49 21 | AwEHoUQDQgAE33lvC4vUBfP+7zbx5UAudqCXn9BhfWt2f/ahx5mEXo7VqioMDyrF 22 | kUZyuYq8fgO4yhgEEquJFyXPe5DYJ9hxiQ== 23 | -----END EC PRIVATE KEY-----' 24 | ); 25 | 26 | config()->set('weather.providers.weatherkit.alg', 'ES256'); 27 | config()->set('weather.providers.weatherkit.kid', 'kid'); 28 | config()->set('weather.providers.weatherkit.id', 'id'); 29 | 30 | config()->set('weather.providers.weatherkit.iss', 'iss'); 31 | config()->set('weather.providers.weatherkit.sub', 'sub'); 32 | } 33 | 34 | 35 | /** 36 | * @test 37 | */ 38 | public function itCheckIfConfigurationIsValid(): void 39 | { 40 | config()->set('weather.providers.weatherkit.private-key', 'not-a-private-key'); 41 | 42 | $this->expectExceptionMessage('Missing or misconfigured WeatherKit settings, please add/edit it in .env'); 43 | $this->expectExceptionCode(WeatherException::noApiKey()->getCode()); 44 | 45 | weather()->getForecast(new Request('test address')); 46 | } 47 | 48 | /** 49 | * @test 50 | */ 51 | public function itShouldReturnCorrectForecastData(): void 52 | { 53 | $this->addMockHandler(200, $this->getFile('geocoder.json')); 54 | $this->addMockHandler(200, $this->getFile('weatherkit/forecast.json')); 55 | 56 | $request = (new Request('1 Infinite Loop, Cupertino, CA 95014, USA')) 57 | ->atDates([Carbon::parse('2020-01-01 13:59'), Carbon::parse('2020-01-02 13:59')]) 58 | ->withUnits( 'm') 59 | ->withLocale('en'); 60 | 61 | $responses = (new WeatherKit)->getForecast($request); 62 | $this->checkWeatherResponse($responses); 63 | } 64 | 65 | /** 66 | * @test 67 | */ 68 | public function itShouldReturnCorrectHistoricalData(): void 69 | { 70 | $this->addMockHandler(200, $this->getFile('geocoder.json')); 71 | $this->addMockHandler(200, $this->getFile('weatherkit/historical-1.json')); 72 | $this->addMockHandler(200, $this->getFile('weatherkit/historical-2.json')); 73 | 74 | $requests = (new Request('1 Infinite Loop, Cupertino, CA 95014, USA')) 75 | ->withUnits( 'm') 76 | ->withLocale('en'); 77 | 78 | $responses = (new WeatherKit)->getForecast($requests); 79 | $this->checkWeatherResponse($responses); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tests/Providers/WeatherstackTest.php: -------------------------------------------------------------------------------- 1 | addMockHandler(200, $this->getFile('weatherstack/historical.json')); 18 | 19 | $request = (new Request('1 Infinite Loop, Cupertino, CA 95014, USA')) 20 | ->atDates([Carbon::parse('2020-01-01 13:59'), Carbon::parse('2020-01-02 13:59')]) 21 | ->withUnits( 'm') 22 | ->withLocale('en'); 23 | 24 | $responses = (new Weatherstack)->getHistorical($request); 25 | $this->checkWeatherResponse($responses); 26 | } 27 | 28 | /** 29 | * @test 30 | */ 31 | public function itShouldReturnCorrectForecastData(): void 32 | { 33 | $this->addMockHandler(200, $this->getFile('weatherstack/forecast.json')); 34 | 35 | $requests = (new Request('1 Infinite Loop, Cupertino, CA 95014, USA')) 36 | ->withUnits( 'm') 37 | ->withLocale('en'); 38 | 39 | $responses = (new Weatherstack)->getForecast($requests); 40 | $this->checkWeatherResponse($responses); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | baseStubPath = __DIR__ . '/stub/'; 29 | } 30 | 31 | /** 32 | * @param Application $app 33 | * 34 | * @return array 35 | */ 36 | protected function getPackageProviders($app): array 37 | { 38 | return [ 39 | WeatherServiceProvider::class, 40 | ]; 41 | } 42 | 43 | /** 44 | * @param Application $app 45 | */ 46 | protected function getEnvironmentSetUp($app): void 47 | { 48 | config()->set('weather.api_key', '1324'); 49 | config()->set('weather.provider', 'darksky'); 50 | } 51 | 52 | protected function addMockHandler($code, $body, $headers = []): void 53 | { 54 | if (!$this->mockHandler) { 55 | $this->createMockResponse(); 56 | } 57 | 58 | $this->mockHandler->append(new Response($code, $headers, $body)); 59 | } 60 | 61 | protected function createMockResponse(): void 62 | { 63 | $this->mockHandler = new MockHandler(); 64 | 65 | $handler = HandlerStack::create($this->mockHandler); 66 | $this->client = new Client(['handler' => $handler]); 67 | $this->app->instance(Client::class, $this->client); 68 | } 69 | 70 | protected function getFile($filename): string 71 | { 72 | return File::get(($this->baseStubPath . $filename)); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/WeatherTest.php: -------------------------------------------------------------------------------- 1 | set('weather.api_key', null); 17 | 18 | $this->expectExceptionMessage(WeatherException::noApiKey()->getMessage()); 19 | $this->expectExceptionCode(WeatherException::noApiKey()->getCode()); 20 | 21 | weather()->getData(new Request('test address')); 22 | } 23 | 24 | /** 25 | * @test 26 | */ 27 | public function it_tests_no_provider_exception(): void 28 | { 29 | config()->set('weather.provider', null); 30 | 31 | $this->expectExceptionMessage(WeatherException::noProvider()->getMessage()); 32 | $this->expectExceptionCode(WeatherException::noProvider()->getCode()); 33 | 34 | weather()->getData(new Request('test address')); 35 | } 36 | 37 | /** 38 | * @test 39 | */ 40 | public function it_tests_wrong_provider_exception(): void 41 | { 42 | config()->set('weather.provider', 'wrong_provider'); 43 | 44 | $this->expectExceptionMessage(WeatherException::wrongProvider()->getMessage()); 45 | $this->expectExceptionCode(WeatherException::wrongProvider()->getCode()); 46 | 47 | weather()->getData(new Request('test address')); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/stub/darksky/forecast.json: -------------------------------------------------------------------------------- 1 | { 2 | "latitude": 55.5779099, 3 | "longitude": 9.6559581, 4 | "timezone": "Europe/Copenhagen", 5 | "currently": { 6 | "time": 1593603391, 7 | "summary": "Mostly Cloudy", 8 | "icon": "partly-cloudy-day", 9 | "precipIntensity": 0.1297, 10 | "precipProbability": 0.09, 11 | "precipType": "rain", 12 | "temperature": 18.83, 13 | "apparentTemperature": 18.83, 14 | "dewPoint": 13.48, 15 | "humidity": 0.71, 16 | "pressure": 1002.4, 17 | "windSpeed": 5.72, 18 | "windGust": 7.41, 19 | "windBearing": 249, 20 | "cloudCover": 0.66, 21 | "uvIndex": 5, 22 | "visibility": 16.093, 23 | "ozone": 344.9 24 | }, 25 | "hourly": { 26 | "summary": "Possible light rain tonight.", 27 | "icon": "rain", 28 | "data": [ 29 | { 30 | "time": 1593601200, 31 | "summary": "Mostly Cloudy", 32 | "icon": "partly-cloudy-day", 33 | "precipIntensity": 0.0677, 34 | "precipProbability": 0.06, 35 | "precipType": "rain", 36 | "temperature": 18.44, 37 | "apparentTemperature": 18.44, 38 | "dewPoint": 13.51, 39 | "humidity": 0.73, 40 | "pressure": 1002.3, 41 | "windSpeed": 5.77, 42 | "windGust": 7.49, 43 | "windBearing": 249, 44 | "cloudCover": 0.63, 45 | "uvIndex": 5, 46 | "visibility": 16.093, 47 | "ozone": 344.4 48 | }, 49 | { 50 | "time": 1593604800, 51 | "summary": "Mostly Cloudy", 52 | "icon": "partly-cloudy-day", 53 | "precipIntensity": 0.1539, 54 | "precipProbability": 0.1, 55 | "precipType": "rain", 56 | "temperature": 19.08, 57 | "apparentTemperature": 19.08, 58 | "dewPoint": 13.44, 59 | "humidity": 0.7, 60 | "pressure": 1002.4, 61 | "windSpeed": 5.68, 62 | "windGust": 7.32, 63 | "windBearing": 249, 64 | "cloudCover": 0.66, 65 | "uvIndex": 4, 66 | "visibility": 16.093, 67 | "ozone": 345.2 68 | }, 69 | { 70 | "time": 1593608400, 71 | "summary": "Mostly Cloudy", 72 | "icon": "partly-cloudy-day", 73 | "precipIntensity": 0.086, 74 | "precipProbability": 0.11, 75 | "precipType": "rain", 76 | "temperature": 19.64, 77 | "apparentTemperature": 19.64, 78 | "dewPoint": 13.27, 79 | "humidity": 0.67, 80 | "pressure": 1002.3, 81 | "windSpeed": 5.47, 82 | "windGust": 6.93, 83 | "windBearing": 252, 84 | "cloudCover": 0.62, 85 | "uvIndex": 4, 86 | "visibility": 16.093, 87 | "ozone": 346.1 88 | }, 89 | { 90 | "time": 1593612000, 91 | "summary": "Mostly Cloudy", 92 | "icon": "partly-cloudy-day", 93 | "precipIntensity": 0.0506, 94 | "precipProbability": 0.1, 95 | "precipType": "rain", 96 | "temperature": 19.97, 97 | "apparentTemperature": 19.97, 98 | "dewPoint": 13.01, 99 | "humidity": 0.64, 100 | "pressure": 1002.1, 101 | "windSpeed": 5.38, 102 | "windGust": 6.41, 103 | "windBearing": 255, 104 | "cloudCover": 0.63, 105 | "uvIndex": 4, 106 | "visibility": 16.093, 107 | "ozone": 346.9 108 | }, 109 | { 110 | "time": 1593615600, 111 | "summary": "Mostly Cloudy", 112 | "icon": "partly-cloudy-day", 113 | "precipIntensity": 0.0563, 114 | "precipProbability": 0.11, 115 | "precipType": "rain", 116 | "temperature": 19.72, 117 | "apparentTemperature": 19.72, 118 | "dewPoint": 13, 119 | "humidity": 0.65, 120 | "pressure": 1002, 121 | "windSpeed": 5.08, 122 | "windGust": 5.93, 123 | "windBearing": 250, 124 | "cloudCover": 0.65, 125 | "uvIndex": 3, 126 | "visibility": 16.093, 127 | "ozone": 347.3 128 | }, 129 | { 130 | "time": 1593619200, 131 | "summary": "Mostly Cloudy", 132 | "icon": "partly-cloudy-day", 133 | "precipIntensity": 0.0506, 134 | "precipProbability": 0.11, 135 | "precipType": "rain", 136 | "temperature": 18.93, 137 | "apparentTemperature": 18.93, 138 | "dewPoint": 12.82, 139 | "humidity": 0.68, 140 | "pressure": 1001.9, 141 | "windSpeed": 4.45, 142 | "windGust": 5.43, 143 | "windBearing": 234, 144 | "cloudCover": 0.71, 145 | "uvIndex": 2, 146 | "visibility": 16.093, 147 | "ozone": 347 148 | }, 149 | { 150 | "time": 1593622800, 151 | "summary": "Mostly Cloudy", 152 | "icon": "partly-cloudy-day", 153 | "precipIntensity": 0.0532, 154 | "precipProbability": 0.1, 155 | "precipType": "rain", 156 | "temperature": 17.88, 157 | "apparentTemperature": 17.88, 158 | "dewPoint": 12.63, 159 | "humidity": 0.71, 160 | "pressure": 1001.7, 161 | "windSpeed": 3.76, 162 | "windGust": 4.95, 163 | "windBearing": 284, 164 | "cloudCover": 0.71, 165 | "uvIndex": 1, 166 | "visibility": 16.093, 167 | "ozone": 346.4 168 | }, 169 | { 170 | "time": 1593626400, 171 | "summary": "Mostly Cloudy", 172 | "icon": "partly-cloudy-day", 173 | "precipIntensity": 0.0666, 174 | "precipProbability": 0.1, 175 | "precipType": "rain", 176 | "temperature": 17.17, 177 | "apparentTemperature": 17.17, 178 | "dewPoint": 12.82, 179 | "humidity": 0.76, 180 | "pressure": 1001.7, 181 | "windSpeed": 3.07, 182 | "windGust": 4.72, 183 | "windBearing": 274, 184 | "cloudCover": 0.71, 185 | "uvIndex": 0, 186 | "visibility": 16.093, 187 | "ozone": 346.4 188 | }, 189 | { 190 | "time": 1593630000, 191 | "summary": "Mostly Cloudy", 192 | "icon": "partly-cloudy-day", 193 | "precipIntensity": 0.1014, 194 | "precipProbability": 0.11, 195 | "precipType": "rain", 196 | "temperature": 16.39, 197 | "apparentTemperature": 16.39, 198 | "dewPoint": 13.13, 199 | "humidity": 0.81, 200 | "pressure": 1001.7, 201 | "windSpeed": 2.7, 202 | "windGust": 4.98, 203 | "windBearing": 261, 204 | "cloudCover": 0.76, 205 | "uvIndex": 0, 206 | "visibility": 16.093, 207 | "ozone": 347.7 208 | }, 209 | { 210 | "time": 1593633600, 211 | "summary": "Mostly Cloudy", 212 | "icon": "partly-cloudy-night", 213 | "precipIntensity": 0.1878, 214 | "precipProbability": 0.14, 215 | "precipType": "rain", 216 | "temperature": 15.63, 217 | "apparentTemperature": 15.63, 218 | "dewPoint": 13.16, 219 | "humidity": 0.85, 220 | "pressure": 1002, 221 | "windSpeed": 2.51, 222 | "windGust": 5.47, 223 | "windBearing": 280, 224 | "cloudCover": 0.79, 225 | "uvIndex": 0, 226 | "visibility": 16.093, 227 | "ozone": 349.6 228 | }, 229 | { 230 | "time": 1593637200, 231 | "summary": "Mostly Cloudy", 232 | "icon": "partly-cloudy-night", 233 | "precipIntensity": 0.258, 234 | "precipProbability": 0.14, 235 | "precipType": "rain", 236 | "temperature": 15.01, 237 | "apparentTemperature": 15.01, 238 | "dewPoint": 13.06, 239 | "humidity": 0.88, 240 | "pressure": 1002.2, 241 | "windSpeed": 2.57, 242 | "windGust": 5.74, 243 | "windBearing": 271, 244 | "cloudCover": 0.8, 245 | "uvIndex": 0, 246 | "visibility": 16.093, 247 | "ozone": 350.7 248 | }, 249 | { 250 | "time": 1593640800, 251 | "summary": "Mostly Cloudy", 252 | "icon": "partly-cloudy-night", 253 | "precipIntensity": 0.233, 254 | "precipProbability": 0.14, 255 | "precipType": "rain", 256 | "temperature": 14.38, 257 | "apparentTemperature": 14.38, 258 | "dewPoint": 13.22, 259 | "humidity": 0.93, 260 | "pressure": 1002.4, 261 | "windSpeed": 2.69, 262 | "windGust": 5.38, 263 | "windBearing": 255, 264 | "cloudCover": 0.78, 265 | "uvIndex": 0, 266 | "visibility": 16.093, 267 | "ozone": 350.3 268 | }, 269 | { 270 | "time": 1593644400, 271 | "summary": "Mostly Cloudy", 272 | "icon": "partly-cloudy-night", 273 | "precipIntensity": 0.1394, 274 | "precipProbability": 0.12, 275 | "precipType": "rain", 276 | "temperature": 13.83, 277 | "apparentTemperature": 13.83, 278 | "dewPoint": 13.05, 279 | "humidity": 0.95, 280 | "pressure": 1002.7, 281 | "windSpeed": 2.85, 282 | "windGust": 4.78, 283 | "windBearing": 286, 284 | "cloudCover": 0.76, 285 | "uvIndex": 0, 286 | "visibility": 16.093, 287 | "ozone": 349.3 288 | }, 289 | { 290 | "time": 1593648000, 291 | "summary": "Mostly Cloudy", 292 | "icon": "partly-cloudy-night", 293 | "precipIntensity": 0.0793, 294 | "precipProbability": 0.09, 295 | "precipType": "rain", 296 | "temperature": 13.47, 297 | "apparentTemperature": 13.47, 298 | "dewPoint": 12.94, 299 | "humidity": 0.97, 300 | "pressure": 1003, 301 | "windSpeed": 2.98, 302 | "windGust": 4.65, 303 | "windBearing": 279, 304 | "cloudCover": 0.69, 305 | "uvIndex": 0, 306 | "visibility": 16.093, 307 | "ozone": 348.1 308 | }, 309 | { 310 | "time": 1593651600, 311 | "summary": "Mostly Cloudy", 312 | "icon": "partly-cloudy-night", 313 | "precipIntensity": 0.0374, 314 | "precipProbability": 0.07, 315 | "precipType": "rain", 316 | "temperature": 13.12, 317 | "apparentTemperature": 13.12, 318 | "dewPoint": 12.86, 319 | "humidity": 0.98, 320 | "pressure": 1003.2, 321 | "windSpeed": 3.1, 322 | "windGust": 5.54, 323 | "windBearing": 324, 324 | "cloudCover": 0.65, 325 | "uvIndex": 0, 326 | "visibility": 16.093, 327 | "ozone": 346.7 328 | }, 329 | { 330 | "time": 1593655200, 331 | "summary": "Mostly Cloudy", 332 | "icon": "partly-cloudy-night", 333 | "precipIntensity": 0.013, 334 | "precipProbability": 0.06, 335 | "precipType": "rain", 336 | "temperature": 12.88, 337 | "apparentTemperature": 12.88, 338 | "dewPoint": 12.82, 339 | "humidity": 1, 340 | "pressure": 1003.8, 341 | "windSpeed": 3.21, 342 | "windGust": 6.9, 343 | "windBearing": 226, 344 | "cloudCover": 0.65, 345 | "uvIndex": 0, 346 | "visibility": 16.093, 347 | "ozone": 345.2 348 | }, 349 | { 350 | "time": 1593658800, 351 | "summary": "Mostly Cloudy", 352 | "icon": "partly-cloudy-day", 353 | "precipIntensity": 0.0059, 354 | "precipProbability": 0.03, 355 | "precipType": "rain", 356 | "temperature": 12.64, 357 | "apparentTemperature": 12.64, 358 | "dewPoint": 12.64, 359 | "humidity": 1, 360 | "pressure": 1004.5, 361 | "windSpeed": 3.33, 362 | "windGust": 7.7, 363 | "windBearing": 265, 364 | "cloudCover": 0.61, 365 | "uvIndex": 0, 366 | "visibility": 16.093, 367 | "ozone": 344.6 368 | }, 369 | { 370 | "time": 1593662400, 371 | "summary": "Partly Cloudy", 372 | "icon": "partly-cloudy-day", 373 | "precipIntensity": 0.0077, 374 | "precipProbability": 0.04, 375 | "precipType": "rain", 376 | "temperature": 12.97, 377 | "apparentTemperature": 12.97, 378 | "dewPoint": 12.97, 379 | "humidity": 1, 380 | "pressure": 1004.7, 381 | "windSpeed": 3.3, 382 | "windGust": 7.39, 383 | "windBearing": 217, 384 | "cloudCover": 0.58, 385 | "uvIndex": 0, 386 | "visibility": 16.093, 387 | "ozone": 345.6 388 | }, 389 | { 390 | "time": 1593666000, 391 | "summary": "Partly Cloudy", 392 | "icon": "partly-cloudy-day", 393 | "precipIntensity": 0.0196, 394 | "precipProbability": 0.07, 395 | "precipType": "rain", 396 | "temperature": 13.64, 397 | "apparentTemperature": 13.67, 398 | "dewPoint": 13.63, 399 | "humidity": 1, 400 | "pressure": 1004.9, 401 | "windSpeed": 3.42, 402 | "windGust": 6.52, 403 | "windBearing": 357, 404 | "cloudCover": 0.48, 405 | "uvIndex": 0, 406 | "visibility": 16.093, 407 | "ozone": 347.5 408 | }, 409 | { 410 | "time": 1593669600, 411 | "summary": "Partly Cloudy", 412 | "icon": "partly-cloudy-day", 413 | "precipIntensity": 0.0335, 414 | "precipProbability": 0.08, 415 | "precipType": "rain", 416 | "temperature": 14.45, 417 | "apparentTemperature": 14.5, 418 | "dewPoint": 14.05, 419 | "humidity": 0.97, 420 | "pressure": 1005.2, 421 | "windSpeed": 3.65, 422 | "windGust": 5.81, 423 | "windBearing": 292, 424 | "cloudCover": 0.39, 425 | "uvIndex": 1, 426 | "visibility": 16.093, 427 | "ozone": 348.9 428 | }, 429 | { 430 | "time": 1593673200, 431 | "summary": "Mostly Cloudy", 432 | "icon": "partly-cloudy-day", 433 | "precipIntensity": 0.042, 434 | "precipProbability": 0.07, 435 | "precipType": "rain", 436 | "temperature": 15.12, 437 | "apparentTemperature": 15.12, 438 | "dewPoint": 13.97, 439 | "humidity": 0.93, 440 | "pressure": 1005.8, 441 | "windSpeed": 3.74, 442 | "windGust": 5.48, 443 | "windBearing": 282, 444 | "cloudCover": 0.6, 445 | "uvIndex": 2, 446 | "visibility": 16.093, 447 | "ozone": 348.8 448 | }, 449 | { 450 | "time": 1593676800, 451 | "summary": "Mostly Cloudy", 452 | "icon": "partly-cloudy-day", 453 | "precipIntensity": 0.0535, 454 | "precipProbability": 0.06, 455 | "precipType": "rain", 456 | "temperature": 16.01, 457 | "apparentTemperature": 16.01, 458 | "dewPoint": 13.87, 459 | "humidity": 0.87, 460 | "pressure": 1006.3, 461 | "windSpeed": 3.88, 462 | "windGust": 5.3, 463 | "windBearing": 283, 464 | "cloudCover": 0.68, 465 | "uvIndex": 3, 466 | "visibility": 16.093, 467 | "ozone": 348.3 468 | }, 469 | { 470 | "time": 1593680400, 471 | "summary": "Partly Cloudy", 472 | "icon": "partly-cloudy-day", 473 | "precipIntensity": 0.0619, 474 | "precipProbability": 0.05, 475 | "precipType": "rain", 476 | "temperature": 16.92, 477 | "apparentTemperature": 16.92, 478 | "dewPoint": 13.87, 479 | "humidity": 0.82, 480 | "pressure": 1006.7, 481 | "windSpeed": 4.08, 482 | "windGust": 5.24, 483 | "windBearing": 289, 484 | "cloudCover": 0.58, 485 | "uvIndex": 4, 486 | "visibility": 16.093, 487 | "ozone": 347.7 488 | }, 489 | { 490 | "time": 1593684000, 491 | "summary": "Mostly Cloudy", 492 | "icon": "partly-cloudy-day", 493 | "precipIntensity": 0.0484, 494 | "precipProbability": 0.04, 495 | "precipType": "rain", 496 | "temperature": 17.65, 497 | "apparentTemperature": 17.65, 498 | "dewPoint": 13.6, 499 | "humidity": 0.77, 500 | "pressure": 1007.2, 501 | "windSpeed": 4.33, 502 | "windGust": 5.34, 503 | "windBearing": 288, 504 | "cloudCover": 0.66, 505 | "uvIndex": 4, 506 | "visibility": 16.093, 507 | "ozone": 347.1 508 | }, 509 | { 510 | "time": 1593687600, 511 | "summary": "Mostly Cloudy", 512 | "icon": "partly-cloudy-day", 513 | "precipIntensity": 0.0295, 514 | "precipProbability": 0.04, 515 | "precipType": "rain", 516 | "temperature": 18.12, 517 | "apparentTemperature": 18.12, 518 | "dewPoint": 13.1, 519 | "humidity": 0.73, 520 | "pressure": 1007.6, 521 | "windSpeed": 4.61, 522 | "windGust": 5.57, 523 | "windBearing": 285, 524 | "cloudCover": 0.77, 525 | "uvIndex": 4, 526 | "visibility": 16.093, 527 | "ozone": 346.6 528 | }, 529 | { 530 | "time": 1593691200, 531 | "summary": "Mostly Cloudy", 532 | "icon": "partly-cloudy-day", 533 | "precipIntensity": 0.0168, 534 | "precipProbability": 0.03, 535 | "precipType": "rain", 536 | "temperature": 18.73, 537 | "apparentTemperature": 18.73, 538 | "dewPoint": 12.44, 539 | "humidity": 0.67, 540 | "pressure": 1008.2, 541 | "windSpeed": 4.9, 542 | "windGust": 5.79, 543 | "windBearing": 282, 544 | "cloudCover": 0.76, 545 | "uvIndex": 4, 546 | "visibility": 16.093, 547 | "ozone": 346.7 548 | }, 549 | { 550 | "time": 1593694800, 551 | "summary": "Mostly Cloudy", 552 | "icon": "partly-cloudy-day", 553 | "precipIntensity": 0.0078, 554 | "precipProbability": 0.02, 555 | "precipType": "rain", 556 | "temperature": 19.25, 557 | "apparentTemperature": 19.25, 558 | "dewPoint": 11.69, 559 | "humidity": 0.62, 560 | "pressure": 1008.4, 561 | "windSpeed": 5.2, 562 | "windGust": 5.96, 563 | "windBearing": 285, 564 | "cloudCover": 0.69, 565 | "uvIndex": 4, 566 | "visibility": 16.093, 567 | "ozone": 348 568 | }, 569 | { 570 | "time": 1593698400, 571 | "summary": "Mostly Cloudy", 572 | "icon": "partly-cloudy-day", 573 | "precipIntensity": 0, 574 | "precipProbability": 0, 575 | "temperature": 19.42, 576 | "apparentTemperature": 19.42, 577 | "dewPoint": 10.79, 578 | "humidity": 0.57, 579 | "pressure": 1008.4, 580 | "windSpeed": 5.37, 581 | "windGust": 6.11, 582 | "windBearing": 291, 583 | "cloudCover": 0.72, 584 | "uvIndex": 3, 585 | "visibility": 16.093, 586 | "ozone": 350 587 | }, 588 | { 589 | "time": 1593702000, 590 | "summary": "Partly Cloudy", 591 | "icon": "partly-cloudy-day", 592 | "precipIntensity": 0, 593 | "precipProbability": 0, 594 | "temperature": 19.16, 595 | "apparentTemperature": 19.16, 596 | "dewPoint": 10.08, 597 | "humidity": 0.56, 598 | "pressure": 1008.6, 599 | "windSpeed": 5.46, 600 | "windGust": 6.26, 601 | "windBearing": 295, 602 | "cloudCover": 0.58, 603 | "uvIndex": 3, 604 | "visibility": 16.093, 605 | "ozone": 351.7 606 | }, 607 | { 608 | "time": 1593705600, 609 | "summary": "Partly Cloudy", 610 | "icon": "partly-cloudy-day", 611 | "precipIntensity": 0, 612 | "precipProbability": 0, 613 | "temperature": 18.51, 614 | "apparentTemperature": 18.51, 615 | "dewPoint": 9.1, 616 | "humidity": 0.54, 617 | "pressure": 1008.9, 618 | "windSpeed": 5.49, 619 | "windGust": 6.4, 620 | "windBearing": 297, 621 | "cloudCover": 0.42, 622 | "uvIndex": 2, 623 | "visibility": 16.093, 624 | "ozone": 352.6 625 | }, 626 | { 627 | "time": 1593709200, 628 | "summary": "Clear", 629 | "icon": "clear-day", 630 | "precipIntensity": 0, 631 | "precipProbability": 0, 632 | "temperature": 17.67, 633 | "apparentTemperature": 17.67, 634 | "dewPoint": 8.45, 635 | "humidity": 0.55, 636 | "pressure": 1009.1, 637 | "windSpeed": 5.35, 638 | "windGust": 6.51, 639 | "windBearing": 301, 640 | "cloudCover": 0.31, 641 | "uvIndex": 1, 642 | "visibility": 16.093, 643 | "ozone": 353.4 644 | }, 645 | { 646 | "time": 1593712800, 647 | "summary": "Clear", 648 | "icon": "clear-day", 649 | "precipIntensity": 0, 650 | "precipProbability": 0, 651 | "temperature": 16.93, 652 | "apparentTemperature": 16.93, 653 | "dewPoint": 8.08, 654 | "humidity": 0.56, 655 | "pressure": 1009.5, 656 | "windSpeed": 5.05, 657 | "windGust": 6.58, 658 | "windBearing": 303, 659 | "cloudCover": 0.21, 660 | "uvIndex": 0, 661 | "visibility": 16.093, 662 | "ozone": 354.3 663 | }, 664 | { 665 | "time": 1593716400, 666 | "summary": "Clear", 667 | "icon": "clear-day", 668 | "precipIntensity": 0, 669 | "precipProbability": 0, 670 | "temperature": 15.88, 671 | "apparentTemperature": 15.88, 672 | "dewPoint": 7.95, 673 | "humidity": 0.59, 674 | "pressure": 1010, 675 | "windSpeed": 4.65, 676 | "windGust": 6.63, 677 | "windBearing": 303, 678 | "cloudCover": 0.12, 679 | "uvIndex": 0, 680 | "visibility": 16.093, 681 | "ozone": 356 682 | }, 683 | { 684 | "time": 1593720000, 685 | "summary": "Clear", 686 | "icon": "clear-night", 687 | "precipIntensity": 0, 688 | "precipProbability": 0, 689 | "temperature": 14.35, 690 | "apparentTemperature": 14.35, 691 | "dewPoint": 7.82, 692 | "humidity": 0.65, 693 | "pressure": 1011, 694 | "windSpeed": 4.13, 695 | "windGust": 6.62, 696 | "windBearing": 301, 697 | "cloudCover": 0.05, 698 | "uvIndex": 0, 699 | "visibility": 16.093, 700 | "ozone": 357.9 701 | }, 702 | { 703 | "time": 1593723600, 704 | "summary": "Clear", 705 | "icon": "clear-night", 706 | "precipIntensity": 0.0051, 707 | "precipProbability": 0.01, 708 | "precipType": "rain", 709 | "temperature": 13.07, 710 | "apparentTemperature": 13.07, 711 | "dewPoint": 7.67, 712 | "humidity": 0.7, 713 | "pressure": 1011.4, 714 | "windSpeed": 3.84, 715 | "windGust": 6.49, 716 | "windBearing": 297, 717 | "cloudCover": 0.03, 718 | "uvIndex": 0, 719 | "visibility": 16.093, 720 | "ozone": 358.6 721 | }, 722 | { 723 | "time": 1593727200, 724 | "summary": "Clear", 725 | "icon": "clear-night", 726 | "precipIntensity": 0.0062, 727 | "precipProbability": 0.01, 728 | "precipType": "rain", 729 | "temperature": 12.15, 730 | "apparentTemperature": 12.15, 731 | "dewPoint": 8, 732 | "humidity": 0.76, 733 | "pressure": 1011.7, 734 | "windSpeed": 3.67, 735 | "windGust": 6.15, 736 | "windBearing": 291, 737 | "cloudCover": 0.05, 738 | "uvIndex": 0, 739 | "visibility": 16.093, 740 | "ozone": 357.1 741 | }, 742 | { 743 | "time": 1593730800, 744 | "summary": "Clear", 745 | "icon": "clear-night", 746 | "precipIntensity": 0.0051, 747 | "precipProbability": 0.01, 748 | "precipType": "rain", 749 | "temperature": 11.49, 750 | "apparentTemperature": 11.49, 751 | "dewPoint": 8.15, 752 | "humidity": 0.8, 753 | "pressure": 1012, 754 | "windSpeed": 3.54, 755 | "windGust": 5.69, 756 | "windBearing": 291, 757 | "cloudCover": 0.02, 758 | "uvIndex": 0, 759 | "visibility": 16.093, 760 | "ozone": 354.5 761 | }, 762 | { 763 | "time": 1593734400, 764 | "summary": "Clear", 765 | "icon": "clear-night", 766 | "precipIntensity": 0, 767 | "precipProbability": 0, 768 | "temperature": 10.98, 769 | "apparentTemperature": 10.98, 770 | "dewPoint": 8.33, 771 | "humidity": 0.84, 772 | "pressure": 1012.3, 773 | "windSpeed": 3.36, 774 | "windGust": 5.32, 775 | "windBearing": 288, 776 | "cloudCover": 0.02, 777 | "uvIndex": 0, 778 | "visibility": 16.093, 779 | "ozone": 352 780 | }, 781 | { 782 | "time": 1593738000, 783 | "summary": "Clear", 784 | "icon": "clear-night", 785 | "precipIntensity": 0, 786 | "precipProbability": 0, 787 | "temperature": 10.38, 788 | "apparentTemperature": 10.38, 789 | "dewPoint": 8.5, 790 | "humidity": 0.88, 791 | "pressure": 1012.5, 792 | "windSpeed": 3.17, 793 | "windGust": 5.03, 794 | "windBearing": 289, 795 | "cloudCover": 0.04, 796 | "uvIndex": 0, 797 | "visibility": 16.093, 798 | "ozone": 350.2 799 | }, 800 | { 801 | "time": 1593741600, 802 | "summary": "Clear", 803 | "icon": "clear-night", 804 | "precipIntensity": 0, 805 | "precipProbability": 0, 806 | "temperature": 9.86, 807 | "apparentTemperature": 8.43, 808 | "dewPoint": 8.58, 809 | "humidity": 0.92, 810 | "pressure": 1012.6, 811 | "windSpeed": 2.86, 812 | "windGust": 4.82, 813 | "windBearing": 267, 814 | "cloudCover": 0.12, 815 | "uvIndex": 0, 816 | "visibility": 16.093, 817 | "ozone": 348.5 818 | }, 819 | { 820 | "time": 1593745200, 821 | "summary": "Clear", 822 | "icon": "clear-day", 823 | "precipIntensity": 0, 824 | "precipProbability": 0, 825 | "temperature": 9.65, 826 | "apparentTemperature": 8.27, 827 | "dewPoint": 8.65, 828 | "humidity": 0.93, 829 | "pressure": 1012.8, 830 | "windSpeed": 2.71, 831 | "windGust": 4.87, 832 | "windBearing": 271, 833 | "cloudCover": 0.13, 834 | "uvIndex": 0, 835 | "visibility": 16.093, 836 | "ozone": 346.8 837 | }, 838 | { 839 | "time": 1593748800, 840 | "summary": "Mostly Cloudy", 841 | "icon": "partly-cloudy-day", 842 | "precipIntensity": 0, 843 | "precipProbability": 0, 844 | "temperature": 10.36, 845 | "apparentTemperature": 10.36, 846 | "dewPoint": 8.92, 847 | "humidity": 0.91, 848 | "pressure": 1013.2, 849 | "windSpeed": 2.72, 850 | "windGust": 5.42, 851 | "windBearing": 273, 852 | "cloudCover": 0.65, 853 | "uvIndex": 0, 854 | "visibility": 16.093, 855 | "ozone": 345 856 | }, 857 | { 858 | "time": 1593752400, 859 | "summary": "Mostly Cloudy", 860 | "icon": "partly-cloudy-day", 861 | "precipIntensity": 0, 862 | "precipProbability": 0, 863 | "temperature": 11.67, 864 | "apparentTemperature": 11.67, 865 | "dewPoint": 9.22, 866 | "humidity": 0.85, 867 | "pressure": 1013.3, 868 | "windSpeed": 2.95, 869 | "windGust": 6.23, 870 | "windBearing": 257, 871 | "cloudCover": 0.62, 872 | "uvIndex": 0, 873 | "visibility": 16.093, 874 | "ozone": 343.3 875 | }, 876 | { 877 | "time": 1593756000, 878 | "summary": "Mostly Cloudy", 879 | "icon": "partly-cloudy-day", 880 | "precipIntensity": 0, 881 | "precipProbability": 0, 882 | "temperature": 13.13, 883 | "apparentTemperature": 13.13, 884 | "dewPoint": 9.37, 885 | "humidity": 0.78, 886 | "pressure": 1013.4, 887 | "windSpeed": 3.27, 888 | "windGust": 6.75, 889 | "windBearing": 258, 890 | "cloudCover": 0.76, 891 | "uvIndex": 1, 892 | "visibility": 16.093, 893 | "ozone": 341.8 894 | }, 895 | { 896 | "time": 1593759600, 897 | "summary": "Partly Cloudy", 898 | "icon": "partly-cloudy-day", 899 | "precipIntensity": 0, 900 | "precipProbability": 0, 901 | "temperature": 14.44, 902 | "apparentTemperature": 14.44, 903 | "dewPoint": 9.5, 904 | "humidity": 0.72, 905 | "pressure": 1013.9, 906 | "windSpeed": 3.79, 907 | "windGust": 6.65, 908 | "windBearing": 271, 909 | "cloudCover": 0.55, 910 | "uvIndex": 2, 911 | "visibility": 16.093, 912 | "ozone": 341 913 | }, 914 | { 915 | "time": 1593763200, 916 | "summary": "Mostly Cloudy", 917 | "icon": "partly-cloudy-day", 918 | "precipIntensity": 0, 919 | "precipProbability": 0, 920 | "temperature": 15.71, 921 | "apparentTemperature": 15.71, 922 | "dewPoint": 9.5, 923 | "humidity": 0.67, 924 | "pressure": 1014.3, 925 | "windSpeed": 4.58, 926 | "windGust": 6.25, 927 | "windBearing": 248, 928 | "cloudCover": 0.74, 929 | "uvIndex": 3, 930 | "visibility": 16.093, 931 | "ozone": 340.6 932 | }, 933 | { 934 | "time": 1593766800, 935 | "summary": "Mostly Cloudy", 936 | "icon": "partly-cloudy-day", 937 | "precipIntensity": 0, 938 | "precipProbability": 0, 939 | "temperature": 16.68, 940 | "apparentTemperature": 16.68, 941 | "dewPoint": 9.37, 942 | "humidity": 0.62, 943 | "pressure": 1014.4, 944 | "windSpeed": 5.09, 945 | "windGust": 6, 946 | "windBearing": 263, 947 | "cloudCover": 0.8, 948 | "uvIndex": 3, 949 | "visibility": 16.093, 950 | "ozone": 339.8 951 | }, 952 | { 953 | "time": 1593770400, 954 | "summary": "Mostly Cloudy", 955 | "icon": "partly-cloudy-day", 956 | "precipIntensity": 0.0062, 957 | "precipProbability": 0.02, 958 | "precipType": "rain", 959 | "temperature": 17.44, 960 | "apparentTemperature": 17.44, 961 | "dewPoint": 9.33, 962 | "humidity": 0.59, 963 | "pressure": 1014.3, 964 | "windSpeed": 5.34, 965 | "windGust": 6.03, 966 | "windBearing": 280, 967 | "cloudCover": 0.77, 968 | "uvIndex": 4, 969 | "visibility": 16.093, 970 | "ozone": 337.9 971 | }, 972 | { 973 | "time": 1593774000, 974 | "summary": "Overcast", 975 | "icon": "cloudy", 976 | "precipIntensity": 0.0062, 977 | "precipProbability": 0.02, 978 | "precipType": "rain", 979 | "temperature": 18.07, 980 | "apparentTemperature": 18.07, 981 | "dewPoint": 9.27, 982 | "humidity": 0.56, 983 | "pressure": 1014.1, 984 | "windSpeed": 5.71, 985 | "windGust": 6.2, 986 | "windBearing": 231, 987 | "cloudCover": 0.93, 988 | "uvIndex": 4, 989 | "visibility": 16.093, 990 | "ozone": 335.8 991 | } 992 | ] 993 | }, 994 | "daily": { 995 | "summary": "Rain today through Monday.", 996 | "icon": "rain", 997 | "data": [ 998 | { 999 | "time": 1593554400, 1000 | "summary": "Possible light rain overnight.", 1001 | "icon": "rain", 1002 | "sunriseTime": 1593571500, 1003 | "sunsetTime": 1593634080, 1004 | "moonPhase": 0.38, 1005 | "precipIntensity": 0.0551, 1006 | "precipIntensityMax": 0.2602, 1007 | "precipIntensityMaxTime": 1593637920, 1008 | "precipProbability": 0.6, 1009 | "precipType": "rain", 1010 | "temperatureHigh": 20.25, 1011 | "temperatureHighTime": 1593612240, 1012 | "temperatureLow": 12.36, 1013 | "temperatureLowTime": 1593658680, 1014 | "apparentTemperatureHigh": 19.97, 1015 | "apparentTemperatureHighTime": 1593612240, 1016 | "apparentTemperatureLow": 12.63, 1017 | "apparentTemperatureLowTime": 1593658680, 1018 | "dewPoint": 12.71, 1019 | "humidity": 0.81, 1020 | "pressure": 1002, 1021 | "windSpeed": 5.16, 1022 | "windGust": 14.13, 1023 | "windGustTime": 1593554400, 1024 | "windBearing": 255, 1025 | "cloudCover": 0.68, 1026 | "uvIndex": 5, 1027 | "uvIndexTime": 1593601560, 1028 | "visibility": 16.093, 1029 | "ozone": 343.1, 1030 | "temperatureMin": 12.5, 1031 | "temperatureMinTime": 1593571980, 1032 | "temperatureMax": 20.25, 1033 | "temperatureMaxTime": 1593612240, 1034 | "apparentTemperatureMin": 12.77, 1035 | "apparentTemperatureMinTime": 1593571980, 1036 | "apparentTemperatureMax": 19.97, 1037 | "apparentTemperatureMaxTime": 1593612240 1038 | }, 1039 | { 1040 | "time": 1593640800, 1041 | "summary": "Partly cloudy throughout the day.", 1042 | "icon": "rain", 1043 | "sunriseTime": 1593657960, 1044 | "sunsetTime": 1593720480, 1045 | "moonPhase": 0.41, 1046 | "precipIntensity": 0.03, 1047 | "precipIntensityMax": 0.233, 1048 | "precipIntensityMaxTime": 1593640800, 1049 | "precipProbability": 0.48, 1050 | "precipType": "rain", 1051 | "temperatureHigh": 19.7, 1052 | "temperatureHighTime": 1593698100, 1053 | "temperatureLow": 9.36, 1054 | "temperatureLowTime": 1593744540, 1055 | "apparentTemperatureHigh": 19.42, 1056 | "apparentTemperatureHighTime": 1593698100, 1057 | "apparentTemperatureLow": 8.09, 1058 | "apparentTemperatureLowTime": 1593743760, 1059 | "dewPoint": 11.58, 1060 | "humidity": 0.78, 1061 | "pressure": 1006.9, 1062 | "windSpeed": 4.13, 1063 | "windGust": 7.72, 1064 | "windGustTime": 1593659340, 1065 | "windBearing": 290, 1066 | "cloudCover": 0.52, 1067 | "uvIndex": 4, 1068 | "uvIndexTime": 1593685080, 1069 | "visibility": 16.093, 1070 | "ozone": 349.9, 1071 | "temperatureMin": 11.88, 1072 | "temperatureMinTime": 1593727200, 1073 | "temperatureMax": 19.7, 1074 | "temperatureMaxTime": 1593698100, 1075 | "apparentTemperatureMin": 12.15, 1076 | "apparentTemperatureMinTime": 1593727200, 1077 | "apparentTemperatureMax": 19.42, 1078 | "apparentTemperatureMaxTime": 1593698100 1079 | }, 1080 | { 1081 | "time": 1593727200, 1082 | "summary": "Light rain overnight.", 1083 | "icon": "rain", 1084 | "sunriseTime": 1593744360, 1085 | "sunsetTime": 1593806820, 1086 | "moonPhase": 0.45, 1087 | "precipIntensity": 0.013, 1088 | "precipIntensityMax": 0.0934, 1089 | "precipIntensityMaxTime": 1593813600, 1090 | "precipProbability": 0.47, 1091 | "precipType": "rain", 1092 | "temperatureHigh": 18.97, 1093 | "temperatureHighTime": 1593782160, 1094 | "temperatureLow": 14.7, 1095 | "temperatureLowTime": 1593819840, 1096 | "apparentTemperatureHigh": 18.69, 1097 | "apparentTemperatureHighTime": 1593782160, 1098 | "apparentTemperatureLow": 14.98, 1099 | "apparentTemperatureLowTime": 1593819720, 1100 | "dewPoint": 9.41, 1101 | "humidity": 0.71, 1102 | "pressure": 1012.8, 1103 | "windSpeed": 5.07, 1104 | "windGust": 14.85, 1105 | "windGustTime": 1593813600, 1106 | "windBearing": 248, 1107 | "cloudCover": 0.66, 1108 | "uvIndex": 4, 1109 | "uvIndexTime": 1593777600, 1110 | "visibility": 16.093, 1111 | "ozone": 339.9, 1112 | "temperatureMin": 9.36, 1113 | "temperatureMinTime": 1593744540, 1114 | "temperatureMax": 18.97, 1115 | "temperatureMaxTime": 1593782160, 1116 | "apparentTemperatureMin": 8.09, 1117 | "apparentTemperatureMinTime": 1593743760, 1118 | "apparentTemperatureMax": 18.69, 1119 | "apparentTemperatureMaxTime": 1593782160 1120 | }, 1121 | { 1122 | "time": 1593813600, 1123 | "summary": "Rain throughout the day.", 1124 | "icon": "rain", 1125 | "sunriseTime": 1593830820, 1126 | "sunsetTime": 1593893220, 1127 | "moonPhase": 0.49, 1128 | "precipIntensity": 0.4153, 1129 | "precipIntensityMax": 1.0357, 1130 | "precipIntensityMaxTime": 1593886020, 1131 | "precipProbability": 1, 1132 | "precipType": "rain", 1133 | "temperatureHigh": 18.54, 1134 | "temperatureHighTime": 1593868080, 1135 | "temperatureLow": 16.22, 1136 | "temperatureLowTime": 1593917340, 1137 | "apparentTemperatureHigh": 18.37, 1138 | "apparentTemperatureHighTime": 1593868020, 1139 | "apparentTemperatureLow": 16.61, 1140 | "apparentTemperatureLowTime": 1593896640, 1141 | "dewPoint": 15.13, 1142 | "humidity": 0.93, 1143 | "pressure": 1008.5, 1144 | "windSpeed": 7.83, 1145 | "windGust": 17.49, 1146 | "windGustTime": 1593884760, 1147 | "windBearing": 231, 1148 | "cloudCover": 1, 1149 | "uvIndex": 4, 1150 | "uvIndexTime": 1593861720, 1151 | "visibility": 15.342, 1152 | "ozone": 335.6, 1153 | "temperatureMin": 14.7, 1154 | "temperatureMinTime": 1593819840, 1155 | "temperatureMax": 18.54, 1156 | "temperatureMaxTime": 1593868080, 1157 | "apparentTemperatureMin": 14.98, 1158 | "apparentTemperatureMinTime": 1593819720, 1159 | "apparentTemperatureMax": 18.37, 1160 | "apparentTemperatureMaxTime": 1593868020 1161 | }, 1162 | { 1163 | "time": 1593900000, 1164 | "summary": "Possible light rain and windy until evening.", 1165 | "icon": "rain", 1166 | "sunriseTime": 1593917280, 1167 | "sunsetTime": 1593979560, 1168 | "moonPhase": 0.52, 1169 | "precipIntensity": 0.2418, 1170 | "precipIntensityMax": 0.5842, 1171 | "precipIntensityMaxTime": 1593960840, 1172 | "precipProbability": 0.86, 1173 | "precipType": "rain", 1174 | "temperatureHigh": 18.6, 1175 | "temperatureHighTime": 1593940860, 1176 | "temperatureLow": 12.14, 1177 | "temperatureLowTime": 1594002360, 1178 | "apparentTemperatureHigh": 18.66, 1179 | "apparentTemperatureHighTime": 1593940800, 1180 | "apparentTemperatureLow": 12.41, 1181 | "apparentTemperatureLowTime": 1594002360, 1182 | "dewPoint": 15.32, 1183 | "humidity": 0.91, 1184 | "pressure": 1005.2, 1185 | "windSpeed": 8.98, 1186 | "windGust": 20.81, 1187 | "windGustTime": 1593953460, 1188 | "windBearing": 237, 1189 | "cloudCover": 0.82, 1190 | "uvIndex": 4, 1191 | "uvIndexTime": 1593948540, 1192 | "visibility": 16.007, 1193 | "ozone": 327.4, 1194 | "temperatureMin": 13.12, 1195 | "temperatureMinTime": 1593986400, 1196 | "temperatureMax": 18.6, 1197 | "temperatureMaxTime": 1593940860, 1198 | "apparentTemperatureMin": 13.39, 1199 | "apparentTemperatureMinTime": 1593986400, 1200 | "apparentTemperatureMax": 18.66, 1201 | "apparentTemperatureMaxTime": 1593940800 1202 | }, 1203 | { 1204 | "time": 1593986400, 1205 | "summary": "Windy until morning, starting again in the evening.", 1206 | "icon": "rain", 1207 | "sunriseTime": 1594003740, 1208 | "sunsetTime": 1594065900, 1209 | "moonPhase": 0.55, 1210 | "precipIntensity": 0.0934, 1211 | "precipIntensityMax": 0.1673, 1212 | "precipIntensityMaxTime": 1594027380, 1213 | "precipProbability": 0.54, 1214 | "precipType": "rain", 1215 | "temperatureHigh": 15.59, 1216 | "temperatureHighTime": 1594028820, 1217 | "temperatureLow": 11.06, 1218 | "temperatureLowTime": 1594076880, 1219 | "apparentTemperatureHigh": 15.31, 1220 | "apparentTemperatureHighTime": 1594028820, 1221 | "apparentTemperatureLow": 11.33, 1222 | "apparentTemperatureLowTime": 1594076880, 1223 | "dewPoint": 10.08, 1224 | "humidity": 0.8, 1225 | "pressure": 1004.9, 1226 | "windSpeed": 10.09, 1227 | "windGust": 15.38, 1228 | "windGustTime": 1594072800, 1229 | "windBearing": 266, 1230 | "cloudCover": 0.39, 1231 | "uvIndex": 5, 1232 | "uvIndexTime": 1594032780, 1233 | "visibility": 16.093, 1234 | "ozone": 371.3, 1235 | "temperatureMin": 11.25, 1236 | "temperatureMinTime": 1594072800, 1237 | "temperatureMax": 15.59, 1238 | "temperatureMaxTime": 1594028820, 1239 | "apparentTemperatureMin": 11.52, 1240 | "apparentTemperatureMinTime": 1594072800, 1241 | "apparentTemperatureMax": 15.31, 1242 | "apparentTemperatureMaxTime": 1594028820 1243 | }, 1244 | { 1245 | "time": 1594072800, 1246 | "summary": "Windy in the morning.", 1247 | "icon": "wind", 1248 | "sunriseTime": 1594090260, 1249 | "sunsetTime": 1594152240, 1250 | "moonPhase": 0.59, 1251 | "precipIntensity": 0.0318, 1252 | "precipIntensityMax": 0.13, 1253 | "precipIntensityMaxTime": 1594112580, 1254 | "precipProbability": 0.33, 1255 | "precipType": "rain", 1256 | "temperatureHigh": 16.54, 1257 | "temperatureHighTime": 1594130880, 1258 | "temperatureLow": 10.44, 1259 | "temperatureLowTime": 1594172400, 1260 | "apparentTemperatureHigh": 16.26, 1261 | "apparentTemperatureHighTime": 1594130880, 1262 | "apparentTemperatureLow": 10.71, 1263 | "apparentTemperatureLowTime": 1594172400, 1264 | "dewPoint": 8.59, 1265 | "humidity": 0.72, 1266 | "pressure": 1012.4, 1267 | "windSpeed": 9.99, 1268 | "windGust": 17.9, 1269 | "windGustTime": 1594091100, 1270 | "windBearing": 287, 1271 | "cloudCover": 0.56, 1272 | "uvIndex": 5, 1273 | "uvIndexTime": 1594118580, 1274 | "visibility": 16.093, 1275 | "ozone": 372.9, 1276 | "temperatureMin": 11.06, 1277 | "temperatureMinTime": 1594076880, 1278 | "temperatureMax": 16.54, 1279 | "temperatureMaxTime": 1594130880, 1280 | "apparentTemperatureMin": 11.33, 1281 | "apparentTemperatureMinTime": 1594076880, 1282 | "apparentTemperatureMax": 16.26, 1283 | "apparentTemperatureMaxTime": 1594130880 1284 | }, 1285 | { 1286 | "time": 1594159200, 1287 | "summary": "Mostly cloudy throughout the day.", 1288 | "icon": "partly-cloudy-day", 1289 | "sunriseTime": 1594176720, 1290 | "sunsetTime": 1594238640, 1291 | "moonPhase": 0.62, 1292 | "precipIntensity": 0.0145, 1293 | "precipIntensityMax": 0.0337, 1294 | "precipIntensityMaxTime": 1594198920, 1295 | "precipProbability": 0.15, 1296 | "precipType": "rain", 1297 | "temperatureHigh": 17.48, 1298 | "temperatureHighTime": 1594227600, 1299 | "temperatureLow": 12.04, 1300 | "temperatureLowTime": 1594261200, 1301 | "apparentTemperatureHigh": 17.2, 1302 | "apparentTemperatureHighTime": 1594227600, 1303 | "apparentTemperatureLow": 12.31, 1304 | "apparentTemperatureLowTime": 1594261200, 1305 | "dewPoint": 9.02, 1306 | "humidity": 0.71, 1307 | "pressure": 1014.8, 1308 | "windSpeed": 5.62, 1309 | "windGust": 13.14, 1310 | "windGustTime": 1594166640, 1311 | "windBearing": 237, 1312 | "cloudCover": 0.68, 1313 | "uvIndex": 4, 1314 | "uvIndexTime": 1594207080, 1315 | "visibility": 16.093, 1316 | "ozone": 357.7, 1317 | "temperatureMin": 10.44, 1318 | "temperatureMinTime": 1594172400, 1319 | "temperatureMax": 17.72, 1320 | "temperatureMaxTime": 1594230840, 1321 | "apparentTemperatureMin": 10.71, 1322 | "apparentTemperatureMinTime": 1594172400, 1323 | "apparentTemperatureMax": 17.44, 1324 | "apparentTemperatureMaxTime": 1594230840 1325 | } 1326 | ] 1327 | }, 1328 | "flags": { 1329 | "sources": [ 1330 | "meteoalarm", 1331 | "cmc", 1332 | "gfs", 1333 | "icon", 1334 | "isd", 1335 | "madis" 1336 | ], 1337 | "meteoalarm-license": "Based on data from EUMETNET - MeteoAlarm [https://www.meteoalarm.eu/]. Time delays between this website and the MeteoAlarm website are possible; for the most up to date information about alert levels as published by the participating National Meteorological Services please use the MeteoAlarm website.", 1338 | "nearest-station": 25.659, 1339 | "units": "si" 1340 | }, 1341 | "offset": 2 1342 | } -------------------------------------------------------------------------------- /tests/stub/darksky/historical_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "latitude": 55.5779099, 3 | "longitude": 9.6559581, 4 | "timezone": "Europe/Copenhagen", 5 | "currently": { 6 | "time": 1577887140, 7 | "summary": "Partly cloudy", 8 | "icon": "partly-cloudy-day", 9 | "precipIntensity": 0, 10 | "precipProbability": 0, 11 | "temperature": 18.83, 12 | "apparentTemperature": -1, 13 | "dewPoint": 2, 14 | "humidity": 90, 15 | "pressure": 1031, 16 | "windSpeed": 15, 17 | "windGust": 29, 18 | "windBearing": 270, 19 | "cloudCover": 15, 20 | "uvIndex": 1, 21 | "visibility": 10, 22 | "ozone": 0 23 | }, 24 | "offset": "2.0" 25 | } 26 | -------------------------------------------------------------------------------- /tests/stub/darksky/historical_2.json: -------------------------------------------------------------------------------- 1 | { 2 | "latitude": 55.5779099, 3 | "longitude": 9.6559581, 4 | "timezone": "Europe/Copenhagen", 5 | "currently": { 6 | "time": 1577887140, 7 | "summary": "Partly cloudy", 8 | "icon": "partly-cloudy-day", 9 | "precipIntensity": 0, 10 | "precipProbability": 0, 11 | "temperature": 18.83, 12 | "apparentTemperature": -1, 13 | "dewPoint": 2, 14 | "humidity": 90, 15 | "pressure": 1031, 16 | "windSpeed": 15, 17 | "windGust": 29, 18 | "windBearing": 270, 19 | "cloudCover": 15, 20 | "uvIndex": 1, 21 | "visibility": 10, 22 | "ozone": 0 23 | }, 24 | "offset": "2.0" 25 | } 26 | -------------------------------------------------------------------------------- /tests/stub/geocoder.json: -------------------------------------------------------------------------------- 1 | { 2 | "results" : [ 3 | { 4 | "address_components" : [ 5 | { 6 | "long_name" : "1600", 7 | "short_name" : "1600", 8 | "types" : [ "street_number" ] 9 | }, 10 | { 11 | "long_name" : "Amphitheatre Pkwy", 12 | "short_name" : "Amphitheatre Pkwy", 13 | "types" : [ "route" ] 14 | }, 15 | { 16 | "long_name" : "Mountain View", 17 | "short_name" : "Mountain View", 18 | "types" : [ "locality", "political" ] 19 | }, 20 | { 21 | "long_name" : "Santa Clara County", 22 | "short_name" : "Santa Clara County", 23 | "types" : [ "administrative_area_level_2", "political" ] 24 | }, 25 | { 26 | "long_name" : "California", 27 | "short_name" : "CA", 28 | "types" : [ "administrative_area_level_1", "political" ] 29 | }, 30 | { 31 | "long_name" : "United States", 32 | "short_name" : "US", 33 | "types" : [ "country", "political" ] 34 | }, 35 | { 36 | "long_name" : "94043", 37 | "short_name" : "94043", 38 | "types" : [ "postal_code" ] 39 | } 40 | ], 41 | "formatted_address" : "1600 Amphitheatre Parkway, Mountain View, CA 94043, USA", 42 | "geometry" : { 43 | "location" : { 44 | "lat" : 37.4224764, 45 | "lng" : -122.0842499 46 | }, 47 | "location_type" : "ROOFTOP", 48 | "viewport" : { 49 | "northeast" : { 50 | "lat" : 37.4238253802915, 51 | "lng" : -122.0829009197085 52 | }, 53 | "southwest" : { 54 | "lat" : 37.4211274197085, 55 | "lng" : -122.0855988802915 56 | } 57 | } 58 | }, 59 | "place_id" : "ChIJ2eUgeAK6j4ARbn5u_wAGqWA", 60 | "plus_code": { 61 | "compound_code": "CWC8+W5 Mountain View, California, United States", 62 | "global_code": "849VCWC8+W5" 63 | }, 64 | "types" : [ "street_address" ] 65 | } 66 | ], 67 | "status" : "OK" 68 | } -------------------------------------------------------------------------------- /tests/stub/response.json: -------------------------------------------------------------------------------- 1 | { 2 | "latitude": 55.35, 3 | "longitude": 10.5, 4 | "timezone": "Europe/Copenhagen", 5 | "currently": { 6 | "time": 1577887140, 7 | "summary": "Partly cloudy", 8 | "icon": "partly-cloudy-day", 9 | "precipIntensity": 0, 10 | "precipProbability": 0, 11 | "temperature": 3, 12 | "apparentTemperature": -1, 13 | "dewPoint": 2, 14 | "humidity": 90, 15 | "pressure": 1031, 16 | "windSpeed": 15, 17 | "windGust": 29, 18 | "windBearing": 270, 19 | "cloudCover": 15, 20 | "uvIndex": 1, 21 | "visibility": 10, 22 | "ozone": 0 23 | }, 24 | "offset": "2.0" 25 | } -------------------------------------------------------------------------------- /tests/stub/weatherkit/forecast.json: -------------------------------------------------------------------------------- 1 | {"forecastDaily":{"name":"DailyForecast","metadata":{"attributionURL":"https:\/\/weatherkit.apple.com\/legal-attribution.html","expireTime":"2022-09-15T09:32:34Z","latitude":55.5779099,"longitude":9.6559581,"readTime":"2022-09-15T08:32:34Z","reportedTime":"2022-09-15T08:32:34Z","units":"m","version":1},"days":[{"forecastStart":"2022-09-14T22:00:00Z","forecastEnd":"2022-09-15T22:00:00Z","conditionCode":"Breezy","maxUvIndex":3,"moonPhase":"waningGibbous","moonrise":"2022-09-15T19:10:19Z","moonset":"2022-09-15T11:18:48Z","precipitationAmount":0.03,"precipitationChance":0.17,"precipitationType":"rain","snowfallAmount":0,"solarMidnight":"2022-09-14T23:14:11Z","solarNoon":"2022-09-15T11:13:18Z","sunrise":"2022-09-15T04:50:17Z","sunriseCivil":"2022-09-15T04:13:13Z","sunriseNautical":"2022-09-15T03:28:30Z","sunriseAstronomical":"2022-09-15T02:39:57Z","sunset":"2022-09-15T17:36:05Z","sunsetCivil":"2022-09-15T18:12:55Z","sunsetNautical":"2022-09-15T18:57:25Z","sunsetAstronomical":"2022-09-15T19:45:45Z","temperatureMax":18.83,"temperatureMin":10.07,"daytimeForecast":{"forecastStart":"2022-09-15T05:00:00Z","forecastEnd":"2022-09-15T17:00:00Z","cloudCover":0.38,"conditionCode":"Breezy","humidity":0.66,"precipitationAmount":0,"precipitationChance":0.09,"precipitationType":"clear","snowfallAmount":0,"windDirection":288,"windSpeed":30.64},"overnightForecast":{"forecastStart":"2022-09-15T17:00:00Z","forecastEnd":"2022-09-16T05:00:00Z","cloudCover":0.45,"conditionCode":"Breezy","humidity":0.79,"precipitationAmount":0.34,"precipitationChance":0.27,"precipitationType":"rain","snowfallAmount":0,"windDirection":277,"windSpeed":28.43},"restOfDayForecast":{"forecastStart":"2022-09-15T08:32:34Z","forecastEnd":"2022-09-15T22:00:00Z","cloudCover":0.42,"conditionCode":"Breezy","humidity":0.65,"precipitationAmount":0.03,"precipitationChance":0.13,"precipitationType":"rain","snowfallAmount":0,"windDirection":287,"windSpeed":31.12}},{"forecastStart":"2022-09-15T22:00:00Z","forecastEnd":"2022-09-16T22:00:00Z","conditionCode":"Drizzle","maxUvIndex":3,"moonPhase":"thirdQuarter","moonrise":"2022-09-16T19:30:34Z","moonset":"2022-09-16T12:39:07Z","precipitationAmount":1.77,"precipitationChance":0.63,"precipitationType":"rain","snowfallAmount":0,"solarMidnight":"2022-09-15T23:13:50Z","solarNoon":"2022-09-16T11:12:56Z","sunrise":"2022-09-16T04:52:10Z","sunriseCivil":"2022-09-16T04:15:12Z","sunriseNautical":"2022-09-16T03:30:40Z","sunriseAstronomical":"2022-09-16T02:42:29Z","sunset":"2022-09-16T17:33:29Z","sunsetCivil":"2022-09-16T18:10:13Z","sunsetNautical":"2022-09-16T18:54:36Z","sunsetAstronomical":"2022-09-16T19:42:33Z","temperatureMax":16.93,"temperatureMin":9.37,"daytimeForecast":{"forecastStart":"2022-09-16T05:00:00Z","forecastEnd":"2022-09-16T17:00:00Z","cloudCover":0.57,"conditionCode":"Drizzle","humidity":0.74,"precipitationAmount":1.31,"precipitationChance":0.51,"precipitationType":"rain","snowfallAmount":0,"windDirection":294,"windSpeed":20.75},"overnightForecast":{"forecastStart":"2022-09-16T17:00:00Z","forecastEnd":"2022-09-17T05:00:00Z","cloudCover":0.16,"conditionCode":"Drizzle","humidity":0.87,"precipitationAmount":0.15,"precipitationChance":0.32,"precipitationType":"rain","snowfallAmount":0,"windDirection":295,"windSpeed":10.88}},{"forecastStart":"2022-09-16T22:00:00Z","forecastEnd":"2022-09-17T22:00:00Z","conditionCode":"Drizzle","maxUvIndex":3,"moonPhase":"thirdQuarter","moonrise":"2022-09-17T19:59:47Z","moonset":"2022-09-17T13:53:09Z","precipitationAmount":2.28,"precipitationChance":0.47,"precipitationType":"rain","snowfallAmount":0,"solarMidnight":"2022-09-16T23:13:28Z","solarNoon":"2022-09-17T11:12:34Z","sunrise":"2022-09-17T04:54:03Z","sunriseCivil":"2022-09-17T04:17:11Z","sunriseNautical":"2022-09-17T03:32:50Z","sunriseAstronomical":"2022-09-17T02:44:59Z","sunset":"2022-09-17T17:30:53Z","sunsetCivil":"2022-09-17T18:07:31Z","sunsetNautical":"2022-09-17T18:51:46Z","sunsetAstronomical":"2022-09-17T19:39:22Z","temperatureMax":16.66,"temperatureMin":8.99,"daytimeForecast":{"forecastStart":"2022-09-17T05:00:00Z","forecastEnd":"2022-09-17T17:00:00Z","cloudCover":0.57,"conditionCode":"Drizzle","humidity":0.75,"precipitationAmount":2.12,"precipitationChance":0.37,"precipitationType":"rain","snowfallAmount":0,"windDirection":299,"windSpeed":14.42},"overnightForecast":{"forecastStart":"2022-09-17T17:00:00Z","forecastEnd":"2022-09-18T05:00:00Z","cloudCover":0.5,"conditionCode":"Drizzle","humidity":0.82,"precipitationAmount":0.46,"precipitationChance":0.38,"precipitationType":"rain","snowfallAmount":0,"windDirection":290,"windSpeed":22.89}},{"forecastStart":"2022-09-17T22:00:00Z","forecastEnd":"2022-09-18T22:00:00Z","conditionCode":"Drizzle","maxUvIndex":2,"moonPhase":"thirdQuarter","moonrise":"2022-09-18T20:42:35Z","moonset":"2022-09-18T14:56:02Z","precipitationAmount":2.99,"precipitationChance":0.54,"precipitationType":"rain","snowfallAmount":0,"solarMidnight":"2022-09-17T23:13:07Z","solarNoon":"2022-09-18T11:12:13Z","sunrise":"2022-09-18T04:55:55Z","sunriseCivil":"2022-09-18T04:19:10Z","sunriseNautical":"2022-09-18T03:34:58Z","sunriseAstronomical":"2022-09-18T02:47:26Z","sunset":"2022-09-18T17:28:16Z","sunsetCivil":"2022-09-18T18:04:49Z","sunsetNautical":"2022-09-18T18:48:58Z","sunsetAstronomical":"2022-09-18T19:36:12Z","temperatureMax":14.53,"temperatureMin":8.43,"daytimeForecast":{"forecastStart":"2022-09-18T05:00:00Z","forecastEnd":"2022-09-18T17:00:00Z","cloudCover":0.7,"conditionCode":"Drizzle","humidity":0.72,"precipitationAmount":2.57,"precipitationChance":0.41,"precipitationType":"rain","snowfallAmount":0,"windDirection":335,"windSpeed":23.1},"overnightForecast":{"forecastStart":"2022-09-18T17:00:00Z","forecastEnd":"2022-09-19T05:00:00Z","cloudCover":0.47,"conditionCode":"Drizzle","humidity":0.86,"precipitationAmount":0.11,"precipitationChance":0.32,"precipitationType":"rain","snowfallAmount":0,"windDirection":326,"windSpeed":17.01}},{"forecastStart":"2022-09-18T22:00:00Z","forecastEnd":"2022-09-19T22:00:00Z","conditionCode":"Drizzle","maxUvIndex":3,"moonPhase":"thirdQuarter","moonrise":"2022-09-19T21:40:12Z","moonset":"2022-09-19T15:44:08Z","precipitationAmount":2.33,"precipitationChance":0.38,"precipitationType":"rain","snowfallAmount":0,"solarMidnight":"2022-09-18T23:12:45Z","solarNoon":"2022-09-19T11:11:51Z","sunrise":"2022-09-19T04:57:48Z","sunriseCivil":"2022-09-19T04:21:08Z","sunriseNautical":"2022-09-19T03:37:06Z","sunriseAstronomical":"2022-09-19T02:49:52Z","sunset":"2022-09-19T17:25:39Z","sunsetCivil":"2022-09-19T18:02:08Z","sunsetNautical":"2022-09-19T18:46:09Z","sunsetAstronomical":"2022-09-19T19:33:03Z","temperatureMax":16.22,"temperatureMin":8.75,"daytimeForecast":{"forecastStart":"2022-09-19T05:00:00Z","forecastEnd":"2022-09-19T17:00:00Z","cloudCover":0.5,"conditionCode":"PartlyCloudy","humidity":0.72,"precipitationAmount":1.7,"precipitationChance":0.29,"precipitationType":"rain","snowfallAmount":0,"windDirection":316,"windSpeed":16.83},"overnightForecast":{"forecastStart":"2022-09-19T17:00:00Z","forecastEnd":"2022-09-20T05:00:00Z","cloudCover":0.53,"conditionCode":"PartlyCloudy","humidity":0.88,"precipitationAmount":0.63,"precipitationChance":0.29,"precipitationType":"rain","snowfallAmount":0,"windDirection":333,"windSpeed":11.71}},{"forecastStart":"2022-09-19T22:00:00Z","forecastEnd":"2022-09-20T22:00:00Z","conditionCode":"PartlyCloudy","maxUvIndex":3,"moonPhase":"thirdQuarter","moonset":"2022-09-20T16:17:38Z","precipitationAmount":0.12,"precipitationChance":0.23,"precipitationType":"rain","snowfallAmount":0,"solarMidnight":"2022-09-19T23:12:24Z","solarNoon":"2022-09-20T11:11:30Z","sunrise":"2022-09-20T04:59:40Z","sunriseCivil":"2022-09-20T04:23:07Z","sunriseNautical":"2022-09-20T03:39:13Z","sunriseAstronomical":"2022-09-20T02:52:16Z","sunset":"2022-09-20T17:23:02Z","sunsetCivil":"2022-09-20T17:59:28Z","sunsetNautical":"2022-09-20T18:43:22Z","sunsetAstronomical":"2022-09-20T19:29:56Z","temperatureMax":15.95,"temperatureMin":8.23,"daytimeForecast":{"forecastStart":"2022-09-20T05:00:00Z","forecastEnd":"2022-09-20T17:00:00Z","cloudCover":0.45,"conditionCode":"PartlyCloudy","humidity":0.7,"precipitationAmount":0.04,"precipitationChance":0.13,"precipitationType":"rain","snowfallAmount":0,"windDirection":348,"windSpeed":15.02},"overnightForecast":{"forecastStart":"2022-09-20T17:00:00Z","forecastEnd":"2022-09-21T05:00:00Z","cloudCover":0.42,"conditionCode":"PartlyCloudy","humidity":0.86,"precipitationAmount":0.07,"precipitationChance":0.15,"precipitationType":"rain","snowfallAmount":0,"windDirection":316,"windSpeed":7.96}},{"forecastStart":"2022-09-20T22:00:00Z","forecastEnd":"2022-09-21T22:00:00Z","conditionCode":"PartlyCloudy","maxUvIndex":3,"moonPhase":"waningCrescent","moonrise":"2022-09-20T22:50:04Z","moonset":"2022-09-21T16:40:26Z","precipitationAmount":0.57,"precipitationChance":0.29,"precipitationType":"rain","snowfallAmount":0,"solarMidnight":"2022-09-20T23:12:02Z","solarNoon":"2022-09-21T11:11:08Z","sunrise":"2022-09-21T05:01:35Z","sunriseCivil":"2022-09-21T04:25:04Z","sunriseNautical":"2022-09-21T03:41:18Z","sunriseAstronomical":"2022-09-21T02:54:37Z","sunset":"2022-09-21T17:20:25Z","sunsetCivil":"2022-09-21T17:56:51Z","sunsetNautical":"2022-09-21T18:40:34Z","sunsetAstronomical":"2022-09-21T19:26:50Z","temperatureMax":15.78,"temperatureMin":7.84,"daytimeForecast":{"forecastStart":"2022-09-21T05:00:00Z","forecastEnd":"2022-09-21T17:00:00Z","cloudCover":0.6,"conditionCode":"PartlyCloudy","humidity":0.74,"precipitationAmount":0.22,"precipitationChance":0.25,"precipitationType":"rain","snowfallAmount":0,"windDirection":283,"windSpeed":9.74},"overnightForecast":{"forecastStart":"2022-09-21T17:00:00Z","forecastEnd":"2022-09-22T05:00:00Z","cloudCover":0.69,"conditionCode":"MostlyCloudy","humidity":0.9,"precipitationAmount":0.4,"precipitationChance":0.2,"precipitationType":"rain","snowfallAmount":0,"windDirection":243,"windSpeed":7.66}},{"forecastStart":"2022-09-21T22:00:00Z","forecastEnd":"2022-09-22T22:00:00Z","conditionCode":"MostlyCloudy","maxUvIndex":3,"moonPhase":"waningCrescent","moonrise":"2022-09-22T00:07:03Z","moonset":"2022-09-22T16:56:06Z","precipitationAmount":0.05,"precipitationChance":0.23,"precipitationType":"rain","snowfallAmount":0,"solarMidnight":"2022-09-21T23:11:41Z","solarNoon":"2022-09-22T11:10:47Z","sunrise":"2022-09-22T05:03:30Z","sunriseCivil":"2022-09-22T04:27:02Z","sunriseNautical":"2022-09-22T03:43:23Z","sunriseAstronomical":"2022-09-22T02:56:57Z","sunset":"2022-09-22T17:17:48Z","sunsetCivil":"2022-09-22T17:54:14Z","sunsetNautical":"2022-09-22T18:37:48Z","sunsetAstronomical":"2022-09-22T19:23:46Z","temperatureMax":17.06,"temperatureMin":8.8,"daytimeForecast":{"forecastStart":"2022-09-22T05:00:00Z","forecastEnd":"2022-09-22T17:00:00Z","cloudCover":0.78,"conditionCode":"MostlyCloudy","humidity":0.75,"precipitationAmount":0,"precipitationChance":0.12,"precipitationType":"clear","snowfallAmount":0,"windDirection":252,"windSpeed":9.5},"overnightForecast":{"forecastStart":"2022-09-22T17:00:00Z","forecastEnd":"2022-09-23T05:00:00Z","cloudCover":0.6,"conditionCode":"PartlyCloudy","humidity":0.87,"precipitationAmount":0,"precipitationChance":0.09,"precipitationType":"clear","snowfallAmount":0,"windDirection":202,"windSpeed":7.85}},{"forecastStart":"2022-09-22T22:00:00Z","forecastEnd":"2022-09-23T22:00:00Z","conditionCode":"PartlyCloudy","maxUvIndex":3,"moonPhase":"waningCrescent","moonrise":"2022-09-23T01:27:00Z","moonset":"2022-09-23T17:07:41Z","precipitationAmount":0,"precipitationChance":0.2,"precipitationType":"clear","snowfallAmount":0,"solarMidnight":"2022-09-22T23:11:20Z","solarNoon":"2022-09-23T11:10:26Z","sunrise":"2022-09-23T05:05:25Z","sunriseCivil":"2022-09-23T04:28:59Z","sunriseNautical":"2022-09-23T03:45:27Z","sunriseAstronomical":"2022-09-23T02:59:15Z","sunset":"2022-09-23T17:15:10Z","sunsetCivil":"2022-09-23T17:51:37Z","sunsetNautical":"2022-09-23T18:35:02Z","sunsetAstronomical":"2022-09-23T19:20:43Z","temperatureMax":17.18,"temperatureMin":10.55,"daytimeForecast":{"forecastStart":"2022-09-23T05:00:00Z","forecastEnd":"2022-09-23T17:00:00Z","cloudCover":0.58,"conditionCode":"PartlyCloudy","humidity":0.75,"precipitationAmount":0,"precipitationChance":0.13,"precipitationType":"clear","snowfallAmount":0,"windDirection":145,"windSpeed":10.01},"overnightForecast":{"forecastStart":"2022-09-23T17:00:00Z","forecastEnd":"2022-09-24T05:00:00Z","cloudCover":0.23,"conditionCode":"MostlyClear","humidity":0.86,"precipitationAmount":0,"precipitationChance":0.08,"precipitationType":"clear","snowfallAmount":0,"windDirection":138,"windSpeed":9.58}},{"forecastStart":"2022-09-23T22:00:00Z","forecastEnd":"2022-09-24T22:00:00Z","conditionCode":"PartlyCloudy","maxUvIndex":3,"moonPhase":"waningCrescent","moonrise":"2022-09-24T02:47:19Z","moonset":"2022-09-24T17:16:54Z","precipitationAmount":0,"precipitationChance":0.21,"precipitationType":"clear","snowfallAmount":0,"solarMidnight":"2022-09-23T23:10:58Z","solarNoon":"2022-09-24T11:10:05Z","sunrise":"2022-09-24T05:07:21Z","sunriseCivil":"2022-09-24T04:30:56Z","sunriseNautical":"2022-09-24T03:47:31Z","sunriseAstronomical":"2022-09-24T03:01:35Z","sunset":"2022-09-24T17:12:33Z","sunsetCivil":"2022-09-24T17:49:00Z","sunsetNautical":"2022-09-24T18:32:16Z","sunsetAstronomical":"2022-09-24T19:17:42Z","temperatureMax":18.6,"temperatureMin":10.16,"daytimeForecast":{"forecastStart":"2022-09-24T05:00:00Z","forecastEnd":"2022-09-24T17:00:00Z","cloudCover":0.47,"conditionCode":"PartlyCloudy","humidity":0.74,"precipitationAmount":0,"precipitationChance":0.13,"precipitationType":"clear","snowfallAmount":0,"windDirection":135,"windSpeed":12.75},"overnightForecast":{"forecastStart":"2022-09-24T17:00:00Z","forecastEnd":"2022-09-25T05:00:00Z","cloudCover":0.81,"conditionCode":"MostlyCloudy","humidity":0.86,"precipitationAmount":0,"precipitationChance":0.1,"precipitationType":"clear","snowfallAmount":0,"windDirection":112,"windSpeed":12.22}}]}} -------------------------------------------------------------------------------- /tests/stub/weatherkit/historical-1.json: -------------------------------------------------------------------------------- 1 | {"currentWeather":{"name":"CurrentWeather","metadata":{"attributionURL":"https:\/\/weatherkit.apple.com\/legal-attribution.html","expireTime":"2022-09-14T09:33:50Z","latitude":55.5779099,"longitude":9.6559581,"readTime":"2022-09-14T09:28:50Z","reportedTime":"2022-09-14T09:28:50Z","units":"m","version":1},"asOf":"2022-09-12T14:00:00Z","cloudCover":0.93,"conditionCode":"Cloudy","daylight":true,"humidity":0.66,"precipitationIntensity":0,"pressure":1014.46,"pressureTrend":"falling","temperature":18.83,"temperatureApparent":18.73,"temperatureDewPoint":12.41,"uvIndex":2,"visibility":28750.8,"windDirection":181,"windGust":29.01,"windSpeed":18.2}} -------------------------------------------------------------------------------- /tests/stub/weatherkit/historical-2.json: -------------------------------------------------------------------------------- 1 | {"currentWeather":{"name":"CurrentWeather","metadata":{"attributionURL":"https:\/\/weatherkit.apple.com\/legal-attribution.html","expireTime":"2022-09-14T09:33:50Z","latitude":55.5779099,"longitude":9.6559581,"readTime":"2022-09-14T09:28:50Z","reportedTime":"2022-09-14T09:28:50Z","units":"m","version":1},"asOf":"2022-09-12T14:00:00Z","cloudCover":0.93,"conditionCode":"Cloudy","daylight":true,"humidity":0.66,"precipitationIntensity":0,"pressure":1014.46,"pressureTrend":"falling","temperature":18.83,"temperatureApparent":18.73,"temperatureDewPoint":12.41,"uvIndex":2,"visibility":28750.8,"windDirection":181,"windGust":29.01,"windSpeed":18.2}} -------------------------------------------------------------------------------- /tests/stub/weatherstack/forecast.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "type": "City", 4 | "query": "Fredericia, Denmark", 5 | "language": "en", 6 | "unit": "m" 7 | }, 8 | "location": { 9 | "name": "Fredericia", 10 | "country": "Denmark", 11 | "region": "Syddanmark", 12 | "lat": "55.5779099", 13 | "lon": "9.6559581", 14 | "timezone_id": "Europe\/Copenhagen", 15 | "localtime": "2020-07-01 13:43", 16 | "localtime_epoch": 1593610980, 17 | "utc_offset": "2.0" 18 | }, 19 | "current": { 20 | "observation_time": "11:43 AM", 21 | "temperature": 18.83, 22 | "weather_code": 116, 23 | "weather_icons": [ 24 | "https:\/\/assets.weatherstack.com\/images\/wsymbols01_png_64\/wsymbol_0002_sunny_intervals.png" 25 | ], 26 | "weather_descriptions": [ 27 | "Partly cloudy" 28 | ], 29 | "wind_speed": 15, 30 | "wind_degree": 270, 31 | "wind_dir": "W", 32 | "pressure": 1003, 33 | "precip": 1.6, 34 | "humidity": 73, 35 | "cloudcover": 75, 36 | "feelslike": 18, 37 | "uv_index": 4, 38 | "visibility": 10, 39 | "is_day": "yes" 40 | }, 41 | "forecast": { 42 | "2020-07-01": { 43 | "date": "2020-07-01", 44 | "date_epoch": 1593561600, 45 | "astro": { 46 | "sunrise": "04:43 AM", 47 | "sunset": "10:06 PM", 48 | "moonrise": "05:51 PM", 49 | "moonset": "02:19 AM", 50 | "moon_phase": "Full Moon", 51 | "moon_illumination": 76 52 | }, 53 | "mintemp": 15, 54 | "maxtemp": 18, 55 | "avgtemp": 16, 56 | "totalsnow": 0, 57 | "sunhour": 13.9, 58 | "uv_index": 5, 59 | "hourly": [ 60 | { 61 | "time": 0, 62 | "temperature": 18, 63 | "wind_speed": 34, 64 | "wind_degree": 258, 65 | "wind_dir": "WSW", 66 | "weather_code": 353, 67 | "weather_icons": [ 68 | "https:\/\/assets.weatherstack.com\/images\/wsymbols01_png_64\/wsymbol_0009_light_rain_showers.png" 69 | ], 70 | "weather_descriptions": [ 71 | "Light rain shower" 72 | ], 73 | "precip": 7.5, 74 | "humidity": 81, 75 | "visibility": 10, 76 | "pressure": 1001, 77 | "cloudcover": 68, 78 | "heatindex": 16, 79 | "dewpoint": 13, 80 | "windchill": 16, 81 | "windgust": 36, 82 | "feelslike": 16, 83 | "chanceofrain": 89, 84 | "chanceofremdry": 0, 85 | "chanceofwindy": 0, 86 | "chanceofovercast": 89, 87 | "chanceofsunshine": 0, 88 | "chanceoffrost": 0, 89 | "chanceofhightemp": 0, 90 | "chanceoffog": 0, 91 | "chanceofsnow": 0, 92 | "chanceofthunder": 0, 93 | "uv_index": 5 94 | } 95 | ] 96 | }, 97 | "2020-07-02": { 98 | "date": "2020-07-02", 99 | "date_epoch": 1593648000, 100 | "astro": { 101 | "sunrise": "04:44 AM", 102 | "sunset": "10:06 PM", 103 | "moonrise": "07:18 PM", 104 | "moonset": "02:40 AM", 105 | "moon_phase": "Full Moon", 106 | "moon_illumination": 83 107 | }, 108 | "mintemp": 13, 109 | "maxtemp": 16, 110 | "avgtemp": 15, 111 | "totalsnow": 0, 112 | "sunhour": 15.3, 113 | "uv_index": 5, 114 | "hourly": [ 115 | { 116 | "time": 0, 117 | "temperature": 16, 118 | "wind_speed": 23, 119 | "wind_degree": 292, 120 | "wind_dir": "WNW", 121 | "weather_code": 176, 122 | "weather_icons": [ 123 | "https:\/\/assets.weatherstack.com\/images\/wsymbols01_png_64\/wsymbol_0009_light_rain_showers.png" 124 | ], 125 | "weather_descriptions": [ 126 | "Patchy rain possible" 127 | ], 128 | "precip": 0.4, 129 | "humidity": 70, 130 | "visibility": 10, 131 | "pressure": 1008, 132 | "cloudcover": 41, 133 | "heatindex": 15, 134 | "dewpoint": 9, 135 | "windchill": 14, 136 | "windgust": 28, 137 | "feelslike": 14, 138 | "chanceofrain": 72, 139 | "chanceofremdry": 0, 140 | "chanceofwindy": 0, 141 | "chanceofovercast": 84, 142 | "chanceofsunshine": 0, 143 | "chanceoffrost": 0, 144 | "chanceofhightemp": 0, 145 | "chanceoffog": 0, 146 | "chanceofsnow": 0, 147 | "chanceofthunder": 0, 148 | "uv_index": 5 149 | } 150 | ] 151 | }, 152 | "2020-07-03": { 153 | "date": "2020-07-03", 154 | "date_epoch": 1593734400, 155 | "astro": { 156 | "sunrise": "04:45 AM", 157 | "sunset": "10:05 PM", 158 | "moonrise": "08:39 PM", 159 | "moonset": "03:08 AM", 160 | "moon_phase": "Full Moon", 161 | "moon_illumination": 90 162 | }, 163 | "mintemp": 12, 164 | "maxtemp": 18, 165 | "avgtemp": 15, 166 | "totalsnow": 0, 167 | "sunhour": 13.2, 168 | "uv_index": 5, 169 | "hourly": [ 170 | { 171 | "time": 0, 172 | "temperature": 18, 173 | "wind_speed": 12, 174 | "wind_degree": 278, 175 | "wind_dir": "W", 176 | "weather_code": 116, 177 | "weather_icons": [ 178 | "https:\/\/assets.weatherstack.com\/images\/wsymbols01_png_64\/wsymbol_0002_sunny_intervals.png" 179 | ], 180 | "weather_descriptions": [ 181 | "Partly cloudy" 182 | ], 183 | "precip": 0, 184 | "humidity": 66, 185 | "visibility": 10, 186 | "pressure": 1013, 187 | "cloudcover": 47, 188 | "heatindex": 15, 189 | "dewpoint": 9, 190 | "windchill": 14, 191 | "windgust": 15, 192 | "feelslike": 14, 193 | "chanceofrain": 0, 194 | "chanceofremdry": 89, 195 | "chanceofwindy": 0, 196 | "chanceofovercast": 41, 197 | "chanceofsunshine": 84, 198 | "chanceoffrost": 0, 199 | "chanceofhightemp": 0, 200 | "chanceoffog": 0, 201 | "chanceofsnow": 0, 202 | "chanceofthunder": 0, 203 | "uv_index": 5 204 | } 205 | ] 206 | }, 207 | "2020-07-04": { 208 | "date": "2020-07-04", 209 | "date_epoch": 1593820800, 210 | "astro": { 211 | "sunrise": "04:46 AM", 212 | "sunset": "10:04 PM", 213 | "moonrise": "09:49 PM", 214 | "moonset": "03:47 AM", 215 | "moon_phase": "Full Moon", 216 | "moon_illumination": 97 217 | }, 218 | "mintemp": 7, 219 | "maxtemp": 17, 220 | "avgtemp": 15, 221 | "totalsnow": 0, 222 | "sunhour": 0, 223 | "uv_index": 1, 224 | "hourly": [ 225 | { 226 | "time": 0, 227 | "temperature": 17, 228 | "wind_speed": 44, 229 | "wind_degree": 228, 230 | "wind_dir": "SW", 231 | "weather_code": 356, 232 | "weather_icons": [ 233 | "https:\/\/assets.weatherstack.com\/images\/wsymbols01_png_64\/wsymbol_0010_heavy_rain_showers.png" 234 | ], 235 | "weather_descriptions": [ 236 | "Moderate or heavy rain shower" 237 | ], 238 | "precip": 6.5, 239 | "humidity": 87, 240 | "visibility": 9, 241 | "pressure": 1011, 242 | "cloudcover": 96, 243 | "heatindex": 15, 244 | "dewpoint": 15, 245 | "windchill": 15, 246 | "windgust": 50, 247 | "feelslike": 15, 248 | "chanceofrain": 85, 249 | "chanceofremdry": 0, 250 | "chanceofwindy": 0, 251 | "chanceofovercast": 82, 252 | "chanceofsunshine": 0, 253 | "chanceoffrost": 0, 254 | "chanceofhightemp": 0, 255 | "chanceoffog": 0, 256 | "chanceofsnow": 0, 257 | "chanceofthunder": 0, 258 | "uv_index": 1 259 | } 260 | ] 261 | }, 262 | "2020-07-05": { 263 | "date": "2020-07-05", 264 | "date_epoch": 1593907200, 265 | "astro": { 266 | "sunrise": "04:47 AM", 267 | "sunset": "10:04 PM", 268 | "moonrise": "10:42 PM", 269 | "moonset": "04:38 AM", 270 | "moon_phase": "Waning Gibbous", 271 | "moon_illumination": 97 272 | }, 273 | "mintemp": 12, 274 | "maxtemp": 19, 275 | "avgtemp": 17, 276 | "totalsnow": 0, 277 | "sunhour": 0, 278 | "uv_index": 4, 279 | "hourly": [ 280 | { 281 | "time": 0, 282 | "temperature": 19, 283 | "wind_speed": 44, 284 | "wind_degree": 251, 285 | "wind_dir": "WSW", 286 | "weather_code": 176, 287 | "weather_icons": [ 288 | "https:\/\/assets.weatherstack.com\/images\/wsymbols01_png_64\/wsymbol_0009_light_rain_showers.png" 289 | ], 290 | "weather_descriptions": [ 291 | "Patchy rain possible" 292 | ], 293 | "precip": 5.1, 294 | "humidity": 82, 295 | "visibility": 10, 296 | "pressure": 1009, 297 | "cloudcover": 79, 298 | "heatindex": 17, 299 | "dewpoint": 14, 300 | "windchill": 17, 301 | "windgust": 44, 302 | "feelslike": 17, 303 | "chanceofrain": 87, 304 | "chanceofremdry": 0, 305 | "chanceofwindy": 0, 306 | "chanceofovercast": 85, 307 | "chanceofsunshine": 0, 308 | "chanceoffrost": 0, 309 | "chanceofhightemp": 0, 310 | "chanceoffog": 0, 311 | "chanceofsnow": 0, 312 | "chanceofthunder": 0, 313 | "uv_index": 4 314 | } 315 | ] 316 | }, 317 | "2020-07-06": { 318 | "date": "2020-07-06", 319 | "date_epoch": 1593993600, 320 | "astro": { 321 | "sunrise": "04:48 AM", 322 | "sunset": "10:03 PM", 323 | "moonrise": "11:21 PM", 324 | "moonset": "05:43 AM", 325 | "moon_phase": "Waning Gibbous", 326 | "moon_illumination": 90 327 | }, 328 | "mintemp": 14, 329 | "maxtemp": 17, 330 | "avgtemp": 16, 331 | "totalsnow": 0, 332 | "sunhour": 0, 333 | "uv_index": 4, 334 | "hourly": [ 335 | { 336 | "time": 0, 337 | "temperature": 17, 338 | "wind_speed": 43, 339 | "wind_degree": 263, 340 | "wind_dir": "W", 341 | "weather_code": 305, 342 | "weather_icons": [ 343 | "https:\/\/assets.weatherstack.com\/images\/wsymbols01_png_64\/wsymbol_0010_heavy_rain_showers.png" 344 | ], 345 | "weather_descriptions": [ 346 | "Heavy rain at times" 347 | ], 348 | "precip": 1, 349 | "humidity": 74, 350 | "visibility": 9, 351 | "pressure": 1008, 352 | "cloudcover": 55, 353 | "heatindex": 16, 354 | "dewpoint": 11, 355 | "windchill": 16, 356 | "windgust": 51, 357 | "feelslike": 16, 358 | "chanceofrain": 89, 359 | "chanceofremdry": 0, 360 | "chanceofwindy": 0, 361 | "chanceofovercast": 91, 362 | "chanceofsunshine": 0, 363 | "chanceoffrost": 0, 364 | "chanceofhightemp": 0, 365 | "chanceoffog": 0, 366 | "chanceofsnow": 0, 367 | "chanceofthunder": 0, 368 | "uv_index": 4 369 | } 370 | ] 371 | }, 372 | "2020-07-07": { 373 | "date": "2020-07-07", 374 | "date_epoch": 1594080000, 375 | "astro": { 376 | "sunrise": "04:49 AM", 377 | "sunset": "10:02 PM", 378 | "moonrise": "11:49 PM", 379 | "moonset": "06:56 AM", 380 | "moon_phase": "Waning Gibbous", 381 | "moon_illumination": 83 382 | }, 383 | "mintemp": 13, 384 | "maxtemp": 15, 385 | "avgtemp": 14, 386 | "totalsnow": 0, 387 | "sunhour": 0, 388 | "uv_index": 3, 389 | "hourly": [ 390 | { 391 | "time": 0, 392 | "temperature": 15, 393 | "wind_speed": 29, 394 | "wind_degree": 265, 395 | "wind_dir": "W", 396 | "weather_code": 305, 397 | "weather_icons": [ 398 | "https:\/\/assets.weatherstack.com\/images\/wsymbols01_png_64\/wsymbol_0010_heavy_rain_showers.png" 399 | ], 400 | "weather_descriptions": [ 401 | "Heavy rain at times" 402 | ], 403 | "precip": 1.3, 404 | "humidity": 75, 405 | "visibility": 10, 406 | "pressure": 1010, 407 | "cloudcover": 38, 408 | "heatindex": 14, 409 | "dewpoint": 10, 410 | "windchill": 13, 411 | "windgust": 30, 412 | "feelslike": 13, 413 | "chanceofrain": 70, 414 | "chanceofremdry": 0, 415 | "chanceofwindy": 0, 416 | "chanceofovercast": 86, 417 | "chanceofsunshine": 0, 418 | "chanceoffrost": 0, 419 | "chanceofhightemp": 0, 420 | "chanceoffog": 0, 421 | "chanceofsnow": 0, 422 | "chanceofthunder": 0, 423 | "uv_index": 3 424 | } 425 | ] 426 | } 427 | } 428 | } -------------------------------------------------------------------------------- /tests/stub/weatherstack/historical.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "type": "LatLon", 4 | "query": "Lat 55.36 and Lon 10.49", 5 | "language": "en", 6 | "unit": "m" 7 | }, 8 | "location": { 9 | "name": "Fraugde", 10 | "country": "Denmark", 11 | "region": "Syddanmark", 12 | "lat": "55.5779099", 13 | "lon": "9.6559581", 14 | "timezone_id": "Europe/Copenhagen", 15 | "localtime": "2020-06-22 15:12", 16 | "localtime_epoch": 1592838720, 17 | "utc_offset": "2.0" 18 | }, 19 | "current": { 20 | "observation_time": "01:12 PM", 21 | "temperature": 21, 22 | "weather_code": 116, 23 | "weather_icons": [ 24 | "https://assets.weatherstack.com/images/wsymbols01_png_64/wsymbol_0002_sunny_intervals.png" 25 | ], 26 | "weather_descriptions": [ 27 | "Partly cloudy" 28 | ], 29 | "wind_speed": 20, 30 | "wind_degree": 280, 31 | "wind_dir": "W", 32 | "pressure": 1022, 33 | "precip": 0.5, 34 | "humidity": 53, 35 | "cloudcover": 50, 36 | "feelslike": 21, 37 | "uv_index": 6, 38 | "visibility": 10, 39 | "is_day": "yes" 40 | }, 41 | "historical": { 42 | "2020-01-01": { 43 | "date": "2020-01-01", 44 | "date_epoch": 1577836800, 45 | "astro": [], 46 | "mintemp": 3, 47 | "maxtemp": 6, 48 | "avgtemp": 4, 49 | "totalsnow": 0, 50 | "sunhour": 5.8, 51 | "uv_index": 2, 52 | "hourly": [ 53 | { 54 | "time": "0", 55 | "temperature": 18.83, 56 | "wind_speed": 15, 57 | "wind_degree": 270, 58 | "wind_dir": "W", 59 | "weather_code": 116, 60 | "weather_icons": [ 61 | [ 62 | "https://assets.weatherstack.com/images/wsymbols01_png_64/wsymbol_0004_black_low_cloud.png" 63 | ] 64 | ], 65 | "weather_descriptions": [ 66 | [ 67 | "Partly cloudy" 68 | ] 69 | ], 70 | "precip": 0, 71 | "humidity": 90, 72 | "visibility": 10, 73 | "pressure": 1031, 74 | "cloudcover": 15, 75 | "heatindex": 3, 76 | "dewpoint": 2, 77 | "windchilld": -1, 78 | "windgust": 29, 79 | "feelslike": -1, 80 | "chanceofrain": 0, 81 | "chanceofremdry": 0, 82 | "chanceofwindy": 0, 83 | "chanceofovercast": 0, 84 | "chanceofsunshine": 0, 85 | "chanceoffrost": 0, 86 | "chanceofhightemp": 0, 87 | "chanceoffog": 0, 88 | "chanceofsnow": 0, 89 | "chanceofthunder": 0, 90 | "uv_index": 1 91 | }, 92 | { 93 | "time": "100", 94 | "temperature": 18.83, 95 | "wind_speed": 15, 96 | "wind_degree": 270, 97 | "wind_dir": "W", 98 | "weather_code": 116, 99 | "weather_icons": [ 100 | [ 101 | "https://assets.weatherstack.com/images/wsymbols01_png_64/wsymbol_0004_black_low_cloud.png" 102 | ] 103 | ], 104 | "weather_descriptions": [ 105 | [ 106 | "Partly cloudy" 107 | ] 108 | ], 109 | "precip": 0, 110 | "humidity": 90, 111 | "visibility": 10, 112 | "pressure": 1031, 113 | "cloudcover": 15, 114 | "heatindex": 3, 115 | "dewpoint": 2, 116 | "windchilld": -1, 117 | "windgust": 29, 118 | "feelslike": -1, 119 | "chanceofrain": 0, 120 | "chanceofremdry": 0, 121 | "chanceofwindy": 0, 122 | "chanceofovercast": 0, 123 | "chanceofsunshine": 0, 124 | "chanceoffrost": 0, 125 | "chanceofhightemp": 0, 126 | "chanceoffog": 0, 127 | "chanceofsnow": 0, 128 | "chanceofthunder": 0, 129 | "uv_index": 1 130 | }, 131 | { 132 | "time": "200", 133 | "temperature": 18.83, 134 | "wind_speed": 15, 135 | "wind_degree": 270, 136 | "wind_dir": "W", 137 | "weather_code": 116, 138 | "weather_icons": [ 139 | [ 140 | "https://assets.weatherstack.com/images/wsymbols01_png_64/wsymbol_0004_black_low_cloud.png" 141 | ] 142 | ], 143 | "weather_descriptions": [ 144 | [ 145 | "Partly cloudy" 146 | ] 147 | ], 148 | "precip": 0, 149 | "humidity": 90, 150 | "visibility": 10, 151 | "pressure": 1031, 152 | "cloudcover": 15, 153 | "heatindex": 3, 154 | "dewpoint": 2, 155 | "windchilld": -1, 156 | "windgust": 29, 157 | "feelslike": -1, 158 | "chanceofrain": 0, 159 | "chanceofremdry": 0, 160 | "chanceofwindy": 0, 161 | "chanceofovercast": 0, 162 | "chanceofsunshine": 0, 163 | "chanceoffrost": 0, 164 | "chanceofhightemp": 0, 165 | "chanceoffog": 0, 166 | "chanceofsnow": 0, 167 | "chanceofthunder": 0, 168 | "uv_index": 1 169 | }, 170 | { 171 | "time": "300", 172 | "temperature": 18.83, 173 | "wind_speed": 15, 174 | "wind_degree": 270, 175 | "wind_dir": "W", 176 | "weather_code": 116, 177 | "weather_icons": [ 178 | [ 179 | "https://assets.weatherstack.com/images/wsymbols01_png_64/wsymbol_0004_black_low_cloud.png" 180 | ] 181 | ], 182 | "weather_descriptions": [ 183 | [ 184 | "Partly cloudy" 185 | ] 186 | ], 187 | "precip": 0, 188 | "humidity": 90, 189 | "visibility": 10, 190 | "pressure": 1031, 191 | "cloudcover": 15, 192 | "heatindex": 3, 193 | "dewpoint": 2, 194 | "windchilld": -1, 195 | "windgust": 29, 196 | "feelslike": -1, 197 | "chanceofrain": 0, 198 | "chanceofremdry": 0, 199 | "chanceofwindy": 0, 200 | "chanceofovercast": 0, 201 | "chanceofsunshine": 0, 202 | "chanceoffrost": 0, 203 | "chanceofhightemp": 0, 204 | "chanceoffog": 0, 205 | "chanceofsnow": 0, 206 | "chanceofthunder": 0, 207 | "uv_index": 1 208 | }, 209 | { 210 | "time": "400", 211 | "temperature": 18.83, 212 | "wind_speed": 15, 213 | "wind_degree": 270, 214 | "wind_dir": "W", 215 | "weather_code": 116, 216 | "weather_icons": [ 217 | [ 218 | "https://assets.weatherstack.com/images/wsymbols01_png_64/wsymbol_0004_black_low_cloud.png" 219 | ] 220 | ], 221 | "weather_descriptions": [ 222 | [ 223 | "Partly cloudy" 224 | ] 225 | ], 226 | "precip": 0, 227 | "humidity": 90, 228 | "visibility": 10, 229 | "pressure": 1031, 230 | "cloudcover": 15, 231 | "heatindex": 3, 232 | "dewpoint": 2, 233 | "windchilld": -1, 234 | "windgust": 29, 235 | "feelslike": -1, 236 | "chanceofrain": 0, 237 | "chanceofremdry": 0, 238 | "chanceofwindy": 0, 239 | "chanceofovercast": 0, 240 | "chanceofsunshine": 0, 241 | "chanceoffrost": 0, 242 | "chanceofhightemp": 0, 243 | "chanceoffog": 0, 244 | "chanceofsnow": 0, 245 | "chanceofthunder": 0, 246 | "uv_index": 1 247 | }, 248 | { 249 | "time": "500", 250 | "temperature": 18.83, 251 | "wind_speed": 15, 252 | "wind_degree": 270, 253 | "wind_dir": "W", 254 | "weather_code": 116, 255 | "weather_icons": [ 256 | [ 257 | "https://assets.weatherstack.com/images/wsymbols01_png_64/wsymbol_0004_black_low_cloud.png" 258 | ] 259 | ], 260 | "weather_descriptions": [ 261 | [ 262 | "Partly cloudy" 263 | ] 264 | ], 265 | "precip": 0, 266 | "humidity": 90, 267 | "visibility": 10, 268 | "pressure": 1031, 269 | "cloudcover": 15, 270 | "heatindex": 3, 271 | "dewpoint": 2, 272 | "windchilld": -1, 273 | "windgust": 29, 274 | "feelslike": -1, 275 | "chanceofrain": 0, 276 | "chanceofremdry": 0, 277 | "chanceofwindy": 0, 278 | "chanceofovercast": 0, 279 | "chanceofsunshine": 0, 280 | "chanceoffrost": 0, 281 | "chanceofhightemp": 0, 282 | "chanceoffog": 0, 283 | "chanceofsnow": 0, 284 | "chanceofthunder": 0, 285 | "uv_index": 1 286 | }, 287 | { 288 | "time": "600", 289 | "temperature": 18.83, 290 | "wind_speed": 15, 291 | "wind_degree": 270, 292 | "wind_dir": "W", 293 | "weather_code": 116, 294 | "weather_icons": [ 295 | [ 296 | "https://assets.weatherstack.com/images/wsymbols01_png_64/wsymbol_0004_black_low_cloud.png" 297 | ] 298 | ], 299 | "weather_descriptions": [ 300 | [ 301 | "Partly cloudy" 302 | ] 303 | ], 304 | "precip": 0, 305 | "humidity": 90, 306 | "visibility": 10, 307 | "pressure": 1031, 308 | "cloudcover": 15, 309 | "heatindex": 3, 310 | "dewpoint": 2, 311 | "windchilld": -1, 312 | "windgust": 29, 313 | "feelslike": -1, 314 | "chanceofrain": 0, 315 | "chanceofremdry": 0, 316 | "chanceofwindy": 0, 317 | "chanceofovercast": 0, 318 | "chanceofsunshine": 0, 319 | "chanceoffrost": 0, 320 | "chanceofhightemp": 0, 321 | "chanceoffog": 0, 322 | "chanceofsnow": 0, 323 | "chanceofthunder": 0, 324 | "uv_index": 1 325 | }, 326 | { 327 | "time": "700", 328 | "temperature": 18.83, 329 | "wind_speed": 15, 330 | "wind_degree": 270, 331 | "wind_dir": "W", 332 | "weather_code": 116, 333 | "weather_icons": [ 334 | [ 335 | "https://assets.weatherstack.com/images/wsymbols01_png_64/wsymbol_0004_black_low_cloud.png" 336 | ] 337 | ], 338 | "weather_descriptions": [ 339 | [ 340 | "Partly cloudy" 341 | ] 342 | ], 343 | "precip": 0, 344 | "humidity": 90, 345 | "visibility": 10, 346 | "pressure": 1031, 347 | "cloudcover": 15, 348 | "heatindex": 3, 349 | "dewpoint": 2, 350 | "windchilld": -1, 351 | "windgust": 29, 352 | "feelslike": -1, 353 | "chanceofrain": 0, 354 | "chanceofremdry": 0, 355 | "chanceofwindy": 0, 356 | "chanceofovercast": 0, 357 | "chanceofsunshine": 0, 358 | "chanceoffrost": 0, 359 | "chanceofhightemp": 0, 360 | "chanceoffog": 0, 361 | "chanceofsnow": 0, 362 | "chanceofthunder": 0, 363 | "uv_index": 1 364 | }, 365 | { 366 | "time": "800", 367 | "temperature": 18.83, 368 | "wind_speed": 15, 369 | "wind_degree": 270, 370 | "wind_dir": "W", 371 | "weather_code": 116, 372 | "weather_icons": [ 373 | [ 374 | "https://assets.weatherstack.com/images/wsymbols01_png_64/wsymbol_0004_black_low_cloud.png" 375 | ] 376 | ], 377 | "weather_descriptions": [ 378 | [ 379 | "Partly cloudy" 380 | ] 381 | ], 382 | "precip": 0, 383 | "humidity": 90, 384 | "visibility": 10, 385 | "pressure": 1031, 386 | "cloudcover": 15, 387 | "heatindex": 3, 388 | "dewpoint": 2, 389 | "windchilld": -1, 390 | "windgust": 29, 391 | "feelslike": -1, 392 | "chanceofrain": 0, 393 | "chanceofremdry": 0, 394 | "chanceofwindy": 0, 395 | "chanceofovercast": 0, 396 | "chanceofsunshine": 0, 397 | "chanceoffrost": 0, 398 | "chanceofhightemp": 0, 399 | "chanceoffog": 0, 400 | "chanceofsnow": 0, 401 | "chanceofthunder": 0, 402 | "uv_index": 1 403 | }, 404 | { 405 | "time": "900", 406 | "temperature": 18.83, 407 | "wind_speed": 15, 408 | "wind_degree": 270, 409 | "wind_dir": "W", 410 | "weather_code": 116, 411 | "weather_icons": [ 412 | [ 413 | "https://assets.weatherstack.com/images/wsymbols01_png_64/wsymbol_0004_black_low_cloud.png" 414 | ] 415 | ], 416 | "weather_descriptions": [ 417 | [ 418 | "Partly cloudy" 419 | ] 420 | ], 421 | "precip": 0, 422 | "humidity": 90, 423 | "visibility": 10, 424 | "pressure": 1031, 425 | "cloudcover": 15, 426 | "heatindex": 3, 427 | "dewpoint": 2, 428 | "windchilld": -1, 429 | "windgust": 29, 430 | "feelslike": -1, 431 | "chanceofrain": 0, 432 | "chanceofremdry": 0, 433 | "chanceofwindy": 0, 434 | "chanceofovercast": 0, 435 | "chanceofsunshine": 0, 436 | "chanceoffrost": 0, 437 | "chanceofhightemp": 0, 438 | "chanceoffog": 0, 439 | "chanceofsnow": 0, 440 | "chanceofthunder": 0, 441 | "uv_index": 1 442 | }, 443 | { 444 | "time": "1000", 445 | "temperature": 18.83, 446 | "wind_speed": 15, 447 | "wind_degree": 270, 448 | "wind_dir": "W", 449 | "weather_code": 116, 450 | "weather_icons": [ 451 | [ 452 | "https://assets.weatherstack.com/images/wsymbols01_png_64/wsymbol_0004_black_low_cloud.png" 453 | ] 454 | ], 455 | "weather_descriptions": [ 456 | [ 457 | "Partly cloudy" 458 | ] 459 | ], 460 | "precip": 0, 461 | "humidity": 90, 462 | "visibility": 10, 463 | "pressure": 1031, 464 | "cloudcover": 15, 465 | "heatindex": 3, 466 | "dewpoint": 2, 467 | "windchilld": -1, 468 | "windgust": 29, 469 | "feelslike": -1, 470 | "chanceofrain": 0, 471 | "chanceofremdry": 0, 472 | "chanceofwindy": 0, 473 | "chanceofovercast": 0, 474 | "chanceofsunshine": 0, 475 | "chanceoffrost": 0, 476 | "chanceofhightemp": 0, 477 | "chanceoffog": 0, 478 | "chanceofsnow": 0, 479 | "chanceofthunder": 0, 480 | "uv_index": 1 481 | }, 482 | { 483 | "time": "1100", 484 | "temperature": 18.83, 485 | "wind_speed": 15, 486 | "wind_degree": 270, 487 | "wind_dir": "W", 488 | "weather_code": 116, 489 | "weather_icons": [ 490 | [ 491 | "https://assets.weatherstack.com/images/wsymbols01_png_64/wsymbol_0004_black_low_cloud.png" 492 | ] 493 | ], 494 | "weather_descriptions": [ 495 | [ 496 | "Partly cloudy" 497 | ] 498 | ], 499 | "precip": 0, 500 | "humidity": 90, 501 | "visibility": 10, 502 | "pressure": 1031, 503 | "cloudcover": 15, 504 | "heatindex": 3, 505 | "dewpoint": 2, 506 | "windchilld": -1, 507 | "windgust": 29, 508 | "feelslike": -1, 509 | "chanceofrain": 0, 510 | "chanceofremdry": 0, 511 | "chanceofwindy": 0, 512 | "chanceofovercast": 0, 513 | "chanceofsunshine": 0, 514 | "chanceoffrost": 0, 515 | "chanceofhightemp": 0, 516 | "chanceoffog": 0, 517 | "chanceofsnow": 0, 518 | "chanceofthunder": 0, 519 | "uv_index": 1 520 | }, 521 | { 522 | "time": "1200", 523 | "temperature": 18.83, 524 | "wind_speed": 15, 525 | "wind_degree": 270, 526 | "wind_dir": "W", 527 | "weather_code": 116, 528 | "weather_icons": [ 529 | [ 530 | "https://assets.weatherstack.com/images/wsymbols01_png_64/wsymbol_0004_black_low_cloud.png" 531 | ] 532 | ], 533 | "weather_descriptions": [ 534 | [ 535 | "Partly cloudy" 536 | ] 537 | ], 538 | "precip": 0, 539 | "humidity": 90, 540 | "visibility": 10, 541 | "pressure": 1031, 542 | "cloudcover": 15, 543 | "heatindex": 3, 544 | "dewpoint": 2, 545 | "windchilld": -1, 546 | "windgust": 29, 547 | "feelslike": -1, 548 | "chanceofrain": 0, 549 | "chanceofremdry": 0, 550 | "chanceofwindy": 0, 551 | "chanceofovercast": 0, 552 | "chanceofsunshine": 0, 553 | "chanceoffrost": 0, 554 | "chanceofhightemp": 0, 555 | "chanceoffog": 0, 556 | "chanceofsnow": 0, 557 | "chanceofthunder": 0, 558 | "uv_index": 1 559 | }, 560 | { 561 | "time": "1300", 562 | "temperature": 18.83, 563 | "wind_speed": 15, 564 | "wind_degree": 270, 565 | "wind_dir": "W", 566 | "weather_code": 116, 567 | "weather_icons": [ 568 | [ 569 | "https://assets.weatherstack.com/images/wsymbols01_png_64/wsymbol_0004_black_low_cloud.png" 570 | ] 571 | ], 572 | "weather_descriptions": [ 573 | [ 574 | "Partly cloudy" 575 | ] 576 | ], 577 | "precip": 0, 578 | "humidity": 90, 579 | "visibility": 10, 580 | "pressure": 1031, 581 | "cloudcover": 15, 582 | "heatindex": 3, 583 | "dewpoint": 2, 584 | "windchilld": -1, 585 | "windgust": 29, 586 | "feelslike": -1, 587 | "chanceofrain": 0, 588 | "chanceofremdry": 0, 589 | "chanceofwindy": 0, 590 | "chanceofovercast": 0, 591 | "chanceofsunshine": 0, 592 | "chanceoffrost": 0, 593 | "chanceofhightemp": 0, 594 | "chanceoffog": 0, 595 | "chanceofsnow": 0, 596 | "chanceofthunder": 0, 597 | "uv_index": 1 598 | }, 599 | { 600 | "time": "1400", 601 | "temperature": 18.83, 602 | "wind_speed": 15, 603 | "wind_degree": 270, 604 | "wind_dir": "W", 605 | "weather_code": 116, 606 | "weather_icons": [ 607 | [ 608 | "https://assets.weatherstack.com/images/wsymbols01_png_64/wsymbol_0004_black_low_cloud.png" 609 | ] 610 | ], 611 | "weather_descriptions": [ 612 | [ 613 | "Partly cloudy" 614 | ] 615 | ], 616 | "precip": 0, 617 | "humidity": 90, 618 | "visibility": 10, 619 | "pressure": 1031, 620 | "cloudcover": 15, 621 | "heatindex": 3, 622 | "dewpoint": 2, 623 | "windchilld": -1, 624 | "windgust": 29, 625 | "feelslike": -1, 626 | "chanceofrain": 0, 627 | "chanceofremdry": 0, 628 | "chanceofwindy": 0, 629 | "chanceofovercast": 0, 630 | "chanceofsunshine": 0, 631 | "chanceoffrost": 0, 632 | "chanceofhightemp": 0, 633 | "chanceoffog": 0, 634 | "chanceofsnow": 0, 635 | "chanceofthunder": 0, 636 | "uv_index": 1 637 | }, 638 | { 639 | "time": "1500", 640 | "temperature": 18.83, 641 | "wind_speed": 15, 642 | "wind_degree": 270, 643 | "wind_dir": "W", 644 | "weather_code": 116, 645 | "weather_icons": [ 646 | [ 647 | "https://assets.weatherstack.com/images/wsymbols01_png_64/wsymbol_0004_black_low_cloud.png" 648 | ] 649 | ], 650 | "weather_descriptions": [ 651 | [ 652 | "Partly cloudy" 653 | ] 654 | ], 655 | "precip": 0, 656 | "humidity": 90, 657 | "visibility": 10, 658 | "pressure": 1031, 659 | "cloudcover": 15, 660 | "heatindex": 3, 661 | "dewpoint": 2, 662 | "windchilld": -1, 663 | "windgust": 29, 664 | "feelslike": -1, 665 | "chanceofrain": 0, 666 | "chanceofremdry": 0, 667 | "chanceofwindy": 0, 668 | "chanceofovercast": 0, 669 | "chanceofsunshine": 0, 670 | "chanceoffrost": 0, 671 | "chanceofhightemp": 0, 672 | "chanceoffog": 0, 673 | "chanceofsnow": 0, 674 | "chanceofthunder": 0, 675 | "uv_index": 1 676 | }, 677 | { 678 | "time": "1600", 679 | "temperature": 18.83, 680 | "wind_speed": 15, 681 | "wind_degree": 270, 682 | "wind_dir": "W", 683 | "weather_code": 116, 684 | "weather_icons": [ 685 | [ 686 | "https://assets.weatherstack.com/images/wsymbols01_png_64/wsymbol_0004_black_low_cloud.png" 687 | ] 688 | ], 689 | "weather_descriptions": [ 690 | [ 691 | "Partly cloudy" 692 | ] 693 | ], 694 | "precip": 0, 695 | "humidity": 90, 696 | "visibility": 10, 697 | "pressure": 1031, 698 | "cloudcover": 15, 699 | "heatindex": 3, 700 | "dewpoint": 2, 701 | "windchilld": -1, 702 | "windgust": 29, 703 | "feelslike": -1, 704 | "chanceofrain": 0, 705 | "chanceofremdry": 0, 706 | "chanceofwindy": 0, 707 | "chanceofovercast": 0, 708 | "chanceofsunshine": 0, 709 | "chanceoffrost": 0, 710 | "chanceofhightemp": 0, 711 | "chanceoffog": 0, 712 | "chanceofsnow": 0, 713 | "chanceofthunder": 0, 714 | "uv_index": 1 715 | }, 716 | { 717 | "time": "1700", 718 | "temperature": 18.83, 719 | "wind_speed": 15, 720 | "wind_degree": 270, 721 | "wind_dir": "W", 722 | "weather_code": 116, 723 | "weather_icons": [ 724 | [ 725 | "https://assets.weatherstack.com/images/wsymbols01_png_64/wsymbol_0004_black_low_cloud.png" 726 | ] 727 | ], 728 | "weather_descriptions": [ 729 | [ 730 | "Partly cloudy" 731 | ] 732 | ], 733 | "precip": 0, 734 | "humidity": 90, 735 | "visibility": 10, 736 | "pressure": 1031, 737 | "cloudcover": 15, 738 | "heatindex": 3, 739 | "dewpoint": 2, 740 | "windchilld": -1, 741 | "windgust": 29, 742 | "feelslike": -1, 743 | "chanceofrain": 0, 744 | "chanceofremdry": 0, 745 | "chanceofwindy": 0, 746 | "chanceofovercast": 0, 747 | "chanceofsunshine": 0, 748 | "chanceoffrost": 0, 749 | "chanceofhightemp": 0, 750 | "chanceoffog": 0, 751 | "chanceofsnow": 0, 752 | "chanceofthunder": 0, 753 | "uv_index": 1 754 | }, 755 | { 756 | "time": "1800", 757 | "temperature": 18.83, 758 | "wind_speed": 15, 759 | "wind_degree": 270, 760 | "wind_dir": "W", 761 | "weather_code": 116, 762 | "weather_icons": [ 763 | [ 764 | "https://assets.weatherstack.com/images/wsymbols01_png_64/wsymbol_0004_black_low_cloud.png" 765 | ] 766 | ], 767 | "weather_descriptions": [ 768 | [ 769 | "Partly cloudy" 770 | ] 771 | ], 772 | "precip": 0, 773 | "humidity": 90, 774 | "visibility": 10, 775 | "pressure": 1031, 776 | "cloudcover": 15, 777 | "heatindex": 3, 778 | "dewpoint": 2, 779 | "windchilld": -1, 780 | "windgust": 29, 781 | "feelslike": -1, 782 | "chanceofrain": 0, 783 | "chanceofremdry": 0, 784 | "chanceofwindy": 0, 785 | "chanceofovercast": 0, 786 | "chanceofsunshine": 0, 787 | "chanceoffrost": 0, 788 | "chanceofhightemp": 0, 789 | "chanceoffog": 0, 790 | "chanceofsnow": 0, 791 | "chanceofthunder": 0, 792 | "uv_index": 1 793 | }, 794 | { 795 | "time": "1900", 796 | "temperature": 18.83, 797 | "wind_speed": 15, 798 | "wind_degree": 270, 799 | "wind_dir": "W", 800 | "weather_code": 116, 801 | "weather_icons": [ 802 | [ 803 | "https://assets.weatherstack.com/images/wsymbols01_png_64/wsymbol_0004_black_low_cloud.png" 804 | ] 805 | ], 806 | "weather_descriptions": [ 807 | [ 808 | "Partly cloudy" 809 | ] 810 | ], 811 | "precip": 0, 812 | "humidity": 90, 813 | "visibility": 10, 814 | "pressure": 1031, 815 | "cloudcover": 15, 816 | "heatindex": 3, 817 | "dewpoint": 2, 818 | "windchilld": -1, 819 | "windgust": 29, 820 | "feelslike": -1, 821 | "chanceofrain": 0, 822 | "chanceofremdry": 0, 823 | "chanceofwindy": 0, 824 | "chanceofovercast": 0, 825 | "chanceofsunshine": 0, 826 | "chanceoffrost": 0, 827 | "chanceofhightemp": 0, 828 | "chanceoffog": 0, 829 | "chanceofsnow": 0, 830 | "chanceofthunder": 0, 831 | "uv_index": 1 832 | }, 833 | { 834 | "time": "2000", 835 | "temperature": 18.83, 836 | "wind_speed": 15, 837 | "wind_degree": 270, 838 | "wind_dir": "W", 839 | "weather_code": 116, 840 | "weather_icons": [ 841 | [ 842 | "https://assets.weatherstack.com/images/wsymbols01_png_64/wsymbol_0004_black_low_cloud.png" 843 | ] 844 | ], 845 | "weather_descriptions": [ 846 | [ 847 | "Partly cloudy" 848 | ] 849 | ], 850 | "precip": 0, 851 | "humidity": 90, 852 | "visibility": 10, 853 | "pressure": 1031, 854 | "cloudcover": 15, 855 | "heatindex": 3, 856 | "dewpoint": 2, 857 | "windchilld": -1, 858 | "windgust": 29, 859 | "feelslike": -1, 860 | "chanceofrain": 0, 861 | "chanceofremdry": 0, 862 | "chanceofwindy": 0, 863 | "chanceofovercast": 0, 864 | "chanceofsunshine": 0, 865 | "chanceoffrost": 0, 866 | "chanceofhightemp": 0, 867 | "chanceoffog": 0, 868 | "chanceofsnow": 0, 869 | "chanceofthunder": 0, 870 | "uv_index": 1 871 | }, 872 | { 873 | "time": "2100", 874 | "temperature": 18.83, 875 | "wind_speed": 15, 876 | "wind_degree": 270, 877 | "wind_dir": "W", 878 | "weather_code": 116, 879 | "weather_icons": [ 880 | [ 881 | "https://assets.weatherstack.com/images/wsymbols01_png_64/wsymbol_0004_black_low_cloud.png" 882 | ] 883 | ], 884 | "weather_descriptions": [ 885 | [ 886 | "Partly cloudy" 887 | ] 888 | ], 889 | "precip": 0, 890 | "humidity": 90, 891 | "visibility": 10, 892 | "pressure": 1031, 893 | "cloudcover": 15, 894 | "heatindex": 3, 895 | "dewpoint": 2, 896 | "windchilld": -1, 897 | "windgust": 29, 898 | "feelslike": -1, 899 | "chanceofrain": 0, 900 | "chanceofremdry": 0, 901 | "chanceofwindy": 0, 902 | "chanceofovercast": 0, 903 | "chanceofsunshine": 0, 904 | "chanceoffrost": 0, 905 | "chanceofhightemp": 0, 906 | "chanceoffog": 0, 907 | "chanceofsnow": 0, 908 | "chanceofthunder": 0, 909 | "uv_index": 1 910 | }, 911 | { 912 | "time": "2200", 913 | "temperature": 18.83, 914 | "wind_speed": 15, 915 | "wind_degree": 270, 916 | "wind_dir": "W", 917 | "weather_code": 116, 918 | "weather_icons": [ 919 | [ 920 | "https://assets.weatherstack.com/images/wsymbols01_png_64/wsymbol_0004_black_low_cloud.png" 921 | ] 922 | ], 923 | "weather_descriptions": [ 924 | [ 925 | "Partly cloudy" 926 | ] 927 | ], 928 | "precip": 0, 929 | "humidity": 90, 930 | "visibility": 10, 931 | "pressure": 1031, 932 | "cloudcover": 15, 933 | "heatindex": 3, 934 | "dewpoint": 2, 935 | "windchilld": -1, 936 | "windgust": 29, 937 | "feelslike": -1, 938 | "chanceofrain": 0, 939 | "chanceofremdry": 0, 940 | "chanceofwindy": 0, 941 | "chanceofovercast": 0, 942 | "chanceofsunshine": 0, 943 | "chanceoffrost": 0, 944 | "chanceofhightemp": 0, 945 | "chanceoffog": 0, 946 | "chanceofsnow": 0, 947 | "chanceofthunder": 0, 948 | "uv_index": 1 949 | }, 950 | { 951 | "time": "2300", 952 | "temperature": 18.83, 953 | "wind_speed": 15, 954 | "wind_degree": 270, 955 | "wind_dir": "W", 956 | "weather_code": 116, 957 | "weather_icons": [ 958 | [ 959 | "https://assets.weatherstack.com/images/wsymbols01_png_64/wsymbol_0004_black_low_cloud.png" 960 | ] 961 | ], 962 | "weather_descriptions": [ 963 | [ 964 | "Partly cloudy" 965 | ] 966 | ], 967 | "precip": 0, 968 | "humidity": 90, 969 | "visibility": 10, 970 | "pressure": 1031, 971 | "cloudcover": 15, 972 | "heatindex": 3, 973 | "dewpoint": 2, 974 | "windchilld": -1, 975 | "windgust": 29, 976 | "feelslike": -1, 977 | "chanceofrain": 0, 978 | "chanceofremdry": 0, 979 | "chanceofwindy": 0, 980 | "chanceofovercast": 0, 981 | "chanceofsunshine": 0, 982 | "chanceoffrost": 0, 983 | "chanceofhightemp": 0, 984 | "chanceoffog": 0, 985 | "chanceofsnow": 0, 986 | "chanceofthunder": 0, 987 | "uv_index": 1 988 | } 989 | ] 990 | } 991 | } 992 | } --------------------------------------------------------------------------------