Passed
Push — master ( 9bdf0e...8b8917 )
by Roeland
14:23 queued 10s
created
apps/weather_status/lib/Controller/WeatherStatusController.php 1 patch
Indentation   +101 added lines, -101 removed lines patch added patch discarded remove patch
@@ -33,115 +33,115 @@
 block discarded – undo
33 33
 
34 34
 class WeatherStatusController extends OCSController {
35 35
 
36
-	/** @var string */
37
-	private $userId;
36
+    /** @var string */
37
+    private $userId;
38 38
 
39
-	/** @var ILogger */
40
-	private $logger;
39
+    /** @var ILogger */
40
+    private $logger;
41 41
 
42
-	/** @var WeatherStatusService */
43
-	private $service;
42
+    /** @var WeatherStatusService */
43
+    private $service;
44 44
 
45
-	public function __construct(string $appName,
46
-								IRequest $request,
47
-								ILogger $logger,
48
-								WeatherStatusService $service,
49
-								?string $userId) {
50
-		parent::__construct($appName, $request);
51
-		$this->userId = $userId;
52
-		$this->logger = $logger;
53
-		$this->service = $service;
54
-	}
45
+    public function __construct(string $appName,
46
+                                IRequest $request,
47
+                                ILogger $logger,
48
+                                WeatherStatusService $service,
49
+                                ?string $userId) {
50
+        parent::__construct($appName, $request);
51
+        $this->userId = $userId;
52
+        $this->logger = $logger;
53
+        $this->service = $service;
54
+    }
55 55
 
56
-	/**
57
-	 * @NoAdminRequired
58
-	 *
59
-	 * Try to use the address set in user personal settings as weather location
60
-	 *
61
-	 * @return DataResponse with success state and address information
62
-	 */
63
-	public function usePersonalAddress(): DataResponse {
64
-		return new DataResponse($this->service->usePersonalAddress());
65
-	}
56
+    /**
57
+     * @NoAdminRequired
58
+     *
59
+     * Try to use the address set in user personal settings as weather location
60
+     *
61
+     * @return DataResponse with success state and address information
62
+     */
63
+    public function usePersonalAddress(): DataResponse {
64
+        return new DataResponse($this->service->usePersonalAddress());
65
+    }
66 66
 
67
-	/**
68
-	 * @NoAdminRequired
69
-	 *
70
-	 * Change the weather status mode. There are currently 2 modes:
71
-	 * - ask the browser
72
-	 * - use the user defined address
73
-	 *
74
-	 * @param int $mode New mode
75
-	 * @return DataResponse success state
76
-	 */
77
-	public function setMode(int $mode): DataResponse {
78
-		return new DataResponse($this->service->setMode($mode));
79
-	}
67
+    /**
68
+     * @NoAdminRequired
69
+     *
70
+     * Change the weather status mode. There are currently 2 modes:
71
+     * - ask the browser
72
+     * - use the user defined address
73
+     *
74
+     * @param int $mode New mode
75
+     * @return DataResponse success state
76
+     */
77
+    public function setMode(int $mode): DataResponse {
78
+        return new DataResponse($this->service->setMode($mode));
79
+    }
80 80
 
81
-	/**
82
-	 * @NoAdminRequired
83
-	 *
84
-	 * Set address and resolve it to get coordinates
85
-	 * or directly set coordinates and get address with reverse geocoding
86
-	 *
87
-	 * @param string|null $address Any approximative or exact address
88
-	 * @param float|null $lat Latitude in decimal degree format
89
-	 * @param float|null $lon Longitude in decimal degree format
90
-	 * @return DataResponse with success state and address information
91
-	 */
92
-	public function setLocation(?string $address, ?float $lat, ?float $lon): DataResponse {
93
-		$currentWeather = $this->service->setLocation($address, $lat, $lon);
94
-		return new DataResponse($currentWeather);
95
-	}
81
+    /**
82
+     * @NoAdminRequired
83
+     *
84
+     * Set address and resolve it to get coordinates
85
+     * or directly set coordinates and get address with reverse geocoding
86
+     *
87
+     * @param string|null $address Any approximative or exact address
88
+     * @param float|null $lat Latitude in decimal degree format
89
+     * @param float|null $lon Longitude in decimal degree format
90
+     * @return DataResponse with success state and address information
91
+     */
92
+    public function setLocation(?string $address, ?float $lat, ?float $lon): DataResponse {
93
+        $currentWeather = $this->service->setLocation($address, $lat, $lon);
94
+        return new DataResponse($currentWeather);
95
+    }
96 96
 
97
-	/**
98
-	 * @NoAdminRequired
99
-	 *
100
-	 * Get stored user location
101
-	 *
102
-	 * @return DataResponse which contains coordinates, formatted address and current weather status mode
103
-	 */
104
-	public function getLocation(): DataResponse {
105
-		$location = $this->service->getLocation();
106
-		return new DataResponse($location);
107
-	}
97
+    /**
98
+     * @NoAdminRequired
99
+     *
100
+     * Get stored user location
101
+     *
102
+     * @return DataResponse which contains coordinates, formatted address and current weather status mode
103
+     */
104
+    public function getLocation(): DataResponse {
105
+        $location = $this->service->getLocation();
106
+        return new DataResponse($location);
107
+    }
108 108
 
109
-	/**
110
-	 * @NoAdminRequired
111
-	 *
112
-	 * Get forecast for current location
113
-	 *
114
-	 * @return DataResponse which contains success state and filtered forecast data
115
-	 */
116
-	public function getForecast(): DataResponse {
117
-		$forecast = $this->service->getForecast();
118
-		if (isset($forecast['success']) && $forecast['success'] === false) {
119
-			return new DataResponse($forecast, Http::STATUS_NOT_FOUND);
120
-		} else {
121
-			return new DataResponse($forecast);
122
-		}
123
-	}
109
+    /**
110
+     * @NoAdminRequired
111
+     *
112
+     * Get forecast for current location
113
+     *
114
+     * @return DataResponse which contains success state and filtered forecast data
115
+     */
116
+    public function getForecast(): DataResponse {
117
+        $forecast = $this->service->getForecast();
118
+        if (isset($forecast['success']) && $forecast['success'] === false) {
119
+            return new DataResponse($forecast, Http::STATUS_NOT_FOUND);
120
+        } else {
121
+            return new DataResponse($forecast);
122
+        }
123
+    }
124 124
 
125
-	/**
126
-	 * @NoAdminRequired
127
-	 *
128
-	 * Get favorites list
129
-	 *
130
-	 * @return DataResponse which contains the favorite list
131
-	 */
132
-	public function getFavorites(): DataResponse {
133
-		return new DataResponse($this->service->getFavorites());
134
-	}
125
+    /**
126
+     * @NoAdminRequired
127
+     *
128
+     * Get favorites list
129
+     *
130
+     * @return DataResponse which contains the favorite list
131
+     */
132
+    public function getFavorites(): DataResponse {
133
+        return new DataResponse($this->service->getFavorites());
134
+    }
135 135
 
136
-	/**
137
-	 * @NoAdminRequired
138
-	 *
139
-	 * Set favorites list
140
-	 *
141
-	 * @param array $favorites
142
-	 * @return DataResponse success state
143
-	 */
144
-	public function setFavorites(array $favorites): DataResponse {
145
-		return new DataResponse($this->service->setFavorites($favorites));
146
-	}
136
+    /**
137
+     * @NoAdminRequired
138
+     *
139
+     * Set favorites list
140
+     *
141
+     * @param array $favorites
142
+     * @return DataResponse success state
143
+     */
144
+    public function setFavorites(array $favorites): DataResponse {
145
+        return new DataResponse($this->service->setFavorites($favorites));
146
+    }
147 147
 }
Please login to merge, or discard this patch.
apps/weather_status/lib/Service/WeatherStatusService.php 1 patch
Indentation   +401 added lines, -401 removed lines patch added patch discarded remove patch
@@ -45,405 +45,405 @@
 block discarded – undo
45 45
  * @package OCA\WeatherStatus\Service
46 46
  */
47 47
 class WeatherStatusService {
48
-	public const MODE_BROWSER_LOCATION = 1;
49
-	public const MODE_MANUAL_LOCATION = 2;
50
-
51
-	/** @var IClientService */
52
-	private $clientService;
53
-
54
-	/** @var IClient */
55
-	private $client;
56
-
57
-	/** @var IConfig */
58
-	private $config;
59
-
60
-	/** @var IL10N */
61
-	private $l10n;
62
-
63
-	/** @var ILogger */
64
-	private $logger;
65
-
66
-	/** @var IAccountManager */
67
-	private $accountManager;
68
-
69
-	/** @var IUserManager */
70
-	private $userManager;
71
-
72
-	/** @var IAppManager */
73
-	private $appManager;
74
-
75
-	/** @var ICacheFactory */
76
-	private $cacheFactory;
77
-
78
-	/** @var ICache */
79
-	private $cache;
80
-
81
-	/** @var string */
82
-	private $userId;
83
-
84
-	/** @var string */
85
-	private $version;
86
-
87
-	/**
88
-	 * WeatherStatusService constructor
89
-	 *
90
-	 * @param IClientService $clientService
91
-	 * @param IConfig $config
92
-	 * @param IL10N $l10n
93
-	 * @param ILogger $logger
94
-	 * @param IAccountManager $accountManager
95
-	 * @param IUserManager $userManager
96
-	 * @param IAppManager $appManager
97
-	 * @param ICacheFactory $cacheFactory
98
-	 * @param string $userId
99
-	 */
100
-	public function __construct(IClientService $clientService,
101
-								IConfig $config,
102
-								IL10N $l10n,
103
-								ILogger $logger,
104
-								IAccountManager $accountManager,
105
-								IUserManager $userManager,
106
-								IAppManager $appManager,
107
-								ICacheFactory $cacheFactory,
108
-								?string $userId) {
109
-		$this->config = $config;
110
-		$this->userId = $userId;
111
-		$this->l10n = $l10n;
112
-		$this->logger = $logger;
113
-		$this->accountManager = $accountManager;
114
-		$this->userManager = $userManager;
115
-		$this->appManager = $appManager;
116
-		$this->version = $appManager->getAppVersion(Application::APP_ID);
117
-		$this->clientService = $clientService;
118
-		$this->client = $clientService->newClient();
119
-		if ($cacheFactory->isAvailable()) {
120
-			$this->cache = $cacheFactory->createDistributed();
121
-		}
122
-	}
123
-
124
-	/**
125
-	 * Change the weather status mode. There are currently 2 modes:
126
-	 * - ask the browser
127
-	 * - use the user defined address
128
-	 * @param int $mode New mode
129
-	 * @return array success state
130
-	 */
131
-	public function setMode(int $mode): array {
132
-		$this->config->setUserValue($this->userId, Application::APP_ID, 'mode', strval($mode));
133
-		return ['success' => true];
134
-	}
135
-
136
-	/**
137
-	 * Get favorites list
138
-	 * @param array $favorites
139
-	 * @return array success state
140
-	 */
141
-	public function getFavorites(): array {
142
-		$favoritesJson = $this->config->getUserValue($this->userId, Application::APP_ID, 'favorites', '');
143
-		return json_decode($favoritesJson, true) ?: [];
144
-	}
145
-
146
-	/**
147
-	 * Set favorites list
148
-	 * @param array $favorites
149
-	 * @return array success state
150
-	 */
151
-	public function setFavorites(array $favorites): array {
152
-		$this->config->setUserValue($this->userId, Application::APP_ID, 'favorites', json_encode($favorites));
153
-		return ['success' => true];
154
-	}
155
-
156
-	/**
157
-	 * Try to use the address set in user personal settings as weather location
158
-	 *
159
-	 * @return array with success state and address information
160
-	 */
161
-	public function usePersonalAddress(): array {
162
-		$account = $this->accountManager->getAccount($this->userManager->get($this->userId));
163
-		try {
164
-			$address = $account->getProperty('address')->getValue();
165
-		} catch (PropertyDoesNotExistException $e) {
166
-			return ['success' => false];
167
-		}
168
-		if ($address === '') {
169
-			return ['success' => false];
170
-		}
171
-		return $this->setAddress($address);
172
-	}
173
-
174
-	/**
175
-	 * Set address and resolve it to get coordinates
176
-	 * or directly set coordinates and get address with reverse geocoding
177
-	 *
178
-	 * @param string|null $address Any approximative or exact address
179
-	 * @param float|null $lat Latitude in decimal degree format
180
-	 * @param float|null $lon Longitude in decimal degree format
181
-	 * @return array with success state and address information
182
-	 */
183
-	public function setLocation(?string $address, ?float $lat, ?float $lon): array {
184
-		if (!is_null($lat) && !is_null($lon)) {
185
-			// store coordinates
186
-			$this->config->setUserValue($this->userId, Application::APP_ID, 'lat', strval($lat));
187
-			$this->config->setUserValue($this->userId, Application::APP_ID, 'lon', strval($lon));
188
-			// resolve and store formatted address
189
-			$address = $this->resolveLocation($lat, $lon);
190
-			$address = $address ? $address : $this->l10n->t('Unknown address');
191
-			$this->config->setUserValue($this->userId, Application::APP_ID, 'address', $address);
192
-			// get and store altitude
193
-			$altitude = $this->getAltitude($lat, $lon);
194
-			$this->config->setUserValue($this->userId, Application::APP_ID, 'altitude', strval($altitude));
195
-			return [
196
-				'address' => $address,
197
-				'success' => true,
198
-			];
199
-		} elseif ($address) {
200
-			return $this->setAddress($address);
201
-		} else {
202
-			return ['success' => false];
203
-		}
204
-	}
205
-
206
-	/**
207
-	 * Provide address information from coordinates
208
-	 *
209
-	 * @param float $lat Latitude in decimal degree format
210
-	 * @param float $lon Longitude in decimal degree format
211
-	 */
212
-	private function resolveLocation(float $lat, float $lon): ?string {
213
-		$params = [
214
-			'lat' => number_format($lat, 2),
215
-			'lon' => number_format($lon, 2),
216
-			'addressdetails' => 1,
217
-			'format' => 'json',
218
-		];
219
-		$url = 'https://nominatim.openstreetmap.org/reverse';
220
-		$result = $this->requestJSON($url, $params);
221
-		return $this->formatOsmAddress($result);
222
-	}
223
-
224
-	/**
225
-	 * Get altitude from coordinates
226
-	 *
227
-	 * @param float $lat Latitude in decimal degree format
228
-	 * @param float $lon Longitude in decimal degree format
229
-	 * @return float altitude in meter
230
-	 */
231
-	private function getAltitude(float $lat, float $lon): float {
232
-		$params = [
233
-			'locations' => $lat . ',' . $lon,
234
-		];
235
-		$url = 'https://api.opentopodata.org/v1/srtm30m';
236
-		$result = $this->requestJSON($url, $params);
237
-		$altitude = 0;
238
-		if (isset($result['results']) && is_array($result['results']) && count($result['results']) > 0
239
-			&& is_array($result['results'][0]) && isset($result['results'][0]['elevation'])) {
240
-			$altitude = floatval($result['results'][0]['elevation']);
241
-		}
242
-		return $altitude;
243
-	}
244
-
245
-	/**
246
-	 * @return string Formatted address from JSON nominatim result
247
-	 */
248
-	private function formatOsmAddress(array $json): ?string {
249
-		if (isset($json['address']) && isset($json['display_name'])) {
250
-			$jsonAddr = $json['address'];
251
-			$cityAddress = '';
252
-			// priority : city, town, village, municipality
253
-			if (isset($jsonAddr['city'])) {
254
-				$cityAddress .= $jsonAddr['city'];
255
-			} elseif (isset($jsonAddr['town'])) {
256
-				$cityAddress .= $jsonAddr['town'];
257
-			} elseif (isset($jsonAddr['village'])) {
258
-				$cityAddress .= $jsonAddr['village'];
259
-			} elseif (isset($jsonAddr['municipality'])) {
260
-				$cityAddress .= $jsonAddr['municipality'];
261
-			} else {
262
-				return $json['display_name'];
263
-			}
264
-			// post code
265
-			if (isset($jsonAddr['postcode'])) {
266
-				$cityAddress .= ', ' . $jsonAddr['postcode'];
267
-			}
268
-			// country
269
-			if (isset($jsonAddr['country'])) {
270
-				$cityAddress .= ', ' . $jsonAddr['country'];
271
-				return $cityAddress;
272
-			} else {
273
-				return $json['display_name'];
274
-			}
275
-		} elseif (isset($json['display_name'])) {
276
-			return $json['display_name'];
277
-		}
278
-		return null;
279
-	}
280
-
281
-	/**
282
-	 * Set address and resolve it to get coordinates
283
-	 *
284
-	 * @param string $address Any approximative or exact address
285
-	 * @return array with success state and address information (coordinates and formatted address)
286
-	 */
287
-	public function setAddress(string $address): array {
288
-		$addressInfo = $this->searchForAddress($address);
289
-		if (isset($addressInfo['display_name']) && isset($addressInfo['lat']) && isset($addressInfo['lon'])) {
290
-			$formattedAddress = $this->formatOsmAddress($addressInfo);
291
-			$this->config->setUserValue($this->userId, Application::APP_ID, 'address', $formattedAddress);
292
-			$this->config->setUserValue($this->userId, Application::APP_ID, 'lat', strval($addressInfo['lat']));
293
-			$this->config->setUserValue($this->userId, Application::APP_ID, 'lon', strval($addressInfo['lon']));
294
-			$this->config->setUserValue($this->userId, Application::APP_ID, 'mode', strval(self::MODE_MANUAL_LOCATION));
295
-			// get and store altitude
296
-			$altitude = $this->getAltitude(floatval($addressInfo['lat']), floatval($addressInfo['lon']));
297
-			$this->config->setUserValue($this->userId, Application::APP_ID, 'altitude', strval($altitude));
298
-			return [
299
-				'lat' => $addressInfo['lat'],
300
-				'lon' => $addressInfo['lon'],
301
-				'address' => $formattedAddress,
302
-				'success' => true,
303
-			];
304
-		} else {
305
-			return ['success' => false];
306
-		}
307
-	}
308
-
309
-	/**
310
-	 * Ask nominatim information about an unformatted address
311
-	 *
312
-	 * @param string Unformatted address
313
-	 * @return array Full Nominatim result for the given address
314
-	 */
315
-	private function searchForAddress(string $address): array {
316
-		$params = [
317
-			'format' => 'json',
318
-			'addressdetails' => '1',
319
-			'extratags' => '1',
320
-			'namedetails' => '1',
321
-			'limit' => '1',
322
-		];
323
-		$url = 'https://nominatim.openstreetmap.org/search/' . $address;
324
-		$results = $this->requestJSON($url, $params);
325
-		if (count($results) > 0) {
326
-			return $results[0];
327
-		}
328
-		return ['error' => $this->l10n->t('No result.')];
329
-	}
330
-
331
-	/**
332
-	 * Get stored user location
333
-	 *
334
-	 * @return array which contains coordinates, formatted address and current weather status mode
335
-	 */
336
-	public function getLocation(): array {
337
-		$lat = $this->config->getUserValue($this->userId, Application::APP_ID, 'lat', '');
338
-		$lon = $this->config->getUserValue($this->userId, Application::APP_ID, 'lon', '');
339
-		$address = $this->config->getUserValue($this->userId, Application::APP_ID, 'address', '');
340
-		$mode = $this->config->getUserValue($this->userId, Application::APP_ID, 'mode', self::MODE_MANUAL_LOCATION);
341
-		return [
342
-			'lat' => $lat,
343
-			'lon' => $lon,
344
-			'address' => $address,
345
-			'mode' => intval($mode),
346
-		];
347
-	}
348
-
349
-	/**
350
-	 * Get forecast for current location
351
-	 *
352
-	 * @return array which contains success state and filtered forecast data
353
-	 */
354
-	public function getForecast(): array {
355
-		$lat = $this->config->getUserValue($this->userId, Application::APP_ID, 'lat', '');
356
-		$lon = $this->config->getUserValue($this->userId, Application::APP_ID, 'lon', '');
357
-		$alt = $this->config->getUserValue($this->userId, Application::APP_ID, 'altitude', '');
358
-		if (!is_numeric($alt)) {
359
-			$alt = 0;
360
-		}
361
-		if (is_numeric($lat) && is_numeric($lon)) {
362
-			return $this->forecastRequest(floatval($lat), floatval($lon), floatval($alt));
363
-		} else {
364
-			return ['success' => false];
365
-		}
366
-	}
367
-
368
-	/**
369
-	 * Actually make the request to the forecast service
370
-	 *
371
-	 * @param float $lat Latitude of requested forecast, in decimal degree format
372
-	 * @param float $lon Longitude of requested forecast, in decimal degree format
373
-	 * @param float $altitude Altitude of requested forecast, in meter
374
-	 * @param int $nbValues Number of forecast values (hours)
375
-	 * @return array Filtered forecast data
376
-	 */
377
-	private function forecastRequest(float $lat, float $lon, float $altitude, int $nbValues = 10): array {
378
-		$params = [
379
-			'lat' => number_format($lat, 2),
380
-			'lon' => number_format($lon, 2),
381
-			'altitude' => $altitude,
382
-		];
383
-		$url = 'https://api.met.no/weatherapi/locationforecast/2.0/compact';
384
-		$weather = $this->requestJSON($url, $params);
385
-		if (isset($weather['properties']) && isset($weather['properties']['timeseries']) && is_array($weather['properties']['timeseries'])) {
386
-			return array_slice($weather['properties']['timeseries'], 0, $nbValues);
387
-		}
388
-		return ['error' => $this->l10n->t('Malformed JSON data.')];
389
-	}
390
-
391
-	/**
392
-	 * Make a HTTP GET request and parse JSON result.
393
-	 * Request results are cached until the 'Expires' response header says so
394
-	 *
395
-	 * @param string $url Base URL to query
396
-	 * @param array $params GET parameters
397
-	 * @return array which contains the error message or the parsed JSON result
398
-	 */
399
-	private function requestJSON(string $url, array $params = []): array {
400
-		if (isset($this->cache)) {
401
-			$cacheKey = $url . '|' . implode(',', $params) . '|' . implode(',', array_keys($params));
402
-			if ($this->cache->hasKey($cacheKey)) {
403
-				return $this->cache->get($cacheKey);
404
-			}
405
-		}
406
-		try {
407
-			$options = [
408
-				'headers' => [
409
-					'User-Agent' => 'NextcloudWeatherStatus/' . $this->version . ' nextcloud.com'
410
-				],
411
-			];
412
-
413
-			$reqUrl = $url;
414
-			if (count($params) > 0) {
415
-				$paramsContent = http_build_query($params);
416
-				$reqUrl = $url . '?' . $paramsContent;
417
-			}
418
-
419
-			$response = $this->client->get($reqUrl, $options);
420
-			$body = $response->getBody();
421
-			$headers = $response->getHeaders();
422
-			$respCode = $response->getStatusCode();
423
-
424
-			if ($respCode >= 400) {
425
-				return ['error' => $this->l10n->t('Error')];
426
-			} else {
427
-				$json = json_decode($body, true);
428
-				if (isset($this->cache)) {
429
-					// default cache duration is one hour
430
-					$cacheDuration = 60 * 60;
431
-					if (isset($headers['Expires']) && count($headers['Expires']) > 0) {
432
-						// if the Expires response header is set, use it to define cache duration
433
-						$expireTs = (new \Datetime($headers['Expires'][0]))->getTimestamp();
434
-						$nowTs = (new \Datetime())->getTimestamp();
435
-						$duration = $expireTs - $nowTs;
436
-						if ($duration > $cacheDuration) {
437
-							$cacheDuration = $duration;
438
-						}
439
-					}
440
-					$this->cache->set($cacheKey, $json, $cacheDuration);
441
-				}
442
-				return $json;
443
-			}
444
-		} catch (\Exception $e) {
445
-			$this->logger->warning($url . 'API error : ' . $e, ['app' => Application::APP_ID]);
446
-			return ['error' => $e->getMessage()];
447
-		}
448
-	}
48
+    public const MODE_BROWSER_LOCATION = 1;
49
+    public const MODE_MANUAL_LOCATION = 2;
50
+
51
+    /** @var IClientService */
52
+    private $clientService;
53
+
54
+    /** @var IClient */
55
+    private $client;
56
+
57
+    /** @var IConfig */
58
+    private $config;
59
+
60
+    /** @var IL10N */
61
+    private $l10n;
62
+
63
+    /** @var ILogger */
64
+    private $logger;
65
+
66
+    /** @var IAccountManager */
67
+    private $accountManager;
68
+
69
+    /** @var IUserManager */
70
+    private $userManager;
71
+
72
+    /** @var IAppManager */
73
+    private $appManager;
74
+
75
+    /** @var ICacheFactory */
76
+    private $cacheFactory;
77
+
78
+    /** @var ICache */
79
+    private $cache;
80
+
81
+    /** @var string */
82
+    private $userId;
83
+
84
+    /** @var string */
85
+    private $version;
86
+
87
+    /**
88
+     * WeatherStatusService constructor
89
+     *
90
+     * @param IClientService $clientService
91
+     * @param IConfig $config
92
+     * @param IL10N $l10n
93
+     * @param ILogger $logger
94
+     * @param IAccountManager $accountManager
95
+     * @param IUserManager $userManager
96
+     * @param IAppManager $appManager
97
+     * @param ICacheFactory $cacheFactory
98
+     * @param string $userId
99
+     */
100
+    public function __construct(IClientService $clientService,
101
+                                IConfig $config,
102
+                                IL10N $l10n,
103
+                                ILogger $logger,
104
+                                IAccountManager $accountManager,
105
+                                IUserManager $userManager,
106
+                                IAppManager $appManager,
107
+                                ICacheFactory $cacheFactory,
108
+                                ?string $userId) {
109
+        $this->config = $config;
110
+        $this->userId = $userId;
111
+        $this->l10n = $l10n;
112
+        $this->logger = $logger;
113
+        $this->accountManager = $accountManager;
114
+        $this->userManager = $userManager;
115
+        $this->appManager = $appManager;
116
+        $this->version = $appManager->getAppVersion(Application::APP_ID);
117
+        $this->clientService = $clientService;
118
+        $this->client = $clientService->newClient();
119
+        if ($cacheFactory->isAvailable()) {
120
+            $this->cache = $cacheFactory->createDistributed();
121
+        }
122
+    }
123
+
124
+    /**
125
+     * Change the weather status mode. There are currently 2 modes:
126
+     * - ask the browser
127
+     * - use the user defined address
128
+     * @param int $mode New mode
129
+     * @return array success state
130
+     */
131
+    public function setMode(int $mode): array {
132
+        $this->config->setUserValue($this->userId, Application::APP_ID, 'mode', strval($mode));
133
+        return ['success' => true];
134
+    }
135
+
136
+    /**
137
+     * Get favorites list
138
+     * @param array $favorites
139
+     * @return array success state
140
+     */
141
+    public function getFavorites(): array {
142
+        $favoritesJson = $this->config->getUserValue($this->userId, Application::APP_ID, 'favorites', '');
143
+        return json_decode($favoritesJson, true) ?: [];
144
+    }
145
+
146
+    /**
147
+     * Set favorites list
148
+     * @param array $favorites
149
+     * @return array success state
150
+     */
151
+    public function setFavorites(array $favorites): array {
152
+        $this->config->setUserValue($this->userId, Application::APP_ID, 'favorites', json_encode($favorites));
153
+        return ['success' => true];
154
+    }
155
+
156
+    /**
157
+     * Try to use the address set in user personal settings as weather location
158
+     *
159
+     * @return array with success state and address information
160
+     */
161
+    public function usePersonalAddress(): array {
162
+        $account = $this->accountManager->getAccount($this->userManager->get($this->userId));
163
+        try {
164
+            $address = $account->getProperty('address')->getValue();
165
+        } catch (PropertyDoesNotExistException $e) {
166
+            return ['success' => false];
167
+        }
168
+        if ($address === '') {
169
+            return ['success' => false];
170
+        }
171
+        return $this->setAddress($address);
172
+    }
173
+
174
+    /**
175
+     * Set address and resolve it to get coordinates
176
+     * or directly set coordinates and get address with reverse geocoding
177
+     *
178
+     * @param string|null $address Any approximative or exact address
179
+     * @param float|null $lat Latitude in decimal degree format
180
+     * @param float|null $lon Longitude in decimal degree format
181
+     * @return array with success state and address information
182
+     */
183
+    public function setLocation(?string $address, ?float $lat, ?float $lon): array {
184
+        if (!is_null($lat) && !is_null($lon)) {
185
+            // store coordinates
186
+            $this->config->setUserValue($this->userId, Application::APP_ID, 'lat', strval($lat));
187
+            $this->config->setUserValue($this->userId, Application::APP_ID, 'lon', strval($lon));
188
+            // resolve and store formatted address
189
+            $address = $this->resolveLocation($lat, $lon);
190
+            $address = $address ? $address : $this->l10n->t('Unknown address');
191
+            $this->config->setUserValue($this->userId, Application::APP_ID, 'address', $address);
192
+            // get and store altitude
193
+            $altitude = $this->getAltitude($lat, $lon);
194
+            $this->config->setUserValue($this->userId, Application::APP_ID, 'altitude', strval($altitude));
195
+            return [
196
+                'address' => $address,
197
+                'success' => true,
198
+            ];
199
+        } elseif ($address) {
200
+            return $this->setAddress($address);
201
+        } else {
202
+            return ['success' => false];
203
+        }
204
+    }
205
+
206
+    /**
207
+     * Provide address information from coordinates
208
+     *
209
+     * @param float $lat Latitude in decimal degree format
210
+     * @param float $lon Longitude in decimal degree format
211
+     */
212
+    private function resolveLocation(float $lat, float $lon): ?string {
213
+        $params = [
214
+            'lat' => number_format($lat, 2),
215
+            'lon' => number_format($lon, 2),
216
+            'addressdetails' => 1,
217
+            'format' => 'json',
218
+        ];
219
+        $url = 'https://nominatim.openstreetmap.org/reverse';
220
+        $result = $this->requestJSON($url, $params);
221
+        return $this->formatOsmAddress($result);
222
+    }
223
+
224
+    /**
225
+     * Get altitude from coordinates
226
+     *
227
+     * @param float $lat Latitude in decimal degree format
228
+     * @param float $lon Longitude in decimal degree format
229
+     * @return float altitude in meter
230
+     */
231
+    private function getAltitude(float $lat, float $lon): float {
232
+        $params = [
233
+            'locations' => $lat . ',' . $lon,
234
+        ];
235
+        $url = 'https://api.opentopodata.org/v1/srtm30m';
236
+        $result = $this->requestJSON($url, $params);
237
+        $altitude = 0;
238
+        if (isset($result['results']) && is_array($result['results']) && count($result['results']) > 0
239
+            && is_array($result['results'][0]) && isset($result['results'][0]['elevation'])) {
240
+            $altitude = floatval($result['results'][0]['elevation']);
241
+        }
242
+        return $altitude;
243
+    }
244
+
245
+    /**
246
+     * @return string Formatted address from JSON nominatim result
247
+     */
248
+    private function formatOsmAddress(array $json): ?string {
249
+        if (isset($json['address']) && isset($json['display_name'])) {
250
+            $jsonAddr = $json['address'];
251
+            $cityAddress = '';
252
+            // priority : city, town, village, municipality
253
+            if (isset($jsonAddr['city'])) {
254
+                $cityAddress .= $jsonAddr['city'];
255
+            } elseif (isset($jsonAddr['town'])) {
256
+                $cityAddress .= $jsonAddr['town'];
257
+            } elseif (isset($jsonAddr['village'])) {
258
+                $cityAddress .= $jsonAddr['village'];
259
+            } elseif (isset($jsonAddr['municipality'])) {
260
+                $cityAddress .= $jsonAddr['municipality'];
261
+            } else {
262
+                return $json['display_name'];
263
+            }
264
+            // post code
265
+            if (isset($jsonAddr['postcode'])) {
266
+                $cityAddress .= ', ' . $jsonAddr['postcode'];
267
+            }
268
+            // country
269
+            if (isset($jsonAddr['country'])) {
270
+                $cityAddress .= ', ' . $jsonAddr['country'];
271
+                return $cityAddress;
272
+            } else {
273
+                return $json['display_name'];
274
+            }
275
+        } elseif (isset($json['display_name'])) {
276
+            return $json['display_name'];
277
+        }
278
+        return null;
279
+    }
280
+
281
+    /**
282
+     * Set address and resolve it to get coordinates
283
+     *
284
+     * @param string $address Any approximative or exact address
285
+     * @return array with success state and address information (coordinates and formatted address)
286
+     */
287
+    public function setAddress(string $address): array {
288
+        $addressInfo = $this->searchForAddress($address);
289
+        if (isset($addressInfo['display_name']) && isset($addressInfo['lat']) && isset($addressInfo['lon'])) {
290
+            $formattedAddress = $this->formatOsmAddress($addressInfo);
291
+            $this->config->setUserValue($this->userId, Application::APP_ID, 'address', $formattedAddress);
292
+            $this->config->setUserValue($this->userId, Application::APP_ID, 'lat', strval($addressInfo['lat']));
293
+            $this->config->setUserValue($this->userId, Application::APP_ID, 'lon', strval($addressInfo['lon']));
294
+            $this->config->setUserValue($this->userId, Application::APP_ID, 'mode', strval(self::MODE_MANUAL_LOCATION));
295
+            // get and store altitude
296
+            $altitude = $this->getAltitude(floatval($addressInfo['lat']), floatval($addressInfo['lon']));
297
+            $this->config->setUserValue($this->userId, Application::APP_ID, 'altitude', strval($altitude));
298
+            return [
299
+                'lat' => $addressInfo['lat'],
300
+                'lon' => $addressInfo['lon'],
301
+                'address' => $formattedAddress,
302
+                'success' => true,
303
+            ];
304
+        } else {
305
+            return ['success' => false];
306
+        }
307
+    }
308
+
309
+    /**
310
+     * Ask nominatim information about an unformatted address
311
+     *
312
+     * @param string Unformatted address
313
+     * @return array Full Nominatim result for the given address
314
+     */
315
+    private function searchForAddress(string $address): array {
316
+        $params = [
317
+            'format' => 'json',
318
+            'addressdetails' => '1',
319
+            'extratags' => '1',
320
+            'namedetails' => '1',
321
+            'limit' => '1',
322
+        ];
323
+        $url = 'https://nominatim.openstreetmap.org/search/' . $address;
324
+        $results = $this->requestJSON($url, $params);
325
+        if (count($results) > 0) {
326
+            return $results[0];
327
+        }
328
+        return ['error' => $this->l10n->t('No result.')];
329
+    }
330
+
331
+    /**
332
+     * Get stored user location
333
+     *
334
+     * @return array which contains coordinates, formatted address and current weather status mode
335
+     */
336
+    public function getLocation(): array {
337
+        $lat = $this->config->getUserValue($this->userId, Application::APP_ID, 'lat', '');
338
+        $lon = $this->config->getUserValue($this->userId, Application::APP_ID, 'lon', '');
339
+        $address = $this->config->getUserValue($this->userId, Application::APP_ID, 'address', '');
340
+        $mode = $this->config->getUserValue($this->userId, Application::APP_ID, 'mode', self::MODE_MANUAL_LOCATION);
341
+        return [
342
+            'lat' => $lat,
343
+            'lon' => $lon,
344
+            'address' => $address,
345
+            'mode' => intval($mode),
346
+        ];
347
+    }
348
+
349
+    /**
350
+     * Get forecast for current location
351
+     *
352
+     * @return array which contains success state and filtered forecast data
353
+     */
354
+    public function getForecast(): array {
355
+        $lat = $this->config->getUserValue($this->userId, Application::APP_ID, 'lat', '');
356
+        $lon = $this->config->getUserValue($this->userId, Application::APP_ID, 'lon', '');
357
+        $alt = $this->config->getUserValue($this->userId, Application::APP_ID, 'altitude', '');
358
+        if (!is_numeric($alt)) {
359
+            $alt = 0;
360
+        }
361
+        if (is_numeric($lat) && is_numeric($lon)) {
362
+            return $this->forecastRequest(floatval($lat), floatval($lon), floatval($alt));
363
+        } else {
364
+            return ['success' => false];
365
+        }
366
+    }
367
+
368
+    /**
369
+     * Actually make the request to the forecast service
370
+     *
371
+     * @param float $lat Latitude of requested forecast, in decimal degree format
372
+     * @param float $lon Longitude of requested forecast, in decimal degree format
373
+     * @param float $altitude Altitude of requested forecast, in meter
374
+     * @param int $nbValues Number of forecast values (hours)
375
+     * @return array Filtered forecast data
376
+     */
377
+    private function forecastRequest(float $lat, float $lon, float $altitude, int $nbValues = 10): array {
378
+        $params = [
379
+            'lat' => number_format($lat, 2),
380
+            'lon' => number_format($lon, 2),
381
+            'altitude' => $altitude,
382
+        ];
383
+        $url = 'https://api.met.no/weatherapi/locationforecast/2.0/compact';
384
+        $weather = $this->requestJSON($url, $params);
385
+        if (isset($weather['properties']) && isset($weather['properties']['timeseries']) && is_array($weather['properties']['timeseries'])) {
386
+            return array_slice($weather['properties']['timeseries'], 0, $nbValues);
387
+        }
388
+        return ['error' => $this->l10n->t('Malformed JSON data.')];
389
+    }
390
+
391
+    /**
392
+     * Make a HTTP GET request and parse JSON result.
393
+     * Request results are cached until the 'Expires' response header says so
394
+     *
395
+     * @param string $url Base URL to query
396
+     * @param array $params GET parameters
397
+     * @return array which contains the error message or the parsed JSON result
398
+     */
399
+    private function requestJSON(string $url, array $params = []): array {
400
+        if (isset($this->cache)) {
401
+            $cacheKey = $url . '|' . implode(',', $params) . '|' . implode(',', array_keys($params));
402
+            if ($this->cache->hasKey($cacheKey)) {
403
+                return $this->cache->get($cacheKey);
404
+            }
405
+        }
406
+        try {
407
+            $options = [
408
+                'headers' => [
409
+                    'User-Agent' => 'NextcloudWeatherStatus/' . $this->version . ' nextcloud.com'
410
+                ],
411
+            ];
412
+
413
+            $reqUrl = $url;
414
+            if (count($params) > 0) {
415
+                $paramsContent = http_build_query($params);
416
+                $reqUrl = $url . '?' . $paramsContent;
417
+            }
418
+
419
+            $response = $this->client->get($reqUrl, $options);
420
+            $body = $response->getBody();
421
+            $headers = $response->getHeaders();
422
+            $respCode = $response->getStatusCode();
423
+
424
+            if ($respCode >= 400) {
425
+                return ['error' => $this->l10n->t('Error')];
426
+            } else {
427
+                $json = json_decode($body, true);
428
+                if (isset($this->cache)) {
429
+                    // default cache duration is one hour
430
+                    $cacheDuration = 60 * 60;
431
+                    if (isset($headers['Expires']) && count($headers['Expires']) > 0) {
432
+                        // if the Expires response header is set, use it to define cache duration
433
+                        $expireTs = (new \Datetime($headers['Expires'][0]))->getTimestamp();
434
+                        $nowTs = (new \Datetime())->getTimestamp();
435
+                        $duration = $expireTs - $nowTs;
436
+                        if ($duration > $cacheDuration) {
437
+                            $cacheDuration = $duration;
438
+                        }
439
+                    }
440
+                    $this->cache->set($cacheKey, $json, $cacheDuration);
441
+                }
442
+                return $json;
443
+            }
444
+        } catch (\Exception $e) {
445
+            $this->logger->warning($url . 'API error : ' . $e, ['app' => Application::APP_ID]);
446
+            return ['error' => $e->getMessage()];
447
+        }
448
+    }
449 449
 }
Please login to merge, or discard this patch.
apps/weather_status/appinfo/routes.php 1 patch
Indentation   +9 added lines, -9 removed lines patch added patch discarded remove patch
@@ -24,13 +24,13 @@
 block discarded – undo
24 24
  */
25 25
 
26 26
 return [
27
-	'ocs' => [
28
-		['name' => 'WeatherStatus#setMode', 'url' => '/api/v1/mode', 'verb' => 'PUT'],
29
-		['name' => 'WeatherStatus#usePersonalAddress', 'url' => '/api/v1/use-personal', 'verb' => 'PUT'],
30
-		['name' => 'WeatherStatus#getLocation', 'url' => '/api/v1/location', 'verb' => 'GET'],
31
-		['name' => 'WeatherStatus#setLocation', 'url' => '/api/v1/location', 'verb' => 'PUT'],
32
-		['name' => 'WeatherStatus#getForecast', 'url' => '/api/v1/forecast', 'verb' => 'GET'],
33
-		['name' => 'WeatherStatus#getFavorites', 'url' => '/api/v1/favorites', 'verb' => 'GET'],
34
-		['name' => 'WeatherStatus#setFavorites', 'url' => '/api/v1/favorites', 'verb' => 'PUT'],
35
-	],
27
+    'ocs' => [
28
+        ['name' => 'WeatherStatus#setMode', 'url' => '/api/v1/mode', 'verb' => 'PUT'],
29
+        ['name' => 'WeatherStatus#usePersonalAddress', 'url' => '/api/v1/use-personal', 'verb' => 'PUT'],
30
+        ['name' => 'WeatherStatus#getLocation', 'url' => '/api/v1/location', 'verb' => 'GET'],
31
+        ['name' => 'WeatherStatus#setLocation', 'url' => '/api/v1/location', 'verb' => 'PUT'],
32
+        ['name' => 'WeatherStatus#getForecast', 'url' => '/api/v1/forecast', 'verb' => 'GET'],
33
+        ['name' => 'WeatherStatus#getFavorites', 'url' => '/api/v1/favorites', 'verb' => 'GET'],
34
+        ['name' => 'WeatherStatus#setFavorites', 'url' => '/api/v1/favorites', 'verb' => 'PUT'],
35
+    ],
36 36
 ];
Please login to merge, or discard this patch.