2 * Copyright 2017-2018 Gregory Moyer and contributors.
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
16 package org.openhab.binding.lametrictime.api.local.impl;
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;
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;
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;
70 import com.google.gson.reflect.TypeToken;
72 public class LaMetricTimeLocalImpl extends AbstractClient implements LaMetricTimeLocal {
73 private static final String HEADER_ACCESS_TOKEN = "X-Access-Token";
75 private final LocalConfiguration config;
77 private volatile Api api;
79 public LaMetricTimeLocalImpl(LocalConfiguration config) {
83 public LaMetricTimeLocalImpl(LocalConfiguration config, ClientBuilder clientBuilder) {
93 api = getClient().target(config.getBaseUri()).request(MediaType.APPLICATION_JSON_TYPE)
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.");
109 public Device getDevice() {
110 return getClient().target(getApi().getEndpoints().getDeviceUrl()).request(MediaType.APPLICATION_JSON_TYPE)
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));
119 if (!Status.Family.SUCCESSFUL.equals(response.getStatusInfo().getFamily())) {
120 throw new NotificationCreationException(response.readEntity(Failure.class));
124 return response.readEntity(NotificationResult.class).getSuccess().getId();
125 } catch (Exception e) {
126 throw new NotificationCreationException("Invalid JSON returned from service", e);
131 public List<Notification> getNotifications() {
132 Response response = getClient().target(getApi().getEndpoints().getNotificationsUrl())
133 .request(MediaType.APPLICATION_JSON_TYPE).get();
135 Reader entity = new BufferedReader(
136 new InputStreamReader((InputStream) response.getEntity(), StandardCharsets.UTF_8));
139 return getGson().fromJson(entity, new TypeToken<List<Notification>>(){}.getType());
144 public Notification getCurrentNotification() {
145 Notification notification = getClient().target(getApi().getEndpoints().getCurrentNotificationUrl())
146 .request(MediaType.APPLICATION_JSON_TYPE).get(Notification.class);
148 // when there is no current notification, return null
149 if (notification.getId() == null) {
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();
162 if (!Status.Family.SUCCESSFUL.equals(response.getStatusInfo().getFamily())) {
163 throw new NotificationNotFoundException(response.readEntity(Failure.class));
166 return response.readEntity(Notification.class);
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();
175 if (!Status.Family.SUCCESSFUL.equals(response.getStatusInfo().getFamily())) {
176 throw new NotificationNotFoundException(response.readEntity(Failure.class));
183 public Display getDisplay() {
184 return getClient().target(getApi().getEndpoints().getDisplayUrl()).request(MediaType.APPLICATION_JSON_TYPE)
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));
193 if (!Status.Family.SUCCESSFUL.equals(response.getStatusInfo().getFamily())) {
194 throw new UpdateException(response.readEntity(Failure.class));
197 return response.readEntity(DisplayUpdateResult.class).getSuccess().getData();
201 public Audio getAudio() {
202 return getClient().target(getApi().getEndpoints().getAudioUrl()).request(MediaType.APPLICATION_JSON_TYPE)
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));
211 if (!Status.Family.SUCCESSFUL.equals(response.getStatusInfo().getFamily())) {
212 throw new UpdateException(response.readEntity(Failure.class));
215 return response.readEntity(AudioUpdateResult.class).getSuccess().getData();
219 public Bluetooth getBluetooth() {
220 return getClient().target(getApi().getEndpoints().getBluetoothUrl()).request(MediaType.APPLICATION_JSON_TYPE)
221 .get(Bluetooth.class);
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));
229 if (!Status.Family.SUCCESSFUL.equals(response.getStatusInfo().getFamily())) {
230 throw new UpdateException(response.readEntity(Failure.class));
233 return response.readEntity(BluetoothUpdateResult.class).getSuccess().getData();
237 public Wifi getWifi() {
238 return getClient().target(getApi().getEndpoints().getWifiUrl()).request(MediaType.APPLICATION_JSON_TYPE)
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));
250 if (!Status.Family.SUCCESSFUL.equals(response.getStatusInfo().getFamily())) {
251 throw new UpdateException(response.readEntity(Failure.class));
258 public SortedMap<String, Application> getApplications() {
259 Response response = getClient().target(getApi().getEndpoints().getAppsListUrl())
260 .request(MediaType.APPLICATION_JSON_TYPE).get();
262 Reader entity = new BufferedReader(
263 new InputStreamReader((InputStream) response.getEntity(), StandardCharsets.UTF_8));
266 return getGson().fromJson(entity,
267 new TypeToken<SortedMap<String, Application>>(){}.getType());
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();
276 if (!Status.Family.SUCCESSFUL.equals(response.getStatusInfo().getFamily())) {
277 throw new ApplicationNotFoundException(response.readEntity(Failure.class));
280 return response.readEntity(Application.class);
284 public void activatePreviousApplication() {
285 getClient().target(getApi().getEndpoints().getAppsSwitchPrevUrl()).request(MediaType.APPLICATION_JSON_TYPE)
286 .put(Entity.json(new Object()));
290 public void activateNextApplication() {
291 getClient().target(getApi().getEndpoints().getAppsSwitchNextUrl()).request(MediaType.APPLICATION_JSON_TYPE)
292 .put(Entity.json(new Object()));
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()));
301 if (!Status.Family.SUCCESSFUL.equals(response.getStatusInfo().getFamily())) {
302 throw new ApplicationActivationException(response.readEntity(Failure.class));
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));
313 if (!Status.Family.SUCCESSFUL.equals(response.getStatusInfo().getFamily())) {
314 throw new ApplicationActionException(response.readEntity(Failure.class));
321 protected Client createClient() {
322 ClientBuilder builder = clientBuilder;
324 // setup Gson (de)serialization
325 GsonProvider<Object> gsonProvider = new GsonProvider<>();
326 builder.register(gsonProvider);
328 if (config.isSecure()) {
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.
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.
338 if (config.isIgnoreCertificateValidation()) {
340 SSLContext sslcontext = SSLContext.getInstance("TLS");
341 sslcontext.init(null, new TrustManager[] { new X509TrustManager() {
343 public void checkClientTrusted(X509Certificate[] arg0, String arg1)
344 throws CertificateException {
349 public void checkServerTrusted(X509Certificate[] arg0, String arg1)
350 throws CertificateException {
355 public X509Certificate[] getAcceptedIssuers() {
356 return new X509Certificate[0];
359 } }, new java.security.SecureRandom());
360 builder.sslContext(sslcontext);
361 } catch (KeyManagementException | NoSuchAlgorithmException e) {
362 throw new RuntimeException("Failed to setup secure communication", e);
367 * The self-signed certificate used by LaMetric time does not match
368 * the host when configured on a network. This makes the HTTPS
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
376 if (config.isIgnoreHostnameValidation()) {
377 builder.hostnameVerifier((host, session) -> true);
381 // turn on logging if requested
382 if (config.isLogging()) {
384 new LoggingFilter(Logger.getLogger(LaMetricTimeCloudImpl.class.getName()), config.getLogMax()));
388 builder.register(HttpAuthenticationFeature.basic(config.getAuthUser(), config.getApiKey()));
390 return builder.build();