]> git.basschouten.com Git - openhab-addons.git/blob
7bf2834861a7dfd074e9699bc69ecd8f71338489
[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;
14
15 import static org.openhab.binding.homematic.internal.HomematicBindingConstants.*;
16 import static org.openhab.binding.homematic.internal.misc.HomematicConstants.*;
17
18 import java.io.IOException;
19 import java.util.ArrayList;
20 import java.util.Collection;
21 import java.util.Collections;
22 import java.util.HashMap;
23 import java.util.HashSet;
24 import java.util.List;
25 import java.util.Map;
26 import java.util.Map.Entry;
27 import java.util.Set;
28 import java.util.TreeMap;
29 import java.util.concurrent.ScheduledExecutorService;
30 import java.util.concurrent.ScheduledFuture;
31 import java.util.concurrent.TimeUnit;
32
33 import org.eclipse.jetty.client.HttpClient;
34 import org.openhab.binding.homematic.internal.common.HomematicConfig;
35 import org.openhab.binding.homematic.internal.communicator.client.BinRpcClient;
36 import org.openhab.binding.homematic.internal.communicator.client.RpcClient;
37 import org.openhab.binding.homematic.internal.communicator.client.TransferMode;
38 import org.openhab.binding.homematic.internal.communicator.client.UnknownParameterSetException;
39 import org.openhab.binding.homematic.internal.communicator.client.XmlRpcClient;
40 import org.openhab.binding.homematic.internal.communicator.parser.ListBidcosInterfacesParser;
41 import org.openhab.binding.homematic.internal.communicator.server.BinRpcServer;
42 import org.openhab.binding.homematic.internal.communicator.server.RpcEventListener;
43 import org.openhab.binding.homematic.internal.communicator.server.RpcServer;
44 import org.openhab.binding.homematic.internal.communicator.server.XmlRpcServer;
45 import org.openhab.binding.homematic.internal.communicator.virtual.BatteryTypeVirtualDatapointHandler;
46 import org.openhab.binding.homematic.internal.communicator.virtual.ButtonVirtualDatapointHandler;
47 import org.openhab.binding.homematic.internal.communicator.virtual.DeleteDeviceModeVirtualDatapointHandler;
48 import org.openhab.binding.homematic.internal.communicator.virtual.DeleteDeviceVirtualDatapointHandler;
49 import org.openhab.binding.homematic.internal.communicator.virtual.DisplayOptionsVirtualDatapointHandler;
50 import org.openhab.binding.homematic.internal.communicator.virtual.DisplayTextVirtualDatapoint;
51 import org.openhab.binding.homematic.internal.communicator.virtual.FirmwareVirtualDatapointHandler;
52 import org.openhab.binding.homematic.internal.communicator.virtual.HmwIoModuleVirtualDatapointHandler;
53 import org.openhab.binding.homematic.internal.communicator.virtual.InstallModeDurationVirtualDatapoint;
54 import org.openhab.binding.homematic.internal.communicator.virtual.InstallModeVirtualDatapoint;
55 import org.openhab.binding.homematic.internal.communicator.virtual.OnTimeAutomaticVirtualDatapointHandler;
56 import org.openhab.binding.homematic.internal.communicator.virtual.ReloadAllFromGatewayVirtualDatapointHandler;
57 import org.openhab.binding.homematic.internal.communicator.virtual.ReloadFromGatewayVirtualDatapointHandler;
58 import org.openhab.binding.homematic.internal.communicator.virtual.ReloadRssiVirtualDatapointHandler;
59 import org.openhab.binding.homematic.internal.communicator.virtual.RssiVirtualDatapointHandler;
60 import org.openhab.binding.homematic.internal.communicator.virtual.SignalStrengthVirtualDatapointHandler;
61 import org.openhab.binding.homematic.internal.communicator.virtual.StateContactVirtualDatapointHandler;
62 import org.openhab.binding.homematic.internal.communicator.virtual.VirtualDatapointHandler;
63 import org.openhab.binding.homematic.internal.communicator.virtual.VirtualGateway;
64 import org.openhab.binding.homematic.internal.misc.DelayedExecuter;
65 import org.openhab.binding.homematic.internal.misc.DelayedExecuter.DelayedExecuterCallback;
66 import org.openhab.binding.homematic.internal.misc.HomematicClientException;
67 import org.openhab.binding.homematic.internal.misc.HomematicConstants;
68 import org.openhab.binding.homematic.internal.misc.MiscUtils;
69 import org.openhab.binding.homematic.internal.model.HmChannel;
70 import org.openhab.binding.homematic.internal.model.HmDatapoint;
71 import org.openhab.binding.homematic.internal.model.HmDatapointConfig;
72 import org.openhab.binding.homematic.internal.model.HmDatapointInfo;
73 import org.openhab.binding.homematic.internal.model.HmDevice;
74 import org.openhab.binding.homematic.internal.model.HmGatewayInfo;
75 import org.openhab.binding.homematic.internal.model.HmInterface;
76 import org.openhab.binding.homematic.internal.model.HmParamsetType;
77 import org.openhab.binding.homematic.internal.model.HmRssiInfo;
78 import org.openhab.binding.homematic.internal.model.HmValueType;
79 import org.openhab.core.common.ThreadPoolManager;
80 import org.slf4j.Logger;
81 import org.slf4j.LoggerFactory;
82
83 /**
84  * The {@link AbstractHomematicGateway} is the main class for the communication with a Homematic gateway.
85  *
86  * @author Gerhard Riegler - Initial contribution
87  */
88 public abstract class AbstractHomematicGateway implements RpcEventListener, HomematicGateway, VirtualGateway {
89     private final Logger logger = LoggerFactory.getLogger(AbstractHomematicGateway.class);
90     public static final double DEFAULT_DISABLE_DELAY = 2.0;
91     private static final long RESTART_DELAY = 30;
92     private static final long CONNECTION_TRACKER_INTERVAL_SECONDS = 15;
93
94     private final Map<TransferMode, RpcClient<?>> rpcClients = new HashMap<>();
95     private final Map<TransferMode, RpcServer> rpcServers = new HashMap<>();
96
97     protected HomematicConfig config;
98     protected HttpClient httpClient;
99     private final String id;
100     private final HomematicGatewayAdapter gatewayAdapter;
101     private final DelayedExecuter sendDelayedExecutor = new DelayedExecuter();
102     private final DelayedExecuter receiveDelayedExecutor = new DelayedExecuter();
103     private final Set<HmDatapointInfo> echoEvents = Collections.synchronizedSet(new HashSet<>());
104     private ScheduledFuture<?> connectionTrackerFuture;
105     private ConnectionTrackerThread connectionTrackerThread;
106     private final Map<String, HmDevice> devices = Collections.synchronizedMap(new HashMap<>());
107     private final Map<HmInterface, TransferMode> availableInterfaces = new TreeMap<>();
108     private static List<VirtualDatapointHandler> virtualDatapointHandlers = new ArrayList<>();
109     private boolean cancelLoadAllMetadata;
110     private boolean initialized;
111     private boolean newDeviceEventsEnabled;
112     private ScheduledFuture<?> enableNewDeviceFuture;
113     private final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool(GATEWAY_POOL_NAME);
114
115     static {
116         // loads all virtual datapoints
117         virtualDatapointHandlers.add(new BatteryTypeVirtualDatapointHandler());
118         virtualDatapointHandlers.add(new FirmwareVirtualDatapointHandler());
119         virtualDatapointHandlers.add(new DisplayOptionsVirtualDatapointHandler());
120         virtualDatapointHandlers.add(new ReloadFromGatewayVirtualDatapointHandler());
121         virtualDatapointHandlers.add(new ReloadAllFromGatewayVirtualDatapointHandler());
122         virtualDatapointHandlers.add(new OnTimeAutomaticVirtualDatapointHandler());
123         virtualDatapointHandlers.add(new InstallModeVirtualDatapoint());
124         virtualDatapointHandlers.add(new InstallModeDurationVirtualDatapoint());
125         virtualDatapointHandlers.add(new DeleteDeviceModeVirtualDatapointHandler());
126         virtualDatapointHandlers.add(new DeleteDeviceVirtualDatapointHandler());
127         virtualDatapointHandlers.add(new RssiVirtualDatapointHandler());
128         virtualDatapointHandlers.add(new ReloadRssiVirtualDatapointHandler());
129         virtualDatapointHandlers.add(new StateContactVirtualDatapointHandler());
130         virtualDatapointHandlers.add(new SignalStrengthVirtualDatapointHandler());
131         virtualDatapointHandlers.add(new DisplayTextVirtualDatapoint());
132         virtualDatapointHandlers.add(new HmwIoModuleVirtualDatapointHandler());
133         virtualDatapointHandlers.add(new ButtonVirtualDatapointHandler());
134     }
135
136     public AbstractHomematicGateway(String id, HomematicConfig config, HomematicGatewayAdapter gatewayAdapter,
137             HttpClient httpClient) {
138         this.id = id;
139         this.config = config;
140         this.gatewayAdapter = gatewayAdapter;
141         this.httpClient = httpClient;
142     }
143
144     @Override
145     public void initialize() throws IOException {
146         logger.debug("Initializing gateway with id '{}'", id);
147
148         HmGatewayInfo gatewayInfo = config.getGatewayInfo();
149         if (gatewayInfo.isHomegear()) {
150             // Homegear
151             availableInterfaces.put(HmInterface.RF, TransferMode.BIN_RPC);
152         } else if (gatewayInfo.isCCU()) {
153             // CCU
154             if (gatewayInfo.isRfInterface()) {
155                 availableInterfaces.put(HmInterface.RF, TransferMode.XML_RPC);
156             }
157             if (gatewayInfo.isWiredInterface()) {
158                 availableInterfaces.put(HmInterface.WIRED, TransferMode.XML_RPC);
159             }
160             if (gatewayInfo.isHmipInterface()) {
161                 availableInterfaces.put(HmInterface.HMIP, TransferMode.XML_RPC);
162             }
163             if (gatewayInfo.isCuxdInterface()) {
164                 availableInterfaces.put(HmInterface.CUXD, TransferMode.BIN_RPC);
165             }
166             if (gatewayInfo.isGroupInterface()) {
167                 availableInterfaces.put(HmInterface.GROUP, TransferMode.XML_RPC);
168             }
169         } else {
170             // other
171             if (gatewayInfo.isRfInterface()) {
172                 availableInterfaces.put(HmInterface.RF, TransferMode.XML_RPC);
173             }
174             if (gatewayInfo.isWiredInterface()) {
175                 availableInterfaces.put(HmInterface.WIRED, TransferMode.XML_RPC);
176             }
177             if (gatewayInfo.isHmipInterface()) {
178                 availableInterfaces.put(HmInterface.HMIP, TransferMode.XML_RPC);
179             }
180         }
181
182         logger.info("{}", config.getGatewayInfo());
183         StringBuilder sb = new StringBuilder();
184         for (Entry<HmInterface, TransferMode> entry : availableInterfaces.entrySet()) {
185             sb.append(entry.getKey()).append(":").append(entry.getValue()).append(", ");
186         }
187         if (sb.length() > 2) {
188             sb.setLength(sb.length() - 2);
189         }
190         logger.debug("Used Homematic transfer modes: {}", sb.toString());
191         startClients();
192         startServers();
193         registerCallbacks();
194
195         if (!config.getGatewayInfo().isHomegear()) {
196             // delay the newDevice event handling at startup, reduces some API calls
197             long delay = config.getGatewayInfo().isCCU1() ? 10 : 3;
198             enableNewDeviceFuture = scheduler.schedule(() -> {
199                 newDeviceEventsEnabled = true;
200             }, delay, TimeUnit.MINUTES);
201         } else {
202             newDeviceEventsEnabled = true;
203         }
204     }
205
206     @Override
207     public void dispose() {
208         initialized = false;
209         if (enableNewDeviceFuture != null) {
210             enableNewDeviceFuture.cancel(true);
211         }
212         newDeviceEventsEnabled = false;
213         stopWatchdogs();
214         sendDelayedExecutor.stop();
215         receiveDelayedExecutor.stop();
216         stopServers(true);
217         stopClients();
218         devices.clear();
219         echoEvents.clear();
220         availableInterfaces.clear();
221         config.setGatewayInfo(null);
222     }
223
224     /**
225      * Starts the Homematic gateway client.
226      */
227     protected synchronized void startClients() throws IOException {
228         for (TransferMode mode : availableInterfaces.values()) {
229             if (!rpcClients.containsKey(mode)) {
230                 rpcClients.put(mode,
231                         mode == TransferMode.XML_RPC ? new XmlRpcClient(config, httpClient) : new BinRpcClient(config));
232             }
233         }
234     }
235
236     /**
237      * Stops the Homematic gateway client.
238      */
239     protected synchronized void stopClients() {
240         for (RpcClient<?> rpcClient : rpcClients.values()) {
241             rpcClient.dispose();
242         }
243         rpcClients.clear();
244     }
245
246     /**
247      * Starts the Homematic RPC server.
248      */
249     private synchronized void startServers() throws IOException {
250         for (TransferMode mode : availableInterfaces.values()) {
251             if (!rpcServers.containsKey(mode)) {
252                 RpcServer rpcServer = mode == TransferMode.XML_RPC ? new XmlRpcServer(this, config)
253                         : new BinRpcServer(this, config, id);
254                 rpcServers.put(mode, rpcServer);
255                 rpcServer.start();
256             }
257         }
258     }
259
260     private void registerCallbacks() throws IOException {
261         for (HmInterface hmInterface : availableInterfaces.keySet()) {
262             getRpcClient(hmInterface).init(hmInterface);
263         }
264     }
265
266     /**
267      * Stops the Homematic RPC server.
268      */
269     private synchronized void stopServers(boolean releaseConnection) {
270         if (releaseConnection) {
271             for (HmInterface hmInterface : availableInterfaces.keySet()) {
272                 try {
273                     getRpcClient(hmInterface).release(hmInterface);
274                 } catch (IOException ex) {
275                     // recoverable exception, therefore only debug
276                     logger.debug("Unable to release the connection to the gateway with id '{}': {}", id,
277                             ex.getMessage(), ex);
278                 }
279             }
280         }
281         for (TransferMode mode : rpcServers.keySet()) {
282             rpcServers.get(mode).shutdown();
283         }
284         rpcServers.clear();
285     }
286
287     @Override
288     public void startWatchdogs() {
289         logger.debug("Starting connection tracker for gateway with id '{}'", id);
290         connectionTrackerThread = new ConnectionTrackerThread();
291         connectionTrackerFuture = scheduler.scheduleWithFixedDelay(connectionTrackerThread, 30,
292                 CONNECTION_TRACKER_INTERVAL_SECONDS, TimeUnit.SECONDS);
293     }
294
295     private void stopWatchdogs() {
296         if (connectionTrackerFuture != null) {
297             connectionTrackerFuture.cancel(true);
298         }
299         connectionTrackerThread = null;
300     }
301
302     /**
303      * Returns the default interface to communicate with the Homematic gateway.
304      */
305     protected HmInterface getDefaultInterface() {
306         return availableInterfaces.containsKey(HmInterface.RF) ? HmInterface.RF : HmInterface.HMIP;
307     }
308
309     @Override
310     public RpcClient<?> getRpcClient(HmInterface hmInterface) throws IOException {
311         RpcClient<?> rpcClient = rpcClients.get(availableInterfaces.get(hmInterface));
312         if (rpcClient == null) {
313             throw new IOException("RPC client for interface " + hmInterface + " not available");
314         }
315         return rpcClient;
316     }
317
318     /**
319      * Loads all gateway variables into the given device.
320      */
321     protected abstract void loadVariables(HmChannel channel) throws IOException;
322
323     /**
324      * Loads all gateway scripts into the given device.
325      */
326     protected abstract void loadScripts(HmChannel channel) throws IOException;
327
328     /**
329      * Loads all names of the devices.
330      */
331     protected abstract void loadDeviceNames(Collection<HmDevice> devices) throws IOException;
332
333     /**
334      * Sets a variable on the Homematic gateway.
335      */
336     protected abstract void setVariable(HmDatapoint dp, Object value) throws IOException;
337
338     /**
339      * Execute a script on the Homematic gateway.
340      */
341     protected abstract void executeScript(HmDatapoint dp) throws IOException;
342
343     @Override
344     public HmDatapoint getDatapoint(HmDatapointInfo dpInfo) throws HomematicClientException {
345         HmDevice device = getDevice(dpInfo.getAddress());
346         HmChannel channel = device.getChannel(dpInfo.getChannel());
347         if (channel == null) {
348             throw new HomematicClientException(String.format("Channel %s in device '%s' not found on gateway '%s'",
349                     dpInfo.getChannel(), dpInfo.getAddress(), id));
350         }
351         HmDatapoint dp = channel.getDatapoint(dpInfo);
352         if (dp == null) {
353             throw new HomematicClientException(String.format("Datapoint '%s' not found on gateway '%s'", dpInfo, id));
354         }
355         return dp;
356     }
357
358     @Override
359     public HmDevice getDevice(String address) throws HomematicClientException {
360         HmDevice device = devices.get(address);
361         if (device == null) {
362             throw new HomematicClientException(
363                     String.format("Device with address '%s' not found on gateway '%s'", address, id));
364         }
365         return device;
366     }
367
368     @Override
369     public void cancelLoadAllDeviceMetadata() {
370         cancelLoadAllMetadata = true;
371     }
372
373     @Override
374     public void loadAllDeviceMetadata() throws IOException {
375         cancelLoadAllMetadata = false;
376         // load all device descriptions
377         List<HmDevice> deviceDescriptions = getDeviceDescriptions();
378
379         // loading datapoints for all channels
380         Set<String> loadedDevices = new HashSet<>();
381         Map<String, Collection<HmDatapoint>> datapointsByChannelIdCache = new HashMap<>();
382         for (HmDevice device : deviceDescriptions) {
383             if (!cancelLoadAllMetadata) {
384                 try {
385                     logger.trace("Loading metadata for device '{}' of type '{}'", device.getAddress(),
386                             device.getType());
387                     if (device.isGatewayExtras()) {
388                         loadChannelValues(device.getChannel(HmChannel.CHANNEL_NUMBER_VARIABLE));
389                         loadChannelValues(device.getChannel(HmChannel.CHANNEL_NUMBER_SCRIPT));
390                     } else {
391                         for (HmChannel channel : device.getChannels()) {
392                             logger.trace("  Loading channel {}", channel);
393                             // speed up metadata generation a little bit for equal channels in the gateway devices
394                             if ((DEVICE_TYPE_VIRTUAL.equals(device.getType())
395                                     || DEVICE_TYPE_VIRTUAL_WIRED.equals(device.getType())) && channel.getNumber() > 1) {
396                                 HmChannel previousChannel = device.getChannel(channel.getNumber() - 1);
397                                 cloneAllDatapointsIntoChannel(channel, previousChannel.getDatapoints());
398                             } else {
399                                 String channelId = String.format("%s:%s:%s", channel.getDevice().getType(),
400                                         channel.getDevice().getFirmware(), channel.getNumber());
401                                 Collection<HmDatapoint> cachedDatapoints = datapointsByChannelIdCache.get(channelId);
402                                 if (cachedDatapoints != null) {
403                                     // clone all datapoints
404                                     cloneAllDatapointsIntoChannel(channel, cachedDatapoints);
405                                 } else {
406                                     logger.trace("    Loading datapoints into channel {}", channel);
407                                     addChannelDatapoints(channel, HmParamsetType.MASTER);
408                                     addChannelDatapoints(channel, HmParamsetType.VALUES);
409
410                                     // Make sure to only cache non-reconfigurable channels. For reconfigurable channels,
411                                     // the data point set might change depending on the selected mode.
412                                     if (!channel.isReconfigurable()) {
413                                         datapointsByChannelIdCache.put(channelId, channel.getDatapoints());
414                                     }
415                                 }
416                             }
417                         }
418                     }
419                     prepareDevice(device);
420                     loadedDevices.add(device.getAddress());
421                     gatewayAdapter.onDeviceLoaded(device);
422                 } catch (IOException ex) {
423                     logger.warn("Can't load device with address '{}' from gateway '{}': {}", device.getAddress(), id,
424                             ex.getMessage());
425                 }
426             }
427         }
428         if (!cancelLoadAllMetadata) {
429             devices.keySet().retainAll(loadedDevices);
430         }
431         initialized = true;
432     }
433
434     /**
435      * Loads all datapoints from the gateway.
436      */
437     protected void addChannelDatapoints(HmChannel channel, HmParamsetType paramsetType) throws IOException {
438         try {
439             getRpcClient(channel.getDevice().getHmInterface()).addChannelDatapoints(channel, paramsetType);
440         } catch (UnknownParameterSetException ex) {
441             logger.info(
442                     "Can not load metadata for device: {}, channel: {}, paramset: {}, maybe there are no channels available",
443                     channel.getDevice().getAddress(), channel.getNumber(), paramsetType);
444         }
445     }
446
447     /**
448      * Loads all device descriptions from the gateway.
449      */
450     private List<HmDevice> getDeviceDescriptions() throws IOException {
451         List<HmDevice> deviceDescriptions = new ArrayList<>();
452         for (HmInterface hmInterface : availableInterfaces.keySet()) {
453             deviceDescriptions.addAll(getRpcClient(hmInterface).listDevices(hmInterface));
454         }
455         if (!cancelLoadAllMetadata) {
456             deviceDescriptions.add(createGatewayDevice());
457             loadDeviceNames(deviceDescriptions);
458         }
459         return deviceDescriptions;
460     }
461
462     /**
463      * Clones all datapoints into the given channel.
464      */
465     private void cloneAllDatapointsIntoChannel(HmChannel channel, Collection<HmDatapoint> datapoints) {
466         logger.trace("    Cloning {} datapoints into channel {}", datapoints.size(), channel);
467         for (HmDatapoint dp : datapoints) {
468             if (!dp.isVirtual()) {
469                 HmDatapoint clonedDp = dp.clone();
470                 clonedDp.setValue(null);
471                 channel.addDatapoint(clonedDp);
472             }
473         }
474     }
475
476     @Override
477     public void loadChannelValues(HmChannel channel) throws IOException {
478         if (channel.getDevice().isGatewayExtras()) {
479             if (!HmChannel.CHANNEL_NUMBER_EXTRAS.equals(channel.getNumber())) {
480                 List<HmDatapoint> datapoints = channel.getDatapoints();
481
482                 if (HmChannel.CHANNEL_NUMBER_VARIABLE.equals(channel.getNumber())) {
483                     loadVariables(channel);
484                     logger.debug("Loaded {} gateway variable(s)", datapoints.size());
485                 } else if (HmChannel.CHANNEL_NUMBER_SCRIPT.equals(channel.getNumber())) {
486                     loadScripts(channel);
487                     logger.debug("Loaded {} gateway script(s)", datapoints.size());
488                 }
489             }
490         } else {
491             logger.debug("Loading values for channel {} of device '{}'", channel, channel.getDevice().getAddress());
492             setChannelDatapointValues(channel, HmParamsetType.MASTER);
493             setChannelDatapointValues(channel, HmParamsetType.VALUES);
494         }
495
496         for (HmDatapoint dp : channel.getDatapoints()) {
497             handleVirtualDatapointEvent(dp, false);
498         }
499
500         channel.setInitialized(true);
501     }
502
503     @Override
504     public void updateChannelValueDatapoints(HmChannel channel) throws IOException {
505         logger.debug("Updating value datapoints for channel {} of device '{}', has {} datapoints before", channel,
506                 channel.getDevice().getAddress(), channel.getDatapoints().size());
507
508         channel.removeValueDatapoints();
509         addChannelDatapoints(channel, HmParamsetType.VALUES);
510         setChannelDatapointValues(channel, HmParamsetType.VALUES);
511
512         logger.debug("Updated value datapoints for channel {} of device '{}' (function {}), now has {} datapoints",
513                 channel, channel.getDevice().getAddress(), channel.getCurrentFunction(),
514                 channel.getDatapoints().size());
515     }
516
517     /**
518      * Sets all datapoint values for the given channel.
519      */
520     protected void setChannelDatapointValues(HmChannel channel, HmParamsetType paramsetType) throws IOException {
521         try {
522             getRpcClient(channel.getDevice().getHmInterface()).setChannelDatapointValues(channel, paramsetType);
523         } catch (UnknownParameterSetException ex) {
524             logger.info(
525                     "Can not load values for device: {}, channel: {}, paramset: {}, maybe there are no values available",
526                     channel.getDevice().getAddress(), channel.getNumber(), paramsetType);
527         }
528     }
529
530     @Override
531     public void loadDatapointValue(HmDatapoint dp) throws IOException {
532         getRpcClient(dp.getChannel().getDevice().getHmInterface()).getDatapointValue(dp);
533     }
534
535     @Override
536     public void loadRssiValues() throws IOException {
537         for (HmInterface hmInterface : availableInterfaces.keySet()) {
538             if (hmInterface == HmInterface.RF) {
539                 List<HmRssiInfo> rssiInfos = getRpcClient(hmInterface).loadRssiInfo(hmInterface);
540                 for (HmRssiInfo hmRssiInfo : rssiInfos) {
541                     updateRssiInfo(hmRssiInfo.getAddress(), DATAPOINT_NAME_RSSI_DEVICE, hmRssiInfo.getDevice());
542                     updateRssiInfo(hmRssiInfo.getAddress(), DATAPOINT_NAME_RSSI_PEER, hmRssiInfo.getPeer());
543                 }
544             }
545         }
546     }
547
548     @Override
549     public void setInstallMode(boolean enable, int seconds) throws IOException {
550         HmDevice gwExtrasHm = devices.get(HmDevice.ADDRESS_GATEWAY_EXTRAS);
551
552         if (gwExtrasHm != null) {
553             // since the homematic virtual device exist: try setting install mode via its dataPoints
554             HmDatapoint installModeDataPoint = null;
555             HmDatapoint installModeDurationDataPoint = null;
556
557             // collect virtual datapoints to be accessed
558             HmChannel hmChannel = gwExtrasHm.getChannel(HmChannel.CHANNEL_NUMBER_EXTRAS);
559             HmDatapointInfo installModeDurationDataPointInfo = new HmDatapointInfo(HmParamsetType.VALUES, hmChannel,
560                     HomematicConstants.VIRTUAL_DATAPOINT_NAME_INSTALL_MODE_DURATION);
561             if (enable) {
562                 installModeDurationDataPoint = hmChannel.getDatapoint(installModeDurationDataPointInfo);
563             }
564
565             HmDatapointInfo installModeDataPointInfo = new HmDatapointInfo(HmParamsetType.VALUES, hmChannel,
566                     HomematicConstants.VIRTUAL_DATAPOINT_NAME_INSTALL_MODE);
567
568             installModeDataPoint = hmChannel.getDatapoint(installModeDataPointInfo);
569
570             // first set duration on the datapoint
571             if (installModeDurationDataPoint != null) {
572                 try {
573                     VirtualDatapointHandler handler = getVirtualDatapointHandler(installModeDurationDataPoint, null);
574                     handler.handleCommand(this, installModeDurationDataPoint, new HmDatapointConfig(), seconds);
575
576                     // notify thing if exists
577                     gatewayAdapter.onStateUpdated(installModeDurationDataPoint);
578                 } catch (HomematicClientException ex) {
579                     logger.warn("Failed to send datapoint {}", installModeDurationDataPoint, ex);
580                 }
581             }
582
583             // now that the duration is set, we can enable / disable
584             if (installModeDataPoint != null) {
585                 try {
586                     VirtualDatapointHandler handler = getVirtualDatapointHandler(installModeDataPoint, null);
587                     handler.handleCommand(this, installModeDataPoint, new HmDatapointConfig(), enable);
588
589                     // notify thing if exists
590                     gatewayAdapter.onStateUpdated(installModeDataPoint);
591
592                     return;
593                 } catch (HomematicClientException ex) {
594                     logger.warn("Failed to send datapoint {}", installModeDataPoint, ex);
595                 }
596             }
597         }
598
599         // no gwExtrasHm available (or previous approach failed), therefore use rpc client directly
600         for (HmInterface hmInterface : availableInterfaces.keySet()) {
601             if (hmInterface == HmInterface.RF || hmInterface == HmInterface.CUXD) {
602                 getRpcClient(hmInterface).setInstallMode(hmInterface, enable, seconds);
603             }
604         }
605     }
606
607     @Override
608     public int getInstallMode() throws IOException {
609         for (HmInterface hmInterface : availableInterfaces.keySet()) {
610             if (hmInterface == HmInterface.RF || hmInterface == HmInterface.CUXD) {
611                 return getRpcClient(hmInterface).getInstallMode(hmInterface);
612             }
613         }
614
615         throw new IllegalStateException("Could not determine install mode because no suitable interface exists");
616     }
617
618     private void updateRssiInfo(String address, String datapointName, Integer value) {
619         HmDatapointInfo dpInfo = new HmDatapointInfo(address, HmParamsetType.VALUES, 0, datapointName);
620         HmChannel channel;
621         try {
622             channel = getDevice(dpInfo.getAddress()).getChannel(0);
623             if (channel != null) {
624                 eventReceived(dpInfo, value);
625             }
626         } catch (HomematicClientException e) {
627             // ignore
628         }
629     }
630
631     @Override
632     public void triggerDeviceValuesReload(HmDevice device) {
633         logger.debug("Triggering values reload for device '{}'", device.getAddress());
634         for (HmChannel channel : device.getChannels()) {
635             channel.setInitialized(false);
636         }
637         gatewayAdapter.reloadDeviceValues(device);
638     }
639
640     @Override
641     public void sendDatapointIgnoreVirtual(HmDatapoint dp, HmDatapointConfig dpConfig, Object newValue)
642             throws IOException, HomematicClientException {
643         sendDatapoint(dp, dpConfig, newValue, null, true);
644     }
645
646     @Override
647     public void sendDatapoint(HmDatapoint dp, HmDatapointConfig dpConfig, Object newValue, String rxMode)
648             throws IOException, HomematicClientException {
649         sendDatapoint(dp, dpConfig, newValue, rxMode, false);
650     }
651
652     /**
653      * Main method for sending datapoints to the gateway. It handles scripts, variables, virtual datapoints, delayed
654      * executions and auto disabling.
655      */
656     private void sendDatapoint(final HmDatapoint dp, final HmDatapointConfig dpConfig, final Object newValue,
657             final String rxMode, final boolean ignoreVirtualDatapoints) throws IOException, HomematicClientException {
658         final HmDatapointInfo dpInfo = new HmDatapointInfo(dp);
659         if (dp.isPressDatapoint() || (config.getGatewayInfo().isHomegear() && dp.isVariable())) {
660             echoEvents.add(dpInfo);
661         }
662         if (dp.isReadOnly()) {
663             logger.warn("Datapoint is readOnly, it is not published to the gateway with id '{}': '{}'", id, dpInfo);
664         } else if (HmValueType.ACTION == dp.getType() && MiscUtils.isFalseValue(newValue)) {
665             logger.warn(
666                     "Datapoint of type ACTION cannot be set to false, it is not published to the gateway with id '{}': '{}'",
667                     id, dpInfo);
668         } else {
669             final VirtualGateway gateway = this;
670             sendDelayedExecutor.start(dpInfo, dpConfig.getDelay(), new DelayedExecuterCallback() {
671
672                 @Override
673                 public void execute() throws IOException, HomematicClientException {
674                     VirtualDatapointHandler virtualDatapointHandler = ignoreVirtualDatapoints ? null
675                             : getVirtualDatapointHandler(dp, newValue);
676                     if (virtualDatapointHandler != null) {
677                         logger.debug("Handling virtual datapoint '{}' on gateway with id '{}'", dp.getName(), id);
678                         virtualDatapointHandler.handleCommand(gateway, dp, dpConfig, newValue);
679                     } else if (dp.isScript()) {
680                         if (MiscUtils.isTrueValue(newValue)) {
681                             logger.debug("Executing script '{}' on gateway with id '{}'", dp.getInfo(), id);
682                             executeScript(dp);
683                         }
684                     } else if (dp.isVariable()) {
685                         logger.debug("Sending variable '{}' with value '{}' to gateway with id '{}'", dp.getInfo(),
686                                 newValue, id);
687                         setVariable(dp, newValue);
688                     } else {
689                         logger.debug("Sending datapoint '{}' with value '{}' to gateway with id '{}' using rxMode '{}'",
690                                 dpInfo, newValue, id, rxMode == null ? "DEFAULT" : rxMode);
691                         getRpcClient(dp.getChannel().getDevice().getHmInterface()).setDatapointValue(dp, newValue,
692                                 rxMode);
693                     }
694                     dp.setValue(newValue);
695
696                     if (MiscUtils.isTrueValue(newValue)
697                             && (dp.isPressDatapoint() || dp.isScript() || dp.isActionType())) {
698                         disableDatapoint(dp, DEFAULT_DISABLE_DELAY);
699                     }
700                 }
701             });
702         }
703     }
704
705     /**
706      * Returns a VirtualDatapointHandler for the given datapoint if available.
707      */
708     private VirtualDatapointHandler getVirtualDatapointHandler(HmDatapoint dp, Object value) {
709         for (VirtualDatapointHandler vdph : virtualDatapointHandlers) {
710             if (vdph.canHandleCommand(dp, value)) {
711                 return vdph;
712             }
713         }
714         return null;
715     }
716
717     private void handleVirtualDatapointEvent(HmDatapoint dp, boolean publishToGateway) {
718         for (VirtualDatapointHandler vdph : virtualDatapointHandlers) {
719             if (vdph.canHandleEvent(dp)) {
720                 vdph.handleEvent(this, dp);
721                 if (publishToGateway) {
722                     gatewayAdapter.onStateUpdated(vdph.getVirtualDatapoint(dp.getChannel()));
723                 }
724             }
725         }
726     }
727
728     @Override
729     public void eventReceived(HmDatapointInfo dpInfo, Object newValue) {
730         String className = newValue == null ? "Unknown" : newValue.getClass().getSimpleName();
731         logger.debug("Received new ({}) value '{}' for '{}' from gateway with id '{}'", className, newValue, dpInfo,
732                 id);
733
734         if (echoEvents.remove(dpInfo)) {
735             logger.debug("Echo event detected, ignoring '{}'", dpInfo);
736         } else {
737             try {
738                 if (initialized) {
739                     final HmDatapoint dp = getDatapoint(dpInfo);
740                     HmDatapointConfig config = gatewayAdapter.getDatapointConfig(dp);
741                     receiveDelayedExecutor.start(dpInfo, config.getReceiveDelay(), () -> {
742                         dp.setValue(newValue);
743
744                         gatewayAdapter.onStateUpdated(dp);
745                         handleVirtualDatapointEvent(dp, true);
746                         if (dp.isPressDatapoint() && MiscUtils.isTrueValue(dp.getValue())) {
747                             disableDatapoint(dp, DEFAULT_DISABLE_DELAY);
748                         }
749                     });
750                 }
751             } catch (HomematicClientException | IOException ex) {
752                 // ignore
753             }
754         }
755     }
756
757     @Override
758     public void newDevices(List<String> addresses) {
759         if (initialized && newDeviceEventsEnabled) {
760             for (String address : addresses) {
761                 try {
762                     logger.debug("New device '{}' detected on gateway with id '{}'", address, id);
763                     List<HmDevice> deviceDescriptions = getDeviceDescriptions();
764                     for (HmDevice device : deviceDescriptions) {
765                         if (device.getAddress().equals(address)) {
766                             for (HmChannel channel : device.getChannels()) {
767                                 addChannelDatapoints(channel, HmParamsetType.MASTER);
768                                 addChannelDatapoints(channel, HmParamsetType.VALUES);
769                             }
770                             prepareDevice(device);
771                             gatewayAdapter.onNewDevice(device);
772                         }
773                     }
774                 } catch (Exception ex) {
775                     logger.error("{}", ex.getMessage(), ex);
776                 }
777             }
778         }
779     }
780
781     @Override
782     public void deleteDevices(List<String> addresses) {
783         if (initialized) {
784             for (String address : addresses) {
785                 logger.debug("Device '{}' removed from gateway with id '{}'", address, id);
786                 HmDevice device = devices.remove(address);
787                 if (device != null) {
788                     gatewayAdapter.onDeviceDeleted(device);
789                 }
790             }
791         }
792     }
793
794     @Override
795     public String getId() {
796         return id;
797     }
798
799     @Override
800     public HomematicGatewayAdapter getGatewayAdapter() {
801         return gatewayAdapter;
802     }
803
804     /**
805      * Creates a virtual device for handling variables, scripts and other special gateway functions.
806      */
807     private HmDevice createGatewayDevice() {
808         String type = String.format("%s-%s", HmDevice.TYPE_GATEWAY_EXTRAS, id.toUpperCase());
809         HmDevice device = new HmDevice(HmDevice.ADDRESS_GATEWAY_EXTRAS, getDefaultInterface(), type,
810                 config.getGatewayInfo().getId(), null, null);
811         device.setName(HmDevice.TYPE_GATEWAY_EXTRAS);
812
813         device.addChannel(new HmChannel(HmChannel.TYPE_GATEWAY_EXTRAS, HmChannel.CHANNEL_NUMBER_EXTRAS));
814         device.addChannel(new HmChannel(HmChannel.TYPE_GATEWAY_VARIABLE, HmChannel.CHANNEL_NUMBER_VARIABLE));
815         device.addChannel(new HmChannel(HmChannel.TYPE_GATEWAY_SCRIPT, HmChannel.CHANNEL_NUMBER_SCRIPT));
816
817         return device;
818     }
819
820     /**
821      * Adds virtual datapoints to the device.
822      */
823     private void prepareDevice(HmDevice device) {
824         for (VirtualDatapointHandler vdph : virtualDatapointHandlers) {
825             vdph.initialize(device);
826
827         }
828         devices.put(device.getAddress(), device);
829         logger.debug("Loaded device '{}' ({}) with {} datapoints", device.getAddress(), device.getType(),
830                 device.getDatapointCount());
831
832         if (logger.isTraceEnabled()) {
833             logger.trace("{}", device);
834             for (HmChannel channel : device.getChannels()) {
835                 logger.trace("  {}", channel);
836                 for (HmDatapoint dp : channel.getDatapoints()) {
837                     logger.trace("    {}", dp);
838                 }
839             }
840         }
841     }
842
843     @Override
844     public void disableDatapoint(final HmDatapoint dp, double delay) {
845         try {
846             sendDelayedExecutor.start(new HmDatapointInfo(dp), delay, new DelayedExecuterCallback() {
847
848                 @Override
849                 public void execute() throws IOException {
850                     if (MiscUtils.isTrueValue(dp.getValue())) {
851                         dp.setValue(Boolean.FALSE);
852                         gatewayAdapter.onStateUpdated(dp);
853                         handleVirtualDatapointEvent(dp, true);
854                     } else if (dp.getType() == HmValueType.ENUM && dp.getValue() != null && !dp.getValue().equals(0)) {
855                         dp.setValue(dp.getMinValue());
856                         gatewayAdapter.onStateUpdated(dp);
857                         handleVirtualDatapointEvent(dp, true);
858                     }
859                 }
860             });
861         } catch (IOException | HomematicClientException ex) {
862             logger.error("{}", ex.getMessage(), ex);
863         }
864     }
865
866     @Override
867     public void deleteDevice(String address, boolean reset, boolean force, boolean defer) {
868         for (RpcClient<?> rpcClient : rpcClients.values()) {
869             try {
870                 rpcClient.deleteDevice(getDevice(address), translateFlags(reset, force, defer));
871             } catch (HomematicClientException e) {
872                 // thrown by getDevice(address) if no device for the given address is paired on the gateway
873                 logger.info("Device deletion not possible: {}", e.getMessage());
874             } catch (IOException e) {
875                 logger.warn("Device deletion failed: {}", e.getMessage(), e);
876             }
877         }
878     }
879
880     private int translateFlags(boolean reset, boolean force, boolean defer) {
881         final int resetFlag = 0b001;
882         final int forceFlag = 0b010;
883         final int deferFlag = 0b100;
884         int resultFlag = 0;
885
886         if (reset) {
887             resultFlag += resetFlag;
888         }
889         if (force) {
890             resultFlag += forceFlag;
891         }
892         if (defer) {
893             resultFlag += deferFlag;
894         }
895
896         return resultFlag;
897     }
898
899     /**
900      * Thread which validates the connection to the gateway and restarts the RPC client if necessary.
901      * It also polls for the current duty cycle ratio of the gateway after every successful connection validation.
902      */
903     private class ConnectionTrackerThread implements Runnable {
904         private boolean connectionLost;
905         private boolean reStartPending = false;
906
907         @Override
908         public void run() {
909             try {
910                 if (reStartPending) {
911                     return;
912                 }
913                 ListBidcosInterfacesParser parser = getRpcClient(getDefaultInterface())
914                         .listBidcosInterfaces(getDefaultInterface());
915                 Integer dutyCycleRatio = parser.getDutyCycleRatio();
916                 if (dutyCycleRatio != null) {
917                     gatewayAdapter.onDutyCycleRatioUpdate(dutyCycleRatio);
918                 }
919                 connectionConfirmed();
920             } catch (IOException ex) {
921                 try {
922                     handleInvalidConnection("IOException " + ex.getMessage());
923                 } catch (IOException ex2) {
924                     // ignore
925                 }
926             }
927         }
928
929         private void connectionConfirmed() {
930             if (connectionLost) {
931                 connectionLost = false;
932                 logger.info("Connection resumed on gateway '{}'", id);
933                 try {
934                     registerCallbacks();
935                 } catch (IOException e) {
936                     logger.warn("Connection only partially restored. It is recommended to restart the binding");
937                 }
938                 gatewayAdapter.onConnectionResumed();
939             }
940         }
941
942         private void handleInvalidConnection(String cause) throws IOException {
943             if (!connectionLost) {
944                 connectionLost = true;
945                 logger.warn("Connection lost on gateway '{}', cause: \"{}\"", id, cause);
946                 gatewayAdapter.onConnectionLost();
947             }
948             stopServers(false);
949             stopClients();
950             reStartPending = true;
951             logger.debug("Waiting {}s until restart attempt", RESTART_DELAY);
952             scheduler.schedule(() -> {
953                 try {
954                     startClients();
955                     startServers();
956                 } catch (IOException e) {
957                     logger.debug("Restart failed: {}", e.getMessage());
958                 }
959                 reStartPending = false;
960             }, RESTART_DELAY, TimeUnit.SECONDS);
961         }
962     }
963 }