]> git.basschouten.com Git - openhab-addons.git/blob
ff5ad982bcd27863956ac1cda1dfc3109b766642
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.lametrictime.internal.api.local.impl;
14
15 import java.io.BufferedReader;
16 import java.io.InputStream;
17 import java.io.InputStreamReader;
18 import java.io.Reader;
19 import java.nio.charset.StandardCharsets;
20 import java.security.KeyManagementException;
21 import java.security.NoSuchAlgorithmException;
22 import java.security.cert.CertificateException;
23 import java.security.cert.X509Certificate;
24 import java.util.List;
25 import java.util.SortedMap;
26
27 import javax.net.ssl.SSLContext;
28 import javax.net.ssl.TrustManager;
29 import javax.net.ssl.X509TrustManager;
30 import javax.ws.rs.client.Client;
31 import javax.ws.rs.client.ClientBuilder;
32 import javax.ws.rs.client.Entity;
33 import javax.ws.rs.core.MediaType;
34 import javax.ws.rs.core.Response;
35 import javax.ws.rs.core.Response.Status;
36
37 import org.eclipse.jdt.annotation.NonNullByDefault;
38 import org.eclipse.jdt.annotation.Nullable;
39 import org.openhab.binding.lametrictime.internal.GsonProvider;
40 import org.openhab.binding.lametrictime.internal.api.authentication.HttpAuthenticationFeature;
41 import org.openhab.binding.lametrictime.internal.api.cloud.impl.LaMetricTimeCloudImpl;
42 import org.openhab.binding.lametrictime.internal.api.common.impl.AbstractClient;
43 import org.openhab.binding.lametrictime.internal.api.filter.LoggingFilter;
44 import org.openhab.binding.lametrictime.internal.api.local.ApplicationActionException;
45 import org.openhab.binding.lametrictime.internal.api.local.ApplicationActivationException;
46 import org.openhab.binding.lametrictime.internal.api.local.ApplicationNotFoundException;
47 import org.openhab.binding.lametrictime.internal.api.local.LaMetricTimeLocal;
48 import org.openhab.binding.lametrictime.internal.api.local.LocalConfiguration;
49 import org.openhab.binding.lametrictime.internal.api.local.NotificationCreationException;
50 import org.openhab.binding.lametrictime.internal.api.local.NotificationNotFoundException;
51 import org.openhab.binding.lametrictime.internal.api.local.UpdateException;
52 import org.openhab.binding.lametrictime.internal.api.local.dto.Api;
53 import org.openhab.binding.lametrictime.internal.api.local.dto.Application;
54 import org.openhab.binding.lametrictime.internal.api.local.dto.Audio;
55 import org.openhab.binding.lametrictime.internal.api.local.dto.AudioUpdateResult;
56 import org.openhab.binding.lametrictime.internal.api.local.dto.Bluetooth;
57 import org.openhab.binding.lametrictime.internal.api.local.dto.BluetoothUpdateResult;
58 import org.openhab.binding.lametrictime.internal.api.local.dto.Device;
59 import org.openhab.binding.lametrictime.internal.api.local.dto.Display;
60 import org.openhab.binding.lametrictime.internal.api.local.dto.DisplayUpdateResult;
61 import org.openhab.binding.lametrictime.internal.api.local.dto.Failure;
62 import org.openhab.binding.lametrictime.internal.api.local.dto.Notification;
63 import org.openhab.binding.lametrictime.internal.api.local.dto.NotificationResult;
64 import org.openhab.binding.lametrictime.internal.api.local.dto.UpdateAction;
65 import org.openhab.binding.lametrictime.internal.api.local.dto.WidgetUpdates;
66 import org.openhab.binding.lametrictime.internal.api.local.dto.Wifi;
67 import org.slf4j.Logger;
68 import org.slf4j.LoggerFactory;
69
70 import com.google.gson.reflect.TypeToken;
71
72 /**
73  * Implementation class for LaMeticTimeLocal interface.
74  *
75  * @author Gregory Moyer - Initial contribution
76  */
77 @NonNullByDefault
78 public class LaMetricTimeLocalImpl extends AbstractClient implements LaMetricTimeLocal {
79     private final Logger logger = LoggerFactory.getLogger(LaMetricTimeLocalImpl.class);
80
81     private static final String HEADER_ACCESS_TOKEN = "X-Access-Token";
82
83     private final LocalConfiguration config;
84
85     @Nullable
86     private volatile Api api;
87
88     public LaMetricTimeLocalImpl(LocalConfiguration config) {
89         this.config = config;
90     }
91
92     public LaMetricTimeLocalImpl(LocalConfiguration config, ClientBuilder clientBuilder) {
93         super(clientBuilder);
94         this.config = config;
95     }
96
97     @Override
98     public Api getApi() {
99         Api localApi = api;
100         if (localApi == null) {
101             synchronized (this) {
102                 localApi = getClient().target(config.getBaseUri()).request(MediaType.APPLICATION_JSON_TYPE)
103                         .get(Api.class);
104                 // remove support for v2.0.0 which has several errors in returned endpoints
105                 if ("2.0.0".equals(localApi.getApiVersion())) {
106                     throw new IllegalStateException(
107                             "API version 2.0.0 detected, but 2.1.0 or greater is required. Please upgrade LaMetric Time firmware to version 1.7.7 or later. See http://lametric.com/firmware for more information.");
108                 }
109                 return localApi;
110             }
111         } else {
112             return localApi;
113         }
114     }
115
116     @Override
117     public Device getDevice() {
118         return getClient().target(getApi().getEndpoints().getDeviceUrl()).request(MediaType.APPLICATION_JSON_TYPE)
119                 .get(Device.class);
120     }
121
122     @Override
123     public String createNotification(@Nullable Notification notification) throws NotificationCreationException {
124         Response response = getClient().target(getApi().getEndpoints().getNotificationsUrl())
125                 .request(MediaType.APPLICATION_JSON_TYPE).post(Entity.json(notification));
126
127         if (!Status.Family.SUCCESSFUL.equals(response.getStatusInfo().getFamily())) {
128             throw new NotificationCreationException(response.readEntity(Failure.class));
129         }
130
131         try {
132             return response.readEntity(NotificationResult.class).getSuccess().getId();
133         } catch (Exception e) {
134             throw new NotificationCreationException("Invalid JSON returned from service", e);
135         }
136     }
137
138     @Override
139     public List<Notification> getNotifications() {
140         Response response = getClient().target(getApi().getEndpoints().getNotificationsUrl())
141                 .request(MediaType.APPLICATION_JSON_TYPE).get();
142
143         Reader entity = new BufferedReader(
144                 new InputStreamReader((InputStream) response.getEntity(), StandardCharsets.UTF_8));
145
146         // @formatter:off
147         return getGson().fromJson(entity,  new TypeToken<List<Notification>>(){}.getType());
148         // @formatter:on
149     }
150
151     @Override
152     public @Nullable Notification getCurrentNotification() {
153         Notification notification = getClient().target(getApi().getEndpoints().getCurrentNotificationUrl())
154                 .request(MediaType.APPLICATION_JSON_TYPE).get(Notification.class);
155
156         // when there is no current notification, return null
157         if (notification.getId() == null) {
158             return null;
159         }
160
161         return notification;
162     }
163
164     @Override
165     public Notification getNotification(String id) throws NotificationNotFoundException {
166         Response response = getClient()
167                 .target(getApi().getEndpoints().getConcreteNotificationUrl().replace("{:id}", id))
168                 .request(MediaType.APPLICATION_JSON_TYPE).get();
169
170         if (!Status.Family.SUCCESSFUL.equals(response.getStatusInfo().getFamily())) {
171             throw new NotificationNotFoundException(response.readEntity(Failure.class));
172         }
173
174         return response.readEntity(Notification.class);
175     }
176
177     @Override
178     public void deleteNotification(String id) throws NotificationNotFoundException {
179         Response response = getClient()
180                 .target(getApi().getEndpoints().getConcreteNotificationUrl().replace("{:id}", id))
181                 .request(MediaType.APPLICATION_JSON_TYPE).delete();
182
183         if (!Status.Family.SUCCESSFUL.equals(response.getStatusInfo().getFamily())) {
184             throw new NotificationNotFoundException(response.readEntity(Failure.class));
185         }
186
187         response.close();
188     }
189
190     @Override
191     public Display getDisplay() {
192         return getClient().target(getApi().getEndpoints().getDisplayUrl()).request(MediaType.APPLICATION_JSON_TYPE)
193                 .get(Display.class);
194     }
195
196     @Override
197     public Display updateDisplay(Display display) throws UpdateException {
198         Response response = getClient().target(getApi().getEndpoints().getDisplayUrl())
199                 .request(MediaType.APPLICATION_JSON_TYPE).put(Entity.json(display));
200
201         if (!Status.Family.SUCCESSFUL.equals(response.getStatusInfo().getFamily())) {
202             throw new UpdateException(response.readEntity(Failure.class));
203         }
204
205         return response.readEntity(DisplayUpdateResult.class).getSuccess().getData();
206     }
207
208     @Override
209     public Audio getAudio() {
210         return getClient().target(getApi().getEndpoints().getAudioUrl()).request(MediaType.APPLICATION_JSON_TYPE)
211                 .get(Audio.class);
212     }
213
214     @Override
215     public Audio updateAudio(Audio audio) throws UpdateException {
216         Response response = getClient().target(getApi().getEndpoints().getAudioUrl())
217                 .request(MediaType.APPLICATION_JSON_TYPE).put(Entity.json(audio));
218
219         if (!Status.Family.SUCCESSFUL.equals(response.getStatusInfo().getFamily())) {
220             throw new UpdateException(response.readEntity(Failure.class));
221         }
222
223         return response.readEntity(AudioUpdateResult.class).getSuccess().getData();
224     }
225
226     @Override
227     public Bluetooth getBluetooth() {
228         return getClient().target(getApi().getEndpoints().getBluetoothUrl()).request(MediaType.APPLICATION_JSON_TYPE)
229                 .get(Bluetooth.class);
230     }
231
232     @Override
233     public Bluetooth updateBluetooth(Bluetooth bluetooth) throws UpdateException {
234         Response response = getClient().target(getApi().getEndpoints().getBluetoothUrl())
235                 .request(MediaType.APPLICATION_JSON_TYPE).put(Entity.json(bluetooth));
236
237         if (!Status.Family.SUCCESSFUL.equals(response.getStatusInfo().getFamily())) {
238             throw new UpdateException(response.readEntity(Failure.class));
239         }
240
241         return response.readEntity(BluetoothUpdateResult.class).getSuccess().getData();
242     }
243
244     @Override
245     public Wifi getWifi() {
246         return getClient().target(getApi().getEndpoints().getWifiUrl()).request(MediaType.APPLICATION_JSON_TYPE)
247                 .get(Wifi.class);
248     }
249
250     @Override
251     public void updateApplication(String packageName, String accessToken, WidgetUpdates widgetUpdates)
252             throws UpdateException {
253         Response response = getClient()
254                 .target(getApi().getEndpoints().getWidgetUpdateUrl().replace("{:id}", packageName))
255                 .request(MediaType.APPLICATION_JSON_TYPE).header(HEADER_ACCESS_TOKEN, accessToken)
256                 .post(Entity.json(widgetUpdates));
257
258         if (!Status.Family.SUCCESSFUL.equals(response.getStatusInfo().getFamily())) {
259             throw new UpdateException(response.readEntity(Failure.class));
260         }
261
262         response.close();
263     }
264
265     @Override
266     public SortedMap<String, Application> getApplications() {
267         Response response = getClient().target(getApi().getEndpoints().getAppsListUrl())
268                 .request(MediaType.APPLICATION_JSON_TYPE).get();
269
270         Reader entity = new BufferedReader(
271                 new InputStreamReader((InputStream) response.getEntity(), StandardCharsets.UTF_8));
272
273         // @formatter:off
274         return getGson().fromJson(entity,
275                                   new TypeToken<SortedMap<String, Application>>(){}.getType());
276         // @formatter:on
277     }
278
279     @Override
280     public @Nullable Application getApplication(@Nullable String packageName) throws ApplicationNotFoundException {
281         if (packageName != null) {
282             Response response = getClient()
283                     .target(getApi().getEndpoints().getAppsGetUrl().replace("{:id}", packageName))
284                     .request(MediaType.APPLICATION_JSON_TYPE).get();
285
286             if (!Status.Family.SUCCESSFUL.equals(response.getStatusInfo().getFamily())) {
287                 throw new ApplicationNotFoundException(response.readEntity(Failure.class));
288             }
289
290             return response.readEntity(Application.class);
291         } else {
292             return null;
293         }
294     }
295
296     @Override
297     public void activatePreviousApplication() {
298         getClient().target(getApi().getEndpoints().getAppsSwitchPrevUrl()).request(MediaType.APPLICATION_JSON_TYPE)
299                 .put(Entity.json(new Object()));
300     }
301
302     @Override
303     public void activateNextApplication() {
304         getClient().target(getApi().getEndpoints().getAppsSwitchNextUrl()).request(MediaType.APPLICATION_JSON_TYPE)
305                 .put(Entity.json(new Object()));
306     }
307
308     @Override
309     public void activateApplication(String packageName, String widgetId) throws ApplicationActivationException {
310         Response response = getClient().target(getApi().getEndpoints().getAppsSwitchUrl().replace("{:id}", packageName)
311                 .replace("{:widget_id}", widgetId)).request(MediaType.APPLICATION_JSON_TYPE)
312                 .put(Entity.json(new Object()));
313
314         if (!Status.Family.SUCCESSFUL.equals(response.getStatusInfo().getFamily())) {
315             throw new ApplicationActivationException(response.readEntity(Failure.class));
316         }
317
318         response.close();
319     }
320
321     @Override
322     public void doAction(String packageName, String widgetId, @Nullable UpdateAction action)
323             throws ApplicationActionException {
324         Response response = getClient().target(getApi().getEndpoints().getAppsActionUrl().replace("{:id}", packageName)
325                 .replace("{:widget_id}", widgetId)).request(MediaType.APPLICATION_JSON_TYPE).post(Entity.json(action));
326
327         if (!Status.Family.SUCCESSFUL.equals(response.getStatusInfo().getFamily())) {
328             throw new ApplicationActionException(response.readEntity(Failure.class));
329         }
330
331         response.close();
332     }
333
334     @Override
335     protected Client createClient() {
336         ClientBuilder builder = clientBuilder;
337
338         // setup Gson (de)serialization
339         GsonProvider<Object> gsonProvider = new GsonProvider<>();
340         builder.register(gsonProvider);
341
342         if (config.isSecure()) {
343             /*
344              * The certificate presented by LaMetric time is self-signed.
345              * Therefore, unless the user takes action by adding the certificate
346              * chain to the Java keystore, HTTPS will fail.
347              *
348              * By setting the ignoreCertificateValidation configuration option
349              * to true (default), HTTPS will be used and the connection will be
350              * encrypted, but the validity of the certificate is not confirmed.
351              */
352             if (config.isIgnoreCertificateValidation()) {
353                 try {
354                     SSLContext sslcontext = SSLContext.getInstance("TLS");
355                     sslcontext.init(null, new TrustManager[] { new X509TrustManager() {
356                         @Override
357                         public void checkClientTrusted(X509Certificate @Nullable [] arg0, @Nullable String arg1)
358                                 throws CertificateException {
359                             // noop
360                         }
361
362                         @Override
363                         public void checkServerTrusted(X509Certificate @Nullable [] arg0, @Nullable String arg1)
364                                 throws CertificateException {
365                             // noop
366                         }
367
368                         @Override
369                         public X509Certificate[] getAcceptedIssuers() {
370                             return new X509Certificate[0];
371                         }
372                     } }, new java.security.SecureRandom());
373                     builder.sslContext(sslcontext);
374                 } catch (KeyManagementException | NoSuchAlgorithmException e) {
375                     logger.error("Failed to setup secure communication", e);
376                 }
377             }
378
379             /*
380              * The self-signed certificate used by LaMetric time does not match
381              * the host when configured on a network. This makes the HTTPS
382              * handshake fail.
383              *
384              * By setting the ignoreHostnameValidation configuration option to
385              * true (default), HTTPS will be used and the connection will be
386              * encrypted, but the validity of the hostname in the certificate is
387              * not confirmed.
388              */
389             if (config.isIgnoreHostnameValidation()) {
390                 builder.hostnameVerifier((host, session) -> true);
391             }
392         }
393
394         // turn on logging if requested
395         if (config.isLogging()) {
396             builder.register(new LoggingFilter(
397                     java.util.logging.Logger.getLogger(LaMetricTimeCloudImpl.class.getName()), config.getLogMax()));
398         }
399
400         // setup basic auth
401         builder.register(HttpAuthenticationFeature.basic(config.getAuthUser(), config.getApiKey()));
402
403         return builder.build();
404     }
405 }