2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.lametrictime.internal.api.local.impl;
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;
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;
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;
70 import com.google.gson.reflect.TypeToken;
73 * Implementation class for LaMeticTimeLocal interface.
75 * @author Gregory Moyer - Initial contribution
78 public class LaMetricTimeLocalImpl extends AbstractClient implements LaMetricTimeLocal {
79 private final Logger logger = LoggerFactory.getLogger(LaMetricTimeLocalImpl.class);
81 private static final String HEADER_ACCESS_TOKEN = "X-Access-Token";
83 private final LocalConfiguration config;
86 private volatile Api api;
88 public LaMetricTimeLocalImpl(LocalConfiguration config) {
92 public LaMetricTimeLocalImpl(LocalConfiguration config, ClientBuilder clientBuilder) {
100 if (localApi == null) {
101 synchronized (this) {
102 localApi = getClient().target(config.getBaseUri()).request(MediaType.APPLICATION_JSON_TYPE)
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.");
117 public Device getDevice() {
118 return getClient().target(getApi().getEndpoints().getDeviceUrl()).request(MediaType.APPLICATION_JSON_TYPE)
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));
127 if (!Status.Family.SUCCESSFUL.equals(response.getStatusInfo().getFamily())) {
128 throw new NotificationCreationException(response.readEntity(Failure.class));
132 return response.readEntity(NotificationResult.class).getSuccess().getId();
133 } catch (Exception e) {
134 throw new NotificationCreationException("Invalid JSON returned from service", e);
139 public List<Notification> getNotifications() {
140 Response response = getClient().target(getApi().getEndpoints().getNotificationsUrl())
141 .request(MediaType.APPLICATION_JSON_TYPE).get();
143 Reader entity = new BufferedReader(
144 new InputStreamReader((InputStream) response.getEntity(), StandardCharsets.UTF_8));
147 return getGson().fromJson(entity, new TypeToken<List<Notification>>(){}.getType());
152 public @Nullable Notification getCurrentNotification() {
153 Notification notification = getClient().target(getApi().getEndpoints().getCurrentNotificationUrl())
154 .request(MediaType.APPLICATION_JSON_TYPE).get(Notification.class);
156 // when there is no current notification, return null
157 if (notification.getId() == null) {
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();
170 if (!Status.Family.SUCCESSFUL.equals(response.getStatusInfo().getFamily())) {
171 throw new NotificationNotFoundException(response.readEntity(Failure.class));
174 return response.readEntity(Notification.class);
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();
183 if (!Status.Family.SUCCESSFUL.equals(response.getStatusInfo().getFamily())) {
184 throw new NotificationNotFoundException(response.readEntity(Failure.class));
191 public Display getDisplay() {
192 return getClient().target(getApi().getEndpoints().getDisplayUrl()).request(MediaType.APPLICATION_JSON_TYPE)
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));
201 if (!Status.Family.SUCCESSFUL.equals(response.getStatusInfo().getFamily())) {
202 throw new UpdateException(response.readEntity(Failure.class));
205 return response.readEntity(DisplayUpdateResult.class).getSuccess().getData();
209 public Audio getAudio() {
210 return getClient().target(getApi().getEndpoints().getAudioUrl()).request(MediaType.APPLICATION_JSON_TYPE)
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));
219 if (!Status.Family.SUCCESSFUL.equals(response.getStatusInfo().getFamily())) {
220 throw new UpdateException(response.readEntity(Failure.class));
223 return response.readEntity(AudioUpdateResult.class).getSuccess().getData();
227 public Bluetooth getBluetooth() {
228 return getClient().target(getApi().getEndpoints().getBluetoothUrl()).request(MediaType.APPLICATION_JSON_TYPE)
229 .get(Bluetooth.class);
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));
237 if (!Status.Family.SUCCESSFUL.equals(response.getStatusInfo().getFamily())) {
238 throw new UpdateException(response.readEntity(Failure.class));
241 return response.readEntity(BluetoothUpdateResult.class).getSuccess().getData();
245 public Wifi getWifi() {
246 return getClient().target(getApi().getEndpoints().getWifiUrl()).request(MediaType.APPLICATION_JSON_TYPE)
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));
258 if (!Status.Family.SUCCESSFUL.equals(response.getStatusInfo().getFamily())) {
259 throw new UpdateException(response.readEntity(Failure.class));
266 public SortedMap<String, Application> getApplications() {
267 Response response = getClient().target(getApi().getEndpoints().getAppsListUrl())
268 .request(MediaType.APPLICATION_JSON_TYPE).get();
270 Reader entity = new BufferedReader(
271 new InputStreamReader((InputStream) response.getEntity(), StandardCharsets.UTF_8));
274 return getGson().fromJson(entity,
275 new TypeToken<SortedMap<String, Application>>(){}.getType());
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();
286 if (!Status.Family.SUCCESSFUL.equals(response.getStatusInfo().getFamily())) {
287 throw new ApplicationNotFoundException(response.readEntity(Failure.class));
290 return response.readEntity(Application.class);
297 public void activatePreviousApplication() {
298 getClient().target(getApi().getEndpoints().getAppsSwitchPrevUrl()).request(MediaType.APPLICATION_JSON_TYPE)
299 .put(Entity.json(new Object()));
303 public void activateNextApplication() {
304 getClient().target(getApi().getEndpoints().getAppsSwitchNextUrl()).request(MediaType.APPLICATION_JSON_TYPE)
305 .put(Entity.json(new Object()));
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()));
314 if (!Status.Family.SUCCESSFUL.equals(response.getStatusInfo().getFamily())) {
315 throw new ApplicationActivationException(response.readEntity(Failure.class));
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));
327 if (!Status.Family.SUCCESSFUL.equals(response.getStatusInfo().getFamily())) {
328 throw new ApplicationActionException(response.readEntity(Failure.class));
335 protected Client createClient() {
336 ClientBuilder builder = clientBuilder;
338 // setup Gson (de)serialization
339 GsonProvider<Object> gsonProvider = new GsonProvider<>();
340 builder.register(gsonProvider);
342 if (config.isSecure()) {
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.
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.
352 if (config.isIgnoreCertificateValidation()) {
354 SSLContext sslcontext = SSLContext.getInstance("TLS");
355 sslcontext.init(null, new TrustManager[] { new X509TrustManager() {
357 public void checkClientTrusted(X509Certificate @Nullable [] arg0, @Nullable String arg1)
358 throws CertificateException {
363 public void checkServerTrusted(X509Certificate @Nullable [] arg0, @Nullable String arg1)
364 throws CertificateException {
369 public X509Certificate[] getAcceptedIssuers() {
370 return new X509Certificate[0];
372 } }, new java.security.SecureRandom());
373 builder.sslContext(sslcontext);
374 } catch (KeyManagementException | NoSuchAlgorithmException e) {
375 logger.error("Failed to setup secure communication", e);
380 * The self-signed certificate used by LaMetric time does not match
381 * the host when configured on a network. This makes the HTTPS
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
389 if (config.isIgnoreHostnameValidation()) {
390 builder.hostnameVerifier((host, session) -> true);
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()));
401 builder.register(HttpAuthenticationFeature.basic(config.getAuthUser(), config.getApiKey()));
403 return builder.build();