]> git.basschouten.com Git - openhab-addons.git/blob
5952a305fc77e540616208ebd6d14ffbd850fc8b
[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.tellstick.internal.live;
14
15 import java.math.BigDecimal;
16 import java.math.RoundingMode;
17 import java.time.Duration;
18 import java.time.Instant;
19 import java.util.concurrent.ExecutionException;
20 import java.util.concurrent.Future;
21 import java.util.concurrent.TimeUnit;
22 import java.util.concurrent.TimeoutException;
23
24 import javax.xml.bind.JAXBContext;
25 import javax.xml.bind.JAXBException;
26 import javax.xml.stream.FactoryConfigurationError;
27 import javax.xml.stream.XMLInputFactory;
28 import javax.xml.stream.XMLStreamException;
29 import javax.xml.stream.XMLStreamReader;
30
31 import org.asynchttpclient.AsyncHttpClient;
32 import org.asynchttpclient.AsyncHttpClientConfig;
33 import org.asynchttpclient.DefaultAsyncHttpClient;
34 import org.asynchttpclient.DefaultAsyncHttpClientConfig;
35 import org.asynchttpclient.DefaultAsyncHttpClientConfig.Builder;
36 import org.asynchttpclient.Response;
37 import org.asynchttpclient.oauth.ConsumerKey;
38 import org.asynchttpclient.oauth.OAuthSignatureCalculator;
39 import org.asynchttpclient.oauth.RequestToken;
40 import org.eclipse.jdt.annotation.Nullable;
41 import org.openhab.binding.tellstick.internal.TelldusBindingException;
42 import org.openhab.binding.tellstick.internal.handler.TelldusDeviceController;
43 import org.openhab.binding.tellstick.internal.live.xml.TelldusLiveResponse;
44 import org.openhab.binding.tellstick.internal.live.xml.TellstickNetDevice;
45 import org.openhab.core.library.types.IncreaseDecreaseType;
46 import org.openhab.core.library.types.OnOffType;
47 import org.openhab.core.library.types.PercentType;
48 import org.openhab.core.types.Command;
49 import org.openhab.core.types.State;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
52 import org.tellstick.JNA;
53 import org.tellstick.device.TellstickDevice;
54 import org.tellstick.device.TellstickDeviceEvent;
55 import org.tellstick.device.TellstickException;
56 import org.tellstick.device.TellstickSensorEvent;
57 import org.tellstick.device.iface.Device;
58 import org.tellstick.device.iface.DeviceChangeListener;
59 import org.tellstick.device.iface.SensorListener;
60 import org.tellstick.device.iface.SwitchableDevice;
61
62 /**
63  * {@link TelldusLiveDeviceController} is the communication with Telldus Live service (Tellstick.NET and ZNET)
64  * This controller uses XML based Rest API to communicate with Telldus Live.
65  *
66  * @author Jarle Hjortland - Initial contribution
67  */
68 public class TelldusLiveDeviceController implements DeviceChangeListener, SensorListener, TelldusDeviceController {
69     private final Logger logger = LoggerFactory.getLogger(TelldusLiveDeviceController.class);
70     private long lastSend = 0;
71     public static final long DEFAULT_INTERVAL_BETWEEN_SEND = 250;
72     private static final int REQUEST_TIMEOUT_MS = 15000;
73     private AsyncHttpClient client;
74     static final String HTTP_API_TELLDUS_COM_XML = "http://api.telldus.com/xml/";
75     static final String HTTP_TELLDUS_CLIENTS = HTTP_API_TELLDUS_COM_XML + "clients/list";
76     static final String HTTP_TELLDUS_DEVICES = HTTP_API_TELLDUS_COM_XML + "devices/list?supportedMethods=19";
77     static final String HTTP_TELLDUS_SENSORS = HTTP_API_TELLDUS_COM_XML
78             + "sensors/list?includeValues=1&includeScale=1&includeUnit=1";
79     static final String HTTP_TELLDUS_SENSOR_INFO = HTTP_API_TELLDUS_COM_XML + "sensor/info";
80     static final String HTTP_TELLDUS_DEVICE_DIM = HTTP_API_TELLDUS_COM_XML + "device/dim?id=%d&level=%d";
81     static final String HTTP_TELLDUS_DEVICE_TURNOFF = HTTP_API_TELLDUS_COM_XML + "device/turnOff?id=%d";
82     static final String HTTP_TELLDUS_DEVICE_TURNON = HTTP_API_TELLDUS_COM_XML + "device/turnOn?id=%d";
83
84     private int nbRequest;
85     private long sumRequestDuration;
86     private long minRequestDuration = 1_000_000;
87     private long maxRequestDuration;
88     private int nbTimeouts;
89     private int nbErrors;
90
91     public TelldusLiveDeviceController() {
92     }
93
94     @Override
95     public void dispose() {
96         try {
97             client.close();
98         } catch (Exception e) {
99             logger.debug("Failed to close client", e);
100         }
101     }
102
103     void connectHttpClient(String publicKey, String privateKey, String token, String tokenSecret) {
104         ConsumerKey consumer = new ConsumerKey(publicKey, privateKey);
105         RequestToken user = new RequestToken(token, tokenSecret);
106         OAuthSignatureCalculator calc = new OAuthSignatureCalculator(consumer, user);
107         this.client = new DefaultAsyncHttpClient(createAsyncHttpClientConfig());
108         try {
109             this.client.setSignatureCalculator(calc);
110             Response response = client.prepareGet(HTTP_TELLDUS_CLIENTS).execute().get();
111             logger.debug("Response {} statusText {}", response.getResponseBody(), response.getStatusText());
112         } catch (InterruptedException | ExecutionException e) {
113             logger.warn("Failed to connect", e);
114         }
115     }
116
117     private AsyncHttpClientConfig createAsyncHttpClientConfig() {
118         Builder builder = new DefaultAsyncHttpClientConfig.Builder();
119         builder.setConnectTimeout(REQUEST_TIMEOUT_MS);
120         return builder.build();
121     }
122
123     @Override
124     public void handleSendEvent(Device device, int resendCount, boolean isdimmer, Command command)
125             throws TellstickException {
126         logger.debug("Send {} to {}", command, device);
127         if (device instanceof TellstickNetDevice) {
128             if (command == OnOffType.ON) {
129                 turnOn(device);
130             } else if (command == OnOffType.OFF) {
131                 turnOff(device);
132             } else if (command instanceof PercentType percentCommand) {
133                 dim(device, percentCommand);
134             } else if (command instanceof IncreaseDecreaseType increaseDecreaseCommand) {
135                 increaseDecrease(device, increaseDecreaseCommand);
136             }
137         } else if (device instanceof SwitchableDevice) {
138             if (command == OnOffType.ON) {
139                 if (isdimmer) {
140                     logger.debug("Turn off first in case it is allready on");
141                     turnOff(device);
142                 }
143                 turnOn(device);
144             } else if (command == OnOffType.OFF) {
145                 turnOff(device);
146             }
147         } else {
148             logger.warn("Cannot send to {}", device);
149         }
150     }
151
152     private void increaseDecrease(Device dev, IncreaseDecreaseType increaseDecreaseType) throws TellstickException {
153         String strValue = ((TellstickDevice) dev).getData();
154         double value = 0;
155         if (strValue != null) {
156             value = Double.valueOf(strValue);
157         }
158         int percent = (int) Math.round((value / 255) * 100);
159         if (IncreaseDecreaseType.INCREASE == increaseDecreaseType) {
160             percent = Math.min(percent + 10, 100);
161         } else if (IncreaseDecreaseType.DECREASE == increaseDecreaseType) {
162             percent = Math.max(percent - 10, 0);
163         }
164         dim(dev, new PercentType(percent));
165     }
166
167     private void dim(Device dev, PercentType command) throws TellstickException {
168         double value = command.doubleValue();
169
170         // 0 means OFF and 100 means ON
171         if (value == 0 && dev instanceof TellstickNetDevice) {
172             turnOff(dev);
173         } else if (value == 100 && dev instanceof TellstickNetDevice) {
174             turnOn(dev);
175         } else if (dev instanceof TellstickNetDevice device && (device.getMethods() & JNA.CLibrary.TELLSTICK_DIM) > 0) {
176             long tdVal = Math.round((value / 100) * 255);
177             TelldusLiveResponse response = callRestMethod(String.format(HTTP_TELLDUS_DEVICE_DIM, dev.getId(), tdVal),
178                     TelldusLiveResponse.class);
179             handleResponse(device, response);
180         } else {
181             throw new TelldusBindingException("Cannot send DIM to " + dev);
182         }
183     }
184
185     private void turnOff(Device dev) throws TellstickException {
186         if (dev instanceof TellstickNetDevice device) {
187             TelldusLiveResponse response = callRestMethod(String.format(HTTP_TELLDUS_DEVICE_TURNOFF, dev.getId()),
188                     TelldusLiveResponse.class);
189             handleResponse(device, response);
190         } else {
191             throw new TelldusBindingException("Cannot send OFF to " + dev);
192         }
193     }
194
195     private void handleResponse(TellstickNetDevice device, TelldusLiveResponse response) throws TellstickException {
196         if (response == null || (response.status == null && response.error == null)) {
197             throw new TelldusBindingException("No response " + response);
198         } else if (response.error != null) {
199             if ("The client for this device is currently offline".equals(response.error)) {
200                 device.setOnline(false);
201                 device.setUpdated(true);
202             }
203             throw new TelldusBindingException("Error " + response.error);
204         } else if (!"success".equals(response.status.trim())) {
205             throw new TelldusBindingException("Response " + response.status);
206         }
207     }
208
209     private void turnOn(Device dev) throws TellstickException {
210         if (dev instanceof TellstickNetDevice device) {
211             TelldusLiveResponse response = callRestMethod(String.format(HTTP_TELLDUS_DEVICE_TURNON, dev.getId()),
212                     TelldusLiveResponse.class);
213             handleResponse(device, response);
214         } else {
215             throw new TelldusBindingException("Cannot send ON to " + dev);
216         }
217     }
218
219     @Override
220     public State calcState(Device dev) {
221         TellstickNetDevice device = (TellstickNetDevice) dev;
222         State st = null;
223         if (device.getOnline()) {
224             switch (device.getState()) {
225                 case JNA.CLibrary.TELLSTICK_TURNON:
226                     st = OnOffType.ON;
227                     break;
228                 case JNA.CLibrary.TELLSTICK_TURNOFF:
229                     st = OnOffType.OFF;
230                     break;
231                 case JNA.CLibrary.TELLSTICK_DIM:
232                     BigDecimal dimValue = new BigDecimal(device.getStatevalue());
233                     if (dimValue.intValue() == 0) {
234                         st = OnOffType.OFF;
235                     } else if (dimValue.intValue() >= 255) {
236                         st = OnOffType.ON;
237                     } else {
238                         st = OnOffType.ON;
239                     }
240                     break;
241                 default:
242                     logger.warn("Could not handle {} for {}", device.getState(), device);
243             }
244         }
245         return st;
246     }
247
248     @Override
249     public BigDecimal calcDimValue(Device device) {
250         BigDecimal dimValue = new BigDecimal(0);
251         switch (((TellstickNetDevice) device).getState()) {
252             case JNA.CLibrary.TELLSTICK_TURNON:
253                 dimValue = new BigDecimal(100);
254                 break;
255             case JNA.CLibrary.TELLSTICK_TURNOFF:
256                 break;
257             case JNA.CLibrary.TELLSTICK_DIM:
258                 dimValue = new BigDecimal(((TellstickNetDevice) device).getStatevalue());
259                 dimValue = dimValue.multiply(new BigDecimal(100));
260                 dimValue = dimValue.divide(new BigDecimal(255), 0, RoundingMode.HALF_UP);
261                 break;
262             default:
263                 logger.warn("Could not handle {} for {}", (((TellstickNetDevice) device).getState()), device);
264         }
265         return dimValue;
266     }
267
268     public long getLastSend() {
269         return lastSend;
270     }
271
272     public void setLastSend(long currentTimeMillis) {
273         lastSend = currentTimeMillis;
274     }
275
276     @Override
277     public void onRequest(TellstickSensorEvent newDevices) {
278         setLastSend(newDevices.getTimestamp());
279     }
280
281     @Override
282     public void onRequest(TellstickDeviceEvent newDevices) {
283         setLastSend(newDevices.getTimestamp());
284     }
285
286     <T> T callRestMethod(String uri, Class<T> response) throws TelldusLiveException {
287         Instant start = Instant.now();
288         T resultObj = null;
289         try {
290             resultObj = innerCallRest(uri, response);
291         } catch (InterruptedException e) {
292             Thread.currentThread().interrupt();
293             logResponse(uri, e);
294             monitorAdditionalRequest(start, Instant.now(), e);
295             throw new TelldusLiveException(e);
296         } catch (TimeoutException | ExecutionException | JAXBException | XMLStreamException e) {
297             logResponse(uri, e);
298             monitorAdditionalRequest(start, Instant.now(), e);
299             throw new TelldusLiveException(e);
300         }
301         monitorAdditionalRequest(start, Instant.now(), null);
302         return resultObj;
303     }
304
305     private void monitorAdditionalRequest(Instant start, Instant end, @Nullable Throwable exception) {
306         if (!logger.isDebugEnabled()) {
307             return;
308         }
309         long duration = Duration.between(start, end).toMillis();
310         sumRequestDuration += duration;
311         nbRequest++;
312         if (duration < minRequestDuration) {
313             minRequestDuration = duration;
314         }
315         if (duration > maxRequestDuration) {
316             maxRequestDuration = duration;
317         }
318         if (exception instanceof TimeoutException) {
319             nbTimeouts++;
320         } else if (exception != null) {
321             nbErrors++;
322         }
323     }
324
325     public long getAverageRequestDuration() {
326         return nbRequest == 0 ? 0 : sumRequestDuration / nbRequest;
327     }
328
329     public long getMinRequestDuration() {
330         return minRequestDuration;
331     }
332
333     public long getMaxRequestDuration() {
334         return maxRequestDuration;
335     }
336
337     public int getNbTimeouts() {
338         return nbTimeouts;
339     }
340
341     public int getNbErrors() {
342         return nbErrors;
343     }
344
345     private <T> T innerCallRest(String uri, Class<T> response) throws InterruptedException, ExecutionException,
346             TimeoutException, JAXBException, FactoryConfigurationError, XMLStreamException {
347         Future<Response> future = client.prepareGet(uri).execute();
348         Response resp = future.get(REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS);
349         if (resp.getStatusCode() != 200) {
350             throw new ExecutionException("HTTP request returned status code " + resp.getStatusCode(), null);
351         }
352         // TelldusLiveHandler.logger.info("Devices" + resp.getResponseBody());
353         JAXBContext jc = JAXBContext.newInstance(response);
354         XMLInputFactory xif = XMLInputFactory.newInstance();
355         xif.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
356         xif.setProperty(XMLInputFactory.SUPPORT_DTD, false);
357         XMLStreamReader xsr = xif.createXMLStreamReader(resp.getResponseBodyAsStream());
358         // xsr = new PropertyRenamerDelegate(xsr);
359
360         @SuppressWarnings("unchecked")
361         T obj = (T) jc.createUnmarshaller().unmarshal(xsr);
362         if (logger.isTraceEnabled()) {
363             logger.trace("Request [{}] Response:{}", uri, resp.getResponseBody());
364         }
365         return obj;
366     }
367
368     private void logResponse(String uri, Exception e) {
369         logger.warn("Request [{}] failed: {} {}", uri, e.getClass().getSimpleName(), e.getMessage());
370     }
371 }