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