]> git.basschouten.com Git - openhab-addons.git/blob
3c171b8045263d426fcbfcac1a896b319faaf3a5
[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.homematic.internal.communicator.client;
14
15 import static org.openhab.binding.homematic.internal.HomematicBindingConstants.*;
16
17 import java.io.IOException;
18 import java.util.Collection;
19 import java.util.HashMap;
20 import java.util.List;
21 import java.util.Map;
22 import java.util.UUID;
23 import java.util.concurrent.CancellationException;
24 import java.util.concurrent.ExecutionException;
25 import java.util.concurrent.ScheduledExecutorService;
26 import java.util.concurrent.ScheduledFuture;
27 import java.util.concurrent.TimeUnit;
28 import java.util.concurrent.TimeoutException;
29
30 import org.openhab.binding.homematic.internal.HomematicBindingConstants;
31 import org.openhab.binding.homematic.internal.common.HomematicConfig;
32 import org.openhab.binding.homematic.internal.communicator.message.RpcRequest;
33 import org.openhab.binding.homematic.internal.communicator.parser.GetAllScriptsParser;
34 import org.openhab.binding.homematic.internal.communicator.parser.GetAllSystemVariablesParser;
35 import org.openhab.binding.homematic.internal.communicator.parser.GetDeviceDescriptionParser;
36 import org.openhab.binding.homematic.internal.communicator.parser.GetParamsetDescriptionParser;
37 import org.openhab.binding.homematic.internal.communicator.parser.GetParamsetParser;
38 import org.openhab.binding.homematic.internal.communicator.parser.GetValueParser;
39 import org.openhab.binding.homematic.internal.communicator.parser.HomegearLoadDeviceNamesParser;
40 import org.openhab.binding.homematic.internal.communicator.parser.ListBidcosInterfacesParser;
41 import org.openhab.binding.homematic.internal.communicator.parser.ListDevicesParser;
42 import org.openhab.binding.homematic.internal.communicator.parser.RssiInfoParser;
43 import org.openhab.binding.homematic.internal.misc.MiscUtils;
44 import org.openhab.binding.homematic.internal.model.HmChannel;
45 import org.openhab.binding.homematic.internal.model.HmDatapoint;
46 import org.openhab.binding.homematic.internal.model.HmDevice;
47 import org.openhab.binding.homematic.internal.model.HmGatewayInfo;
48 import org.openhab.binding.homematic.internal.model.HmInterface;
49 import org.openhab.binding.homematic.internal.model.HmParamsetType;
50 import org.openhab.binding.homematic.internal.model.HmRssiInfo;
51 import org.openhab.core.common.ThreadPoolManager;
52 import org.slf4j.Logger;
53 import org.slf4j.LoggerFactory;
54
55 /**
56  * Client implementation for sending messages via BIN-RPC to a Homematic gateway.
57  *
58  * @author Gerhard Riegler - Initial contribution
59  */
60 public abstract class RpcClient<T> {
61     private final Logger logger = LoggerFactory.getLogger(RpcClient.class);
62     protected static final int MAX_RPC_RETRY = 3;
63     protected static final int RESP_BUFFER_SIZE = 8192;
64     private static final int INITIAL_CALLBACK_REG_DELAY = 20; // 20 s before first attempt
65     private static final int CALLBACK_REG_DELAY = 10; // 10 s between two attempts
66
67     protected HomematicConfig config;
68     private String thisUID = UUID.randomUUID().toString();
69     private ScheduledFuture<?> future = null;
70     private int attempt;
71
72     public RpcClient(HomematicConfig config) {
73         this.config = config;
74     }
75
76     /**
77      * Returns a RpcRequest for this client.
78      */
79     protected abstract RpcRequest<T> createRpcRequest(String methodName);
80
81     /**
82      * Returns the callback url for this client.
83      */
84     protected abstract String getRpcCallbackUrl();
85
86     /**
87      * Sends the RPC message to the gateway.
88      */
89     protected abstract Object[] sendMessage(int port, RpcRequest<T> request) throws IOException;
90
91     /**
92      * Register a callback for the specified interface where the Homematic gateway can send its events.
93      */
94     public void init(HmInterface hmInterface) throws IOException {
95         ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool(GATEWAY_POOL_NAME);
96         RpcRequest<T> request = createRpcRequest("init");
97         request.addArg(getRpcCallbackUrl());
98         request.addArg(thisUID);
99         if (config.getGatewayInfo().isHomegear()) {
100             request.addArg(Integer.valueOf(0x22));
101         }
102         logger.debug("Register callback for interface {}", hmInterface.getName());
103         try {
104             attempt = 1;
105             sendMessage(config.getRpcPort(hmInterface), request); // first attempt without delay
106         } catch (IOException e) {
107             future = scheduler.scheduleWithFixedDelay(() -> {
108                 logger.debug("Register callback for interface {}, attempt {}", hmInterface.getName(), ++attempt);
109                 try {
110                     sendMessage(config.getRpcPort(hmInterface), request);
111                     future.cancel(true);
112                 } catch (IOException ex) {
113                     // Ignore, retry
114                 }
115             }, INITIAL_CALLBACK_REG_DELAY, CALLBACK_REG_DELAY, TimeUnit.SECONDS);
116             try {
117                 future.get(config.getCallbackRegTimeout(), TimeUnit.SECONDS);
118             } catch (CancellationException e1) {
119                 logger.debug("Callback for interface {} successfully registered", hmInterface.getName());
120             } catch (InterruptedException | ExecutionException e1) {
121                 throw new IOException("Callback reg. thread interrupted", e1);
122             } catch (TimeoutException e1) {
123                 logger.error("Callback registration for interface {} timed out", hmInterface.getName());
124                 throw new IOException("Unable to reconnect in time");
125             }
126             future = null;
127         }
128     }
129
130     /**
131      * Disposes the client.
132      */
133     public void dispose() {
134         if (future != null) {
135             future.cancel(true);
136         }
137     }
138
139     /**
140      * Release a callback for the specified interface.
141      */
142     public void release(HmInterface hmInterface) throws IOException {
143         RpcRequest<T> request = createRpcRequest("init");
144         request.addArg(getRpcCallbackUrl());
145         sendMessage(config.getRpcPort(hmInterface), request);
146     }
147
148     /**
149      * Sends a ping to the specified interface.
150      */
151     public void ping(HmInterface hmInterface, String callerId) throws IOException {
152         RpcRequest<T> request = createRpcRequest("ping");
153         request.addArg(callerId);
154         sendMessage(config.getRpcPort(hmInterface), request);
155     }
156
157     /**
158      * Returns the info of all BidCos interfaces available on the gateway.
159      */
160     public ListBidcosInterfacesParser listBidcosInterfaces(HmInterface hmInterface) throws IOException {
161         RpcRequest<T> request = createRpcRequest("listBidcosInterfaces");
162         return new ListBidcosInterfacesParser().parse(sendMessage(config.getRpcPort(hmInterface), request));
163     }
164
165     /**
166      * Returns some infos of the gateway.
167      */
168     private GetDeviceDescriptionParser getDeviceDescription(HmInterface hmInterface) throws IOException {
169         RpcRequest<T> request = createRpcRequest("getDeviceDescription");
170         request.addArg("BidCoS-RF");
171         return new GetDeviceDescriptionParser().parse(sendMessage(config.getRpcPort(hmInterface), request));
172     }
173
174     /**
175      * Returns all variable metadata and values from a Homegear gateway.
176      */
177     public void getAllSystemVariables(HmChannel channel) throws IOException {
178         RpcRequest<T> request = createRpcRequest("getAllSystemVariables");
179         new GetAllSystemVariablesParser(channel).parse(sendMessage(config.getRpcPort(channel), request));
180     }
181
182     /**
183      * Loads all device names from a Homegear gateway.
184      */
185     public void loadDeviceNames(HmInterface hmInterface, Collection<HmDevice> devices) throws IOException {
186         RpcRequest<T> request = createRpcRequest("getDeviceInfo");
187         new HomegearLoadDeviceNamesParser(devices).parse(sendMessage(config.getRpcPort(hmInterface), request));
188     }
189
190     /**
191      * Returns true, if the interface is available on the gateway.
192      */
193     public void checkInterface(HmInterface hmInterface) throws IOException {
194         RpcRequest<T> request = createRpcRequest("init");
195         request.addArg("http://openhab.validation:1000");
196         sendMessage(config.getRpcPort(hmInterface), request);
197     }
198
199     /**
200      * Returns all script metadata from a Homegear gateway.
201      */
202     public void getAllScripts(HmChannel channel) throws IOException {
203         RpcRequest<T> request = createRpcRequest("getAllScripts");
204         new GetAllScriptsParser(channel).parse(sendMessage(config.getRpcPort(channel), request));
205     }
206
207     /**
208      * Returns all device and channel metadata.
209      */
210     public Collection<HmDevice> listDevices(HmInterface hmInterface) throws IOException {
211         RpcRequest<T> request = createRpcRequest("listDevices");
212         return new ListDevicesParser(hmInterface, config).parse(sendMessage(config.getRpcPort(hmInterface), request));
213     }
214
215     /**
216      * Loads all datapoint metadata into the given channel.
217      */
218     public void addChannelDatapoints(HmChannel channel, HmParamsetType paramsetType) throws IOException {
219         if (isConfigurationChannel(channel) && paramsetType != HmParamsetType.MASTER) {
220             // The configuration channel only has a MASTER Paramset, so there is nothing to load
221             return;
222         }
223         RpcRequest<T> request = createRpcRequest("getParamsetDescription");
224         request.addArg(getRpcAddress(channel.getDevice().getAddress()) + getChannelSuffix(channel));
225         request.addArg(paramsetType.toString());
226         new GetParamsetDescriptionParser(channel, paramsetType).parse(sendMessage(config.getRpcPort(channel), request));
227     }
228
229     /**
230      * Sets all datapoint values for the given channel.
231      */
232     public void setChannelDatapointValues(HmChannel channel, HmParamsetType paramsetType) throws IOException {
233         if (isConfigurationChannel(channel) && paramsetType != HmParamsetType.MASTER) {
234             // The configuration channel only has a MASTER Paramset, so there is nothing to load
235             return;
236         }
237
238         RpcRequest<T> request = createRpcRequest("getParamset");
239         request.addArg(getRpcAddress(channel.getDevice().getAddress()) + getChannelSuffix(channel));
240         request.addArg(paramsetType.toString());
241         if (channel.getDevice().getHmInterface() == HmInterface.CUXD && paramsetType == HmParamsetType.VALUES) {
242             setChannelDatapointValues(channel);
243         } else {
244             try {
245                 new GetParamsetParser(channel, paramsetType).parse(sendMessage(config.getRpcPort(channel), request));
246             } catch (UnknownRpcFailureException ex) {
247                 if (paramsetType == HmParamsetType.VALUES) {
248                     logger.debug(
249                             "RpcResponse unknown RPC failure (-1 Failure), fetching values with another API method for device: {}, channel: {}, paramset: {}",
250                             channel.getDevice().getAddress(), channel.getNumber(), paramsetType);
251                     setChannelDatapointValues(channel);
252                 } else {
253                     throw ex;
254                 }
255             }
256         }
257     }
258
259     /**
260      * Reads all VALUES datapoints individually, fallback method if setChannelDatapointValues throws a -1 Failure
261      * exception.
262      */
263     private void setChannelDatapointValues(HmChannel channel) throws IOException {
264         for (HmDatapoint dp : channel.getDatapoints()) {
265             getDatapointValue(dp);
266         }
267     }
268
269     /**
270      * Tries to identify the gateway and returns the GatewayInfo.
271      */
272     public HmGatewayInfo getGatewayInfo(String id) throws IOException {
273         boolean isHomegear = false;
274         GetDeviceDescriptionParser ddParser;
275         ListBidcosInterfacesParser biParser;
276
277         try {
278             ddParser = getDeviceDescription(HmInterface.RF);
279             isHomegear = "Homegear".equalsIgnoreCase(ddParser.getType());
280         } catch (IOException ex) {
281             // can't load gateway infos via RF interface
282             ddParser = new GetDeviceDescriptionParser();
283         }
284
285         try {
286             biParser = listBidcosInterfaces(HmInterface.RF);
287         } catch (IOException ex) {
288             biParser = listBidcosInterfaces(HmInterface.HMIP);
289         }
290
291         HmGatewayInfo gatewayInfo = new HmGatewayInfo();
292         gatewayInfo.setAddress(biParser.getGatewayAddress());
293         String gwType = biParser.getType();
294         if (isHomegear) {
295             gatewayInfo.setId(HmGatewayInfo.ID_HOMEGEAR);
296             gatewayInfo.setType(ddParser.getType());
297             gatewayInfo.setFirmware(ddParser.getFirmware());
298         } else if ((MiscUtils.strStartsWithIgnoreCase(gwType, "CCU")
299                 || MiscUtils.strStartsWithIgnoreCase(gwType, "HMIP_CCU")
300                 || MiscUtils.strStartsWithIgnoreCase(ddParser.getType(), "HM-RCV-50") || config.isCCUType())
301                 && !config.isNoCCUType()) {
302             gatewayInfo.setId(HmGatewayInfo.ID_CCU);
303             String type = gwType.isBlank() ? "CCU" : gwType;
304             gatewayInfo.setType(type);
305             gatewayInfo
306                     .setFirmware(!ddParser.getFirmware().isEmpty() ? ddParser.getFirmware() : biParser.getFirmware());
307         } else {
308             gatewayInfo.setId(HmGatewayInfo.ID_DEFAULT);
309             gatewayInfo.setType(gwType);
310             gatewayInfo.setFirmware(biParser.getFirmware());
311         }
312
313         if (gatewayInfo.isCCU() || config.hasRfPort()) {
314             gatewayInfo.setRfInterface(hasInterface(HmInterface.RF, id));
315         }
316
317         if (gatewayInfo.isCCU() || config.hasWiredPort()) {
318             gatewayInfo.setWiredInterface(hasInterface(HmInterface.WIRED, id));
319         }
320
321         if (gatewayInfo.isCCU() || config.hasHmIpPort()) {
322             gatewayInfo.setHmipInterface(hasInterface(HmInterface.HMIP, id));
323         }
324
325         if (gatewayInfo.isCCU() || config.hasCuxdPort()) {
326             gatewayInfo.setCuxdInterface(hasInterface(HmInterface.CUXD, id));
327         }
328
329         if (gatewayInfo.isCCU() || config.hasGroupPort()) {
330             gatewayInfo.setGroupInterface(hasInterface(HmInterface.GROUP, id));
331         }
332
333         return gatewayInfo;
334     }
335
336     /**
337      * Returns true, if a connection is possible with the given interface.
338      */
339     private boolean hasInterface(HmInterface hmInterface, String id) throws IOException {
340         try {
341             checkInterface(hmInterface);
342             return true;
343         } catch (IOException ex) {
344             logger.info("Interface '{}' on gateway '{}' not available, disabling support", hmInterface, id);
345             return false;
346         }
347     }
348
349     /**
350      * Sets the value of the datapoint using the provided rx transmission mode.
351      *
352      * @param dp The datapoint to set
353      * @param value The new value to set on the datapoint
354      * @param rxMode The rx mode to use for the transmission of the datapoint value
355      *            ({@link HomematicBindingConstants#RX_BURST_MODE "BURST"} for burst mode,
356      *            {@link HomematicBindingConstants#RX_WAKEUP_MODE "WAKEUP"} for wakeup mode, or null for the default
357      *            mode)
358      */
359     public void setDatapointValue(HmDatapoint dp, Object value, String rxMode) throws IOException {
360         if (dp.isIntegerType() && value instanceof Double) {
361             value = ((Number) value).intValue();
362         }
363
364         RpcRequest<T> request;
365         if (HmParamsetType.VALUES == dp.getParamsetType()) {
366             request = createRpcRequest("setValue");
367             request.addArg(getRpcAddress(dp.getChannel().getDevice().getAddress()) + getChannelSuffix(dp.getChannel()));
368             request.addArg(dp.getName());
369             request.addArg(value);
370             configureRxMode(request, rxMode);
371         } else {
372             request = createRpcRequest("putParamset");
373             request.addArg(getRpcAddress(dp.getChannel().getDevice().getAddress()) + getChannelSuffix(dp.getChannel()));
374             request.addArg(HmParamsetType.MASTER.toString());
375             Map<String, Object> paramSet = new HashMap<>();
376             paramSet.put(dp.getName(), value);
377             request.addArg(paramSet);
378             configureRxMode(request, rxMode);
379         }
380         sendMessage(config.getRpcPort(dp.getChannel()), request);
381     }
382
383     protected void configureRxMode(RpcRequest<T> request, String rxMode) {
384         if (rxMode != null) {
385             if (RX_BURST_MODE.equals(rxMode) || RX_WAKEUP_MODE.equals(rxMode)) {
386                 request.addArg(rxMode);
387             }
388         }
389     }
390
391     /**
392      * Retrieves the value of a single {@link HmDatapoint} from the device. Can only be used for the paramset "VALUES".
393      *
394      * @param dp The HmDatapoint that shall be loaded
395      * @throws IOException If there is a problem while communicating to the gateway
396      */
397     public void getDatapointValue(HmDatapoint dp) throws IOException {
398         if (dp.isReadable() && !dp.isVirtual() && dp.getParamsetType() == HmParamsetType.VALUES) {
399             RpcRequest<T> request = createRpcRequest("getValue");
400             request.addArg(getRpcAddress(dp.getChannel().getDevice().getAddress()) + getChannelSuffix(dp.getChannel()));
401             request.addArg(dp.getName());
402             new GetValueParser(dp).parse(sendMessage(config.getRpcPort(dp.getChannel()), request));
403         }
404     }
405
406     /**
407      * Sets the value of a system variable on a Homegear gateway.
408      */
409     public void setSystemVariable(HmDatapoint dp, Object value) throws IOException {
410         RpcRequest<T> request = createRpcRequest("setSystemVariable");
411         request.addArg(dp.getInfo());
412         request.addArg(value);
413         sendMessage(config.getRpcPort(dp.getChannel()), request);
414     }
415
416     /**
417      * Executes a script on the Homegear gateway.
418      */
419     public void executeScript(HmDatapoint dp) throws IOException {
420         RpcRequest<T> request = createRpcRequest("runScript");
421         request.addArg(dp.getInfo());
422         sendMessage(config.getRpcPort(dp.getChannel()), request);
423     }
424
425     /**
426      * Enables/disables the install mode for given seconds.
427      *
428      * @param hmInterface specifies the interface to enable / disable install mode on
429      * @param enable if <i>true</i> it will be enabled, otherwise disabled
430      * @param seconds desired duration of install mode
431      * @throws IOException if RpcClient fails to propagate command
432      */
433     public void setInstallMode(HmInterface hmInterface, boolean enable, int seconds) throws IOException {
434         RpcRequest<T> request = createRpcRequest("setInstallMode");
435         request.addArg(enable);
436         request.addArg(seconds);
437         request.addArg(INSTALL_MODE_NORMAL);
438         logger.debug("Submitting setInstallMode(on={}, time={}, mode={}) ", enable, seconds, INSTALL_MODE_NORMAL);
439         sendMessage(config.getRpcPort(hmInterface), request);
440     }
441
442     /**
443      * Returns the remaining time of <i>install_mode==true</i>
444      *
445      * @param hmInterface specifies the interface on which install mode status is requested
446      * @return current duration in seconds that the controller will remain in install mode,
447      *         value of 0 means that the install mode is disabled
448      * @throws IOException if RpcClient fails to propagate command
449      */
450     public int getInstallMode(HmInterface hmInterface) throws IOException {
451         RpcRequest<T> request = createRpcRequest("getInstallMode");
452         Object[] result = sendMessage(config.getRpcPort(hmInterface), request);
453         if (logger.isTraceEnabled()) {
454             logger.trace(
455                     "Checking InstallMode: getInstallMode() request returned {} (remaining seconds in InstallMode=true)",
456                     result);
457         }
458         try {
459             return (int) result[0];
460         } catch (Exception cause) {
461             IOException wrappedException = new IOException(
462                     "Failed to request install mode from interface " + hmInterface);
463             wrappedException.initCause(cause);
464             throw wrappedException;
465         }
466     }
467
468     /**
469      * Deletes the device from the gateway.
470      */
471     public void deleteDevice(HmDevice device, int flags) throws IOException {
472         RpcRequest<T> request = createRpcRequest("deleteDevice");
473         request.addArg(device.getAddress());
474         request.addArg(flags);
475         sendMessage(config.getRpcPort(device.getHmInterface()), request);
476     }
477
478     /**
479      * Returns the rpc address from a device address, correctly handling groups.
480      */
481     private String getRpcAddress(String address) {
482         if (address != null && address.startsWith("T-")) {
483             address = "*" + address.substring(2);
484         }
485         return address;
486     }
487
488     /**
489      * Returns the rssi values for all devices.
490      */
491     public List<HmRssiInfo> loadRssiInfo(HmInterface hmInterface) throws IOException {
492         RpcRequest<T> request = createRpcRequest("rssiInfo");
493         return new RssiInfoParser(config).parse(sendMessage(config.getRpcPort(hmInterface), request));
494     }
495
496     /**
497      * Returns the address suffix that specifies the channel for a given HmChannel. This is either a colon ":" followed
498      * by the channel number, or the empty string for a configuration channel.
499      */
500     private String getChannelSuffix(HmChannel channel) {
501         return isConfigurationChannel(channel) ? "" : ":" + channel.getNumber();
502     }
503
504     /**
505      * Checks whether a channel is a configuration channel. The configuration channel of a device encapsulates the
506      * MASTER Paramset that does not belong to one of its actual channels.
507      */
508     private boolean isConfigurationChannel(HmChannel channel) {
509         return channel.getNumber() == CONFIGURATION_CHANNEL_NUMBER;
510     }
511 }