2 * Copyright (c) 2010-2023 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() {
134 if (future != null) {
140 * Release a callback for the specified interface.
142 public void release(HmInterface hmInterface) throws IOException {
143 RpcRequest<T> request = createRpcRequest("init");
144 request.addArg(getRpcCallbackUrl());
145 sendMessage(config.getRpcPort(hmInterface), request);
149 * Sends a ping to the specified interface.
151 public void ping(HmInterface hmInterface, String callerId) throws IOException {
152 RpcRequest<T> request = createRpcRequest("ping");
153 request.addArg(callerId);
154 sendMessage(config.getRpcPort(hmInterface), request);
158 * Returns the info of all BidCos interfaces available on the gateway.
160 public ListBidcosInterfacesParser listBidcosInterfaces(HmInterface hmInterface) throws IOException {
161 RpcRequest<T> request = createRpcRequest("listBidcosInterfaces");
162 return new ListBidcosInterfacesParser().parse(sendMessage(config.getRpcPort(hmInterface), request));
166 * Returns some infos of the gateway.
168 private GetDeviceDescriptionParser getDeviceDescription(HmInterface hmInterface) throws IOException {
169 RpcRequest<T> request = createRpcRequest("getDeviceDescription");
170 request.addArg("BidCoS-RF");
171 return new GetDeviceDescriptionParser().parse(sendMessage(config.getRpcPort(hmInterface), request));
175 * Returns all variable metadata and values from a Homegear gateway.
177 public void getAllSystemVariables(HmChannel channel) throws IOException {
178 RpcRequest<T> request = createRpcRequest("getAllSystemVariables");
179 new GetAllSystemVariablesParser(channel).parse(sendMessage(config.getRpcPort(channel), request));
183 * Loads all device names from a Homegear gateway.
185 public void loadDeviceNames(HmInterface hmInterface, Collection<HmDevice> devices) throws IOException {
186 RpcRequest<T> request = createRpcRequest("getDeviceInfo");
187 new HomegearLoadDeviceNamesParser(devices).parse(sendMessage(config.getRpcPort(hmInterface), request));
191 * Returns true, if the interface is available on the gateway.
193 public void checkInterface(HmInterface hmInterface) throws IOException {
194 RpcRequest<T> request = createRpcRequest("init");
195 request.addArg("http://openhab.validation:1000");
196 sendMessage(config.getRpcPort(hmInterface), request);
200 * Returns all script metadata from a Homegear gateway.
202 public void getAllScripts(HmChannel channel) throws IOException {
203 RpcRequest<T> request = createRpcRequest("getAllScripts");
204 new GetAllScriptsParser(channel).parse(sendMessage(config.getRpcPort(channel), request));
208 * Returns all device and channel metadata.
210 public Collection<HmDevice> listDevices(HmInterface hmInterface) throws IOException {
211 RpcRequest<T> request = createRpcRequest("listDevices");
212 return new ListDevicesParser(hmInterface, config).parse(sendMessage(config.getRpcPort(hmInterface), request));
216 * Loads all datapoint metadata into the given channel.
218 public void addChannelDatapoints(HmChannel channel, HmParamsetType paramsetType) throws IOException {
219 if (isConfigurationChannel(channel) && paramsetType != HmParamsetType.MASTER) {
220 // The configuration channel only has a MASTER Paramset, so there is nothing to load
223 RpcRequest<T> request = createRpcRequest("getParamsetDescription");
224 request.addArg(getRpcAddress(channel.getDevice().getAddress()) + getChannelSuffix(channel));
225 request.addArg(paramsetType.toString());
226 new GetParamsetDescriptionParser(channel, paramsetType).parse(sendMessage(config.getRpcPort(channel), request));
230 * Sets all datapoint values for the given channel.
232 public void setChannelDatapointValues(HmChannel channel, HmParamsetType paramsetType) throws IOException {
233 if (isConfigurationChannel(channel) && paramsetType != HmParamsetType.MASTER) {
234 // The configuration channel only has a MASTER Paramset, so there is nothing to load
238 RpcRequest<T> request = createRpcRequest("getParamset");
239 request.addArg(getRpcAddress(channel.getDevice().getAddress()) + getChannelSuffix(channel));
240 request.addArg(paramsetType.toString());
241 if (channel.getDevice().getHmInterface() == HmInterface.CUXD && paramsetType == HmParamsetType.VALUES) {
242 setChannelDatapointValues(channel);
245 new GetParamsetParser(channel, paramsetType).parse(sendMessage(config.getRpcPort(channel), request));
246 } catch (UnknownRpcFailureException ex) {
247 if (paramsetType == HmParamsetType.VALUES) {
249 "RpcResponse unknown RPC failure (-1 Failure), fetching values with another API method for device: {}, channel: {}, paramset: {}",
250 channel.getDevice().getAddress(), channel.getNumber(), paramsetType);
251 setChannelDatapointValues(channel);
260 * Reads all VALUES datapoints individually, fallback method if setChannelDatapointValues throws a -1 Failure
263 private void setChannelDatapointValues(HmChannel channel) throws IOException {
264 for (HmDatapoint dp : channel.getDatapoints()) {
265 getDatapointValue(dp);
270 * Tries to identify the gateway and returns the GatewayInfo.
272 public HmGatewayInfo getGatewayInfo(String id) throws IOException {
273 boolean isHomegear = false;
274 GetDeviceDescriptionParser ddParser;
275 ListBidcosInterfacesParser biParser;
278 ddParser = getDeviceDescription(HmInterface.RF);
279 isHomegear = "Homegear".equalsIgnoreCase(ddParser.getType());
280 } catch (IOException ex) {
281 // can't load gateway infos via RF interface
282 ddParser = new GetDeviceDescriptionParser();
286 biParser = listBidcosInterfaces(HmInterface.RF);
287 } catch (IOException ex) {
288 biParser = listBidcosInterfaces(HmInterface.HMIP);
291 HmGatewayInfo gatewayInfo = new HmGatewayInfo();
292 gatewayInfo.setAddress(biParser.getGatewayAddress());
293 String gwType = biParser.getType();
295 gatewayInfo.setId(HmGatewayInfo.ID_HOMEGEAR);
296 gatewayInfo.setType(ddParser.getType());
297 gatewayInfo.setFirmware(ddParser.getFirmware());
298 } else if ((MiscUtils.strStartsWithIgnoreCase(gwType, "CCU")
299 || MiscUtils.strStartsWithIgnoreCase(gwType, "HMIP_CCU")
300 || MiscUtils.strStartsWithIgnoreCase(ddParser.getType(), "HM-RCV-50") || config.isCCUType())
301 && !config.isNoCCUType()) {
302 gatewayInfo.setId(HmGatewayInfo.ID_CCU);
303 String type = gwType.isBlank() ? "CCU" : gwType;
304 gatewayInfo.setType(type);
306 .setFirmware(!ddParser.getFirmware().isEmpty() ? ddParser.getFirmware() : biParser.getFirmware());
308 gatewayInfo.setId(HmGatewayInfo.ID_DEFAULT);
309 gatewayInfo.setType(gwType);
310 gatewayInfo.setFirmware(biParser.getFirmware());
313 if (gatewayInfo.isCCU() || config.hasRfPort()) {
314 gatewayInfo.setRfInterface(hasInterface(HmInterface.RF, id));
317 if (gatewayInfo.isCCU() || config.hasWiredPort()) {
318 gatewayInfo.setWiredInterface(hasInterface(HmInterface.WIRED, id));
321 if (gatewayInfo.isCCU() || config.hasHmIpPort()) {
322 gatewayInfo.setHmipInterface(hasInterface(HmInterface.HMIP, id));
325 if (gatewayInfo.isCCU() || config.hasCuxdPort()) {
326 gatewayInfo.setCuxdInterface(hasInterface(HmInterface.CUXD, id));
329 if (gatewayInfo.isCCU() || config.hasGroupPort()) {
330 gatewayInfo.setGroupInterface(hasInterface(HmInterface.GROUP, id));
337 * Returns true, if a connection is possible with the given interface.
339 private boolean hasInterface(HmInterface hmInterface, String id) throws IOException {
341 checkInterface(hmInterface);
343 } catch (IOException ex) {
344 logger.info("Interface '{}' on gateway '{}' not available, disabling support", hmInterface, id);
350 * Sets the value of the datapoint using the provided rx transmission mode.
352 * @param dp The datapoint to set
353 * @param value The new value to set on the datapoint
354 * @param rxMode The rx mode to use for the transmission of the datapoint value
355 * ({@link HomematicBindingConstants#RX_BURST_MODE "BURST"} for burst mode,
356 * {@link HomematicBindingConstants#RX_WAKEUP_MODE "WAKEUP"} for wakeup mode, or null for the default
359 public void setDatapointValue(HmDatapoint dp, Object value, String rxMode) throws IOException {
360 if (dp.isIntegerType() && value instanceof Double) {
361 value = ((Number) value).intValue();
364 RpcRequest<T> request;
365 if (HmParamsetType.VALUES == dp.getParamsetType()) {
366 request = createRpcRequest("setValue");
367 request.addArg(getRpcAddress(dp.getChannel().getDevice().getAddress()) + getChannelSuffix(dp.getChannel()));
368 request.addArg(dp.getName());
369 request.addArg(value);
370 configureRxMode(request, rxMode);
372 request = createRpcRequest("putParamset");
373 request.addArg(getRpcAddress(dp.getChannel().getDevice().getAddress()) + getChannelSuffix(dp.getChannel()));
374 request.addArg(HmParamsetType.MASTER.toString());
375 Map<String, Object> paramSet = new HashMap<>();
376 paramSet.put(dp.getName(), value);
377 request.addArg(paramSet);
378 configureRxMode(request, rxMode);
380 sendMessage(config.getRpcPort(dp.getChannel()), request);
383 protected void configureRxMode(RpcRequest<T> request, String rxMode) {
384 if (rxMode != null) {
385 if (RX_BURST_MODE.equals(rxMode) || RX_WAKEUP_MODE.equals(rxMode)) {
386 request.addArg(rxMode);
392 * Retrieves the value of a single {@link HmDatapoint} from the device. Can only be used for the paramset "VALUES".
394 * @param dp The HmDatapoint that shall be loaded
395 * @throws IOException If there is a problem while communicating to the gateway
397 public void getDatapointValue(HmDatapoint dp) throws IOException {
398 if (dp.isReadable() && !dp.isVirtual() && dp.getParamsetType() == HmParamsetType.VALUES) {
399 RpcRequest<T> request = createRpcRequest("getValue");
400 request.addArg(getRpcAddress(dp.getChannel().getDevice().getAddress()) + getChannelSuffix(dp.getChannel()));
401 request.addArg(dp.getName());
402 new GetValueParser(dp).parse(sendMessage(config.getRpcPort(dp.getChannel()), request));
407 * Sets the value of a system variable on a Homegear gateway.
409 public void setSystemVariable(HmDatapoint dp, Object value) throws IOException {
410 RpcRequest<T> request = createRpcRequest("setSystemVariable");
411 request.addArg(dp.getInfo());
412 request.addArg(value);
413 sendMessage(config.getRpcPort(dp.getChannel()), request);
417 * Executes a script on the Homegear gateway.
419 public void executeScript(HmDatapoint dp) throws IOException {
420 RpcRequest<T> request = createRpcRequest("runScript");
421 request.addArg(dp.getInfo());
422 sendMessage(config.getRpcPort(dp.getChannel()), request);
426 * Enables/disables the install mode for given seconds.
428 * @param hmInterface specifies the interface to enable / disable install mode on
429 * @param enable if <i>true</i> it will be enabled, otherwise disabled
430 * @param seconds desired duration of install mode
431 * @throws IOException if RpcClient fails to propagate command
433 public void setInstallMode(HmInterface hmInterface, boolean enable, int seconds) throws IOException {
434 RpcRequest<T> request = createRpcRequest("setInstallMode");
435 request.addArg(enable);
436 request.addArg(seconds);
437 request.addArg(INSTALL_MODE_NORMAL);
438 logger.debug("Submitting setInstallMode(on={}, time={}, mode={}) ", enable, seconds, INSTALL_MODE_NORMAL);
439 sendMessage(config.getRpcPort(hmInterface), request);
443 * Returns the remaining time of <i>install_mode==true</i>
445 * @param hmInterface specifies the interface on which install mode status is requested
446 * @return current duration in seconds that the controller will remain in install mode,
447 * value of 0 means that the install mode is disabled
448 * @throws IOException if RpcClient fails to propagate command
450 public int getInstallMode(HmInterface hmInterface) throws IOException {
451 RpcRequest<T> request = createRpcRequest("getInstallMode");
452 Object[] result = sendMessage(config.getRpcPort(hmInterface), request);
453 if (logger.isTraceEnabled()) {
455 "Checking InstallMode: getInstallMode() request returned {} (remaining seconds in InstallMode=true)",
459 return (int) result[0];
460 } catch (Exception cause) {
461 IOException wrappedException = new IOException(
462 "Failed to request install mode from interface " + hmInterface);
463 wrappedException.initCause(cause);
464 throw wrappedException;
469 * Deletes the device from the gateway.
471 public void deleteDevice(HmDevice device, int flags) throws IOException {
472 RpcRequest<T> request = createRpcRequest("deleteDevice");
473 request.addArg(device.getAddress());
474 request.addArg(flags);
475 sendMessage(config.getRpcPort(device.getHmInterface()), request);
479 * Returns the rpc address from a device address, correctly handling groups.
481 private String getRpcAddress(String address) {
482 if (address != null && address.startsWith("T-")) {
483 address = "*" + address.substring(2);
489 * Returns the rssi values for all devices.
491 public List<HmRssiInfo> loadRssiInfo(HmInterface hmInterface) throws IOException {
492 RpcRequest<T> request = createRpcRequest("rssiInfo");
493 return new RssiInfoParser(config).parse(sendMessage(config.getRpcPort(hmInterface), request));
497 * Returns the address suffix that specifies the channel for a given HmChannel. This is either a colon ":" followed
498 * by the channel number, or the empty string for a configuration channel.
500 private String getChannelSuffix(HmChannel channel) {
501 return isConfigurationChannel(channel) ? "" : ":" + channel.getNumber();
505 * Checks whether a channel is a configuration channel. The configuration channel of a device encapsulates the
506 * MASTER Paramset that does not belong to one of its actual channels.
508 private boolean isConfigurationChannel(HmChannel channel) {
509 return channel.getNumber() == CONFIGURATION_CHANNEL_NUMBER;