2 * Copyright (c) 2010-2021 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;
23 import org.openhab.binding.homematic.internal.HomematicBindingConstants;
24 import org.openhab.binding.homematic.internal.common.HomematicConfig;
25 import org.openhab.binding.homematic.internal.communicator.message.RpcRequest;
26 import org.openhab.binding.homematic.internal.communicator.parser.GetAllScriptsParser;
27 import org.openhab.binding.homematic.internal.communicator.parser.GetAllSystemVariablesParser;
28 import org.openhab.binding.homematic.internal.communicator.parser.GetDeviceDescriptionParser;
29 import org.openhab.binding.homematic.internal.communicator.parser.GetParamsetDescriptionParser;
30 import org.openhab.binding.homematic.internal.communicator.parser.GetParamsetParser;
31 import org.openhab.binding.homematic.internal.communicator.parser.GetValueParser;
32 import org.openhab.binding.homematic.internal.communicator.parser.HomegearLoadDeviceNamesParser;
33 import org.openhab.binding.homematic.internal.communicator.parser.ListBidcosInterfacesParser;
34 import org.openhab.binding.homematic.internal.communicator.parser.ListDevicesParser;
35 import org.openhab.binding.homematic.internal.communicator.parser.RssiInfoParser;
36 import org.openhab.binding.homematic.internal.misc.MiscUtils;
37 import org.openhab.binding.homematic.internal.model.HmChannel;
38 import org.openhab.binding.homematic.internal.model.HmDatapoint;
39 import org.openhab.binding.homematic.internal.model.HmDevice;
40 import org.openhab.binding.homematic.internal.model.HmGatewayInfo;
41 import org.openhab.binding.homematic.internal.model.HmInterface;
42 import org.openhab.binding.homematic.internal.model.HmParamsetType;
43 import org.openhab.binding.homematic.internal.model.HmRssiInfo;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
48 * Client implementation for sending messages via BIN-RPC to a Homematic gateway.
50 * @author Gerhard Riegler - Initial contribution
52 public abstract class RpcClient<T> {
53 private final Logger logger = LoggerFactory.getLogger(RpcClient.class);
54 protected static final int MAX_RPC_RETRY = 3;
55 protected static final int RESP_BUFFER_SIZE = 8192;
57 protected HomematicConfig config;
59 public RpcClient(HomematicConfig config) {
64 * Disposes the client.
66 public abstract void dispose();
69 * Returns a RpcRequest for this client.
71 protected abstract RpcRequest<T> createRpcRequest(String methodName);
74 * Returns the callback url for this client.
76 protected abstract String getRpcCallbackUrl();
79 * Sends the RPC message to the gateway.
81 protected abstract Object[] sendMessage(int port, RpcRequest<T> request) throws IOException;
84 * Register a callback for the specified interface where the Homematic gateway can send its events.
86 public void init(HmInterface hmInterface, String clientId) throws IOException {
87 RpcRequest<T> request = createRpcRequest("init");
88 request.addArg(getRpcCallbackUrl());
89 request.addArg(clientId);
90 if (config.getGatewayInfo().isHomegear()) {
91 request.addArg(Integer.valueOf(0x22));
93 sendMessage(config.getRpcPort(hmInterface), request);
97 * Release a callback for the specified interface.
99 public void release(HmInterface hmInterface) throws IOException {
100 RpcRequest<T> request = createRpcRequest("init");
101 request.addArg(getRpcCallbackUrl());
102 sendMessage(config.getRpcPort(hmInterface), request);
106 * Sends a ping to the specified interface.
108 public void ping(HmInterface hmInterface, String callerId) throws IOException {
109 RpcRequest<T> request = createRpcRequest("ping");
110 request.addArg(callerId);
111 sendMessage(config.getRpcPort(hmInterface), request);
115 * Returns the info of all BidCos interfaces available on the gateway.
117 public ListBidcosInterfacesParser listBidcosInterfaces(HmInterface hmInterface) throws IOException {
118 RpcRequest<T> request = createRpcRequest("listBidcosInterfaces");
119 return new ListBidcosInterfacesParser().parse(sendMessage(config.getRpcPort(hmInterface), request));
123 * Returns some infos of the gateway.
125 private GetDeviceDescriptionParser getDeviceDescription(HmInterface hmInterface) throws IOException {
126 RpcRequest<T> request = createRpcRequest("getDeviceDescription");
127 request.addArg("BidCoS-RF");
128 return new GetDeviceDescriptionParser().parse(sendMessage(config.getRpcPort(hmInterface), request));
132 * Returns all variable metadata and values from a Homegear gateway.
134 public void getAllSystemVariables(HmChannel channel) throws IOException {
135 RpcRequest<T> request = createRpcRequest("getAllSystemVariables");
136 new GetAllSystemVariablesParser(channel).parse(sendMessage(config.getRpcPort(channel), request));
140 * Loads all device names from a Homegear gateway.
142 public void loadDeviceNames(HmInterface hmInterface, Collection<HmDevice> devices) throws IOException {
143 RpcRequest<T> request = createRpcRequest("getDeviceInfo");
144 new HomegearLoadDeviceNamesParser(devices).parse(sendMessage(config.getRpcPort(hmInterface), request));
148 * Returns true, if the interface is available on the gateway.
150 public void checkInterface(HmInterface hmInterface) throws IOException {
151 RpcRequest<T> request = createRpcRequest("init");
152 request.addArg("http://openhab.validation:1000");
153 sendMessage(config.getRpcPort(hmInterface), request);
157 * Returns all script metadata from a Homegear gateway.
159 public void getAllScripts(HmChannel channel) throws IOException {
160 RpcRequest<T> request = createRpcRequest("getAllScripts");
161 new GetAllScriptsParser(channel).parse(sendMessage(config.getRpcPort(channel), request));
165 * Returns all device and channel metadata.
167 public Collection<HmDevice> listDevices(HmInterface hmInterface) throws IOException {
168 RpcRequest<T> request = createRpcRequest("listDevices");
169 return new ListDevicesParser(hmInterface, config).parse(sendMessage(config.getRpcPort(hmInterface), request));
173 * Loads all datapoint metadata into the given channel.
175 public void addChannelDatapoints(HmChannel channel, HmParamsetType paramsetType) throws IOException {
176 if (isConfigurationChannel(channel) && paramsetType != HmParamsetType.MASTER) {
177 // The configuration channel only has a MASTER Paramset, so there is nothing to load
180 RpcRequest<T> request = createRpcRequest("getParamsetDescription");
181 request.addArg(getRpcAddress(channel.getDevice().getAddress()) + getChannelSuffix(channel));
182 request.addArg(paramsetType.toString());
183 new GetParamsetDescriptionParser(channel, paramsetType).parse(sendMessage(config.getRpcPort(channel), request));
187 * Sets all datapoint values for the given channel.
189 public void setChannelDatapointValues(HmChannel channel, HmParamsetType paramsetType) throws IOException {
190 if (isConfigurationChannel(channel) && paramsetType != HmParamsetType.MASTER) {
191 // The configuration channel only has a MASTER Paramset, so there is nothing to load
195 RpcRequest<T> request = createRpcRequest("getParamset");
196 request.addArg(getRpcAddress(channel.getDevice().getAddress()) + getChannelSuffix(channel));
197 request.addArg(paramsetType.toString());
198 if (channel.getDevice().getHmInterface() == HmInterface.CUXD && paramsetType == HmParamsetType.VALUES) {
199 setChannelDatapointValues(channel);
202 new GetParamsetParser(channel, paramsetType).parse(sendMessage(config.getRpcPort(channel), request));
203 } catch (UnknownRpcFailureException ex) {
204 if (paramsetType == HmParamsetType.VALUES) {
206 "RpcResponse unknown RPC failure (-1 Failure), fetching values with another API method for device: {}, channel: {}, paramset: {}",
207 channel.getDevice().getAddress(), channel.getNumber(), paramsetType);
208 setChannelDatapointValues(channel);
217 * Reads all VALUES datapoints individually, fallback method if setChannelDatapointValues throws a -1 Failure
220 private void setChannelDatapointValues(HmChannel channel) throws IOException {
221 for (HmDatapoint dp : channel.getDatapoints()) {
222 getDatapointValue(dp);
227 * Tries to identify the gateway and returns the GatewayInfo.
229 public HmGatewayInfo getGatewayInfo(String id) throws IOException {
230 boolean isHomegear = false;
231 GetDeviceDescriptionParser ddParser;
232 ListBidcosInterfacesParser biParser;
235 ddParser = getDeviceDescription(HmInterface.RF);
236 isHomegear = "Homegear".equalsIgnoreCase(ddParser.getType());
237 } catch (IOException ex) {
238 // can't load gateway infos via RF interface
239 ddParser = new GetDeviceDescriptionParser();
243 biParser = listBidcosInterfaces(HmInterface.RF);
244 } catch (IOException ex) {
245 biParser = listBidcosInterfaces(HmInterface.HMIP);
248 HmGatewayInfo gatewayInfo = new HmGatewayInfo();
249 gatewayInfo.setAddress(biParser.getGatewayAddress());
250 String gwType = biParser.getType();
252 gatewayInfo.setId(HmGatewayInfo.ID_HOMEGEAR);
253 gatewayInfo.setType(ddParser.getType());
254 gatewayInfo.setFirmware(ddParser.getFirmware());
255 } else if ((MiscUtils.strStartsWithIgnoreCase(gwType, "CCU")
256 || MiscUtils.strStartsWithIgnoreCase(gwType, "HMIP_CCU")
257 || MiscUtils.strStartsWithIgnoreCase(ddParser.getType(), "HM-RCV-50") || config.isCCUType())
258 && !config.isNoCCUType()) {
259 gatewayInfo.setId(HmGatewayInfo.ID_CCU);
260 String type = gwType.isBlank() ? "CCU" : gwType;
261 gatewayInfo.setType(type);
263 .setFirmware(!ddParser.getFirmware().isEmpty() ? ddParser.getFirmware() : biParser.getFirmware());
265 gatewayInfo.setId(HmGatewayInfo.ID_DEFAULT);
266 gatewayInfo.setType(gwType);
267 gatewayInfo.setFirmware(biParser.getFirmware());
270 if (gatewayInfo.isCCU() || config.hasRfPort()) {
271 gatewayInfo.setRfInterface(hasInterface(HmInterface.RF, id));
274 if (gatewayInfo.isCCU() || config.hasWiredPort()) {
275 gatewayInfo.setWiredInterface(hasInterface(HmInterface.WIRED, id));
278 if (gatewayInfo.isCCU() || config.hasHmIpPort()) {
279 gatewayInfo.setHmipInterface(hasInterface(HmInterface.HMIP, id));
282 if (gatewayInfo.isCCU() || config.hasCuxdPort()) {
283 gatewayInfo.setCuxdInterface(hasInterface(HmInterface.CUXD, id));
286 if (gatewayInfo.isCCU() || config.hasGroupPort()) {
287 gatewayInfo.setGroupInterface(hasInterface(HmInterface.GROUP, id));
294 * Returns true, if a connection is possible with the given interface.
296 private boolean hasInterface(HmInterface hmInterface, String id) throws IOException {
298 checkInterface(hmInterface);
300 } catch (IOException ex) {
301 logger.info("Interface '{}' on gateway '{}' not available, disabling support", hmInterface, id);
307 * Sets the value of the datapoint using the provided rx transmission mode.
309 * @param dp The datapoint to set
310 * @param value The new value to set on the datapoint
311 * @param rxMode The rx mode to use for the transmission of the datapoint value
312 * ({@link HomematicBindingConstants#RX_BURST_MODE "BURST"} for burst mode,
313 * {@link HomematicBindingConstants#RX_WAKEUP_MODE "WAKEUP"} for wakeup mode, or null for the default
316 public void setDatapointValue(HmDatapoint dp, Object value, String rxMode) throws IOException {
317 if (dp.isIntegerType() && value instanceof Double) {
318 value = ((Number) value).intValue();
321 RpcRequest<T> request;
322 if (HmParamsetType.VALUES == dp.getParamsetType()) {
323 request = createRpcRequest("setValue");
324 request.addArg(getRpcAddress(dp.getChannel().getDevice().getAddress()) + getChannelSuffix(dp.getChannel()));
325 request.addArg(dp.getName());
326 request.addArg(value);
327 configureRxMode(request, rxMode);
329 request = createRpcRequest("putParamset");
330 request.addArg(getRpcAddress(dp.getChannel().getDevice().getAddress()) + getChannelSuffix(dp.getChannel()));
331 request.addArg(HmParamsetType.MASTER.toString());
332 Map<String, Object> paramSet = new HashMap<>();
333 paramSet.put(dp.getName(), value);
334 request.addArg(paramSet);
335 configureRxMode(request, rxMode);
337 sendMessage(config.getRpcPort(dp.getChannel()), request);
340 protected void configureRxMode(RpcRequest<T> request, String rxMode) {
341 if (rxMode != null) {
342 if (RX_BURST_MODE.equals(rxMode) || RX_WAKEUP_MODE.equals(rxMode)) {
343 request.addArg(rxMode);
349 * Retrieves the value of a single {@link HmDatapoint} from the device. Can only be used for the paramset "VALUES".
351 * @param dp The HmDatapoint that shall be loaded
352 * @throws IOException If there is a problem while communicating to the gateway
354 public void getDatapointValue(HmDatapoint dp) throws IOException {
355 if (dp.isReadable() && !dp.isVirtual() && dp.getParamsetType() == HmParamsetType.VALUES) {
356 RpcRequest<T> request = createRpcRequest("getValue");
357 request.addArg(getRpcAddress(dp.getChannel().getDevice().getAddress()) + getChannelSuffix(dp.getChannel()));
358 request.addArg(dp.getName());
359 new GetValueParser(dp).parse(sendMessage(config.getRpcPort(dp.getChannel()), request));
364 * Sets the value of a system variable on a Homegear gateway.
366 public void setSystemVariable(HmDatapoint dp, Object value) throws IOException {
367 RpcRequest<T> request = createRpcRequest("setSystemVariable");
368 request.addArg(dp.getInfo());
369 request.addArg(value);
370 sendMessage(config.getRpcPort(dp.getChannel()), request);
374 * Executes a script on the Homegear gateway.
376 public void executeScript(HmDatapoint dp) throws IOException {
377 RpcRequest<T> request = createRpcRequest("runScript");
378 request.addArg(dp.getInfo());
379 sendMessage(config.getRpcPort(dp.getChannel()), request);
383 * Enables/disables the install mode for given seconds.
385 * @param hmInterface specifies the interface to enable / disable install mode on
386 * @param enable if <i>true</i> it will be enabled, otherwise disabled
387 * @param seconds desired duration of install mode
388 * @throws IOException if RpcClient fails to propagate command
390 public void setInstallMode(HmInterface hmInterface, boolean enable, int seconds) throws IOException {
391 RpcRequest<T> request = createRpcRequest("setInstallMode");
392 request.addArg(enable);
393 request.addArg(seconds);
394 request.addArg(INSTALL_MODE_NORMAL);
395 logger.debug("Submitting setInstallMode(on={}, time={}, mode={}) ", enable, seconds, INSTALL_MODE_NORMAL);
396 sendMessage(config.getRpcPort(hmInterface), request);
400 * Returns the remaining time of <i>install_mode==true</i>
402 * @param hmInterface specifies the interface on which install mode status is requested
403 * @return current duration in seconds that the controller will remain in install mode,
404 * value of 0 means that the install mode is disabled
405 * @throws IOException if RpcClient fails to propagate command
407 public int getInstallMode(HmInterface hmInterface) throws IOException {
408 RpcRequest<T> request = createRpcRequest("getInstallMode");
409 Object[] result = sendMessage(config.getRpcPort(hmInterface), request);
410 if (logger.isTraceEnabled()) {
412 "Checking InstallMode: getInstallMode() request returned {} (remaining seconds in InstallMode=true)",
416 return (int) result[0];
417 } catch (Exception cause) {
418 IOException wrappedException = new IOException(
419 "Failed to request install mode from interface " + hmInterface);
420 wrappedException.initCause(cause);
421 throw wrappedException;
426 * Deletes the device from the gateway.
428 public void deleteDevice(HmDevice device, int flags) throws IOException {
429 RpcRequest<T> request = createRpcRequest("deleteDevice");
430 request.addArg(device.getAddress());
431 request.addArg(flags);
432 sendMessage(config.getRpcPort(device.getHmInterface()), request);
436 * Returns the rpc address from a device address, correctly handling groups.
438 private String getRpcAddress(String address) {
439 if (address != null && address.startsWith("T-")) {
440 address = "*" + address.substring(2);
446 * Returns the rssi values for all devices.
448 public List<HmRssiInfo> loadRssiInfo(HmInterface hmInterface) throws IOException {
449 RpcRequest<T> request = createRpcRequest("rssiInfo");
450 return new RssiInfoParser(config).parse(sendMessage(config.getRpcPort(hmInterface), request));
454 * Returns the address suffix that specifies the channel for a given HmChannel. This is either a colon ":" followed
455 * by the channel number, or the empty string for a configuration channel.
457 private String getChannelSuffix(HmChannel channel) {
458 return isConfigurationChannel(channel) ? "" : ":" + channel.getNumber();
462 * Checks whether a channel is a configuration channel. The configuration channel of a device encapsulates the
463 * MASTER Paramset that does not belong to one of its actual channels.
465 private boolean isConfigurationChannel(HmChannel channel) {
466 return channel.getNumber() == CONFIGURATION_CHANNEL_NUMBER;