]> git.basschouten.com Git - openhab-addons.git/blob
3d4b5b55c107ce6952d451eb25d98c29bd5466e1
[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) {
133                 dim(device, (PercentType) command);
134             } else if (command instanceof IncreaseDecreaseType) {
135                 increaseDecrease(device, ((IncreaseDecreaseType) command));
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
176                 && (((TellstickNetDevice) dev).getMethods() & JNA.CLibrary.TELLSTICK_DIM) > 0) {
177             long tdVal = Math.round((value / 100) * 255);
178             TelldusLiveResponse response = callRestMethod(String.format(HTTP_TELLDUS_DEVICE_DIM, dev.getId(), tdVal),
179                     TelldusLiveResponse.class);
180             handleResponse((TellstickNetDevice) dev, response);
181         } else {
182             throw new TelldusBindingException("Cannot send DIM to " + dev);
183         }
184     }
185
186     private void turnOff(Device dev) throws TellstickException {
187         if (dev instanceof TellstickNetDevice) {
188             TelldusLiveResponse response = callRestMethod(String.format(HTTP_TELLDUS_DEVICE_TURNOFF, dev.getId()),
189                     TelldusLiveResponse.class);
190             handleResponse((TellstickNetDevice) dev, response);
191         } else {
192             throw new TelldusBindingException("Cannot send OFF to " + dev);
193         }
194     }
195
196     private void handleResponse(TellstickNetDevice device, TelldusLiveResponse response) throws TellstickException {
197         if (response == null || (response.status == null && response.error == null)) {
198             throw new TelldusBindingException("No response " + response);
199         } else if (response.error != null) {
200             if (response.error.equals("The client for this device is currently offline")) {
201                 device.setOnline(false);
202                 device.setUpdated(true);
203             }
204             throw new TelldusBindingException("Error " + response.error);
205         } else if (!response.status.trim().equals("success")) {
206             throw new TelldusBindingException("Response " + response.status);
207         }
208     }
209
210     private void turnOn(Device dev) throws TellstickException {
211         if (dev instanceof TellstickNetDevice) {
212             TelldusLiveResponse response = callRestMethod(String.format(HTTP_TELLDUS_DEVICE_TURNON, dev.getId()),
213                     TelldusLiveResponse.class);
214             handleResponse((TellstickNetDevice) dev, response);
215         } else {
216             throw new TelldusBindingException("Cannot send ON to " + dev);
217         }
218     }
219
220     @Override
221     public State calcState(Device dev) {
222         TellstickNetDevice device = (TellstickNetDevice) dev;
223         State st = null;
224         if (device.getOnline()) {
225             switch (device.getState()) {
226                 case JNA.CLibrary.TELLSTICK_TURNON:
227                     st = OnOffType.ON;
228                     break;
229                 case JNA.CLibrary.TELLSTICK_TURNOFF:
230                     st = OnOffType.OFF;
231                     break;
232                 case JNA.CLibrary.TELLSTICK_DIM:
233                     BigDecimal dimValue = new BigDecimal(device.getStatevalue());
234                     if (dimValue.intValue() == 0) {
235                         st = OnOffType.OFF;
236                     } else if (dimValue.intValue() >= 255) {
237                         st = OnOffType.ON;
238                     } else {
239                         st = OnOffType.ON;
240                     }
241                     break;
242                 default:
243                     logger.warn("Could not handle {} for {}", device.getState(), device);
244             }
245         }
246         return st;
247     }
248
249     @Override
250     public BigDecimal calcDimValue(Device device) {
251         BigDecimal dimValue = new BigDecimal(0);
252         switch (((TellstickNetDevice) device).getState()) {
253             case JNA.CLibrary.TELLSTICK_TURNON:
254                 dimValue = new BigDecimal(100);
255                 break;
256             case JNA.CLibrary.TELLSTICK_TURNOFF:
257                 break;
258             case JNA.CLibrary.TELLSTICK_DIM:
259                 dimValue = new BigDecimal(((TellstickNetDevice) device).getStatevalue());
260                 dimValue = dimValue.multiply(new BigDecimal(100));
261                 dimValue = dimValue.divide(new BigDecimal(255), 0, RoundingMode.HALF_UP);
262                 break;
263             default:
264                 logger.warn("Could not handle {} for {}", (((TellstickNetDevice) device).getState()), device);
265         }
266         return dimValue;
267     }
268
269     public long getLastSend() {
270         return lastSend;
271     }
272
273     public void setLastSend(long currentTimeMillis) {
274         lastSend = currentTimeMillis;
275     }
276
277     @Override
278     public void onRequest(TellstickSensorEvent newDevices) {
279         setLastSend(newDevices.getTimestamp());
280     }
281
282     @Override
283     public void onRequest(TellstickDeviceEvent newDevices) {
284         setLastSend(newDevices.getTimestamp());
285     }
286
287     <T> T callRestMethod(String uri, Class<T> response) throws TelldusLiveException {
288         Instant start = Instant.now();
289         T resultObj = null;
290         try {
291             resultObj = innerCallRest(uri, response);
292         } catch (InterruptedException e) {
293             Thread.currentThread().interrupt();
294             logResponse(uri, e);
295             monitorAdditionalRequest(start, Instant.now(), e);
296             throw new TelldusLiveException(e);
297         } catch (TimeoutException | ExecutionException | JAXBException | XMLStreamException e) {
298             logResponse(uri, e);
299             monitorAdditionalRequest(start, Instant.now(), e);
300             throw new TelldusLiveException(e);
301         }
302         monitorAdditionalRequest(start, Instant.now(), null);
303         return resultObj;
304     }
305
306     private void monitorAdditionalRequest(Instant start, Instant end, @Nullable Throwable exception) {
307         if (!logger.isDebugEnabled()) {
308             return;
309         }
310         long duration = Duration.between(start, end).toMillis();
311         sumRequestDuration += duration;
312         nbRequest++;
313         if (duration < minRequestDuration) {
314             minRequestDuration = duration;
315         }
316         if (duration > maxRequestDuration) {
317             maxRequestDuration = duration;
318         }
319         if (exception instanceof TimeoutException) {
320             nbTimeouts++;
321         } else if (exception != null) {
322             nbErrors++;
323         }
324     }
325
326     public long getAverageRequestDuration() {
327         return nbRequest == 0 ? 0 : sumRequestDuration / nbRequest;
328     }
329
330     public long getMinRequestDuration() {
331         return minRequestDuration;
332     }
333
334     public long getMaxRequestDuration() {
335         return maxRequestDuration;
336     }
337
338     public int getNbTimeouts() {
339         return nbTimeouts;
340     }
341
342     public int getNbErrors() {
343         return nbErrors;
344     }
345
346     private <T> T innerCallRest(String uri, Class<T> response) throws InterruptedException, ExecutionException,
347             TimeoutException, JAXBException, FactoryConfigurationError, XMLStreamException {
348         Future<Response> future = client.prepareGet(uri).execute();
349         Response resp = future.get(REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS);
350         if (resp.getStatusCode() != 200) {
351             throw new ExecutionException("HTTP request returned status code " + resp.getStatusCode(), null);
352         }
353         // TelldusLiveHandler.logger.info("Devices" + resp.getResponseBody());
354         JAXBContext jc = JAXBContext.newInstance(response);
355         XMLInputFactory xif = XMLInputFactory.newInstance();
356         xif.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
357         xif.setProperty(XMLInputFactory.SUPPORT_DTD, false);
358         XMLStreamReader xsr = xif.createXMLStreamReader(resp.getResponseBodyAsStream());
359         // xsr = new PropertyRenamerDelegate(xsr);
360
361         @SuppressWarnings("unchecked")
362         T obj = (T) jc.createUnmarshaller().unmarshal(xsr);
363         if (logger.isTraceEnabled()) {
364             logger.trace("Request [{}] Response:{}", uri, resp.getResponseBody());
365         }
366         return obj;
367     }
368
369     private void logResponse(String uri, Exception e) {
370         logger.warn("Request [{}] failed: {} {}", uri, e.getClass().getSimpleName(), e.getMessage());
371     }
372 }