952 lines
37 KiB
PHP
952 lines
37 KiB
PHP
<?php
|
|
/*
|
|
* Copyright (C) 2024 Xibo Signage Ltd
|
|
*
|
|
* Xibo - Digital Signage - https://xibosignage.com
|
|
*
|
|
* This file is part of Xibo.
|
|
*
|
|
* Xibo is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* any later version.
|
|
*
|
|
* Xibo is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
namespace Xibo\Connector;
|
|
|
|
use Carbon\Carbon;
|
|
use GuzzleHttp\Exception\RequestException;
|
|
use Illuminate\Support\Str;
|
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
|
use Xibo\Event\ScheduleCriteriaRequestEvent;
|
|
use Xibo\Event\ScheduleCriteriaRequestInterface;
|
|
use Xibo\Event\WidgetDataRequestEvent;
|
|
use Xibo\Event\XmdsWeatherRequestEvent;
|
|
use Xibo\Support\Exception\ConfigurationException;
|
|
use Xibo\Support\Exception\GeneralException;
|
|
use Xibo\Support\Sanitizer\SanitizerInterface;
|
|
use Xibo\Widget\DataType\Forecast;
|
|
use Xibo\Widget\Provider\DataProviderInterface;
|
|
|
|
/**
|
|
* A connector to get data from the Open Weather Map API for use by the Weather Widget
|
|
*/
|
|
class OpenWeatherMapConnector implements ConnectorInterface
|
|
{
|
|
use ConnectorTrait;
|
|
|
|
private $apiUrl = 'https://api.openweathermap.org/data/';
|
|
private $forecastCurrent = '2.5/weather';
|
|
private $forecast3Hourly = '2.5/forecast';
|
|
private $forecastDaily = '2.5/forecast/daily';
|
|
private $forecastCombinedV3 = '3.0/onecall';
|
|
|
|
/** @var string */
|
|
protected $timezone;
|
|
|
|
/** @var \Xibo\Widget\DataType\Forecast */
|
|
protected $currentDay;
|
|
|
|
public function registerWithDispatcher(EventDispatcherInterface $dispatcher): ConnectorInterface
|
|
{
|
|
$dispatcher->addListener(WidgetDataRequestEvent::$NAME, [$this, 'onDataRequest']);
|
|
$dispatcher->addListener(ScheduleCriteriaRequestEvent::$NAME, [$this, 'onScheduleCriteriaRequest']);
|
|
$dispatcher->addListener(XmdsWeatherRequestEvent::$NAME, [$this, 'onXmdsWeatherRequest']);
|
|
return $this;
|
|
}
|
|
|
|
public function getSourceName(): string
|
|
{
|
|
return 'openweathermap';
|
|
}
|
|
|
|
public function getTitle(): string
|
|
{
|
|
return 'Open Weather Map';
|
|
}
|
|
|
|
public function getDescription(): string
|
|
{
|
|
return 'Get Weather data from Open Weather Map API';
|
|
}
|
|
|
|
public function getThumbnail(): string
|
|
{
|
|
return 'theme/default/img/connectors/owm.png';
|
|
}
|
|
|
|
public function getSettingsFormTwig(): string
|
|
{
|
|
return 'openweathermap-form-settings';
|
|
}
|
|
|
|
public function processSettingsForm(SanitizerInterface $params, array $settings): array
|
|
{
|
|
if (!$this->isProviderSetting('owmApiKey')) {
|
|
$settings['owmApiKey'] = $params->getString('owmApiKey');
|
|
$settings['owmIsPaidPlan'] = $params->getCheckbox('owmIsPaidPlan');
|
|
$settings['cachePeriod'] = $params->getInt('cachePeriod');
|
|
$settings['xmdsCachePeriod'] = $params->getInt('xmdsCachePeriod');
|
|
}
|
|
return $settings;
|
|
}
|
|
|
|
/**
|
|
* If the requested dataSource is forecastio, get the data, process it and add to dataProvider
|
|
*
|
|
* @param WidgetDataRequestEvent $event
|
|
* @return void
|
|
*/
|
|
public function onDataRequest(WidgetDataRequestEvent $event)
|
|
{
|
|
if ($event->getDataProvider()->getDataSource() === 'forecastio') {
|
|
if (empty($this->getSetting('owmApiKey'))) {
|
|
$this->getLogger()->debug('onDataRequest: Open Weather Map not configured.');
|
|
return;
|
|
}
|
|
|
|
$event->stopPropagation();
|
|
|
|
if ($this->isProviderSetting('apiUrl')) {
|
|
$this->apiUrl = $this->getSetting('apiUrl');
|
|
}
|
|
|
|
try {
|
|
$this->getWeatherData($event->getDataProvider());
|
|
|
|
// If we've got data, then set our cache period.
|
|
$event->getDataProvider()->setCacheTtl($this->getSetting('cachePeriod', 3600));
|
|
$event->getDataProvider()->setIsHandled();
|
|
} catch (\Exception $exception) {
|
|
$this->getLogger()->error('onDataRequest: Failed to get results. e = ' . $exception->getMessage());
|
|
$event->getDataProvider()->addError(__('Unable to get weather results.'));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get a combined forecast
|
|
* @throws \Xibo\Support\Exception\GeneralException
|
|
*/
|
|
private function getWeatherData(DataProviderInterface $dataProvider)
|
|
{
|
|
// Convert units to an acceptable format
|
|
$units = in_array($dataProvider->getProperty('units', 'auto'), ['auto', 'us', 'uk2']) ? 'imperial' : 'metric';
|
|
|
|
// Temperature and Wind Speed Unit Mappings
|
|
$unit = $this->getUnit($dataProvider->getProperty('units'));
|
|
|
|
if ($dataProvider->getProperty('useDisplayLocation') == 0) {
|
|
$providedLat = $dataProvider->getProperty('latitude', $dataProvider->getDisplayLatitude());
|
|
$providedLon = $dataProvider->getProperty('longitude', $dataProvider->getDisplayLongitude());
|
|
} else {
|
|
$providedLat = $dataProvider->getDisplayLatitude();
|
|
$providedLon = $dataProvider->getDisplayLongitude();
|
|
}
|
|
|
|
// Build the URL
|
|
$url = '?lat=' . $providedLat
|
|
. '&lon=' . $providedLon
|
|
. '&units=' . $units
|
|
. '&lang=' . $dataProvider->getProperty('lang', 'en')
|
|
. '&appid=[API_KEY]';
|
|
|
|
// Cache expiry date
|
|
$cacheExpire = Carbon::now()->addSeconds($this->getSetting('cachePeriod'));
|
|
|
|
if ($this->getSetting('owmIsPaidPlan') ?? 0 == 1) {
|
|
// We build our data from multiple API calls
|
|
// Current data first.
|
|
$data = $this->queryApi($this->apiUrl . $this->forecastCurrent . $url, $cacheExpire);
|
|
$data['current'] = $this->parseCurrentIntoFormat($data);
|
|
|
|
// initialize timezone
|
|
$timezoneOffset = (int)$data['timezone'];
|
|
|
|
// Calculate the number of whole hours in the offset
|
|
$offsetHours = floor($timezoneOffset / 3600);
|
|
|
|
// Calculate the remaining minutes after extracting the whole hours
|
|
$offsetMinutes = ($timezoneOffset % 3600) / 60;
|
|
|
|
// Determine the sign of the offset (positive or negative)
|
|
$sign = $offsetHours < 0 ? '-' : '+';
|
|
|
|
// Ensure the format is as follows: +/-hh:mm
|
|
$formattedOffset = sprintf("%s%02d:%02d", $sign, abs($offsetHours), abs($offsetMinutes));
|
|
|
|
// Get the timezone name
|
|
$this->timezone = (new \DateTimeZone($formattedOffset))->getName();
|
|
|
|
// Pick out the country
|
|
$country = $data['sys']['country'] ?? null;
|
|
|
|
$this->getLogger()->debug('Trying to determine units for Country: ' . $country);
|
|
|
|
// If we don't have a unit, then can we base it on the timezone we got back?
|
|
if ($dataProvider->getProperty('units', 'auto') === 'auto' && $country !== null) {
|
|
// Pick out some countries to set the units
|
|
if ($country === 'GB') {
|
|
$unit = $this->getUnit('uk2');
|
|
} else if ($country === 'US') {
|
|
$unit = $this->getUnit('us');
|
|
} else if ($country === 'CA') {
|
|
$unit = $this->getUnit('ca');
|
|
} else {
|
|
$unit = $this->getUnit('si');
|
|
}
|
|
}
|
|
|
|
// Then the 16 day forecast API, which we will cache a day
|
|
$data['daily'] = $this->queryApi(
|
|
$this->apiUrl . $this->forecastDaily . $url,
|
|
$cacheExpire->copy()->addDay()->startOfDay()
|
|
)['list'];
|
|
} else {
|
|
// We use one call API 3.0
|
|
$data = $this->queryApi($this->apiUrl . $this->forecastCombinedV3 . $url, $cacheExpire);
|
|
|
|
$this->timezone = $data['timezone'];
|
|
|
|
// Country based on timezone (this is harder than using the real country)
|
|
if ($dataProvider->getProperty('units', 'auto') === 'auto') {
|
|
if (Str::startsWith($this->timezone, 'America')) {
|
|
$unit = $this->getUnit('us');
|
|
} else if ($this->timezone === 'Europe/London') {
|
|
$unit = $this->getUnit('uk2');
|
|
} else {
|
|
$unit = $this->getUnit('si');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Using units:
|
|
$this->getLogger()->debug('Using units: ' . json_encode($unit));
|
|
|
|
$forecasts = [];
|
|
|
|
// Parse into our forecast.
|
|
// Load this data into our objects
|
|
$this->currentDay = new Forecast();
|
|
$this->currentDay->temperatureUnit = $unit['tempUnit'] ?: 'C';
|
|
$this->currentDay->windSpeedUnit = $unit['windUnit'] ?: 'KPH';
|
|
$this->currentDay->visibilityDistanceUnit = $unit['visibilityUnit'] ?: 'km';
|
|
$this->currentDay->location = $data['name'] ?? '';
|
|
$this->processItemIntoDay($this->currentDay, $data['current'], $units, true);
|
|
|
|
$countForecast = 0;
|
|
// Process each day into a forecast
|
|
foreach ($data['daily'] as $dayItem) {
|
|
// Skip first item as this is the currentDay
|
|
if ($countForecast++ === 0) {
|
|
continue;
|
|
}
|
|
|
|
$day = new Forecast();
|
|
$day->temperatureUnit = $this->currentDay->temperatureUnit;
|
|
$day->windSpeedUnit = $this->currentDay->windSpeedUnit;
|
|
$day->visibilityDistanceUnit = $this->currentDay->visibilityDistanceUnit;
|
|
$day->location = $this->currentDay->location;
|
|
$this->processItemIntoDay($day, $dayItem, $units);
|
|
|
|
$forecasts[] = $day;
|
|
}
|
|
|
|
// Enhance the currently with the high/low from the first daily forecast
|
|
$this->currentDay->temperatureHigh = $forecasts[0]->temperatureHigh;
|
|
$this->currentDay->temperatureMaxRound = $forecasts[0]->temperatureMaxRound;
|
|
$this->currentDay->temperatureLow = $forecasts[0]->temperatureLow;
|
|
$this->currentDay->temperatureMinRound = $forecasts[0]->temperatureMinRound;
|
|
$this->currentDay->temperatureMorning = $forecasts[0]->temperatureMorning;
|
|
$this->currentDay->temperatureMorningRound = $forecasts[0]->temperatureMorningRound;
|
|
$this->currentDay->temperatureNight = $forecasts[0]->temperatureNight;
|
|
$this->currentDay->temperatureNightRound = $forecasts[0]->temperatureNightRound;
|
|
$this->currentDay->temperatureEvening = $forecasts[0]->temperatureEvening;
|
|
$this->currentDay->temperatureEveningRound = $forecasts[0]->temperatureEveningRound;
|
|
$this->currentDay->temperatureMean = $forecasts[0]->temperatureMean;
|
|
$this->currentDay->temperatureMeanRound = $forecasts[0]->temperatureMeanRound;
|
|
|
|
if ($dataProvider->getProperty('dayConditionsOnly', 0) == 1) {
|
|
// Swap the night icons for their day equivalents
|
|
$this->currentDay->icon = str_replace('-night', '', $this->currentDay->icon);
|
|
$this->currentDay->wicon = str_replace('-night', '', $this->currentDay->wicon);
|
|
}
|
|
|
|
$dataProvider->addItem($this->currentDay);
|
|
|
|
if (count($forecasts) > 0) {
|
|
foreach ($forecasts as $forecast) {
|
|
$dataProvider->addItem($forecast);
|
|
}
|
|
}
|
|
|
|
$dataProvider->addOrUpdateMeta('Attribution', 'Powered by OpenWeather');
|
|
}
|
|
|
|
/**
|
|
* @param string $url
|
|
* @param Carbon $cacheExpiresAt
|
|
* @return array
|
|
* @throws \Xibo\Support\Exception\GeneralException
|
|
*/
|
|
private function queryApi(string $url, Carbon $cacheExpiresAt): array
|
|
{
|
|
$cache = $this->pool->getItem('/weather/owm/' . md5($url));
|
|
$data = $cache->get();
|
|
|
|
if ($cache->isMiss()) {
|
|
$cache->lock();
|
|
$this->getLogger()->debug('Getting Forecast from API');
|
|
|
|
$url = str_replace('[API_KEY]', $this->getSetting('owmApiKey'), $url);
|
|
|
|
try {
|
|
$response = $this->getClient()->get($url);
|
|
|
|
// Success?
|
|
if ($response->getStatusCode() != 200) {
|
|
throw new GeneralException('Non-200 response from Open Weather Map');
|
|
}
|
|
|
|
// Parse out header and body
|
|
$data = json_decode($response->getBody(), true);
|
|
|
|
// Cache
|
|
$cache->set($data);
|
|
$cache->expiresAt($cacheExpiresAt);
|
|
$this->pool->saveDeferred($cache);
|
|
|
|
} catch (RequestException $e) {
|
|
$this->getLogger()->error('Unable to reach Open Weather Map API: '
|
|
. str_replace($this->getSetting('owmApiKey'), '[API_KEY]', $e->getMessage()));
|
|
throw new GeneralException('API responded with an error.');
|
|
}
|
|
} else {
|
|
$this->getLogger()->debug('Getting Forecast from cache');
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Parse the response from the current API into the format provided by the onecall API
|
|
* this means easier processing down the line
|
|
* @param array $source
|
|
* @return array
|
|
*/
|
|
private function parseCurrentIntoFormat(array $source): array
|
|
{
|
|
return [
|
|
'timezone' => $source['timezone'],
|
|
'dt' => $source['dt'],
|
|
'sunrise' => $source['sys']['sunrise'],
|
|
'sunset' => $source['sys']['sunset'],
|
|
'temp' => $source['main']['temp'],
|
|
'feels_like' => $source['main']['feels_like'],
|
|
'pressure' => $source['main']['pressure'],
|
|
'humidity' => $source['main']['humidity'],
|
|
'dew_point' => null,
|
|
'uvi' => null,
|
|
'clouds' => $source['clouds']['all'],
|
|
'visibility' => $source['visibility'] ?? 0,
|
|
'wind_speed' => $source['wind']['speed'],
|
|
'wind_deg' => $source['wind']['deg'] ?? 0,
|
|
'weather' => $source['weather'],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param \Xibo\Weather\Forecast $day
|
|
* @param array $item
|
|
* @param $requestUnit
|
|
* @param bool $isCurrent
|
|
*/
|
|
private function processItemIntoDay($day, $item, $requestUnit, $isCurrent = false)
|
|
{
|
|
$day->time = $item['dt'];
|
|
$day->sunRise = $item['sunrise'];
|
|
$day->sunSet = $item['sunset'];
|
|
$day->summary = ucfirst($item['weather'][0]['description']);
|
|
|
|
// Temperature
|
|
// imperial = F
|
|
// metric = C
|
|
if ($isCurrent) {
|
|
$day->temperature = $item['temp'];
|
|
$day->apparentTemperature = $item['feels_like'];
|
|
$day->temperatureHigh = $day->temperature;
|
|
$day->temperatureLow = $day->temperature;
|
|
$day->temperatureNight = $day->temperature;
|
|
$day->temperatureEvening = $day->temperature;
|
|
$day->temperatureMorning = $day->temperature;
|
|
} else {
|
|
$day->temperature = $item['temp']['day'];
|
|
$day->apparentTemperature = $item['feels_like']['day'];
|
|
$day->temperatureHigh = $item['temp']['max'] ?? $day->temperature;
|
|
$day->temperatureLow = $item['temp']['min'] ?? $day->temperature;
|
|
$day->temperatureNight = $item['temp']['night'];
|
|
$day->temperatureEvening = $item['temp']['eve'];
|
|
$day->temperatureMorning = $item['temp']['morn'];
|
|
}
|
|
|
|
if ($requestUnit === 'metric' && $day->temperatureUnit === 'F') {
|
|
// Convert C to F
|
|
$day->temperature = ($day->temperature) * 9 / 5 + 32;
|
|
$day->apparentTemperature = ($day->apparentTemperature) * 9 / 5 + 32;
|
|
$day->temperatureHigh = ($day->temperatureHigh) * 9 / 5 + 32;
|
|
$day->temperatureLow = ($day->temperatureLow) * 9 / 5 + 32;
|
|
$day->temperatureNight = ($day->temperatureNight) * 9 / 5 + 32;
|
|
$day->temperatureEvening = ($day->temperatureEvening) * 9 / 5 + 32;
|
|
$day->temperatureMorning = ($day->temperatureMorning) * 9 / 5 + 32;
|
|
|
|
} else if ($requestUnit === 'imperial' && $day->temperatureUnit === 'C') {
|
|
// Convert F to C
|
|
$day->temperature = ($day->temperature - 32) * 5 / 9;
|
|
$day->apparentTemperature = ($day->apparentTemperature - 32) * 5 / 9;
|
|
$day->temperatureHigh = ($day->temperatureHigh - 32) * 5 / 9;
|
|
$day->temperatureLow = ($day->temperatureLow - 32) * 5 / 9;
|
|
$day->temperatureNight = ($day->temperatureNight - 32) * 5 / 9;
|
|
$day->temperatureEvening = ($day->temperatureEvening - 32) * 5 / 9;
|
|
$day->temperatureMorning = ($day->temperatureMorning - 32) * 5 / 9;
|
|
}
|
|
|
|
// Work out the mean
|
|
$day->temperatureMean = ($day->temperatureHigh + $day->temperatureLow) / 2;
|
|
|
|
// Round those off
|
|
$day->temperatureRound = round($day->temperature, 0);
|
|
$day->temperatureNightRound = round($day->temperatureNight, 0);
|
|
$day->temperatureMorningRound = round($day->temperatureMorning, 0);
|
|
$day->temperatureEveningRound = round($day->temperatureEvening, 0);
|
|
$day->apparentTemperatureRound = round($day->apparentTemperature, 0);
|
|
$day->temperatureMaxRound = round($day->temperatureHigh, 0);
|
|
$day->temperatureMinRound = round($day->temperatureLow, 0);
|
|
$day->temperatureMeanRound = round($day->temperatureMean, 0);
|
|
|
|
// Humidity
|
|
$day->humidityPercent = $item['humidity'];
|
|
$day->humidity = $day->humidityPercent / 100;
|
|
|
|
// Pressure
|
|
// received in hPa, display in mB
|
|
$day->pressure = $item['pressure'] / 100;
|
|
|
|
// Wind
|
|
// metric = meters per second
|
|
// imperial = miles per hour
|
|
$day->windSpeed = $item['wind_speed'] ?? $item['speed'] ?? null;
|
|
$day->windBearing = $item['wind_deg'] ?? $item['deg'] ?? null;
|
|
|
|
if ($requestUnit === 'metric' && $day->windSpeedUnit !== 'MPS') {
|
|
// We have MPS and need to go to something else
|
|
if ($day->windSpeedUnit === 'MPH') {
|
|
// Convert MPS to MPH
|
|
$day->windSpeed = round($day->windSpeed * 2.237, 2);
|
|
} else if ($day->windSpeedUnit === 'KPH') {
|
|
// Convert MPS to KPH
|
|
$day->windSpeed = round($day->windSpeed * 3.6, 2);
|
|
}
|
|
} else if ($requestUnit === 'imperial' && $day->windSpeedUnit !== 'MPH') {
|
|
if ($day->windSpeedUnit === 'MPS') {
|
|
// Convert MPH to MPS
|
|
$day->windSpeed = round($day->windSpeed / 2.237, 2);
|
|
} else if ($day->windSpeedUnit === 'KPH') {
|
|
// Convert MPH to KPH
|
|
$day->windSpeed = round($day->windSpeed * 1.609344, 2);
|
|
}
|
|
}
|
|
|
|
// Wind direction
|
|
$day->windDirection = '--';
|
|
if ($day->windBearing !== null && $day->windBearing !== 0) {
|
|
foreach (self::cardinalDirections() as $dir => $angles) {
|
|
if ($day->windBearing >= $angles[0] && $day->windBearing < $angles[1]) {
|
|
$day->windDirection = $dir;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Clouds
|
|
$day->cloudCover = $item['clouds'];
|
|
|
|
// Visibility
|
|
// metric = meters
|
|
// imperial = meters?
|
|
$day->visibility = $item['visibility'] ?? '--';
|
|
|
|
if ($day->visibility !== '--') {
|
|
// Always in meters
|
|
if ($day->visibilityDistanceUnit === 'mi') {
|
|
// Convert meters to miles
|
|
$day->visibility = $day->visibility / 1609;
|
|
} else {
|
|
if ($day->visibilityDistanceUnit === 'km') {
|
|
// Convert meters to KM
|
|
$day->visibility = $day->visibility / 1000;
|
|
}
|
|
}
|
|
}
|
|
|
|
// not available
|
|
$day->dewPoint = $item['dew_point'] ?? '--';
|
|
$day->uvIndex = $item['uvi'] ?? '--';
|
|
$day->ozone = '--';
|
|
|
|
// Map icon
|
|
$icons = self::iconMap();
|
|
$icon = $item['weather'][0]['icon'];
|
|
$day->icon = $icons['backgrounds'][$icon] ?? 'wi-na';
|
|
$day->wicon = $icons['weather-icons'][$icon] ?? 'wi-na';
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
public static function supportedLanguages()
|
|
{
|
|
return [
|
|
['id' => 'af', 'value' => __('Afrikaans')],
|
|
['id' => 'ar', 'value' => __('Arabic')],
|
|
['id' => 'az', 'value' => __('Azerbaijani')],
|
|
['id' => 'bg', 'value' => __('Bulgarian')],
|
|
['id' => 'ca', 'value' => __('Catalan')],
|
|
['id' => 'zh_cn', 'value' => __('Chinese Simplified')],
|
|
['id' => 'zh_tw', 'value' => __('Chinese Traditional')],
|
|
['id' => 'cz', 'value' => __('Czech')],
|
|
['id' => 'da', 'value' => __('Danish')],
|
|
['id' => 'de', 'value' => __('German')],
|
|
['id' => 'el', 'value' => __('Greek')],
|
|
['id' => 'en', 'value' => __('English')],
|
|
['id' => 'eu', 'value' => __('Basque')],
|
|
['id' => 'fa', 'value' => __('Persian (Farsi)')],
|
|
['id' => 'fi', 'value' => __('Finnish')],
|
|
['id' => 'fr', 'value' => __('French')],
|
|
['id' => 'gl', 'value' => __('Galician')],
|
|
['id' => 'he', 'value' => __('Hebrew')],
|
|
['id' => 'hi', 'value' => __('Hindi')],
|
|
['id' => 'hr', 'value' => __('Croatian')],
|
|
['id' => 'hu', 'value' => __('Hungarian')],
|
|
['id' => 'id', 'value' => __('Indonesian')],
|
|
['id' => 'it', 'value' => __('Italian')],
|
|
['id' => 'ja', 'value' => __('Japanese')],
|
|
['id' => 'kr', 'value' => __('Korean')],
|
|
['id' => 'la', 'value' => __('Latvian')],
|
|
['id' => 'lt', 'value' => __('Lithuanian')],
|
|
['id' => 'mk', 'value' => __('Macedonian')],
|
|
['id' => 'no', 'value' => __('Norwegian')],
|
|
['id' => 'nl', 'value' => __('Dutch')],
|
|
['id' => 'pl', 'value' => __('Polish')],
|
|
['id' => 'pt', 'value' => __('Portuguese')],
|
|
['id' => 'pt_br', 'value' => __('Português Brasil')],
|
|
['id' => 'ro', 'value' => __('Romanian')],
|
|
['id' => 'ru', 'value' => __('Russian')],
|
|
['id' => 'se', 'value' => __('Swedish')],
|
|
['id' => 'sk', 'value' => __('Slovak')],
|
|
['id' => 'sl', 'value' => __('Slovenian')],
|
|
['id' => 'es', 'value' => __('Spanish')],
|
|
['id' => 'sr', 'value' => __('Serbian')],
|
|
['id' => 'th', 'value' => __('Thai')],
|
|
['id' => 'tr', 'value' => __('Turkish')],
|
|
['id' => 'uk', 'value' => __('Ukrainian')],
|
|
['id' => 'vi', 'value' => __('Vietnamese')],
|
|
['id' => 'zu', 'value' => __('Zulu')]
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array
|
|
*/
|
|
private function iconMap()
|
|
{
|
|
return [
|
|
'weather-icons' => [
|
|
'01d' => 'wi-day-sunny',
|
|
'01n' => 'wi-night-clear',
|
|
'02d' => 'wi-day-cloudy',
|
|
'02n' => 'wi-night-partly-cloudy',
|
|
'03d' => 'wi-cloudy',
|
|
'03n' => 'wi-night-cloudy',
|
|
'04d' => 'wi-day-cloudy',
|
|
'04n' => 'wi-night-partly-cloudy',
|
|
'09d' => 'wi-rain',
|
|
'09n' => 'wi-night-rain',
|
|
'10d' => 'wi-rain',
|
|
'10n' => 'wi-night-rain',
|
|
'11d' => 'wi-day-thunderstorm',
|
|
'11n' => 'wi-night-thunderstorm',
|
|
'13d' => 'wi-day-snow',
|
|
'13n' => 'wi-night-snow',
|
|
'50d' => 'wi-day-fog',
|
|
'50n' => 'wi-night-fog'
|
|
],
|
|
'backgrounds' => [
|
|
'01d' => 'clear-day',
|
|
'01n' => 'clear-night',
|
|
'02d' => 'partly-cloudy-day',
|
|
'02n' => 'partly-cloudy-night',
|
|
'03d' => 'cloudy',
|
|
'03n' => 'cloudy',
|
|
'04d' => 'partly-cloudy-day',
|
|
'04n' => 'partly-cloudy-night',
|
|
'09d' => 'rain',
|
|
'09n' => 'rain',
|
|
'10d' => 'rain',
|
|
'10n' => 'rain',
|
|
'11d' => 'wind',
|
|
'11n' => 'wind',
|
|
'13d' => 'snow',
|
|
'13n' => 'snow',
|
|
'50d' => 'fog',
|
|
'50n' => 'fog'
|
|
]
|
|
];
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public static function unitsAvailable()
|
|
{
|
|
return [
|
|
['id' => 'auto', 'value' => 'Automatically select based on geographic location', 'tempUnit' => '', 'windUnit' => '', 'visibilityUnit' => ''],
|
|
['id' => 'ca', 'value' => 'Canada', 'tempUnit' => 'C', 'windUnit' => 'KPH', 'visibilityUnit' => 'km'],
|
|
['id' => 'si', 'value' => 'Standard International Units', 'tempUnit' => 'C', 'windUnit' => 'MPS', 'visibilityUnit' => 'km'],
|
|
['id' => 'uk2', 'value' => 'United Kingdom', 'tempUnit' => 'C', 'windUnit' => 'MPH', 'visibilityUnit' => 'mi'],
|
|
['id' => 'us', 'value' => 'United States', 'tempUnit' => 'F', 'windUnit' => 'MPH', 'visibilityUnit' => 'mi'],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param $code
|
|
* @return mixed|null
|
|
*/
|
|
public function getUnit($code)
|
|
{
|
|
foreach (self::unitsAvailable() as $unit) {
|
|
if ($unit['id'] == $code) {
|
|
return $unit;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @return array
|
|
*/
|
|
private static function cardinalDirections()
|
|
{
|
|
return [
|
|
'N' => [337.5, 22.5],
|
|
'NE' => [22.5, 67.5],
|
|
'E' => [67.5, 112.5],
|
|
'SE' => [112.5, 157.5],
|
|
'S' => [157.5, 202.5],
|
|
'SW' => [202.5, 247.5],
|
|
'W' => [247.5, 292.5],
|
|
'NW' => [292.5, 337.5]
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param ScheduleCriteriaRequestInterface $event
|
|
* @return void
|
|
* @throws ConfigurationException
|
|
*/
|
|
public function onScheduleCriteriaRequest(ScheduleCriteriaRequestInterface $event): void
|
|
{
|
|
// Initialize Open Weather Schedule Criteria parameters
|
|
$event->addType('weather', __('Weather'))
|
|
->addMetric('weather_condition', __('Weather Condition'))
|
|
->addCondition([
|
|
'eq' => __('Equal to')
|
|
])
|
|
->addValues('dropdown', [
|
|
'thunderstorm' => __('Thunderstorm'),
|
|
'drizzle' => __('Drizzle'),
|
|
'rain' => __('Rain'),
|
|
'snow' => __('Snow'),
|
|
'clear' => __('Clear'),
|
|
'clouds' => __('Clouds')
|
|
])
|
|
->addMetric('weather_temp_imperial', __('Temperature (Imperial)'))
|
|
->addCondition([
|
|
'lt' => __('Less than'),
|
|
'lte' => __('Less than or equal to'),
|
|
'eq' => __('Equal to'),
|
|
'gte' => __('Greater than or equal to'),
|
|
'gt' => __('Greater than')
|
|
])
|
|
->addValues('number', [])
|
|
->addMetric('weather_temp_metric', __('Temperature (Metric)'))
|
|
->addCondition([
|
|
'lt' => __('Less than'),
|
|
'lte' => __('Less than or equal to'),
|
|
'eq' => __('Equal to'),
|
|
'gte' => __('Greater than or equal to'),
|
|
'gt' => __('Greater than')
|
|
])
|
|
->addValues('number', [])
|
|
->addMetric('weather_feels_like_imperial', __('Apparent Temperature (Imperial)'))
|
|
->addCondition([
|
|
'lt' => __('Less than'),
|
|
'lte' => __('Less than or equal to'),
|
|
'eq' => __('Equal to'),
|
|
'gte' => __('Greater than or equal to'),
|
|
'gt' => __('Greater than')
|
|
])
|
|
->addValues('number', [])
|
|
->addMetric('weather_feels_like_metric', __('Apparent Temperature (Metric)'))
|
|
->addCondition([
|
|
'lt' => __('Less than'),
|
|
'lte' => __('Less than or equal to'),
|
|
'eq' => __('Equal to'),
|
|
'gte' => __('Greater than or equal to'),
|
|
'gt' => __('Greater than')
|
|
])
|
|
->addValues('number', [])
|
|
->addMetric('weather_wind_speed', __('Wind Speed'))
|
|
->addCondition([
|
|
'lt' => __('Less than'),
|
|
'lte' => __('Less than or equal to'),
|
|
'eq' => __('Equal to'),
|
|
'gte' => __('Greater than or equal to'),
|
|
'gt' => __('Greater than')
|
|
])
|
|
->addValues('number', [])
|
|
->addMetric('weather_wind_direction', __('Wind Direction'))
|
|
->addCondition([
|
|
'eq' => __('Equal to')
|
|
])
|
|
->addValues('dropdown', [
|
|
'N' => __('North'),
|
|
'NE' => __('Northeast'),
|
|
'E' => __('East'),
|
|
'SE' => __('Southeast'),
|
|
'S' => __('South'),
|
|
'SW' => __('Southwest'),
|
|
'W' => __('West'),
|
|
'NW' => __('Northwest'),
|
|
])
|
|
->addMetric('weather_wind_degrees', __('Wind Direction (degrees)'))
|
|
->addCondition([
|
|
'lt' => __('Less than'),
|
|
'lte' => __('Less than or equal to'),
|
|
'eq' => __('Equal to'),
|
|
'gte' => __('Greater than or equal to'),
|
|
'gt' => __('Greater than')
|
|
])
|
|
->addValues('number', [])
|
|
->addMetric('weather_humidity', __('Humidity (Percent)'))
|
|
->addCondition([
|
|
'lt' => __('Less than'),
|
|
'lte' => __('Less than or equal to'),
|
|
'eq' => __('Equal to'),
|
|
'gte' => __('Greater than or equal to'),
|
|
'gt' => __('Greater than')
|
|
])
|
|
->addValues('number', [])
|
|
->addMetric('weather_pressure', __('Pressure'))
|
|
->addCondition([
|
|
'lt' => __('Less than'),
|
|
'lte' => __('Less than or equal to'),
|
|
'eq' => __('Equal to'),
|
|
'gte' => __('Greater than or equal to'),
|
|
'gt' => __('Greater than')
|
|
])
|
|
->addValues('number', [])
|
|
->addMetric('weather_visibility', __('Visibility (meters)'))
|
|
->addCondition([
|
|
'lt' => __('Less than'),
|
|
'lte' => __('Less than or equal to'),
|
|
'eq' => __('Equal to'),
|
|
'gte' => __('Greater than or equal to'),
|
|
'gt' => __('Greater than')
|
|
])
|
|
->addValues('number', []);
|
|
}
|
|
|
|
/**
|
|
* @param $item
|
|
* @param $unit
|
|
* @param $requestUnit
|
|
* @return array
|
|
*/
|
|
private function processXmdsWeatherData($item, $unit, $requestUnit): array
|
|
{
|
|
$windSpeedUnit = $unit['windUnit'] ?? 'KPH';
|
|
$visibilityDistanceUnit = $unit['visibilityUnit'] ?? 'km';
|
|
|
|
// var to store output/response
|
|
$data = array();
|
|
|
|
// format the weather condition
|
|
$data['weather_condition'] = str_replace(' ', '_', strtolower($item['weather'][0]['main']));
|
|
|
|
// Temperature
|
|
// imperial = F
|
|
// metric = C
|
|
$tempImperial = $item['temp'];
|
|
$apparentTempImperial = $item['feels_like'];
|
|
|
|
// Convert F to C
|
|
$tempMetric = ($tempImperial - 32) * 5 / 9;
|
|
$apparentTempMetric = ($apparentTempImperial - 32) * 5 / 9;
|
|
|
|
// Round those temperature values
|
|
$data['weather_temp_imperial'] = round($tempImperial, 0);
|
|
$data['weather_feels_like_imperial'] = round($apparentTempImperial, 0);
|
|
$data['weather_temp_metric'] = round($tempMetric, 0);
|
|
$data['weather_feels_like_metric'] = round($apparentTempMetric, 0);
|
|
|
|
// Humidity
|
|
$data['weather_humidity'] = $item['humidity'];
|
|
|
|
// Pressure
|
|
// received in hPa, display in mB
|
|
$data['weather_pressure'] = $item['pressure'] / 100;
|
|
|
|
// Wind
|
|
// metric = meters per second
|
|
// imperial = miles per hour
|
|
$data['weather_wind_speed'] = $item['wind_speed'] ?? $item['speed'] ?? null;
|
|
$data['weather_wind_degrees'] = $item['wind_deg'] ?? $item['deg'] ?? null;
|
|
|
|
if ($requestUnit === 'metric' && $windSpeedUnit !== 'MPS') {
|
|
// We have MPS and need to go to something else
|
|
if ($windSpeedUnit === 'MPH') {
|
|
// Convert MPS to MPH
|
|
$data['weather_wind_degrees'] = round($data['weather_wind_degrees'] * 2.237, 2);
|
|
} else if ($windSpeedUnit === 'KPH') {
|
|
// Convert MPS to KPH
|
|
$data['weather_wind_degrees'] = round($data['weather_wind_degrees'] * 3.6, 2);
|
|
}
|
|
} else if ($requestUnit === 'imperial' && $windSpeedUnit !== 'MPH') {
|
|
if ($windSpeedUnit === 'MPS') {
|
|
// Convert MPH to MPS
|
|
$data['weather_wind_degrees'] = round($data['weather_wind_degrees'] / 2.237, 2);
|
|
} else if ($windSpeedUnit === 'KPH') {
|
|
// Convert MPH to KPH
|
|
$data['weather_wind_degrees'] = round($data['weather_wind_degrees'] * 1.609344, 2);
|
|
}
|
|
}
|
|
|
|
// Wind direction
|
|
$data['weather_wind_direction'] = '--';
|
|
if ($data['weather_wind_degrees'] !== null && $data['weather_wind_degrees'] !== 0) {
|
|
foreach (self::cardinalDirections() as $dir => $angles) {
|
|
if ($data['weather_wind_degrees'] >= $angles[0] && $data['weather_wind_degrees'] < $angles[1]) {
|
|
$data['weather_wind_direction'] = $dir;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Visibility
|
|
// metric = meters
|
|
// imperial = meters?
|
|
$data['weather_visibility'] = $item['visibility'] ?? '--';
|
|
|
|
if ($data['weather_visibility'] !== '--') {
|
|
// Always in meters
|
|
if ($visibilityDistanceUnit === 'mi') {
|
|
// Convert meters to miles
|
|
$data['weather_visibility'] = $data['weather_visibility'] / 1609;
|
|
} else {
|
|
if ($visibilityDistanceUnit === 'km') {
|
|
// Convert meters to KM
|
|
$data['weather_visibility'] = $data['weather_visibility'] / 1000;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* @param XmdsWeatherRequestEvent $event
|
|
* @return void
|
|
* @throws GeneralException|\SoapFault
|
|
*/
|
|
public function onXmdsWeatherRequest(XmdsWeatherRequestEvent $event): void
|
|
{
|
|
// check for API Key
|
|
if (empty($this->getSetting('owmApiKey'))) {
|
|
$this->getLogger()->debug('onXmdsWeatherRequest: Open Weather Map not configured.');
|
|
|
|
throw new \SoapFault(
|
|
'Receiver',
|
|
'Open Weather Map API key is not configured'
|
|
);
|
|
}
|
|
|
|
$latitude = $event->getLatitude();
|
|
$longitude = $event->getLongitude();
|
|
|
|
// Cache expiry date
|
|
$cacheExpire = Carbon::now()->addHours($this->getSetting('xmdsCachePeriod'));
|
|
|
|
// use imperial as the default units, so we can get the right value when converting to metric
|
|
$units = 'imperial';
|
|
|
|
// Temperature and Wind Speed Unit Mappings
|
|
$unit = $this->getUnit('auto');
|
|
|
|
// Build the URL
|
|
$url = '?lat=' . $latitude
|
|
. '&lon=' . $longitude
|
|
. '&units=' . $units
|
|
. '&appid=[API_KEY]';
|
|
|
|
// check API plan
|
|
if ($this->getSetting('owmIsPaidPlan') ?? 0 == 1) {
|
|
// use weather data endpoints for Paid Plan
|
|
$data = $this->queryApi($this->apiUrl . $this->forecastCurrent . $url, $cacheExpire);
|
|
$data['current'] = $this->parseCurrentIntoFormat($data);
|
|
|
|
// Pick out the country
|
|
$country = $data['sys']['country'] ?? null;
|
|
|
|
// If we don't have a unit, then can we base it on the timezone we got back?
|
|
if ($country !== null) {
|
|
// Pick out some countries to set the units
|
|
if ($country === 'GB') {
|
|
$unit = $this->getUnit('uk2');
|
|
} else if ($country === 'US') {
|
|
$unit = $this->getUnit('us');
|
|
} else if ($country === 'CA') {
|
|
$unit = $this->getUnit('ca');
|
|
} else {
|
|
$unit = $this->getUnit('si');
|
|
}
|
|
}
|
|
} else {
|
|
// We use one call API 3.0 for Free Plan
|
|
$data = $this->queryApi($this->apiUrl . $this->forecastCombinedV3 . $url, $cacheExpire);
|
|
|
|
// Country based on timezone (this is harder than using the real country)
|
|
if (Str::startsWith($data['timezone'], 'America')) {
|
|
$unit = $this->getUnit('us');
|
|
} else if ($data['timezone'] === 'Europe/London') {
|
|
$unit = $this->getUnit('uk2');
|
|
} else {
|
|
$unit = $this->getUnit('si');
|
|
}
|
|
}
|
|
|
|
// process weather data
|
|
$weatherData = $this->processXmdsWeatherData($data['current'], $unit, 'imperial');
|
|
|
|
// Set the processed weather data in the event as a JSON-encoded string
|
|
$event->setWeatherData(json_encode($weatherData));
|
|
}
|
|
}
|