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