2 * Copyright (c) 2010-2022 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.homematic.internal.communicator.client;
15 import static org.openhab.binding.homematic.internal.HomematicBindingConstants.*;
17 import java.io.IOException;
18 import java.util.Collection;
19 import java.util.HashMap;
20 import java.util.List;
22 import java.util.UUID;
23 import java.util.concurrent.CancellationException;
24 import java.util.concurrent.ExecutionException;
25 import java.util.concurrent.ScheduledExecutorService;
26 import java.util.concurrent.ScheduledFuture;
27 import java.util.concurrent.TimeUnit;
28 import java.util.concurrent.TimeoutException;
30 import org.openhab.binding.homematic.internal.HomematicBindingConstants;
31 import org.openhab.binding.homematic.internal.common.HomematicConfig;
32 import org.openhab.binding.homematic.internal.communicator.message.RpcRequest;
33 import org.openhab.binding.homematic.internal.communicator.parser.GetAllScriptsParser;
34 import org.openhab.binding.homematic.internal.communicator.parser.GetAllSystemVariablesParser;
35 import org.openhab.binding.homematic.internal.communicator.parser.GetDeviceDescriptionParser;
36 import org.openhab.binding.homematic.internal.communicator.parser.GetParamsetDescriptionParser;
37 import org.openhab.binding.homematic.internal.communicator.parser.GetParamsetParser;
38 import org.openhab.binding.homematic.internal.communicator.parser.GetValueParser;
39 import org.openhab.binding.homematic.internal.communicator.parser.HomegearLoadDeviceNamesParser;
40 import org.openhab.binding.homematic.internal.communicator.parser.ListBidcosInterfacesParser;
41 import org.openhab.binding.homematic.internal.communicator.parser.ListDevicesParser;
42 import org.openhab.binding.homematic.internal.communicator.parser.RssiInfoParser;
43 import org.openhab.binding.homematic.internal.misc.MiscUtils;
44 import org.openhab.binding.homematic.internal.model.HmChannel;
45 import org.openhab.binding.homematic.internal.model.HmDatapoint;
46 import org.openhab.binding.homematic.internal.model.HmDevice;
47 import org.openhab.binding.homematic.internal.model.HmGatewayInfo;
48 import org.openhab.binding.homematic.internal.model.HmInterface;
49 import org.openhab.binding.homematic.internal.model.HmParamsetType;
50 import org.openhab.binding.homematic.internal.model.HmRssiInfo;
51 import org.openhab.core.common.ThreadPoolManager;
52 import org.slf4j.Logger;
53 import org.slf4j.LoggerFactory;
56 * Client implementation for sending messages via BIN-RPC to a Homematic gateway.
58 * @author Gerhard Riegler - Initial contribution
60 public abstract class RpcClient<T> {
61 private final Logger logger = LoggerFactory.getLogger(RpcClient.class);
62 protected static final int MAX_RPC_RETRY = 3;
63 protected static final int RESP_BUFFER_SIZE = 8192;
64 private static final int INITIAL_CALLBACK_REG_DELAY = 20; // 20 s before first attempt
65 private static final int CALLBACK_REG_DELAY = 10; // 10 s between two attempts
67 protected HomematicConfig config;
68 private String thisUID = UUID.randomUUID().toString();
69 private ScheduledFuture<?> future = null;
72 public RpcClient(HomematicConfig config) {
77 * Returns a RpcRequest for this client.
79 protected abstract RpcRequest<T> createRpcRequest(String methodName);
82 * Returns the callback url for this client.
84 protected abstract String getRpcCallbackUrl();
87 * Sends the RPC message to the gateway.
89 protected abstract Object[] sendMessage(int port, RpcRequest<T> request) throws IOException;
92 * Register a callback for the specified interface where the Homematic gateway can send its events.
94 public void init(HmInterface hmInterface) throws IOException {
95 ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool(GATEWAY_POOL_NAME);
96 RpcRequest<T> request = createRpcRequest("init");
97 request.addArg(getRpcCallbackUrl());
98 request.addArg(thisUID);
99 if (config.getGatewayInfo().isHomegear()) {
100 request.addArg(Integer.valueOf(0x22));
102 logger.debug("Register callback for interface {}", hmInterface.getName());
105 sendMessage(config.getRpcPort(hmInterface), request); // first attempt without delay
106 } catch (IOException e) {
107 future = scheduler.scheduleWithFixedDelay(() -> {
108 logger.debug("Register callback for interface {}, attempt {}", hmInterface.getName(), ++attempt);
110 sendMessage(config.getRpcPort(hmInterface), request);
112 } catch (IOException ex) {
115 }, INITIAL_CALLBACK_REG_DELAY, CALLBACK_REG_DELAY, TimeUnit.SECONDS);
117 future.get(config.getCallbackRegTimeout(), TimeUnit.SECONDS);
118 } catch (CancellationException e1) {
119 logger.debug("Callback for interface {} successfully registered", hmInterface.getName());
120 } catch (InterruptedException | ExecutionException e1) {
121 throw new IOException("Callback reg. thread interrupted", e1);
122 } catch (TimeoutException e1) {
123 logger.error("Callback registration for interface {} timed out", hmInterface.getName());
124 throw new IOException("Unable to reconnect in time");
131 * Disposes the client.
133 public void dispose() {
139 * Release a callback for the specified interface.
141 public void release(HmInterface hmInterface) throws IOException {
142 RpcRequest<T> request = createRpcRequest("init");
143 request.addArg(getRpcCallbackUrl());
144 sendMessage(config.getRpcPort(hmInterface), request);
148 * Sends a ping to the specified interface.
150 public void ping(HmInterface hmInterface, String callerId) throws IOException {
151 RpcRequest<T> request = createRpcRequest("ping");
152 request.addArg(callerId);
153 sendMessage(config.getRpcPort(hmInterface), request);
157 * Returns the info of all BidCos interfaces available on the gateway.
159 public ListBidcosInterfacesParser listBidcosInterfaces(HmInterface hmInterface) throws IOException {
160 RpcRequest<T> request = createRpcRequest("listBidcosInterfaces");
161 return new ListBidcosInterfacesParser().parse(sendMessage(config.getRpcPort(hmInterface), request));
165 * Returns some infos of the gateway.
167 private GetDeviceDescriptionParser getDeviceDescription(HmInterface hmInterface) throws IOException {
168 RpcRequest<T> request = createRpcRequest("getDeviceDescription");
169 request.addArg("BidCoS-RF");
170 return new GetDeviceDescriptionParser().parse(sendMessage(config.getRpcPort(hmInterface), request));
174 * Returns all variable metadata and values from a Homegear gateway.
176 public void getAllSystemVariables(HmChannel channel) throws IOException {
177 RpcRequest<T> request = createRpcRequest("getAllSystemVariables");
178 new GetAllSystemVariablesParser(channel).parse(sendMessage(config.getRpcPort(channel), request));
182 * Loads all device names from a Homegear gateway.
184 public void loadDeviceNames(HmInterface hmInterface, Collection<HmDevice> devices) throws IOException {
185 RpcRequest<T> request = createRpcRequest("getDeviceInfo");
186 new HomegearLoadDeviceNamesParser(devices).parse(sendMessage(config.getRpcPort(hmInterface), request));
190 * Returns true, if the interface is available on the gateway.
192 public void checkInterface(HmInterface hmInterface) throws IOException {
193 RpcRequest<T> request = createRpcRequest("init");
194 request.addArg("http://openhab.validation:1000");
195 sendMessage(config.getRpcPort(hmInterface), request);
199 * Returns all script metadata from a Homegear gateway.
201 public void getAllScripts(HmChannel channel) throws IOException {
202 RpcRequest<T> request = createRpcRequest("getAllScripts");
203 new GetAllScriptsParser(channel).parse(sendMessage(config.getRpcPort(channel), request));
207 * Returns all device and channel metadata.
209 public Collection<HmDevice> listDevices(HmInterface hmInterface) throws IOException {
210 RpcRequest<T> request = createRpcRequest("listDevices");
211 return new ListDevicesParser(hmInterface, config).parse(sendMessage(config.getRpcPort(hmInterface), request));
215 * Loads all datapoint metadata into the given channel.
217 public void addChannelDatapoints(HmChannel channel, HmParamsetType paramsetType) throws IOException {
218 if (isConfigurationChannel(channel) && paramsetType != HmParamsetType.MASTER) {
219 // The configuration channel only has a MASTER Paramset, so there is nothing to load
222 RpcRequest<T> request = createRpcRequest("getParamsetDescription");
223 request.addArg(getRpcAddress(channel.getDevice().getAddress()) + getChannelSuffix(channel));
224 request.addArg(paramsetType.toString());
225 new GetParamsetDescriptionParser(channel, paramsetType).parse(sendMessage(config.getRpcPort(channel), request));
229 * Sets all datapoint values for the given channel.
231 public void setChannelDatapointValues(HmChannel channel, HmParamsetType paramsetType) throws IOException {
232 if (isConfigurationChannel(channel) && paramsetType != HmParamsetType.MASTER) {
233 // The configuration channel only has a MASTER Paramset, so there is nothing to load
237 RpcRequest<T> request = createRpcRequest("getParamset");
238 request.addArg(getRpcAddress(channel.getDevice().getAddress()) + getChannelSuffix(channel));
239 request.addArg(paramsetType.toString());
240 if (channel.getDevice().getHmInterface() == HmInterface.CUXD && paramsetType == HmParamsetType.VALUES) {
241 setChannelDatapointValues(channel);
244 new GetParamsetParser(channel, paramsetType).parse(sendMessage(config.getRpcPort(channel), request));
245 } catch (UnknownRpcFailureException ex) {
246 if (paramsetType == HmParamsetType.VALUES) {
248 "RpcResponse unknown RPC failure (-1 Failure), fetching values with another API method for device: {}, channel: {}, paramset: {}",
249 channel.getDevice().getAddress(), channel.getNumber(), paramsetType);
250 setChannelDatapointValues(channel);
259 * Reads all VALUES datapoints individually, fallback method if setChannelDatapointValues throws a -1 Failure
262 private void setChannelDatapointValues(HmChannel channel) throws IOException {
263 for (HmDatapoint dp : channel.getDatapoints()) {
264 getDatapointValue(dp);
269 * Tries to identify the gateway and returns the GatewayInfo.
271 public HmGatewayInfo getGatewayInfo(String id) throws IOException {
272 boolean isHomegear = false;
273 GetDeviceDescriptionParser ddParser;
274 ListBidcosInterfacesParser biParser;
277 ddParser = getDeviceDescription(HmInterface.RF);
278 isHomegear = "Homegear".equalsIgnoreCase(ddParser.getType());
279 } catch (IOException ex) {
280 // can't load gateway infos via RF interface
281 ddParser = new GetDeviceDescriptionParser();
285 biParser = listBidcosInterfaces(HmInterface.RF);
286 } catch (IOException ex) {
287 biParser = listBidcosInterfaces(HmInterface.HMIP);
290 HmGatewayInfo gatewayInfo = new HmGatewayInfo();
291 gatewayInfo.setAddress(biParser.getGatewayAddress());
292 String gwType = biParser.getType();
294 gatewayInfo.setId(HmGatewayInfo.ID_HOMEGEAR);
295 gatewayInfo.setType(ddParser.getType());
296 gatewayInfo.setFirmware(ddParser.getFirmware());
297 } else if ((MiscUtils.strStartsWithIgnoreCase(gwType, "CCU")
298 || MiscUtils.strStartsWithIgnoreCase(gwType, "HMIP_CCU")
299 || MiscUtils.strStartsWithIgnoreCase(ddParser.getType(), "HM-RCV-50") || config.isCCUType())
300 && !config.isNoCCUType()) {
301 gatewayInfo.setId(HmGatewayInfo.ID_CCU);
302 String type = gwType.isBlank() ? "CCU" : gwType;
303 gatewayInfo.setType(type);
305 .setFirmware(!ddParser.getFirmware().isEmpty() ? ddParser.getFirmware() : biParser.getFirmware());
307 gatewayInfo.setId(HmGatewayInfo.ID_DEFAULT);
308 gatewayInfo.setType(gwType);
309 gatewayInfo.setFirmware(biParser.getFirmware());
312 if (gatewayInfo.isCCU() || config.hasRfPort()) {
313 gatewayInfo.setRfInterface(hasInterface(HmInterface.RF, id));
316 if (gatewayInfo.isCCU() || config.hasWiredPort()) {
317 gatewayInfo.setWiredInterface(hasInterface(HmInterface.WIRED, id));
320 if (gatewayInfo.isCCU() || config.hasHmIpPort()) {
321 gatewayInfo.setHmipInterface(hasInterface(HmInterface.HMIP, id));
324 if (gatewayInfo.isCCU() || config.hasCuxdPort()) {
325 gatewayInfo.setCuxdInterface(hasInterface(HmInterface.CUXD, id));
328 if (gatewayInfo.isCCU() || config.hasGroupPort()) {
329 gatewayInfo.setGroupInterface(hasInterface(HmInterface.GROUP, id));
336 * Returns true, if a connection is possible with the given interface.
338 private boolean hasInterface(HmInterface hmInterface, String id) throws IOException {
340 checkInterface(hmInterface);
342 } catch (IOException ex) {
343 logger.info("Interface '{}' on gateway '{}' not available, disabling support", hmInterface, id);
349 * Sets the value of the datapoint using the provided rx transmission mode.
351 * @param dp The datapoint to set
352 * @param value The new value to set on the datapoint
353 * @param rxMode The rx mode to use for the transmission of the datapoint value
354 * ({@link HomematicBindingConstants#RX_BURST_MODE "BURST"} for burst mode,
355 * {@link HomematicBindingConstants#RX_WAKEUP_MODE "WAKEUP"} for wakeup mode, or null for the default
358 public void setDatapointValue(HmDatapoint dp, Object value, String rxMode) throws IOException {
359 if (dp.isIntegerType() && value instanceof Double) {
360 value = ((Number) value).intValue();
363 RpcRequest<T> request;
364 if (HmParamsetType.VALUES == dp.getParamsetType()) {
365 request = createRpcRequest("setValue");
366 request.addArg(getRpcAddress(dp.getChannel().getDevice().getAddress()) + getChannelSuffix(dp.getChannel()));
367 request.addArg(dp.getName());
368 request.addArg(value);
369 configureRxMode(request, rxMode);
371 request = createRpcRequest("putParamset");
372 request.addArg(getRpcAddress(dp.getChannel().getDevice().getAddress()) + getChannelSuffix(dp.getChannel()));
373 request.addArg(HmParamsetType.MASTER.toString());
374 Map<String, Object> paramSet = new HashMap<>();
375 paramSet.put(dp.getName(), value);
376 request.addArg(paramSet);
377 configureRxMode(request, rxMode);
379 sendMessage(config.getRpcPort(dp.getChannel()), request);
382 protected void configureRxMode(RpcRequest<T> request, String rxMode) {
383 if (rxMode != null) {
384 if (RX_BURST_MODE.equals(rxMode) || RX_WAKEUP_MODE.equals(rxMode)) {
385 request.addArg(rxMode);
391 * Retrieves the value of a single {@link HmDatapoint} from the device. Can only be used for the paramset "VALUES".
393 * @param dp The HmDatapoint that shall be loaded
394 * @throws IOException If there is a problem while communicating to the gateway
396 public void getDatapointValue(HmDatapoint dp) throws IOException {
397 if (dp.isReadable() && !dp.isVirtual() && dp.getParamsetType() == HmParamsetType.VALUES) {
398 RpcRequest<T> request = createRpcRequest("getValue");
399 request.addArg(getRpcAddress(dp.getChannel().getDevice().getAddress()) + getChannelSuffix(dp.getChannel()));
400 request.addArg(dp.getName());
401 new GetValueParser(dp).parse(sendMessage(config.getRpcPort(dp.getChannel()), request));
406 * Sets the value of a system variable on a Homegear gateway.
408 public void setSystemVariable(HmDatapoint dp, Object value) throws IOException {
409 RpcRequest<T> request = createRpcRequest("setSystemVariable");
410 request.addArg(dp.getInfo());
411 request.addArg(value);
412 sendMessage(config.getRpcPort(dp.getChannel()), request);
416 * Executes a script on the Homegear gateway.
418 public void executeScript(HmDatapoint dp) throws IOException {
419 RpcRequest<T> request = createRpcRequest("runScript");
420 request.addArg(dp.getInfo());
421 sendMessage(config.getRpcPort(dp.getChannel()), request);
425 * Enables/disables the install mode for given seconds.
427 * @param hmInterface specifies the interface to enable / disable install mode on
428 * @param enable if <i>true</i> it will be enabled, otherwise disabled
429 * @param seconds desired duration of install mode
430 * @throws IOException if RpcClient fails to propagate command
432 public void setInstallMode(HmInterface hmInterface, boolean enable, int seconds) throws IOException {
433 RpcRequest<T> request = createRpcRequest("setInstallMode");
434 request.addArg(enable);
435 request.addArg(seconds);
436 request.addArg(INSTALL_MODE_NORMAL);
437 logger.debug("Submitting setInstallMode(on={}, time={}, mode={}) ", enable, seconds, INSTALL_MODE_NORMAL);
438 sendMessage(config.getRpcPort(hmInterface), request);
442 * Returns the remaining time of <i>install_mode==true</i>
444 * @param hmInterface specifies the interface on which install mode status is requested
445 * @return current duration in seconds that the controller will remain in install mode,
446 * value of 0 means that the install mode is disabled
447 * @throws IOException if RpcClient fails to propagate command
449 public int getInstallMode(HmInterface hmInterface) throws IOException {
450 RpcRequest<T> request = createRpcRequest("getInstallMode");
451 Object[] result = sendMessage(config.getRpcPort(hmInterface), request);
452 if (logger.isTraceEnabled()) {
454 "Checking InstallMode: getInstallMode() request returned {} (remaining seconds in InstallMode=true)",
458 return (int) result[0];
459 } catch (Exception cause) {
460 IOException wrappedException = new IOException(
461 "Failed to request install mode from interface " + hmInterface);
462 wrappedException.initCause(cause);
463 throw wrappedException;
468 * Deletes the device from the gateway.
470 public void deleteDevice(HmDevice device, int flags) throws IOException {
471 RpcRequest<T> request = createRpcRequest("deleteDevice");
472 request.addArg(device.getAddress());
473 request.addArg(flags);
474 sendMessage(config.getRpcPort(device.getHmInterface()), request);
478 * Returns the rpc address from a device address, correctly handling groups.
480 private String getRpcAddress(String address) {
481 if (address != null && address.startsWith("T-")) {
482 address = "*" + address.substring(2);
488 * Returns the rssi values for all devices.
490 public List<HmRssiInfo> loadRssiInfo(HmInterface hmInterface) throws IOException {
491 RpcRequest<T> request = createRpcRequest("rssiInfo");
492 return new RssiInfoParser(config).parse(sendMessage(config.getRpcPort(hmInterface), request));
496 * Returns the address suffix that specifies the channel for a given HmChannel. This is either a colon ":" followed
497 * by the channel number, or the empty string for a configuration channel.
499 private String getChannelSuffix(HmChannel channel) {
500 return isConfigurationChannel(channel) ? "" : ":" + channel.getNumber();
504 * Checks whether a channel is a configuration channel. The configuration channel of a device encapsulates the
505 * MASTER Paramset that does not belong to one of its actual channels.
507 private boolean isConfigurationChannel(HmChannel channel) {
508 return channel.getNumber() == CONFIGURATION_CHANNEL_NUMBER;