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 percentCommand) {
133 dim(device, percentCommand);
134 } else if (command instanceof IncreaseDecreaseType increaseDecreaseCommand) {
135 increaseDecrease(device, increaseDecreaseCommand);
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 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);
181 throw new TelldusBindingException("Cannot send DIM to " + dev);
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);
191 throw new TelldusBindingException("Cannot send OFF to " + dev);
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);
203 throw new TelldusBindingException("Error " + response.error);
204 } else if (!"success".equals(response.status.trim())) {
205 throw new TelldusBindingException("Response " + response.status);
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);
215 throw new TelldusBindingException("Cannot send ON to " + dev);
220 public State calcState(Device dev) {
221 TellstickNetDevice device = (TellstickNetDevice) dev;
223 if (device.getOnline()) {
224 switch (device.getState()) {
225 case JNA.CLibrary.TELLSTICK_TURNON:
228 case JNA.CLibrary.TELLSTICK_TURNOFF:
231 case JNA.CLibrary.TELLSTICK_DIM:
232 BigDecimal dimValue = new BigDecimal(device.getStatevalue());
233 if (dimValue.intValue() == 0) {
235 } else if (dimValue.intValue() >= 255) {
242 logger.warn("Could not handle {} for {}", device.getState(), device);
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);
255 case JNA.CLibrary.TELLSTICK_TURNOFF:
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);
263 logger.warn("Could not handle {} for {}", (((TellstickNetDevice) device).getState()), device);
268 public long getLastSend() {
272 public void setLastSend(long currentTimeMillis) {
273 lastSend = currentTimeMillis;
277 public void onRequest(TellstickSensorEvent newDevices) {
278 setLastSend(newDevices.getTimestamp());
282 public void onRequest(TellstickDeviceEvent newDevices) {
283 setLastSend(newDevices.getTimestamp());
286 <T> T callRestMethod(String uri, Class<T> response) throws TelldusLiveException {
287 Instant start = Instant.now();
290 resultObj = innerCallRest(uri, response);
291 } catch (InterruptedException e) {
292 Thread.currentThread().interrupt();
294 monitorAdditionalRequest(start, Instant.now(), e);
295 throw new TelldusLiveException(e);
296 } catch (TimeoutException | ExecutionException | JAXBException | XMLStreamException e) {
298 monitorAdditionalRequest(start, Instant.now(), e);
299 throw new TelldusLiveException(e);
301 monitorAdditionalRequest(start, Instant.now(), null);
305 private void monitorAdditionalRequest(Instant start, Instant end, @Nullable Throwable exception) {
306 if (!logger.isDebugEnabled()) {
309 long duration = Duration.between(start, end).toMillis();
310 sumRequestDuration += duration;
312 if (duration < minRequestDuration) {
313 minRequestDuration = duration;
315 if (duration > maxRequestDuration) {
316 maxRequestDuration = duration;
318 if (exception instanceof TimeoutException) {
320 } else if (exception != null) {
325 public long getAverageRequestDuration() {
326 return nbRequest == 0 ? 0 : sumRequestDuration / nbRequest;
329 public long getMinRequestDuration() {
330 return minRequestDuration;
333 public long getMaxRequestDuration() {
334 return maxRequestDuration;
337 public int getNbTimeouts() {
341 public int getNbErrors() {
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);
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);
360 @SuppressWarnings("unchecked")
361 T obj = (T) jc.createUnmarshaller().unmarshal(xsr);
362 if (logger.isTraceEnabled()) {
363 logger.trace("Request [{}] Response:{}", uri, resp.getResponseBody());
368 private void logResponse(String uri, Exception e) {
369 logger.warn("Request [{}] failed: {} {}", uri, e.getClass().getSimpleName(), e.getMessage());