diff --git a/.ddev/config.yaml b/.ddev/config.yaml index c399138..1011e92 100644 --- a/.ddev/config.yaml +++ b/.ddev/config.yaml @@ -20,27 +20,31 @@ disable_upload_dirs_warning: true # name: # Name of the project, automatically provides # http://projectname.ddev.site and https://projectname.ddev.site +# If the name is omitted, the project will take the name of the enclosing directory, +# which is useful if you want to have a copy of the project side by side with this one. -# type: # backdrop, craftcms, django4, drupal, drupal6, drupal7, laravel, magento, magento2, php, python, shopware6, silverstripe, typo3, wordpress -# See https://ddev.readthedocs.io/en/stable/users/quickstart/ for more +# type: # backdrop, cakephp, craftcms, drupal, drupal6, drupal7, drupal8, drupal9, drupal10, drupal11, generic, laravel, magento, magento2, php, shopware6, silverstripe, symfony, typo3, wordpress +# See https://docs.ddev.com/en/stable/users/quickstart/ for more # information on the different project types -# "drupal" covers recent Drupal 8+ # docroot: # Relative path to the directory containing index.php. -# php_version: "8.2" # PHP version to use, "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3" +# php_version: "8.3" # PHP version to use, "5.6" through "8.4" # You can explicitly specify the webimage but this # is not recommended, as the images are often closely tied to DDEV's' behavior, # so this can break upgrades. -# webimage: # nginx/php docker image. +# webimage: +# It’s unusual to change this option, and we don’t recommend it without Docker experience and a good reason. +# Typically, this means additions to the existing web image using a .ddev/web-build/Dockerfile.* # database: # type: # mysql, mariadb, postgres # version: # database version, like "10.11" or "8.0" -# MariaDB versions can be 5.5-10.8 and 10.11, MySQL versions can be 5.5-8.0 -# PostgreSQL versions can be 9-16. +# MariaDB versions can be 5.5-10.8, 10.11, 11.4, 11.8 +# MySQL versions can be 5.5-8.0, 8.4 +# PostgreSQL versions can be 9-17 # router_http_port: # Port to be used for http (defaults to global configuration, usually 80) # router_https_port: # Port for https (defaults to global configuration, usually 443) @@ -50,14 +54,25 @@ disable_upload_dirs_warning: true # "ddev xdebug" to enable Xdebug and "ddev xdebug off" to disable it work better, # as leaving Xdebug enabled all the time is a big performance hit. -# xhprof_enabled: false # Set to true to enable Xhprof and "ddev start" or "ddev restart" -# Note that for most people the commands -# "ddev xhprof" to enable Xhprof and "ddev xhprof off" to disable it work better, -# as leaving Xhprof enabled all the time is a big performance hit. +# xhgui_http_port: "8143" +# xhgui_https_port: "8142" +# The XHGui ports can be changed from the default 8143 and 8142 +# Very rarely used + +# host_xhgui_port: "8142" +# Can be used to change the host binding port of the XHGui +# application. Rarely used; only when port conflict and +# bind_all_ports is used (normally with router disabled) + +# xhprof_mode: [prepend|xhgui|global] +# Set to "xhgui" to enable XHGui features +# "xhgui" will become default in a future major release -# webserver_type: nginx-fpm, apache-fpm, or nginx-gunicorn +# webserver_type: nginx-fpm, apache-fpm, generic # timezone: Europe/Berlin +# If timezone is unset, DDEV will attempt to derive it from the host system timezone +# using the $TZ environment variable or the /etc/localtime symlink. # This is the timezone used in the containers and by PHP; # it can be set to any valid timezone, # see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones @@ -77,16 +92,14 @@ disable_upload_dirs_warning: true # - preview # - snapshot # Alternatively, an explicit Composer version may be specified, for example "2.2.18". -# To reinstall Composer after the image was built, run "ddev debug refresh". +# To reinstall Composer after the image was built, run "ddev debug rebuild". -# nodejs_version: "20" +# nodejs_version: "22" # change from the default system Node.js version to any other version. -# Numeric version numbers can be complete (i.e. 18.15.0) or -# incomplete (18, 17.2, 16). 'lts' and 'latest' can be used as well along with -# other named releases. -# see https://www.npmjs.com/package/n#specifying-nodejs-versions -# Note that you can continue using 'ddev nvm' or nvm inside the web container -# to change the project's installed node version if you need to. +# See https://docs.ddev.com/en/stable/users/configuration/config/#nodejs_version for more information +# and https://www.npmjs.com/package/n#specifying-nodejs-versions for the full documentation, +# Note that using of 'ddev nvm' is discouraged because "nodejs_version" is much easier to use, +# can specify any version, and is more robust than using 'nvm'. # corepack_enable: false # Change to 'true' to 'corepack enable' and gain access to latest versions of yarn/pnpm @@ -119,7 +132,7 @@ disable_upload_dirs_warning: true # ddev_version_constraint: "" # Example: -# ddev_version_constraint: ">= 1.22.4" +# ddev_version_constraint: ">= 1.24.8" # This will enforce that the running ddev version is within this constraint. # See https://github.com/Masterminds/semver#checking-version-constraints for # supported constraint formats @@ -148,8 +161,8 @@ disable_upload_dirs_warning: true # - "mutagen": enables Mutagen for this project. # - "nfs": enables NFS for this project. # -# See https://ddev.readthedocs.io/en/stable/users/install/performance/#nfs -# See https://ddev.readthedocs.io/en/stable/users/install/performance/#mutagen +# See https://docs.ddev.com/en/stable/users/install/performance/#nfs +# See https://docs.ddev.com/en/stable/users/install/performance/#mutagen # fail_on_hook_fail: False # Decide whether 'ddev start' should be interrupted by a failing hook @@ -198,17 +211,17 @@ disable_upload_dirs_warning: true # ngrok_args: --basic-auth username:pass1234 # Provide extra flags to the "ngrok http" command, see -# https://ngrok.com/docs/ngrok-agent/config or run "ngrok http -h" +# https://ngrok.com/docs/agent/config/v3/#agent-configuration or run "ngrok http -h" # disable_settings_management: false # If true, DDEV will not create CMS-specific settings files like -# Drupal's settings.php/settings.ddev.php or TYPO3's AdditionalConfiguration.php +# Drupal's settings.php/settings.ddev.php or TYPO3's additional.php # In this case the user must provide all such settings. # You can inject environment variables into the web container with: # web_environment: -# - SOMEENV=somevalue -# - SOMEOTHERENV=someothervalue +# - SOMEENV=somevalue +# - SOMEOTHERENV=someothervalue # no_project_mount: false # (Experimental) If true, DDEV will not mount the project into the web container; @@ -276,7 +289,7 @@ disable_upload_dirs_warning: true # Many DDEV commands can be extended to run tasks before or after the # DDEV command is executed, for example "post-start", "post-import-db", # "pre-composer", "post-composer" -# See https://ddev.readthedocs.io/en/stable/users/extend/custom-commands/ for more +# See https://docs.ddev.com/en/stable/users/extend/custom-commands/ for more # information on the commands that can be extended and the tasks you can define # for them. Example: #hooks: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8274e4a..97b4a8a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php: ['8.1', '8.2', '8.3'] + php: ['8.1', '8.2', '8.3', '8.4'] steps: - name: Checkout code diff --git a/composer.json b/composer.json index 4764058..2697262 100644 --- a/composer.json +++ b/composer.json @@ -6,25 +6,25 @@ "license": "MIT", "authors": [ { - "name": "André Pimpão", - "email": "a.pimpao@programmator.dev", + "name": "Programmator", + "email": "hotline@programmator.dev", "homepage": "https://programmator.dev" } ], "require": { "php": ">=8.1", - "myclabs/deep-copy": "^1.11", - "programmatordev/php-api-sdk": "^1.0", - "programmatordev/yet-another-php-validator": "^1.1" + "myclabs/deep-copy": "^1.13", + "programmatordev/php-api-sdk": "^2.1", + "symfony/options-resolver": "^6.4|^7.3" }, "require-dev": { - "monolog/monolog": "^3.6", + "monolog/monolog": "^3.9", "nyholm/psr7": "^1.8", "php-http/mock-client": "^1.6", "phpunit/phpunit": "^10.5", - "symfony/cache": "^6.4", - "symfony/http-client": "^6.4", - "symfony/var-dumper": "^6.4" + "symfony/cache": "^6.4|^7.3", + "symfony/http-client": "^6.4|^7.3", + "symfony/var-dumper": "^6.4|^7.3" }, "provide": { "psr/http-client-implementation": "1.0", diff --git a/docs/02-configuration.md b/docs/02-configuration.md index 7416324..691a74b 100644 --- a/docs/02-configuration.md +++ b/docs/02-configuration.md @@ -72,7 +72,7 @@ $api = new OpenWeatherMap('yourapikey', [ > To get to know about all the available methods, make sure to check the documentation [here](https://github.com/programmatordev/php-api-sdk?tab=readme-ov-file#documentation). The following sections have examples of some of the most important methods, -particularly related with the configuration of the client, cache and logger. +particularly related to the configuration of the client, cache and logger. ### `setClientBuilder` diff --git a/docs/03-supported-apis.md b/docs/03-supported-apis.md index de77325..a4de66a 100644 --- a/docs/03-supported-apis.md +++ b/docs/03-supported-apis.md @@ -5,6 +5,10 @@ - [getWeather](#getweather) - [getWeatherByDate](#getweatherbydate) - [getWeatherSummaryByDate](#getweathersummarybydate) + - [getWeatherOverviewByDate](#getweatheroverviewbydate) + - [AI Assistant](#ai-assistant) + - [startSession](#startsession) + - [resumeSession](#resumesession) - [Weather](#weather) - [getCurrent](#getcurrent) - [getForecast](#getforecast) @@ -68,6 +72,51 @@ Returns a [`WeatherSummary`](05-entities.md#weathersummary) object: $weatherSummary = $api->oneCall()->getWeatherSummaryByDate(50, 50, new \DateTime('1985-07-19')); ``` +#### `getWeatherOverviewByDate` + +```php +getWeatherOverviewByDate(float $latitude, float $longitude, \DateTimeInterface $date): WeatherOverview +``` + +Get the weather overview with a human-readable summary for today and tomorrow's forecast, +using OpenWeather AI. + +Returns a [`WeatherOverview`](05-entities.md#weatheroverview) object: + +```php +$weatherOverview = $api->oneCall()->getWeatherOverviewByDate(50, 50, new \DateTime('today')); +``` + +### AI Assistant + +#### `startSession` + +```php +startSession(string $prompt): Answer +``` + +Start a new session (create a new conversation) with the Weather AI Assistant. + +Returns a [`Answer`](05-entities.md#answer) object: + +```php +$answer = $api->assistant()->startSession('How is the weather today in Lisbon?'); +``` + +#### `resumeSession` + +```php +resumeSession(string $sessionId, string $prompt): Answer +``` + +Resume a session (continue a conversation) with the Weather AI Assistant. + +Returns a [`Answer`](05-entities.md#answer) object: + +```php +$answer = $api->assistant()->resumeSession('session-id', 'Do I need an umbrella?'); +``` + ### Weather #### `getCurrent` @@ -176,7 +225,7 @@ $locations = $api->geocoding()->getByLocationName('lisbon'); getByCoordinate(float $latitude, float $longitude, int $numResults = 5): array ``` -Get name of the location (city name or area name) by using geographical coordinates (latitude, longitude). +Get the name of the location (city name or area name) by using geographical coordinates (latitude, longitude). Returns an array of [`Location`](05-entities.md#location) objects. @@ -245,7 +294,7 @@ withCacheTtl(?int $ttl): self Makes a request and saves into cache for the provided duration in seconds. Semantics of values: -- `0`, the response will not be cached (if the servers specifies no `max-age`). +- `0`, the response will not be cached (if the server specifies no `max-age`). - `null`, the response will be cached for as long as it can (forever). > [!NOTE] diff --git a/docs/04-error-handling.md b/docs/04-error-handling.md index aee7575..b50e764 100644 --- a/docs/04-error-handling.md +++ b/docs/04-error-handling.md @@ -1,8 +1,5 @@ # Error Handling -- [API Errors](#api-errors) -- [Validation Errors](#validation-errors) - ## API Errors To handle API response errors, multiple exceptions are provided. You can see all available in the following example: @@ -15,13 +12,9 @@ use ProgrammatorDev\OpenWeatherMap\Exception\UnauthorizedException; use ProgrammatorDev\OpenWeatherMap\Exception\UnexpectedErrorException; try { - $location = $api->geocoding()->getByZipCode('1000-001', 'pt'); - $coordinate = $location->getCoordinate(); + // ... - $weather = $api->oneCall()->getWeather( - $coordinate->getLatitude(), - $coordinate->getLongitude() - ); + $weather = $api->oneCall()->getWeather($latitude, $longitude); } // bad request to the API catch (BadRequestException $exception) { @@ -33,7 +26,7 @@ catch (UnauthorizedException $exception) { echo $exception->getCode(); // 401 echo $exception->getMessage(); } -// resource not found +// resource not found, // for example, when trying to get a location with a zip code that does not exist catch (NotFoundException $exception) { echo $exception->getCode(); // 404 @@ -57,35 +50,13 @@ To catch all API errors with a single exception, `ApiErrorException` is availabl use ProgrammatorDev\OpenWeatherMap\Exception\ApiErrorException; try { - $location = $api->geocoding()->getByZipCode('1000-001', 'pt'); - $coordinate = $location->getCoordinate(); - - $weather = $api->oneCall()->getWeather( - $coordinate->getLatitude(), - $coordinate->getLongitude() - ); + // ... + + $weather = $api->oneCall()->getWeather($latitude, $longitude); } // catches all API response errors catch (ApiErrorException $exception) { echo $exception->getCode(); echo $exception->getMessage(); } -``` - -## Validation Errors - -To catch invalid input data (like an out of range coordinate, blank location name, etc.), the `ValidationException` is available: - -```php -use ProgrammatorDev\Validator\Exception\ValidationException; - -try { - // an invalid latitude value is given - $weather = $api->weather()->getCurrent(999, 50); -} -catch (ValidationException $exception) { - // should print: - // The latitude value should be between -90 and 90, 999 given. - echo $exception->getMessage(); -} ``` \ No newline at end of file diff --git a/docs/05-entities.md b/docs/05-entities.md index 5b136e2..20267d4 100644 --- a/docs/05-entities.md +++ b/docs/05-entities.md @@ -4,6 +4,7 @@ - [Weather](#weather) - [WeatherMoment](#weathermoment) - [WeatherSummary](#weathersummary) + - [WeatherOverview](#weatheroverview) - [WeatherData](#weatherdata) - [MinuteData](#minutedata) - [HourData](#hourdata) @@ -11,10 +12,13 @@ - [Alert](#alert) - [MoonPhase](#moonphase) - [Temperature](#temperature) +- [AI Assistant](#ai-assistant) + - [Answer](#answer) + - [WeatherData](#weatherdata-1) - [Weather](#weather-1) - [Weather](#weather-2) - [WeatherCollection](#weathercollection) - - [WeatherData](#weatherdata) + - [WeatherData](#weatherdata-2) - [Air Pollution](#air-pollution) - [AirPollution](#airpollution) - [AirPollutionCollection](#airpollutioncollection) @@ -78,6 +82,13 @@ - `getAtmosphericPressure()`: `int` - `getWind()`: [`Wind`](#wind) +### WeatherOverview + +- `getCoordinate()`: [`Coordinate`](#coordinate) +- `getTimezone()`: [`Timezone`](#timezone) +- `getDateTime()`: `\DateTimeImmutable` +- `getOverview()`: `string` + ### WeatherData - `getDateTime()`: `\DateTimeImmutable` @@ -164,6 +175,33 @@ - `getMin()`: `?float` - `getMax()`: `?float` +## AI Assistant + +### Answer + +- `getAnswer()`: `string` +- `getSessionId()`: `string` +- `getData()`: [`WeatherData[]`](#weatherdata-1) + +### WeatherData + +- `getLocationName()`: `string` +- `getDateTime()`: `\DateTimeImmutable` +- `getTemperature()`: `float` +- `getTemperatureFeelsLike()`: `float` +- `getAtmosphericPressure()`: `int` +- `getVisibility()`: `?int` +- `getHumidity()`: `int` +- `getDewPoint()`: `float` +- `getUltraVioletIndex()`: `?float` +- `getCloudiness()`: `int` +- `getWind()`: [`Wind`](#wind) +- `getConditions()`: [`Condition[]`](#condition) +- `getRainVolume()`: `?float` +- `getSnowVolume()`: `?float` +- `getSunriseAt()`: `?\DateTimeImmutable` +- `getSunsetAt()`: `?\DateTimeImmutable` + ## Weather ### Weather diff --git a/src/Entity/AirPollution/AirPollutionCollection.php b/src/Entity/AirPollution/AirPollutionCollection.php index 6dcbf46..192834a 100644 --- a/src/Entity/AirPollution/AirPollutionCollection.php +++ b/src/Entity/AirPollution/AirPollutionCollection.php @@ -3,12 +3,10 @@ namespace ProgrammatorDev\OpenWeatherMap\Entity\AirPollution; use ProgrammatorDev\OpenWeatherMap\Entity\Coordinate; -use ProgrammatorDev\OpenWeatherMap\Util\EntityTrait; +use ProgrammatorDev\OpenWeatherMap\Helper\EntityHelper; class AirPollutionCollection { - use EntityTrait; - private int $numResults; private Coordinate $coordinate; @@ -18,9 +16,9 @@ class AirPollutionCollection public function __construct(array $data) { - $this->numResults = \count($data['list']); + $this->numResults = count($data['list']); $this->coordinate = new Coordinate($data['coord']); - $this->data = $this->createEntityList(AirPollutionData::class, $data['list']); + $this->data = EntityHelper::createEntityList(AirPollutionData::class, $data['list']); } public function getNumResults(): int diff --git a/src/Entity/AirPollution/AirQuality.php b/src/Entity/AirPollution/AirQuality.php index ddc2d6c..8e3c50e 100644 --- a/src/Entity/AirPollution/AirQuality.php +++ b/src/Entity/AirPollution/AirQuality.php @@ -28,12 +28,12 @@ private function findQualitativeName(int $index): string { // levels based on https://openweathermap.org/api/air-pollution return match ($index) { - 0 => 'Undefined', 1 => 'Good', 2 => 'Fair', 3 => 'Moderate', 4 => 'Poor', - 5 => 'Very Poor' + 5 => 'Very Poor', + default => 'Undefined' }; } } \ No newline at end of file diff --git a/src/Entity/Assistant/Answer.php b/src/Entity/Assistant/Answer.php new file mode 100644 index 0000000..28396e7 --- /dev/null +++ b/src/Entity/Assistant/Answer.php @@ -0,0 +1,40 @@ +answer = $data['answer']; + $this->sessionId = $data['session_id']; + + if (!empty($data['data'])) { + $this->data = EntityHelper::createEntityKeyList(WeatherData::class, $data['data']); + } + } + + public function getAnswer(): string + { + return $this->answer; + } + + public function getSessionId(): string + { + return $this->sessionId; + } + + public function getData(): array + { + return $this->data; + } +} \ No newline at end of file diff --git a/src/Entity/Assistant/WeatherData.php b/src/Entity/Assistant/WeatherData.php new file mode 100644 index 0000000..cd07e37 --- /dev/null +++ b/src/Entity/Assistant/WeatherData.php @@ -0,0 +1,62 @@ +locationName = $locationName; + $this->temperature = $data['temp']; + $this->temperatureFeelsLike = $data['feels_like']; + $this->visibility = $data['visibility']; + $this->sunriseAt = \DateTimeImmutable::createFromFormat('U', $data['sunrise']); + $this->sunsetAt = \DateTimeImmutable::createFromFormat('U', $data['sunset']); + } + + public function getLocationName(): string + { + return $this->locationName; + } + + public function getTemperature(): float + { + return $this->temperature; + } + + public function getTemperatureFeelsLike(): float + { + return $this->temperatureFeelsLike; + } + + public function getVisibility(): int + { + return $this->visibility; + } + + public function getSunriseAt(): \DateTimeImmutable + { + return $this->sunriseAt; + } + + public function getSunsetAt(): \DateTimeImmutable + { + return $this->sunsetAt; + } +} \ No newline at end of file diff --git a/src/Entity/OneCall/BaseWeather.php b/src/Entity/BaseWeather.php similarity index 86% rename from src/Entity/OneCall/BaseWeather.php rename to src/Entity/BaseWeather.php index dfa9f69..a7e897f 100644 --- a/src/Entity/OneCall/BaseWeather.php +++ b/src/Entity/BaseWeather.php @@ -1,15 +1,11 @@ $data['wind_gust'] ?? null ]); - $this->conditions = $this->createEntityList(Condition::class, $data['weather']); + $this->conditions = EntityHelper::createEntityList(Condition::class, $data['weather']); $this->rainVolume = $data['rain']['1h'] ?? $data['rain']['3h'] ?? $data['rain'] ?? null; $this->snowVolume = $data['snow']['1h'] ?? $data['snow']['3h'] ?? $data['snow'] ?? null; } diff --git a/src/Entity/Icon.php b/src/Entity/Icon.php index 7b179ba..903399a 100644 --- a/src/Entity/Icon.php +++ b/src/Entity/Icon.php @@ -11,7 +11,7 @@ class Icon public function __construct(array $data) { $this->id = $data['icon']; - $this->url = \sprintf('https://openweathermap.org/img/wn/%s@4x.png', $this->id); + $this->url = sprintf('https://openweathermap.org/img/wn/%s@4x.png', $this->id); } public function getId(): string diff --git a/src/Entity/OneCall/DayData.php b/src/Entity/OneCall/DayData.php index 11cd28e..e15467b 100644 --- a/src/Entity/OneCall/DayData.php +++ b/src/Entity/OneCall/DayData.php @@ -2,6 +2,8 @@ namespace ProgrammatorDev\OpenWeatherMap\Entity\OneCall; +use ProgrammatorDev\OpenWeatherMap\Entity\BaseWeather; + class DayData extends BaseWeather { private Temperature $temperature; @@ -28,7 +30,7 @@ public function __construct(array $data) $this->temperature = new Temperature($data['temp']); $this->temperatureFeelsLike = new Temperature($data['feels_like']); - $this->precipitationProbability = \round($data['pop'] * 100); + $this->precipitationProbability = round($data['pop'] * 100); $this->summary = $data['summary']; $this->moonPhase = new MoonPhase($data); $this->moonriseAt = \DateTimeImmutable::createFromFormat('U', $data['moonrise']); diff --git a/src/Entity/OneCall/HourData.php b/src/Entity/OneCall/HourData.php index e417dc2..5eaf765 100644 --- a/src/Entity/OneCall/HourData.php +++ b/src/Entity/OneCall/HourData.php @@ -2,6 +2,8 @@ namespace ProgrammatorDev\OpenWeatherMap\Entity\OneCall; +use ProgrammatorDev\OpenWeatherMap\Entity\BaseWeather; + class HourData extends BaseWeather { private float $temperature; @@ -19,7 +21,7 @@ public function __construct(array $data) $this->temperature = $data['temp']; $this->temperatureFeelsLike = $data['feels_like']; $this->visibility = $data['visibility']; - $this->precipitationProbability = \round($data['pop'] * 100); + $this->precipitationProbability = round($data['pop'] * 100); } public function getTemperature(): float diff --git a/src/Entity/OneCall/MoonPhase.php b/src/Entity/OneCall/MoonPhase.php index 3d036f8..b13828b 100644 --- a/src/Entity/OneCall/MoonPhase.php +++ b/src/Entity/OneCall/MoonPhase.php @@ -23,7 +23,7 @@ public function __construct(array $data) { $this->value = $data['moon_phase']; $this->systemName = $this->findSystemName($this->value); - $this->name = \ucwords(\strtolower(\str_replace('_', ' ', $this->systemName))); + $this->name = ucwords(strtolower(str_replace('_', ' ', $this->systemName))); } public function getValue(): float diff --git a/src/Entity/OneCall/Weather.php b/src/Entity/OneCall/Weather.php index 945c6ec..09395e1 100644 --- a/src/Entity/OneCall/Weather.php +++ b/src/Entity/OneCall/Weather.php @@ -4,12 +4,10 @@ use ProgrammatorDev\OpenWeatherMap\Entity\Coordinate; use ProgrammatorDev\OpenWeatherMap\Entity\Timezone; -use ProgrammatorDev\OpenWeatherMap\Util\EntityTrait; +use ProgrammatorDev\OpenWeatherMap\Helper\EntityHelper; class Weather { - use EntityTrait; - private Coordinate $coordinate; private Timezone $timezone; @@ -43,14 +41,14 @@ public function __construct(array $data) $this->current = new WeatherData($data['current']); $this->minutelyForecast = isset($data['minutely']) - ? $this->createEntityList(MinuteData::class, $data['minutely']) + ? EntityHelper::createEntityList(MinuteData::class, $data['minutely']) : null; - $this->hourlyForecast = $this->createEntityList(HourData::class, $data['hourly']); - $this->dailyForecast = $this->createEntityList(DayData::class, $data['daily']); + $this->hourlyForecast = EntityHelper::createEntityList(HourData::class, $data['hourly']); + $this->dailyForecast = EntityHelper::createEntityList(DayData::class, $data['daily']); $this->alerts = isset($data['alerts']) - ? $this->createEntityList(Alert::class, $data['alerts']) + ? EntityHelper::createEntityList(Alert::class, $data['alerts']) : null; } diff --git a/src/Entity/OneCall/WeatherData.php b/src/Entity/OneCall/WeatherData.php index 2811290..0154344 100644 --- a/src/Entity/OneCall/WeatherData.php +++ b/src/Entity/OneCall/WeatherData.php @@ -2,6 +2,8 @@ namespace ProgrammatorDev\OpenWeatherMap\Entity\OneCall; +use ProgrammatorDev\OpenWeatherMap\Entity\BaseWeather; + class WeatherData extends BaseWeather { private float $temperature; diff --git a/src/Entity/OneCall/WeatherOverview.php b/src/Entity/OneCall/WeatherOverview.php new file mode 100644 index 0000000..6e2fed5 --- /dev/null +++ b/src/Entity/OneCall/WeatherOverview.php @@ -0,0 +1,53 @@ +coordinate = new Coordinate($data); + + $this->timezone = new Timezone([ + 'timezone_offset' => \DateTimeImmutable::createFromFormat('P', $data['tz'])->getOffset() + ]); + + $this->dateTime = \DateTimeImmutable::createFromFormat( + 'Y-m-d H:i:s P', + sprintf('%s 00:00:00 %s', $data['date'], $data['tz']) + ); + + $this->overview = $data['weather_overview']; + } + + public function getCoordinate(): Coordinate + { + return $this->coordinate; + } + + public function getTimezone(): Timezone + { + return $this->timezone; + } + + public function getDateTime(): \DateTimeImmutable + { + return $this->dateTime; + } + + public function getOverview(): string + { + return $this->overview; + } +} \ No newline at end of file diff --git a/src/Entity/OneCall/WeatherSummary.php b/src/Entity/OneCall/WeatherSummary.php index 4396839..4fb3af0 100644 --- a/src/Entity/OneCall/WeatherSummary.php +++ b/src/Entity/OneCall/WeatherSummary.php @@ -36,7 +36,7 @@ public function __construct(array $data) $this->dateTime = \DateTimeImmutable::createFromFormat( 'Y-m-d H:i:s P', - \sprintf('%s 00:00:00 %s', $data['date'], $data['tz']) + sprintf('%s 00:00:00 %s', $data['date'], $data['tz']) ); $this->cloudiness = \round($data['cloud_cover']['afternoon']); @@ -52,11 +52,11 @@ public function __construct(array $data) 'max' => $data['temperature']['max'] ]); - $this->atmosphericPressure = \round($data['pressure']['afternoon']); + $this->atmosphericPressure = round($data['pressure']['afternoon']); $this->wind = new Wind([ 'speed' => $data['wind']['max']['speed'], - 'deg' => \round($data['wind']['max']['direction']) + 'deg' => round($data['wind']['max']['direction']) ]); } @@ -70,9 +70,6 @@ public function getTimezone(): Timezone return $this->timezone; } - /** - * DateTime in UTC - */ public function getDateTime(): \DateTimeImmutable { return $this->dateTime; diff --git a/src/Entity/Weather/WeatherCollection.php b/src/Entity/Weather/WeatherCollection.php index ae7486d..dbb7f3d 100644 --- a/src/Entity/Weather/WeatherCollection.php +++ b/src/Entity/Weather/WeatherCollection.php @@ -3,12 +3,10 @@ namespace ProgrammatorDev\OpenWeatherMap\Entity\Weather; use ProgrammatorDev\OpenWeatherMap\Entity\Location; -use ProgrammatorDev\OpenWeatherMap\Util\EntityTrait; +use ProgrammatorDev\OpenWeatherMap\Helper\EntityHelper; class WeatherCollection { - use EntityTrait; - private int $numResults; private Location $location; @@ -32,7 +30,7 @@ public function __construct(array $data) 'timezone_offset' => $data['city']['timezone'] ]); - $this->data = $this->createEntityList(WeatherData::class, $data['list']); + $this->data = EntityHelper::createEntityList(WeatherData::class, $data['list']); } public function getNumResults(): int diff --git a/src/Entity/Weather/WeatherData.php b/src/Entity/Weather/WeatherData.php index fae2605..b44f7ed 100644 --- a/src/Entity/Weather/WeatherData.php +++ b/src/Entity/Weather/WeatherData.php @@ -4,12 +4,10 @@ use ProgrammatorDev\OpenWeatherMap\Entity\Condition; use ProgrammatorDev\OpenWeatherMap\Entity\Wind; -use ProgrammatorDev\OpenWeatherMap\Util\EntityTrait; +use ProgrammatorDev\OpenWeatherMap\Helper\EntityHelper; class WeatherData { - use EntityTrait; - private \DateTimeImmutable $dateTime; private float $temperature; @@ -50,11 +48,11 @@ public function __construct(array $data) $this->cloudiness = $data['clouds']['all']; $this->visibility = $data['visibility']; $this->atmosphericPressure = $data['main']['pressure']; - $this->conditions = $this->createEntityList(Condition::class, $data['weather']); + $this->conditions = EntityHelper::createEntityList(Condition::class, $data['weather']); $this->wind = new Wind($data['wind']); $this->precipitationProbability = isset($data['pop']) - ? \round($data['pop'] * 100) + ? round($data['pop'] * 100) : null; $this->rainVolume = $data['rain']['1h'] ?? $data['rain']['3h'] ?? null; diff --git a/src/Exception/ApiErrorException.php b/src/Exception/ApiErrorException.php index d10f5f3..88209f3 100644 --- a/src/Exception/ApiErrorException.php +++ b/src/Exception/ApiErrorException.php @@ -8,7 +8,9 @@ class ApiErrorException extends \Exception public function __construct(array $error) { - parent::__construct($error['message'], $error['cod']); + $code = $error['cod'] ?? $error['code']; + + parent::__construct($error['message'], $code); $this->parameters = $error['parameters'] ?? null; } diff --git a/src/Helper/EntityHelper.php b/src/Helper/EntityHelper.php new file mode 100644 index 0000000..2993ca6 --- /dev/null +++ b/src/Helper/EntityHelper.php @@ -0,0 +1,20 @@ +getConstants(); // Sort by alphabetical order // to be more intuitive when listing values for error messages - \asort($constants); + asort($constants); return $constants; } diff --git a/src/Language/Language.php b/src/Language/Language.php index a92c795..9c914b9 100644 --- a/src/Language/Language.php +++ b/src/Language/Language.php @@ -2,12 +2,10 @@ namespace ProgrammatorDev\OpenWeatherMap\Language; -use ProgrammatorDev\OpenWeatherMap\Util\ReflectionTrait; +use ProgrammatorDev\OpenWeatherMap\Helper\ReflectionHelper; class Language { - use ReflectionTrait; - public const AFRIKAANS = 'af'; public const ALBANIAN = 'al'; public const ARABIC = 'ar'; @@ -57,6 +55,6 @@ class Language public static function getOptions(): array { - return (new Language)->getClassConstants(self::class); + return ReflectionHelper::getClassConstants(self::class); } } \ No newline at end of file diff --git a/src/OpenWeatherMap.php b/src/OpenWeatherMap.php index 845c29c..af09872 100644 --- a/src/OpenWeatherMap.php +++ b/src/OpenWeatherMap.php @@ -13,22 +13,28 @@ use ProgrammatorDev\OpenWeatherMap\Exception\UnexpectedErrorException; use ProgrammatorDev\OpenWeatherMap\Language\Language; use ProgrammatorDev\OpenWeatherMap\Resource\AirPollutionResource; +use ProgrammatorDev\OpenWeatherMap\Resource\AssistantResource; use ProgrammatorDev\OpenWeatherMap\Resource\GeocodingResource; use ProgrammatorDev\OpenWeatherMap\Resource\OneCallResource; use ProgrammatorDev\OpenWeatherMap\Resource\WeatherResource; use ProgrammatorDev\OpenWeatherMap\UnitSystem\UnitSystem; +use Symfony\Component\OptionsResolver\OptionsResolver; class OpenWeatherMap extends Api { - private array $options; + public readonly array $options; + + private OptionsResolver $optionsResolver; public function __construct( - #[\SensitiveParameter] private string $apiKey, + #[\SensitiveParameter] public readonly string $apiKey, array $options = [] ) { parent::__construct(); + $this->optionsResolver = new OptionsResolver(); + $this->options = $this->configureOptions($options); $this->configureApi(); } @@ -38,6 +44,11 @@ public function oneCall(): OneCallResource return new OneCallResource($this); } + public function assistant(): AssistantResource + { + return new AssistantResource($this); + } + public function weather(): WeatherResource { return new WeatherResource($this); @@ -81,7 +92,7 @@ private function configureApi(): void // if there was a response with an error status code if ($statusCode >= 400) { - $error = \json_decode($response->getBody()->getContents(), true); + $error = json_decode($response->getBody()->getContents(), true); match ($statusCode) { 400 => throw new BadRequestException($error), @@ -96,7 +107,7 @@ private function configureApi(): void $this->addResponseContentsListener(function(ResponseContentsEvent $event) { // decode json string response into an array $contents = $event->getContents(); - $contents = \json_decode($contents, true); + $contents = json_decode($contents, true); $event->setContents($contents); }); diff --git a/src/Resource/AirPollutionResource.php b/src/Resource/AirPollutionResource.php index 4f40ae1..48980cc 100644 --- a/src/Resource/AirPollutionResource.php +++ b/src/Resource/AirPollutionResource.php @@ -5,7 +5,6 @@ use ProgrammatorDev\Api\Method; use ProgrammatorDev\OpenWeatherMap\Entity\AirPollution\AirPollution; use ProgrammatorDev\OpenWeatherMap\Entity\AirPollution\AirPollutionCollection; -use ProgrammatorDev\Validator\Exception\ValidationException; use Psr\Http\Client\ClientExceptionInterface; class AirPollutionResource extends Resource @@ -13,13 +12,10 @@ class AirPollutionResource extends Resource /** * Get access to current air pollution data * - * @throws ValidationException * @throws ClientExceptionInterface */ public function getCurrent(float $latitude, float $longitude): AirPollution { - $this->validateCoordinate($latitude, $longitude); - $data = $this->api->request( method: Method::GET, path: '/data/2.5/air_pollution', @@ -35,13 +31,10 @@ public function getCurrent(float $latitude, float $longitude): AirPollution /** * Get access to air pollution forecast data per hour * - * @throws ValidationException * @throws ClientExceptionInterface */ public function getForecast(float $latitude, float $longitude): AirPollutionCollection { - $this->validateCoordinate($latitude, $longitude); - $data = $this->api->request( method: Method::GET, path: '/data/2.5/air_pollution/forecast', @@ -57,7 +50,6 @@ public function getForecast(float $latitude, float $longitude): AirPollutionColl /** * Get access to historical air pollution data per hour between two dates * - * @throws ValidationException * @throws ClientExceptionInterface */ public function getHistory( @@ -67,9 +59,6 @@ public function getHistory( \DateTimeInterface $endDate ): AirPollutionCollection { - $this->validateCoordinate($latitude, $longitude); - $this->validateDateOrder($startDate, $endDate); - $utcTimezone = new \DateTimeZone('UTC'); $data = $this->api->request( diff --git a/src/Resource/AssistantResource.php b/src/Resource/AssistantResource.php new file mode 100644 index 0000000..7d06c16 --- /dev/null +++ b/src/Resource/AssistantResource.php @@ -0,0 +1,49 @@ +api->setAuthentication(new Header('X-Api-Key', $this->api->apiKey)); + + $data = $this->api->request( + method: Method::POST, + path: '/assistant/session', + body: json_encode(['prompt' => $prompt]) + ); + + return new Answer($data); + } + + /** + * Resume a session (continue a conversation) with the Weather AI Assistant + * + * @throws ClientExceptionInterface + */ + public function resumeSession(string $sessionId, string $prompt): Answer + { + $this->api->setAuthentication(new Header('X-Api-Key', $this->api->apiKey)); + + $data = $this->api->request( + method: Method::POST, + path: $this->api->buildPath('/assistant/session/{sessionId}', [ + 'sessionId' => $sessionId + ]), + body: json_encode(['prompt' => $prompt]) + ); + + return new Answer($data); + } +} \ No newline at end of file diff --git a/src/Resource/GeocodingResource.php b/src/Resource/GeocodingResource.php index 263c597..dfdcd87 100644 --- a/src/Resource/GeocodingResource.php +++ b/src/Resource/GeocodingResource.php @@ -5,28 +5,21 @@ use ProgrammatorDev\Api\Method; use ProgrammatorDev\OpenWeatherMap\Entity\Geocoding\ZipLocation; use ProgrammatorDev\OpenWeatherMap\Entity\Location; -use ProgrammatorDev\OpenWeatherMap\Util\EntityTrait; -use ProgrammatorDev\Validator\Exception\ValidationException; +use ProgrammatorDev\OpenWeatherMap\Helper\EntityHelper; use Psr\Http\Client\ClientExceptionInterface; class GeocodingResource extends Resource { - use EntityTrait; - private const NUM_RESULTS = 5; /** * Get geographical coordinates (latitude, longitude) by using the name of the location (city name or area name) * * @return Location[] - * @throws ValidationException * @throws ClientExceptionInterface */ public function getByLocationName(string $locationName, int $numResults = self::NUM_RESULTS): array { - $this->validateQuery($locationName, 'locationName'); - $this->validatePositive($numResults, 'numResults'); - $data = $this->api->request( method: Method::GET, path: '/geo/1.0/direct', @@ -36,22 +29,18 @@ public function getByLocationName(string $locationName, int $numResults = self:: ] ); - return $this->createEntityList(Location::class, $data); + return EntityHelper::createEntityList(Location::class, $data); } /** * Get geographical coordinates (latitude, longitude) by using the zip/postal code * - * @throws ValidationException * @throws ClientExceptionInterface */ public function getByZipCode(string $zipCode, string $countryCode): ZipLocation { - $this->validateQuery($zipCode, 'zipCode'); - $this->validateCountryCode($countryCode); - $data = $this->api->request( - method: 'GET', + method: Method::GET, path: '/geo/1.0/zip', query: [ 'zip' => \sprintf('%s,%s', $zipCode, $countryCode) @@ -62,17 +51,13 @@ public function getByZipCode(string $zipCode, string $countryCode): ZipLocation } /** - * Get name of the location (city name or area name) by using geographical coordinates (latitude, longitude) + * Get the name of the location (city name or area name) by using geographical coordinates (latitude, longitude) * * @return Location[] - * @throws ValidationException * @throws ClientExceptionInterface */ public function getByCoordinate(float $latitude, float $longitude, int $numResults = self::NUM_RESULTS): array { - $this->validateCoordinate($latitude, $longitude); - $this->validatePositive($numResults, 'numResults'); - $data = $this->api->request( method: Method::GET, path: '/geo/1.0/reverse', @@ -83,6 +68,6 @@ public function getByCoordinate(float $latitude, float $longitude, int $numResul ] ); - return $this->createEntityList(Location::class, $data); + return EntityHelper::createEntityList(Location::class, $data); } } \ No newline at end of file diff --git a/src/Resource/OneCallResource.php b/src/Resource/OneCallResource.php index 427f2ce..375dede 100644 --- a/src/Resource/OneCallResource.php +++ b/src/Resource/OneCallResource.php @@ -5,10 +5,10 @@ use ProgrammatorDev\Api\Method; use ProgrammatorDev\OpenWeatherMap\Entity\OneCall\Weather; use ProgrammatorDev\OpenWeatherMap\Entity\OneCall\WeatherMoment; +use ProgrammatorDev\OpenWeatherMap\Entity\OneCall\WeatherOverview; use ProgrammatorDev\OpenWeatherMap\Entity\OneCall\WeatherSummary; use ProgrammatorDev\OpenWeatherMap\Resource\Util\LanguageTrait; use ProgrammatorDev\OpenWeatherMap\Resource\Util\UnitSystemTrait; -use ProgrammatorDev\Validator\Exception\ValidationException; use Psr\Http\Client\ClientExceptionInterface; class OneCallResource extends Resource @@ -20,13 +20,10 @@ class OneCallResource extends Resource * Get access to current weather, minute forecast for 1 hour, hourly forecast for 48 hours, * daily forecast for 8 days and government weather alerts * - * @throws ValidationException * @throws ClientExceptionInterface */ public function getWeather(float $latitude, float $longitude): Weather { - $this->validateCoordinate($latitude, $longitude); - $data = $this->api->request( method: Method::GET, path: '/data/3.0/onecall', @@ -42,13 +39,10 @@ public function getWeather(float $latitude, float $longitude): Weather /** * Get access to weather data for any datetime * - * @throws ValidationException * @throws ClientExceptionInterface */ public function getWeatherByDate(float $latitude, float $longitude, \DateTimeInterface $dateTime): WeatherMoment { - $this->validateCoordinate($latitude, $longitude); - $utcTimezone = new \DateTimeZone('UTC'); $data = $this->api->request( @@ -67,13 +61,10 @@ public function getWeatherByDate(float $latitude, float $longitude, \DateTimeInt /** * Get access to aggregated weather data for a particular date * - * @throws ValidationException * @throws ClientExceptionInterface */ public function getWeatherSummaryByDate(float $latitude, float $longitude, \DateTimeInterface $date): WeatherSummary { - $this->validateCoordinate($latitude, $longitude); - $data = $this->api->request( method: Method::GET, path: '/data/3.0/onecall/day_summary', @@ -87,4 +78,24 @@ public function getWeatherSummaryByDate(float $latitude, float $longitude, \Date return new WeatherSummary($data); } + + /** + * Get the weather overview with a human-readable summary for today and tomorrow's forecast, using OpenWeather AI + * + * @throws ClientExceptionInterface + */ + public function getWeatherOverviewByDate(float $latitude, float $longitude, \DateTimeInterface $date): WeatherOverview + { + $data = $this->api->request( + method: Method::GET, + path: '/data/3.0/onecall/overview', + query: [ + 'lat' => $latitude, + 'lon' => $longitude, + 'date' => $date->format('Y-m-d') + ] + ); + + return new WeatherOverview($data); + } } \ No newline at end of file diff --git a/src/Resource/Resource.php b/src/Resource/Resource.php index b9f59e7..d17aef8 100644 --- a/src/Resource/Resource.php +++ b/src/Resource/Resource.php @@ -4,12 +4,10 @@ use ProgrammatorDev\OpenWeatherMap\OpenWeatherMap; use ProgrammatorDev\OpenWeatherMap\Resource\Util\CacheTrait; -use ProgrammatorDev\OpenWeatherMap\Resource\Util\ValidationTrait; class Resource { use CacheTrait; - use ValidationTrait; public function __construct(protected OpenWeatherMap $api) {} } \ No newline at end of file diff --git a/src/Resource/Util/UnitSystemTrait.php b/src/Resource/Util/UnitSystemTrait.php index 166ac87..168a220 100644 --- a/src/Resource/Util/UnitSystemTrait.php +++ b/src/Resource/Util/UnitSystemTrait.php @@ -2,20 +2,12 @@ namespace ProgrammatorDev\OpenWeatherMap\Resource\Util; -use ProgrammatorDev\Validator\Exception\ValidationException; use function DeepCopy\deep_copy; trait UnitSystemTrait { - use ValidationTrait; - - /** - * @throws ValidationException - */ public function withUnitSystem(string $unitSystem): static { - $this->validateUnitSystem($unitSystem); - $clone = deep_copy($this, true); $clone->api->addQueryDefault('units', $unitSystem); diff --git a/src/Resource/Util/ValidationTrait.php b/src/Resource/Util/ValidationTrait.php deleted file mode 100644 index 33dd7d6..0000000 --- a/src/Resource/Util/ValidationTrait.php +++ /dev/null @@ -1,71 +0,0 @@ -assert($query, $name); - } - - /** - * @throws ValidationException - */ - protected function validatePositive(int $number, string $name): void - { - Validator::greaterThan(0)->assert($number, $name); - } - - /** - * @throws ValidationException - */ - protected function validateCoordinate(float $latitude, float $longitude): void - { - Validator::range(-90, 90)->assert($latitude, 'latitude'); - Validator::range(-180, 180)->assert($longitude, 'longitude'); - } - - /** - * @throws ValidationException - */ - protected function validateCountryCode(string $countryCode): void - { - Validator::country()->assert($countryCode, 'countryCode'); - } - - /** - * @throws ValidationException - */ - protected function validateLanguage(string $language): void - { - Validator::choice(Language::getOptions())->assert($language, 'language'); - } - - /** - * @throws ValidationException - */ - protected function validateUnitSystem(string $unitSystem): void - { - Validator::choice(UnitSystem::getOptions())->assert($unitSystem, 'unitSystem'); - } - - /** - * @throws ValidationException - */ - protected function validateDateOrder(\DateTimeInterface $startDate, \DateTimeInterface $endDate): void - { - Validator::greaterThan( - constraint: $startDate, - message: 'The endDate must be after the startDate.' - )->assert($endDate); - } -} \ No newline at end of file diff --git a/src/Resource/WeatherResource.php b/src/Resource/WeatherResource.php index c4cd357..96a5b06 100644 --- a/src/Resource/WeatherResource.php +++ b/src/Resource/WeatherResource.php @@ -7,7 +7,6 @@ use ProgrammatorDev\OpenWeatherMap\Entity\Weather\WeatherCollection; use ProgrammatorDev\OpenWeatherMap\Resource\Util\LanguageTrait; use ProgrammatorDev\OpenWeatherMap\Resource\Util\UnitSystemTrait; -use ProgrammatorDev\Validator\Exception\ValidationException; use Psr\Http\Client\ClientExceptionInterface; class WeatherResource extends Resource @@ -20,13 +19,10 @@ class WeatherResource extends Resource /** * Get access to current weather data * - * @throws ValidationException * @throws ClientExceptionInterface */ public function getCurrent(float $latitude, float $longitude): Weather { - $this->validateCoordinate($latitude, $longitude); - $data = $this->api->request( method: Method::GET, path: '/data/2.5/weather', @@ -42,14 +38,10 @@ public function getCurrent(float $latitude, float $longitude): Weather /** * Get access to 5-day weather forecast data with 3-hour steps * - * @throws ValidationException * @throws ClientExceptionInterface */ public function getForecast(float $latitude, float $longitude, int $numResults = self::NUM_RESULTS): WeatherCollection { - $this->validateCoordinate($latitude, $longitude); - $this->validatePositive($numResults, 'numResults'); - $data = $this->api->request( method: Method::GET, path: '/data/2.5/forecast', diff --git a/src/Test/MockResponse.php b/src/Test/MockResponse.php index 1f9ce44..ffcd0c3 100644 --- a/src/Test/MockResponse.php +++ b/src/Test/MockResponse.php @@ -4,9 +4,13 @@ class MockResponse { + public const ASSISTANT_START_SESSION = '{"answer":"Answer text","data":{"Lisbon":{"Current UTC Time":"08 November 2025, 17:16","Current Week Day UTC":"Saturday","clouds":100,"dew_point":272.33,"dt":1762622168,"feels_like":275.34,"humidity":70,"pressure":1010,"sunrise":1762602543,"sunset":1762638067,"temp":277.19,"uvi":1.33,"visibility":10000,"weather":[{"description":"overcast clouds","icon":"04d","id":804,"main":"Clouds"}],"wind_deg":0,"wind_gust":6.17,"wind_speed":2.06}},"session_id":"62168d7b-a58d-4122-95c8-4dff55a57a76"}'; + public const ASSISTANT_RESUME_SESSION = self::ASSISTANT_START_SESSION; + public const ONE_CALL_WEATHER = '{"lat":38.7078,"lon":-9.1366,"timezone":"Europe/Lisbon","timezone_offset":3600,"current":{"dt":1688384139,"sunrise":1688361368,"sunset":1688414697,"temp":25.1,"feels_like":25.21,"pressure":1017,"humidity":59,"dew_point":16.53,"uvi":9.78,"clouds":20,"visibility":10000,"wind_speed":7.2,"wind_deg":10,"weather":[{"id":801,"main":"Clouds","description":"few clouds","icon":"02d"}]},"minutely":[{"dt":1688384160,"precipitation":0},{"dt":1688384220,"precipitation":0},{"dt":1688384280,"precipitation":0},{"dt":1688384340,"precipitation":0},{"dt":1688384400,"precipitation":0},{"dt":1688384460,"precipitation":0},{"dt":1688384520,"precipitation":0},{"dt":1688384580,"precipitation":0},{"dt":1688384640,"precipitation":0},{"dt":1688384700,"precipitation":0},{"dt":1688384760,"precipitation":0},{"dt":1688384820,"precipitation":0},{"dt":1688384880,"precipitation":0},{"dt":1688384940,"precipitation":0},{"dt":1688385000,"precipitation":0},{"dt":1688385060,"precipitation":0},{"dt":1688385120,"precipitation":0},{"dt":1688385180,"precipitation":0},{"dt":1688385240,"precipitation":0},{"dt":1688385300,"precipitation":0},{"dt":1688385360,"precipitation":0},{"dt":1688385420,"precipitation":0},{"dt":1688385480,"precipitation":0},{"dt":1688385540,"precipitation":0},{"dt":1688385600,"precipitation":0},{"dt":1688385660,"precipitation":0},{"dt":1688385720,"precipitation":0},{"dt":1688385780,"precipitation":0},{"dt":1688385840,"precipitation":0.1032},{"dt":1688385900,"precipitation":0.115},{"dt":1688385960,"precipitation":0.1276},{"dt":1688386020,"precipitation":0.1402},{"dt":1688386080,"precipitation":0.1528},{"dt":1688386140,"precipitation":0.1654},{"dt":1688386200,"precipitation":0.178},{"dt":1688386260,"precipitation":0.1654},{"dt":1688386320,"precipitation":0.1528},{"dt":1688386380,"precipitation":0.1402},{"dt":1688386440,"precipitation":0.1276},{"dt":1688386500,"precipitation":0.115},{"dt":1688386560,"precipitation":0.1032},{"dt":1688386620,"precipitation":0},{"dt":1688386680,"precipitation":0},{"dt":1688386740,"precipitation":0},{"dt":1688386800,"precipitation":0},{"dt":1688386860,"precipitation":0},{"dt":1688386920,"precipitation":0},{"dt":1688386980,"precipitation":0},{"dt":1688387040,"precipitation":0},{"dt":1688387100,"precipitation":0},{"dt":1688387160,"precipitation":0},{"dt":1688387220,"precipitation":0},{"dt":1688387280,"precipitation":0},{"dt":1688387340,"precipitation":0},{"dt":1688387400,"precipitation":0},{"dt":1688387460,"precipitation":0},{"dt":1688387520,"precipitation":0},{"dt":1688387580,"precipitation":0},{"dt":1688387640,"precipitation":0},{"dt":1688387700,"precipitation":0},{"dt":1688387760,"precipitation":0}],"hourly":[{"dt":1688382000,"temp":25.38,"feels_like":25.44,"pressure":1017,"humidity":56,"dew_point":15.97,"uvi":8.42,"clouds":16,"visibility":10000,"wind_speed":4.94,"wind_deg":327,"wind_gust":6.14,"weather":[{"id":801,"main":"Clouds","description":"few clouds","icon":"02d"}],"pop":0},{"dt":1688385600,"temp":25.1,"feels_like":25.21,"pressure":1017,"humidity":59,"dew_point":16.53,"uvi":9.78,"clouds":20,"visibility":10000,"wind_speed":5.92,"wind_deg":325,"wind_gust":7.02,"weather":[{"id":801,"main":"Clouds","description":"few clouds","icon":"02d"}],"pop":0},{"dt":1688389200,"temp":25.69,"feels_like":25.75,"pressure":1017,"humidity":55,"dew_point":15.98,"uvi":9.92,"clouds":16,"visibility":10000,"wind_speed":6.69,"wind_deg":328,"wind_gust":7.82,"weather":[{"id":801,"main":"Clouds","description":"few clouds","icon":"02d"}],"pop":0},{"dt":1688392800,"temp":26.43,"feels_like":26.43,"pressure":1017,"humidity":50,"dew_point":15.18,"uvi":8.86,"clouds":12,"visibility":10000,"wind_speed":7.32,"wind_deg":331,"wind_gust":8.69,"weather":[{"id":801,"main":"Clouds","description":"few clouds","icon":"02d"}],"pop":0},{"dt":1688396400,"temp":26.82,"feels_like":27.09,"pressure":1017,"humidity":47,"dew_point":14.57,"uvi":6.86,"clouds":8,"visibility":10000,"wind_speed":7.95,"wind_deg":337,"wind_gust":9.66,"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"pop":0},{"dt":1688400000,"temp":26.88,"feels_like":26.92,"pressure":1017,"humidity":43,"dew_point":13.26,"uvi":4.61,"clouds":4,"visibility":10000,"wind_speed":8.11,"wind_deg":341,"wind_gust":10.42,"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"pop":0},{"dt":1688403600,"temp":26.09,"feels_like":26.09,"pressure":1017,"humidity":45,"dew_point":12.76,"uvi":2.47,"clouds":0,"visibility":10000,"wind_speed":8.65,"wind_deg":344,"wind_gust":11.66,"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"pop":0},{"dt":1688407200,"temp":24.67,"feels_like":24.52,"pressure":1017,"humidity":51,"dew_point":13.62,"uvi":0.99,"clouds":0,"visibility":10000,"wind_speed":8.8,"wind_deg":347,"wind_gust":12.69,"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"pop":0},{"dt":1688410800,"temp":22.93,"feels_like":22.85,"pressure":1018,"humidity":60,"dew_point":14.39,"uvi":0.26,"clouds":0,"visibility":10000,"wind_speed":8.19,"wind_deg":348,"wind_gust":13.04,"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"pop":0},{"dt":1688414400,"temp":21.16,"feels_like":21.13,"pressure":1018,"humidity":69,"dew_point":14.95,"uvi":0,"clouds":0,"visibility":10000,"wind_speed":6.88,"wind_deg":347,"wind_gust":12.91,"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"pop":0},{"dt":1688418000,"temp":20,"feels_like":20.04,"pressure":1019,"humidity":76,"dew_point":15.45,"uvi":0,"clouds":0,"visibility":10000,"wind_speed":6.37,"wind_deg":346,"wind_gust":12.91,"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01n"}],"pop":0},{"dt":1688421600,"temp":19.41,"feels_like":19.5,"pressure":1019,"humidity":80,"dew_point":15.55,"uvi":0,"clouds":0,"visibility":10000,"wind_speed":5.94,"wind_deg":348,"wind_gust":12.71,"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01n"}],"pop":0},{"dt":1688425200,"temp":19.04,"feels_like":19.11,"pressure":1020,"humidity":81,"dew_point":15.48,"uvi":0,"clouds":0,"visibility":10000,"wind_speed":5.74,"wind_deg":348,"wind_gust":12.82,"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01n"}],"pop":0},{"dt":1688428800,"temp":18.7,"feels_like":18.77,"pressure":1020,"humidity":82,"dew_point":15.32,"uvi":0,"clouds":0,"visibility":10000,"wind_speed":5.63,"wind_deg":346,"wind_gust":12.84,"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01n"}],"pop":0},{"dt":1688432400,"temp":18.45,"feels_like":18.52,"pressure":1020,"humidity":83,"dew_point":15.27,"uvi":0,"clouds":0,"visibility":10000,"wind_speed":5.38,"wind_deg":344,"wind_gust":12.56,"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01n"}],"pop":0},{"dt":1688436000,"temp":18.15,"feels_like":18.24,"pressure":1019,"humidity":85,"dew_point":15.27,"uvi":0,"clouds":0,"visibility":10000,"wind_speed":5.28,"wind_deg":344,"wind_gust":12.34,"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01n"}],"pop":0},{"dt":1688439600,"temp":17.93,"feels_like":18.02,"pressure":1019,"humidity":86,"dew_point":15.38,"uvi":0,"clouds":0,"visibility":10000,"wind_speed":5.14,"wind_deg":342,"wind_gust":11.93,"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01n"}],"pop":0},{"dt":1688443200,"temp":17.73,"feels_like":17.83,"pressure":1019,"humidity":87,"dew_point":15.36,"uvi":0,"clouds":0,"visibility":10000,"wind_speed":5.1,"wind_deg":340,"wind_gust":11.84,"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01n"}],"pop":0},{"dt":1688446800,"temp":17.58,"feels_like":17.67,"pressure":1019,"humidity":87,"dew_point":15.24,"uvi":0,"clouds":0,"visibility":10000,"wind_speed":4.88,"wind_deg":339,"wind_gust":11.23,"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01n"}],"pop":0},{"dt":1688450400,"temp":17.79,"feels_like":17.87,"pressure":1019,"humidity":86,"dew_point":15.25,"uvi":0.17,"clouds":0,"visibility":10000,"wind_speed":5.07,"wind_deg":339,"wind_gust":11.53,"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"pop":0},{"dt":1688454000,"temp":18.96,"feels_like":18.97,"pressure":1019,"humidity":79,"dew_point":14.99,"uvi":0.75,"clouds":0,"visibility":10000,"wind_speed":5.6,"wind_deg":344,"wind_gust":10.56,"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"pop":0},{"dt":1688457600,"temp":20.57,"feels_like":20.48,"pressure":1020,"humidity":69,"dew_point":14.39,"uvi":2.05,"clouds":0,"visibility":10000,"wind_speed":6.35,"wind_deg":344,"wind_gust":9.7,"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"pop":0},{"dt":1688461200,"temp":22.34,"feels_like":22.14,"pressure":1020,"humidity":58,"dew_point":13.35,"uvi":4.13,"clouds":0,"visibility":10000,"wind_speed":6.29,"wind_deg":345,"wind_gust":8.74,"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"pop":0},{"dt":1688464800,"temp":23.94,"feels_like":23.7,"pressure":1020,"humidity":50,"dew_point":12.37,"uvi":6.49,"clouds":0,"visibility":10000,"wind_speed":6.28,"wind_deg":341,"wind_gust":8.01,"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"pop":0},{"dt":1688468400,"temp":25.12,"feels_like":24.86,"pressure":1020,"humidity":45,"dew_point":11.69,"uvi":8.74,"clouds":0,"visibility":10000,"wind_speed":6.67,"wind_deg":336,"wind_gust":7.78,"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"pop":0},{"dt":1688472000,"temp":25.82,"feels_like":25.55,"pressure":1019,"humidity":42,"dew_point":11.33,"uvi":10.16,"clouds":0,"visibility":10000,"wind_speed":6.93,"wind_deg":333,"wind_gust":7.78,"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"pop":0},{"dt":1688475600,"temp":26.12,"feels_like":26.12,"pressure":1019,"humidity":41,"dew_point":11.21,"uvi":10.23,"clouds":0,"visibility":10000,"wind_speed":7.39,"wind_deg":331,"wind_gust":8.04,"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"pop":0},{"dt":1688479200,"temp":26.13,"feels_like":26.13,"pressure":1019,"humidity":41,"dew_point":11.01,"uvi":9.13,"clouds":0,"visibility":10000,"wind_speed":7.75,"wind_deg":332,"wind_gust":8.6,"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"pop":0},{"dt":1688482800,"temp":25.79,"feels_like":25.5,"pressure":1019,"humidity":41,"dew_point":10.99,"uvi":7.07,"clouds":0,"visibility":10000,"wind_speed":7.86,"wind_deg":333,"wind_gust":9,"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"pop":0},{"dt":1688486400,"temp":25.14,"feels_like":24.83,"pressure":1018,"humidity":43,"dew_point":11.02,"uvi":4.68,"clouds":0,"visibility":10000,"wind_speed":7.9,"wind_deg":334,"wind_gust":9.28,"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"pop":0},{"dt":1688490000,"temp":24.33,"feels_like":23.97,"pressure":1018,"humidity":44,"dew_point":10.98,"uvi":2.5,"clouds":0,"visibility":10000,"wind_speed":7.8,"wind_deg":336,"wind_gust":9.64,"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"pop":0},{"dt":1688493600,"temp":23.02,"feels_like":22.63,"pressure":1018,"humidity":48,"dew_point":11.18,"uvi":1,"clouds":0,"visibility":10000,"wind_speed":7.77,"wind_deg":339,"wind_gust":10.17,"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"pop":0},{"dt":1688497200,"temp":21.59,"feels_like":21.21,"pressure":1019,"humidity":54,"dew_point":11.6,"uvi":0.26,"clouds":0,"visibility":10000,"wind_speed":7.21,"wind_deg":342,"wind_gust":10.42,"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"pop":0},{"dt":1688500800,"temp":19.87,"feels_like":19.58,"pressure":1019,"humidity":64,"dew_point":12.66,"uvi":0,"clouds":0,"visibility":10000,"wind_speed":6.25,"wind_deg":340,"wind_gust":11.21,"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"pop":0},{"dt":1688504400,"temp":18.72,"feels_like":18.66,"pressure":1019,"humidity":77,"dew_point":14.27,"uvi":0,"clouds":0,"visibility":10000,"wind_speed":5.96,"wind_deg":341,"wind_gust":12.03,"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01n"}],"pop":0},{"dt":1688508000,"temp":18.26,"feels_like":18.31,"pressure":1020,"humidity":83,"dew_point":15.08,"uvi":0,"clouds":0,"visibility":10000,"wind_speed":5.73,"wind_deg":340,"wind_gust":12.21,"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01n"}],"pop":0},{"dt":1688511600,"temp":18.1,"feels_like":18.21,"pressure":1019,"humidity":86,"dew_point":15.47,"uvi":0,"clouds":0,"visibility":10000,"wind_speed":5.61,"wind_deg":344,"wind_gust":12.34,"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01n"}],"pop":0},{"dt":1688515200,"temp":18.1,"feels_like":18.24,"pressure":1019,"humidity":87,"dew_point":15.75,"uvi":0,"clouds":1,"visibility":10000,"wind_speed":5.75,"wind_deg":344,"wind_gust":12.19,"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01n"}],"pop":0},{"dt":1688518800,"temp":18.25,"feels_like":18.38,"pressure":1019,"humidity":86,"dew_point":15.67,"uvi":0,"clouds":16,"visibility":10000,"wind_speed":5.71,"wind_deg":346,"wind_gust":11.73,"weather":[{"id":801,"main":"Clouds","description":"few clouds","icon":"02n"}],"pop":0},{"dt":1688522400,"temp":18.21,"feels_like":18.31,"pressure":1018,"humidity":85,"dew_point":15.32,"uvi":0,"clouds":14,"visibility":10000,"wind_speed":5.25,"wind_deg":345,"wind_gust":11.6,"weather":[{"id":801,"main":"Clouds","description":"few clouds","icon":"02n"}],"pop":0},{"dt":1688526000,"temp":17.82,"feels_like":17.9,"pressure":1018,"humidity":86,"dew_point":15.12,"uvi":0,"clouds":10,"visibility":10000,"wind_speed":4.67,"wind_deg":337,"wind_gust":11.06,"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01n"}],"pop":0},{"dt":1688529600,"temp":17.66,"feels_like":17.73,"pressure":1018,"humidity":86,"dew_point":15.1,"uvi":0,"clouds":9,"visibility":10000,"wind_speed":4.77,"wind_deg":339,"wind_gust":11.79,"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01n"}],"pop":0},{"dt":1688533200,"temp":17.54,"feels_like":17.6,"pressure":1018,"humidity":86,"dew_point":14.95,"uvi":0,"clouds":8,"visibility":10000,"wind_speed":4.65,"wind_deg":336,"wind_gust":10.53,"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01n"}],"pop":0},{"dt":1688536800,"temp":17.83,"feels_like":17.86,"pressure":1018,"humidity":84,"dew_point":14.82,"uvi":0.17,"clouds":11,"visibility":10000,"wind_speed":5.08,"wind_deg":337,"wind_gust":11.85,"weather":[{"id":801,"main":"Clouds","description":"few clouds","icon":"02d"}],"pop":0},{"dt":1688540400,"temp":18.98,"feels_like":18.94,"pressure":1019,"humidity":77,"dew_point":14.66,"uvi":0.73,"clouds":71,"visibility":10000,"wind_speed":6.05,"wind_deg":327,"wind_gust":10.65,"weather":[{"id":803,"main":"Clouds","description":"broken clouds","icon":"04d"}],"pop":0},{"dt":1688544000,"temp":20.08,"feels_like":19.95,"pressure":1018,"humidity":69,"dew_point":13.96,"uvi":2,"clouds":85,"visibility":10000,"wind_speed":6.22,"wind_deg":352,"wind_gust":10.17,"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04d"}],"pop":0},{"dt":1688547600,"temp":20.87,"feels_like":20.68,"pressure":1018,"humidity":64,"dew_point":13.5,"uvi":4.03,"clouds":90,"visibility":10000,"wind_speed":6.36,"wind_deg":346,"wind_gust":9.8,"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04d"}],"pop":0},{"dt":1688551200,"temp":21.11,"feels_like":20.87,"pressure":1018,"humidity":61,"dew_point":13.04,"uvi":5.58,"clouds":92,"visibility":10000,"wind_speed":5.05,"wind_deg":348,"wind_gust":7.7,"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04d"}],"pop":0}],"daily":[{"dt":1688385600,"sunrise":1688361368,"sunset":1688414697,"moonrise":1688417100,"moonset":1688359440,"moon_phase":0.5,"summary":"Expect a day of partly cloudy with clear spells","temp":{"day":25.1,"min":18.28,"max":26.88,"night":19.41,"eve":24.67,"morn":18.39},"feels_like":{"day":25.21,"night":19.5,"eve":24.52,"morn":18.5},"pressure":1017,"humidity":59,"dew_point":16.53,"wind_speed":8.8,"wind_deg":347,"wind_gust":13.04,"weather":[{"id":801,"main":"Clouds","description":"few clouds","icon":"02d"}],"clouds":20,"pop":0,"uvi":9.92},{"dt":1688472000,"sunrise":1688447798,"sunset":1688501087,"moonrise":1688506560,"moonset":1688450100,"moon_phase":0.54,"summary":"There will be clear sky today","temp":{"day":25.82,"min":17.58,"max":26.13,"night":18.26,"eve":23.02,"morn":17.79},"feels_like":{"day":25.55,"night":18.31,"eve":22.63,"morn":17.87},"pressure":1019,"humidity":42,"dew_point":11.33,"wind_speed":7.9,"wind_deg":334,"wind_gust":12.84,"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"clouds":0,"pop":0,"uvi":10.23},{"dt":1688558400,"sunrise":1688534230,"sunset":1688587476,"moonrise":1688595540,"moonset":1688541240,"moon_phase":0.58,"summary":"Expect a day of partly cloudy with clear spells","temp":{"day":25.2,"min":17.54,"max":25.28,"night":18.64,"eve":22.5,"morn":17.83},"feels_like":{"day":24.98,"night":18.7,"eve":22.32,"morn":17.86},"pressure":1018,"humidity":46,"dew_point":12.27,"wind_speed":7.86,"wind_deg":324,"wind_gust":12.34,"weather":[{"id":803,"main":"Clouds","description":"broken clouds","icon":"04d"}],"clouds":71,"pop":0,"uvi":9.93},{"dt":1688644800,"sunrise":1688620663,"sunset":1688673862,"moonrise":1688683980,"moonset":1688632380,"moon_phase":0.62,"summary":"The day will start with partly cloudy through the late morning hours, transitioning to clearing","temp":{"day":25.05,"min":17.57,"max":25.05,"night":19.01,"eve":22.56,"morn":17.59},"feels_like":{"day":24.86,"night":19.06,"eve":22.39,"morn":17.75},"pressure":1016,"humidity":48,"dew_point":12.69,"wind_speed":7.09,"wind_deg":312,"wind_gust":9.69,"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"clouds":2,"pop":0,"uvi":8.84},{"dt":1688731200,"sunrise":1688707097,"sunset":1688760247,"moonrise":0,"moonset":1688723400,"moon_phase":0.65,"summary":"There will be clear sky until morning, then partly cloudy","temp":{"day":24.48,"min":17.64,"max":24.48,"night":19.8,"eve":22.83,"morn":17.64},"feels_like":{"day":24.32,"night":20,"eve":22.81,"morn":17.81},"pressure":1017,"humidity":51,"dew_point":13,"wind_speed":5.56,"wind_deg":305,"wind_gust":7.26,"weather":[{"id":801,"main":"Clouds","description":"few clouds","icon":"02d"}],"clouds":15,"pop":0,"uvi":8.73},{"dt":1688817600,"sunrise":1688793533,"sunset":1688846630,"moonrise":1688772060,"moonset":1688814180,"moon_phase":0.69,"summary":"The day will start with partly cloudy through the late morning hours, transitioning to clearing","temp":{"day":24.93,"min":18.43,"max":25.64,"night":20.37,"eve":23.97,"morn":18.43},"feels_like":{"day":24.99,"night":20.47,"eve":23.96,"morn":18.65},"pressure":1019,"humidity":58,"dew_point":15.5,"wind_speed":5.99,"wind_deg":276,"wind_gust":6.46,"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"clouds":5,"pop":0,"uvi":9},{"dt":1688904000,"sunrise":1688879970,"sunset":1688933011,"moonrise":1688860020,"moonset":1688904780,"moon_phase":0.73,"summary":"Expect a day of partly cloudy with clear spells","temp":{"day":24.48,"min":18.35,"max":25.38,"night":18.35,"eve":22.97,"morn":19.77},"feels_like":{"day":24.42,"night":18.07,"eve":22.55,"morn":20.15},"pressure":1021,"humidity":55,"dew_point":14.3,"wind_speed":7.17,"wind_deg":330,"wind_gust":10.71,"weather":[{"id":803,"main":"Clouds","description":"broken clouds","icon":"04d"}],"clouds":70,"pop":0,"uvi":9},{"dt":1688990400,"sunrise":1688966408,"sunset":1689019390,"moonrise":1688947980,"moonset":1688995260,"moon_phase":0.75,"summary":"There will be clear sky today","temp":{"day":25.17,"min":16.95,"max":25.65,"night":20.15,"eve":24,"morn":17.24},"feels_like":{"day":25.18,"night":20.1,"eve":23.97,"morn":17.08},"pressure":1021,"humidity":55,"dew_point":15.07,"wind_speed":6.84,"wind_deg":315,"wind_gust":8.77,"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"clouds":0,"pop":0,"uvi":9}],"alerts":[{"sender_name":"NWS Portland (Northwest Oregon and Southwest Washington)","event":"Heat Advisory","start":1688490000,"end":1688623200,"description":"...HEAT ADVISORY REMAINS IN EFFECT FROM 10 AM TUESDAY TO 11 PM\nPDT WEDNESDAY...\n* WHAT...Maximum temperatures of 92 to 102 degrees expected\nTuesday and Wednesday. Hottest temperatures will be across\ninner portions of the Portland, Vancouver, Hillsboro, and\nSalem metropolitan areas.\n* WHERE...In Oregon, Greater Portland Metro Area and Central\nWillamette Valley. In Washington, Greater Vancouver Area.\n* WHEN...From 10 AM Tuesday to 11 PM PDT Wednesday.\n* IMPACTS...Hot temperatures may cause heat illnesses to occur.\n* ADDITIONAL DETAILS...Temperatures will struggle to fall much\nbelow 70 degrees Tuesday night for the inner urban core of\nPortland.","tags":["Extreme temperature value"]}]}'; public const ONE_CALL_TIMEMACHINE = '{"lat":38.7078,"lon":-9.1366,"timezone":"Europe/Lisbon","timezone_offset":0,"data":[{"dt":1672531200,"sunrise":1672559671,"sunset":1672593902,"temp":17.48,"feels_like":17.16,"pressure":1019,"humidity":72,"dew_point":12.38,"clouds":20,"visibility":9999,"wind_speed":16.54,"wind_deg":337,"wind_gust":16.54,"weather":[{"id":801,"main":"Clouds","description":"few clouds","icon":"02n"}]}]}'; public const ONE_CALL_DAY_SUMMARY = '{"lat":38.7077507,"lon":-9.1365919,"tz":"+00:00","date":"2023-01-01","units":"metric","cloud_cover":{"afternoon":75.0},"humidity":{"afternoon":71.0},"precipitation":{"total":2.53},"temperature":{"min":12.52,"max":18.29,"afternoon":18.26,"night":17.39,"evening":13.9,"morning":17.23},"pressure":{"afternoon":1017.0},"wind":{"max":{"speed":26.38,"direction":225.0}}}'; + public const ONE_CALL_OVERVIEW = '{"lat":38.7078,"lon":-9.1366,"tz":"+00:00","date":"2025-11-03","units":"metric","weather_overview":"Weather overview text"}'; public const WEATHER_CURRENT = '{"coord":{"lon":-9.1366,"lat":38.7078},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"base":"stations","main":{"temp":27.12,"feels_like":28.16,"temp_min":22.76,"temp_max":29.9,"pressure":1013,"humidity":59,"sea_level":1013,"grnd_level":997},"visibility":10000,"wind":{"speed":9.26,"deg":360,"gust":2.34},"clouds":{"all":0},"rain":{"1h":0.17,"3h":0.81},"snow":{"1h":0.14,"3h":0.46},"dt":1687949133,"sys":{"type":1,"id":6901,"country":"PT","sunrise":1687929236,"sunset":1687982718},"timezone":3600,"id":6930126,"name":"Chiado","cod":200}'; public const WEATHER_FORECAST = '{"cod":"200","message":0,"cnt":1,"list":[{"dt":1687975200,"main":{"temp":26.2,"feels_like":26.2,"temp_min":25.64,"temp_max":26.2,"pressure":1013,"sea_level":1013,"grnd_level":1013,"humidity":56,"temp_kf":0.56},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"clouds":{"all":0},"wind":{"speed":8.88,"deg":340,"gust":13.77},"visibility":10000,"pop":0,"sys":{"pod":"d"},"dt_txt":"2023-06-28 18:00:00"}],"city":{"id":6930126,"name":"Chiado","coord":{"lat":38.7078,"lon":-9.1366},"country":"PT","population":500000,"timezone":3600,"sunrise":1687929236,"sunset":1687982718}}'; diff --git a/src/Test/Util/TestValidationExceptionTrait.php b/src/Test/Util/TestValidationExceptionTrait.php deleted file mode 100644 index 073cd15..0000000 --- a/src/Test/Util/TestValidationExceptionTrait.php +++ /dev/null @@ -1,18 +0,0 @@ -expectException(ValidationException::class); - $this->api->$resource()->$method(...$args); - } - - abstract public static function provideValidationExceptionData(): \Generator; -} \ No newline at end of file diff --git a/src/UnitSystem/UnitSystem.php b/src/UnitSystem/UnitSystem.php index b7239d3..593f853 100644 --- a/src/UnitSystem/UnitSystem.php +++ b/src/UnitSystem/UnitSystem.php @@ -2,18 +2,16 @@ namespace ProgrammatorDev\OpenWeatherMap\UnitSystem; -use ProgrammatorDev\OpenWeatherMap\Util\ReflectionTrait; +use ProgrammatorDev\OpenWeatherMap\Helper\ReflectionHelper; class UnitSystem { - use ReflectionTrait; - public const METRIC = 'metric'; public const IMPERIAL = 'imperial'; public const STANDARD = 'standard'; public static function getOptions(): array { - return (new UnitSystem)->getClassConstants(self::class); + return ReflectionHelper::getClassConstants(self::class); } } \ No newline at end of file diff --git a/src/Util/EntityTrait.php b/src/Util/EntityTrait.php deleted file mode 100644 index da67145..0000000 --- a/src/Util/EntityTrait.php +++ /dev/null @@ -1,13 +0,0 @@ - ['airPollution', 'getCurrent', [-91, 50]]; - yield 'get current, latitude greater than 90' => ['airPollution', 'getCurrent', [91, 50]]; - yield 'get current, longitude lower than -180' => ['airPollution', 'getCurrent', [50, -181]]; - yield 'get current, longitude greater than 180' => ['airPollution', 'getCurrent', [50, 181]]; - yield 'get forecast, latitude lower than -90' => ['airPollution', 'getForecast', [-91, 50]]; - yield 'get forecast, latitude greater than 90' => ['airPollution', 'getForecast', [91, 50]]; - yield 'get forecast, longitude lower than -180' => ['airPollution', 'getForecast', [50, -181]]; - yield 'get forecast, longitude greater than 180' => ['airPollution', 'getForecast', [50, 181]]; - yield 'get history, latitude lower than -90' => ['airPollution', 'getHistory', [-91, 50, new \DateTime('-1 day'), new \DateTime('now')]]; - yield 'get history, latitude greater than 90' => ['airPollution', 'getHistory', [91, 50, new \DateTime('-1 day'), new \DateTime('now')]]; - yield 'get history, longitude lower than -180' => ['airPollution', 'getHistory', [50, -181, new \DateTime('-1 day'), new \DateTime('now')]]; - yield 'get history, longitude greater than 180' => ['airPollution', 'getHistory', [50, 181, new \DateTime('-1 day'), new \DateTime('now')]]; - yield 'get history, end date before start date' => ['airPollution', 'getHistory', [50, 50, new \DateTime('now'), new \DateTime('-1 day')]]; - } } \ No newline at end of file diff --git a/tests/Integration/AssistantResourceTest.php b/tests/Integration/AssistantResourceTest.php new file mode 100644 index 0000000..162c3d6 --- /dev/null +++ b/tests/Integration/AssistantResourceTest.php @@ -0,0 +1,31 @@ + [ + Answer::class, + MockResponse::ASSISTANT_START_SESSION, + 'assistant', + 'startSession', + ['prompt'] + ]; + yield 'resume session' => [ + Answer::class, + MockResponse::ASSISTANT_RESUME_SESSION, + 'assistant', + 'resumeSession', + ['session-id', 'prompt'] + ]; + } +} \ No newline at end of file diff --git a/tests/Integration/CacheTraitTest.php b/tests/Integration/CacheTraitTest.php index ec6395d..9e8cc73 100644 --- a/tests/Integration/CacheTraitTest.php +++ b/tests/Integration/CacheTraitTest.php @@ -33,6 +33,7 @@ public function getCacheTtl(): ?int public function testMethods(): void { + $this->assertSame(60, $this->resource->getCacheTtl()); $this->assertSame(600, $this->resource->withCacheTtl(600)->getCacheTtl()); $this->assertSame(60, $this->resource->getCacheTtl()); // back to default value } diff --git a/tests/Integration/GeocodingResourceTest.php b/tests/Integration/GeocodingResourceTest.php index 269372f..7f8dfe0 100644 --- a/tests/Integration/GeocodingResourceTest.php +++ b/tests/Integration/GeocodingResourceTest.php @@ -7,14 +7,12 @@ use ProgrammatorDev\OpenWeatherMap\Test\AbstractTest; use ProgrammatorDev\OpenWeatherMap\Test\MockResponse; use ProgrammatorDev\OpenWeatherMap\Test\Util\TestCollectionResponseTrait; -use ProgrammatorDev\OpenWeatherMap\Test\Util\TestValidationExceptionTrait; use ProgrammatorDev\OpenWeatherMap\Test\Util\TestItemResponseTrait; class GeocodingResourceTest extends AbstractTest { use TestItemResponseTrait; use TestCollectionResponseTrait; - use TestValidationExceptionTrait; public static function provideItemResponseData(): \Generator { @@ -44,18 +42,4 @@ public static function provideCollectionResponseData(): \Generator [50, 50] ]; } - - public static function provideValidationExceptionData(): \Generator - { - yield 'get by location name, blank value' => ['geocoding', 'getByLocationName', ['']]; - yield 'get by location name, zero num results' => ['geocoding', 'getByLocationName', ['test', 0]]; - yield 'get by coordinate, latitude lower than -90' => ['geocoding', 'getByCoordinate', [-91, 50]]; - yield 'get by coordinate, latitude greater than 90' => ['geocoding', 'getByCoordinate', [91, 50]]; - yield 'get by coordinate, longitude lower than -180' => ['geocoding', 'getByCoordinate', [50, -181]]; - yield 'get by coordinate, longitude greater than 180' => ['geocoding', 'getByCoordinate', [50, 181]]; - yield 'get by coordinate, zero num results' => ['geocoding', 'getByCoordinate', [50, 50, 0]]; - yield 'get by zip code, blank zip code' => ['geocoding', 'getByZipCode', ['', 'pt']]; - yield 'get by zip code, blank country code' => ['geocoding', 'getByZipCode', ['1000-001', '']]; - yield 'get by zip code, invalid country code' => ['geocoding', 'getByZipCode', ['1000-001', 'invalid']]; - } } \ No newline at end of file diff --git a/tests/Integration/LanguageTraitTest.php b/tests/Integration/LanguageTraitTest.php index 79651ee..31cf054 100644 --- a/tests/Integration/LanguageTraitTest.php +++ b/tests/Integration/LanguageTraitTest.php @@ -26,6 +26,7 @@ public function getLanguage(): string public function testMethods(): void { + $this->assertSame('en', $this->resource->getLanguage()); $this->assertSame('pt', $this->resource->withLanguage('pt')->getLanguage()); $this->assertSame('en', $this->resource->getLanguage()); // back to default value } diff --git a/tests/Integration/OneCallResourceTest.php b/tests/Integration/OneCallResourceTest.php index b9f2c29..2528545 100644 --- a/tests/Integration/OneCallResourceTest.php +++ b/tests/Integration/OneCallResourceTest.php @@ -4,16 +4,15 @@ use ProgrammatorDev\OpenWeatherMap\Entity\OneCall\Weather; use ProgrammatorDev\OpenWeatherMap\Entity\OneCall\WeatherMoment; +use ProgrammatorDev\OpenWeatherMap\Entity\OneCall\WeatherOverview; use ProgrammatorDev\OpenWeatherMap\Entity\OneCall\WeatherSummary; use ProgrammatorDev\OpenWeatherMap\Test\AbstractTest; use ProgrammatorDev\OpenWeatherMap\Test\MockResponse; -use ProgrammatorDev\OpenWeatherMap\Test\Util\TestValidationExceptionTrait; use ProgrammatorDev\OpenWeatherMap\Test\Util\TestItemResponseTrait; class OneCallResourceTest extends AbstractTest { use TestItemResponseTrait; - use TestValidationExceptionTrait; public static function provideItemResponseData(): \Generator { @@ -38,21 +37,12 @@ public static function provideItemResponseData(): \Generator 'getWeatherSummaryByDate', [50, 50, new \DateTime()] ]; - } - - public static function provideValidationExceptionData(): \Generator - { - yield 'get weather, latitude lower than -90' => ['oneCall', 'getWeather', [-91, 50]]; - yield 'get weather, latitude greater than 90' => ['oneCall', 'getWeather', [91, 50]]; - yield 'get weather, longitude lower than -180' => ['oneCall', 'getWeather', [50, -181]]; - yield 'get weather, longitude greater than 180' => ['oneCall', 'getWeather', [50, 181]]; - yield 'get weather by date, latitude lower than -90' => ['oneCall', 'getWeatherByDate', [-91, 50, new \DateTime()]]; - yield 'get weather by date, latitude greater than 90' => ['oneCall', 'getWeatherByDate', [91, 50, new \DateTime()]]; - yield 'get weather by date, longitude lower than -180' => ['oneCall', 'getWeatherByDate', [50, -181, new \DateTime()]]; - yield 'get weather by date, longitude greater than 180' => ['oneCall', 'getWeatherByDate', [50, 181, new \DateTime()]]; - yield 'get weather summary by date, latitude lower than -90' => ['oneCall', 'getWeatherSummaryByDate', [-91, 50, new \DateTime()]]; - yield 'get weather summary by date, latitude greater than 90' => ['oneCall', 'getWeatherSummaryByDate', [91, 50, new \DateTime()]]; - yield 'get weather summary by date, longitude lower than -180' => ['oneCall', 'getWeatherSummaryByDate', [50, -181, new \DateTime()]]; - yield 'get weather summary by date, longitude greater than 180' => ['oneCall', 'getWeatherSummaryByDate', [50, 181, new \DateTime()]]; + yield 'get weather overview by date' => [ + WeatherOverview::class, + MockResponse::ONE_CALL_OVERVIEW, + 'oneCall', + 'getWeatherOverviewByDate', + [50, 50, new \DateTime()] + ]; } } \ No newline at end of file diff --git a/tests/Integration/UnitSystemTraitTest.php b/tests/Integration/UnitSystemTraitTest.php index 629c3b6..11e5919 100644 --- a/tests/Integration/UnitSystemTraitTest.php +++ b/tests/Integration/UnitSystemTraitTest.php @@ -5,7 +5,6 @@ use ProgrammatorDev\OpenWeatherMap\Resource\Resource; use ProgrammatorDev\OpenWeatherMap\Resource\Util\UnitSystemTrait; use ProgrammatorDev\OpenWeatherMap\Test\AbstractTest; -use ProgrammatorDev\Validator\Exception\ValidationException; class UnitSystemTraitTest extends AbstractTest { @@ -27,13 +26,8 @@ public function getUnitSystem(): string public function testMethods(): void { + $this->assertSame('metric', $this->resource->getUnitSystem()); $this->assertSame('imperial', $this->resource->withUnitSystem('imperial')->getUnitSystem()); $this->assertSame('metric', $this->resource->getUnitSystem()); // back to default value } - - public function testValidationException(): void - { - $this->expectException(ValidationException::class); - $this->resource->withUnitSystem('invalid'); - } } \ No newline at end of file diff --git a/tests/Integration/WeatherResourceTest.php b/tests/Integration/WeatherResourceTest.php index de960b8..579aa01 100644 --- a/tests/Integration/WeatherResourceTest.php +++ b/tests/Integration/WeatherResourceTest.php @@ -6,13 +6,11 @@ use ProgrammatorDev\OpenWeatherMap\Entity\Weather\WeatherCollection; use ProgrammatorDev\OpenWeatherMap\Test\AbstractTest; use ProgrammatorDev\OpenWeatherMap\Test\MockResponse; -use ProgrammatorDev\OpenWeatherMap\Test\Util\TestValidationExceptionTrait; use ProgrammatorDev\OpenWeatherMap\Test\Util\TestItemResponseTrait; class WeatherResourceTest extends AbstractTest { use TestItemResponseTrait; - use TestValidationExceptionTrait; public static function provideItemResponseData(): \Generator { @@ -31,17 +29,4 @@ public static function provideItemResponseData(): \Generator [50, 50] ]; } - - public static function provideValidationExceptionData(): \Generator - { - yield 'get current, latitude lower than -90' => ['weather', 'getCurrent', [-91, 50]]; - yield 'get current, latitude greater than 90' => ['weather', 'getCurrent', [91, 50]]; - yield 'get current, longitude lower than -180' => ['weather', 'getCurrent', [50, -181]]; - yield 'get current, longitude greater than 180' => ['weather', 'getCurrent', [50, 181]]; - yield 'get forecast, latitude lower than -90' => ['weather', 'getForecast', [-91, 50]]; - yield 'get forecast, latitude greater than 90' => ['weather', 'getForecast', [91, 50]]; - yield 'get forecast, longitude lower than -180' => ['weather', 'getForecast', [50, -181]]; - yield 'get forecast, longitude greater than 180' => ['weather', 'getForecast', [50, 181]]; - yield 'get forecast, zero num results' => ['weather', 'getForecast', [50, 50, 0]]; - } } \ No newline at end of file diff --git a/tests/Unit/Assistant/AnswerTest.php b/tests/Unit/Assistant/AnswerTest.php new file mode 100644 index 0000000..980c527 --- /dev/null +++ b/tests/Unit/Assistant/AnswerTest.php @@ -0,0 +1,48 @@ + 'Answer text', + 'data' => [ + 'location' => [ + 'clouds' => 100, + 'dew_point' => 10, + 'dt' => 1762622549, + 'feels_like' => 10, + 'humidity' => 10, + 'pressure' => 1000, + 'sunrise' => 1762602543, + 'sunset' => 1762638067, + 'temp' => 10, + 'uvi' => 1, + 'visibility' => 10000, + 'weather' => [ + [ + 'description' => 'description', + 'icon' => '01d', + 'id' => 200, + 'main' => 'name' + ] + ], + 'wind_deg' => 10, + 'wind_speed' => 10, + 'wind_gust' => 10, + ] + ], + 'session_id' => '777f049a-c96b-423f-baef-ca34ff725fe9' + ]); + + $this->assertSame('Answer text', $entity->getAnswer()); + $this->assertSame('777f049a-c96b-423f-baef-ca34ff725fe9', $entity->getSessionId()); + $this->assertContainsOnlyInstancesOf(WeatherData::class, $entity->getData()); + } +} \ No newline at end of file diff --git a/tests/Unit/Assistant/WeatherDataTest.php b/tests/Unit/Assistant/WeatherDataTest.php new file mode 100644 index 0000000..080b3aa --- /dev/null +++ b/tests/Unit/Assistant/WeatherDataTest.php @@ -0,0 +1,54 @@ + 100, + 'dew_point' => 10, + 'dt' => 1762622549, + 'feels_like' => 10, + 'humidity' => 10, + 'pressure' => 1000, + 'sunrise' => 1762602543, + 'sunset' => 1762638067, + 'temp' => 10, + 'uvi' => 1, + 'visibility' => 10000, + 'weather' => [ + [ + 'description' => 'description', + 'icon' => '01d', + 'id' => 200, + 'main' => 'name' + ] + ], + 'wind_deg' => 10, + 'wind_speed' => 10, + 'wind_gust' => 10, + ]); + + $this->assertSame('locationName', $entity->getLocationName()); + $this->assertInstanceOf(\DateTimeImmutable::class, $entity->getDateTime()); + $this->assertSame(1000, $entity->getAtmosphericPressure()); + $this->assertSame(10, $entity->getHumidity()); + $this->assertSame(10.0, $entity->getDewPoint()); + $this->assertSame(1.0, $entity->getUltraVioletIndex()); + $this->assertSame(100, $entity->getCloudiness()); + $this->assertInstanceOf(Wind::class, $entity->getWind()); + $this->assertContainsOnlyInstancesOf(Condition::class, $entity->getConditions()); + $this->assertSame(10.0, $entity->getTemperature()); + $this->assertSame(10.0, $entity->getTemperatureFeelsLike()); + $this->assertSame(10000, $entity->getVisibility()); + $this->assertInstanceOf(\DateTimeImmutable::class, $entity->getSunriseAt()); + $this->assertInstanceOf(\DateTimeImmutable::class, $entity->getSunsetAt()); + } +} \ No newline at end of file diff --git a/tests/Unit/OneCall/WeatherOverviewTest.php b/tests/Unit/OneCall/WeatherOverviewTest.php new file mode 100644 index 0000000..cae347e --- /dev/null +++ b/tests/Unit/OneCall/WeatherOverviewTest.php @@ -0,0 +1,27 @@ + 50, + 'lon' => 50, + 'tz' => '+00:00', + 'date' => '2025-01-01', + 'weather_overview' => 'Weather overview text' + ]); + + $this->assertInstanceOf(Coordinate::class, $entity->getCoordinate()); + $this->assertInstanceOf(Timezone::class, $entity->getTimezone()); + $this->assertInstanceOf(\DateTimeImmutable::class, $entity->getDateTime()); + $this->assertSame('Weather overview text', $entity->getOverview()); + } +} \ No newline at end of file diff --git a/tests/Unit/OneCall/WeatherSummaryTest.php b/tests/Unit/OneCall/WeatherSummaryTest.php index 7a438bb..642474c 100644 --- a/tests/Unit/OneCall/WeatherSummaryTest.php +++ b/tests/Unit/OneCall/WeatherSummaryTest.php @@ -2,10 +2,8 @@ namespace ProgrammatorDev\OpenWeatherMap\Test\Unit\OneCall; -use ProgrammatorDev\OpenWeatherMap\Entity\Condition; use ProgrammatorDev\OpenWeatherMap\Entity\Coordinate; use ProgrammatorDev\OpenWeatherMap\Entity\OneCall\Temperature; -use ProgrammatorDev\OpenWeatherMap\Entity\OneCall\WeatherData; use ProgrammatorDev\OpenWeatherMap\Entity\OneCall\WeatherSummary; use ProgrammatorDev\OpenWeatherMap\Entity\Timezone; use ProgrammatorDev\OpenWeatherMap\Entity\Wind;