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