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.tellstick.internal.live;
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;
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;
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;
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.
66 * @author Jarle Hjortland - Initial contribution
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";
84 private int nbRequest;
85 private long sumRequestDuration;
86 private long minRequestDuration = 1_000_000;
87 private long maxRequestDuration;
88 private int nbTimeouts;
91 public TelldusLiveDeviceController() {
95 public void dispose() {
98 } catch (Exception e) {
99 logger.debug("Failed to close client", e);
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());
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);
117 private AsyncHttpClientConfig createAsyncHttpClientConfig() {
118 Builder builder = new DefaultAsyncHttpClientConfig.Builder();
119 builder.setConnectTimeout(REQUEST_TIMEOUT_MS);
120 return builder.build();
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) {
130 } else if (command == OnOffType.OFF) {
132 } else if (command instanceof PercentType) {
133 dim(device, (PercentType) command);
134 } else if (command instanceof IncreaseDecreaseType) {
135 increaseDecrease(device, ((IncreaseDecreaseType) command));
137 } else if (device instanceof SwitchableDevice) {
138 if (command == OnOffType.ON) {
140 logger.debug("Turn off first in case it is allready on");
144 } else if (command == OnOffType.OFF) {
148 logger.warn("Cannot send to {}", device);
152 private void increaseDecrease(Device dev, IncreaseDecreaseType increaseDecreaseType) throws TellstickException {
153 String strValue = ((TellstickDevice) dev).getData();
155 if (strValue != null) {
156 value = Double.valueOf(strValue);
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);
164 dim(dev, new PercentType(percent));
167 private void dim(Device dev, PercentType command) throws TellstickException {
168 double value = command.doubleValue();
170 // 0 means OFF and 100 means ON
171 if (value == 0 && dev instanceof TellstickNetDevice) {
173 } else if (value == 100 && dev instanceof TellstickNetDevice) {
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);
182 throw new TelldusBindingException("Cannot send DIM to " + dev);
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);
192 throw new TelldusBindingException("Cannot send OFF to " + dev);
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);
204 throw new TelldusBindingException("Error " + response.error);
205 } else if (!response.status.trim().equals("success")) {
206 throw new TelldusBindingException("Response " + response.status);
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);
216 throw new TelldusBindingException("Cannot send ON to " + dev);
221 public State calcState(Device dev) {
222 TellstickNetDevice device = (TellstickNetDevice) dev;
224 if (device.getOnline()) {
225 switch (device.getState()) {
226 case JNA.CLibrary.TELLSTICK_TURNON:
229 case JNA.CLibrary.TELLSTICK_TURNOFF:
232 case JNA.CLibrary.TELLSTICK_DIM:
233 BigDecimal dimValue = new BigDecimal(device.getStatevalue());
234 if (dimValue.intValue() == 0) {
236 } else if (dimValue.intValue() >= 255) {
243 logger.warn("Could not handle {} for {}", device.getState(), device);
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);
256 case JNA.CLibrary.TELLSTICK_TURNOFF:
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);
264 logger.warn("Could not handle {} for {}", (((TellstickNetDevice) device).getState()), device);
269 public long getLastSend() {
273 public void setLastSend(long currentTimeMillis) {
274 lastSend = currentTimeMillis;
278 public void onRequest(TellstickSensorEvent newDevices) {
279 setLastSend(newDevices.getTimestamp());
283 public void onRequest(TellstickDeviceEvent newDevices) {
284 setLastSend(newDevices.getTimestamp());
287 <T> T callRestMethod(String uri, Class<T> response) throws TelldusLiveException {
288 Instant start = Instant.now();
291 resultObj = innerCallRest(uri, response);
292 } catch (InterruptedException e) {
293 Thread.currentThread().interrupt();
295 monitorAdditionalRequest(start, Instant.now(), e);
296 throw new TelldusLiveException(e);
297 } catch (TimeoutException | ExecutionException | JAXBException | XMLStreamException e) {
299 monitorAdditionalRequest(start, Instant.now(), e);
300 throw new TelldusLiveException(e);
302 monitorAdditionalRequest(start, Instant.now(), null);
306 private void monitorAdditionalRequest(Instant start, Instant end, @Nullable Throwable exception) {
307 if (!logger.isDebugEnabled()) {
310 long duration = Duration.between(start, end).toMillis();
311 sumRequestDuration += duration;
313 if (duration < minRequestDuration) {
314 minRequestDuration = duration;
316 if (duration > maxRequestDuration) {
317 maxRequestDuration = duration;
319 if (exception instanceof TimeoutException) {
321 } else if (exception != null) {
326 public long getAverageRequestDuration() {
327 return nbRequest == 0 ? 0 : sumRequestDuration / nbRequest;
330 public long getMinRequestDuration() {
331 return minRequestDuration;
334 public long getMaxRequestDuration() {
335 return maxRequestDuration;
338 public int getNbTimeouts() {
342 public int getNbErrors() {
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);
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);
361 @SuppressWarnings("unchecked")
362 T obj = (T) jc.createUnmarshaller().unmarshal(xsr);
363 if (logger.isTraceEnabled()) {
364 logger.trace("Request [{}] Response:{}", uri, resp.getResponseBody());
369 private void logResponse(String uri, Exception e) {
370 logger.warn("Request [{}] failed: {} {}", uri, e.getClass().getSimpleName(), e.getMessage());