2 * Copyright (c) 2010-2020 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.apache.commons.lang.StringUtils;
24 import org.openhab.binding.homematic.internal.HomematicBindingConstants;
25 import org.openhab.binding.homematic.internal.common.HomematicConfig;
26 import org.openhab.binding.homematic.internal.communicator.message.RpcRequest;
27 import org.openhab.binding.homematic.internal.communicator.parser.GetAllScriptsParser;
28 import org.openhab.binding.homematic.internal.communicator.parser.GetAllSystemVariablesParser;
29 import org.openhab.binding.homematic.internal.communicator.parser.GetDeviceDescriptionParser;
30 import org.openhab.binding.homematic.internal.communicator.parser.GetParamsetDescriptionParser;
31 import org.openhab.binding.homematic.internal.communicator.parser.GetParamsetParser;
32 import org.openhab.binding.homematic.internal.communicator.parser.GetValueParser;
33 import org.openhab.binding.homematic.internal.communicator.parser.HomegearLoadDeviceNamesParser;
34 import org.openhab.binding.homematic.internal.communicator.parser.ListBidcosInterfacesParser;
35 import org.openhab.binding.homematic.internal.communicator.parser.ListDevicesParser;
36 import org.openhab.binding.homematic.internal.communicator.parser.RssiInfoParser;
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(new Integer(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 = StringUtils.equalsIgnoreCase(ddParser.getType(), "Homegear");
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());
251 gatewayInfo.setId(HmGatewayInfo.ID_HOMEGEAR);
252 gatewayInfo.setType(ddParser.getType());
253 gatewayInfo.setFirmware(ddParser.getFirmware());
254 } else if ((StringUtils.startsWithIgnoreCase(biParser.getType(), "CCU")
255 || StringUtils.startsWithIgnoreCase(biParser.getType(), "HMIP_CCU")
256 || StringUtils.startsWithIgnoreCase(ddParser.getType(), "HM-RCV-50") || config.isCCUType())
257 && !config.isNoCCUType()) {
258 gatewayInfo.setId(HmGatewayInfo.ID_CCU);
259 String type = StringUtils.isBlank(biParser.getType()) ? "CCU" : biParser.getType();
260 gatewayInfo.setType(type);
261 gatewayInfo.setFirmware(ddParser.getFirmware() != null ? ddParser.getFirmware() : biParser.getFirmware());
263 gatewayInfo.setId(HmGatewayInfo.ID_DEFAULT);
264 gatewayInfo.setType(biParser.getType());
265 gatewayInfo.setFirmware(biParser.getFirmware());
268 if (gatewayInfo.isCCU() || config.hasRfPort()) {
269 gatewayInfo.setRfInterface(hasInterface(HmInterface.RF, id));
272 if (gatewayInfo.isCCU() || config.hasWiredPort()) {
273 gatewayInfo.setWiredInterface(hasInterface(HmInterface.WIRED, id));
276 if (gatewayInfo.isCCU() || config.hasHmIpPort()) {
277 gatewayInfo.setHmipInterface(hasInterface(HmInterface.HMIP, id));
280 if (gatewayInfo.isCCU() || config.hasCuxdPort()) {
281 gatewayInfo.setCuxdInterface(hasInterface(HmInterface.CUXD, id));
284 if (gatewayInfo.isCCU() || config.hasGroupPort()) {
285 gatewayInfo.setGroupInterface(hasInterface(HmInterface.GROUP, id));
292 * Returns true, if a connection is possible with the given interface.
294 private boolean hasInterface(HmInterface hmInterface, String id) throws IOException {
296 checkInterface(hmInterface);
298 } catch (IOException ex) {
299 logger.info("Interface '{}' on gateway '{}' not available, disabling support", hmInterface, id);
305 * Sets the value of the datapoint using the provided rx transmission mode.
307 * @param dp The datapoint to set
308 * @param value The new value to set on the datapoint
309 * @param rxMode The rx mode to use for the transmission of the datapoint value
310 * ({@link HomematicBindingConstants#RX_BURST_MODE "BURST"} for burst mode,
311 * {@link HomematicBindingConstants#RX_WAKEUP_MODE "WAKEUP"} for wakeup mode, or null for the default
314 public void setDatapointValue(HmDatapoint dp, Object value, String rxMode) throws IOException {
315 if (dp.isIntegerType() && value instanceof Double) {
316 value = ((Number) value).intValue();
319 RpcRequest<T> request;
320 if (HmParamsetType.VALUES == dp.getParamsetType()) {
321 request = createRpcRequest("setValue");
322 request.addArg(getRpcAddress(dp.getChannel().getDevice().getAddress()) + getChannelSuffix(dp.getChannel()));
323 request.addArg(dp.getName());
324 request.addArg(value);
325 configureRxMode(request, rxMode);
327 request = createRpcRequest("putParamset");
328 request.addArg(getRpcAddress(dp.getChannel().getDevice().getAddress()) + getChannelSuffix(dp.getChannel()));
329 request.addArg(HmParamsetType.MASTER.toString());
330 Map<String, Object> paramSet = new HashMap<>();
331 paramSet.put(dp.getName(), value);
332 request.addArg(paramSet);
333 configureRxMode(request, rxMode);
335 sendMessage(config.getRpcPort(dp.getChannel()), request);
338 protected void configureRxMode(RpcRequest<T> request, String rxMode) {
339 if (rxMode != null) {
340 if (RX_BURST_MODE.equals(rxMode) || RX_WAKEUP_MODE.equals(rxMode)) {
341 request.addArg(rxMode);
347 * Retrieves the value of a single {@link HmDatapoint} from the device. Can only be used for the paramset "VALUES".
349 * @param dp The HmDatapoint that shall be loaded
350 * @throws IOException If there is a problem while communicating to the gateway
352 public void getDatapointValue(HmDatapoint dp) throws IOException {
353 if (dp.isReadable() && !dp.isVirtual() && dp.getParamsetType() == HmParamsetType.VALUES) {
354 RpcRequest<T> request = createRpcRequest("getValue");
355 request.addArg(getRpcAddress(dp.getChannel().getDevice().getAddress()) + getChannelSuffix(dp.getChannel()));
356 request.addArg(dp.getName());
357 new GetValueParser(dp).parse(sendMessage(config.getRpcPort(dp.getChannel()), request));
362 * Sets the value of a system variable on a Homegear gateway.
364 public void setSystemVariable(HmDatapoint dp, Object value) throws IOException {
365 RpcRequest<T> request = createRpcRequest("setSystemVariable");
366 request.addArg(dp.getInfo());
367 request.addArg(value);
368 sendMessage(config.getRpcPort(dp.getChannel()), request);
372 * Executes a script on the Homegear gateway.
374 public void executeScript(HmDatapoint dp) throws IOException {
375 RpcRequest<T> request = createRpcRequest("runScript");
376 request.addArg(dp.getInfo());
377 sendMessage(config.getRpcPort(dp.getChannel()), request);
381 * Enables/disables the install mode for given seconds.
383 * @param hmInterface specifies the interface to enable / disable install mode on
384 * @param enable if <i>true</i> it will be enabled, otherwise disabled
385 * @param seconds desired duration of install mode
386 * @throws IOException if RpcClient fails to propagate command
388 public void setInstallMode(HmInterface hmInterface, boolean enable, int seconds) throws IOException {
389 RpcRequest<T> request = createRpcRequest("setInstallMode");
390 request.addArg(enable);
391 request.addArg(seconds);
392 request.addArg(INSTALL_MODE_NORMAL);
393 logger.debug("Submitting setInstallMode(on={}, time={}, mode={}) ", enable, seconds, INSTALL_MODE_NORMAL);
394 sendMessage(config.getRpcPort(hmInterface), request);
398 * Returns the remaining time of <i>install_mode==true</i>
400 * @param hmInterface specifies the interface on which install mode status is requested
401 * @return current duration in seconds that the controller will remain in install mode,
402 * value of 0 means that the install mode is disabled
403 * @throws IOException if RpcClient fails to propagate command
405 public int getInstallMode(HmInterface hmInterface) throws IOException {
406 RpcRequest<T> request = createRpcRequest("getInstallMode");
407 Object[] result = sendMessage(config.getRpcPort(hmInterface), request);
408 if (logger.isTraceEnabled()) {
410 "Checking InstallMode: getInstallMode() request returned {} (remaining seconds in InstallMode=true)",
414 return (int) result[0];
415 } catch (Exception cause) {
416 IOException wrappedException = new IOException(
417 "Failed to request install mode from interface " + hmInterface);
418 wrappedException.initCause(cause);
419 throw wrappedException;
424 * Deletes the device from the gateway.
426 public void deleteDevice(HmDevice device, int flags) throws IOException {
427 RpcRequest<T> request = createRpcRequest("deleteDevice");
428 request.addArg(device.getAddress());
429 request.addArg(flags);
430 sendMessage(config.getRpcPort(device.getHmInterface()), request);
434 * Returns the rpc address from a device address, correctly handling groups.
436 private String getRpcAddress(String address) {
437 if (address != null && address.startsWith("T-")) {
438 address = "*" + address.substring(2);
444 * Returns the rssi values for all devices.
446 public List<HmRssiInfo> loadRssiInfo(HmInterface hmInterface) throws IOException {
447 RpcRequest<T> request = createRpcRequest("rssiInfo");
448 return new RssiInfoParser(config).parse(sendMessage(config.getRpcPort(hmInterface), request));
452 * Returns the address suffix that specifies the channel for a given HmChannel. This is either a colon ":" followed
453 * by the channel number, or the empty string for a configuration channel.
455 private String getChannelSuffix(HmChannel channel) {
456 return isConfigurationChannel(channel) ? "" : ":" + channel.getNumber();
460 * Checks whether a channel is a configuration channel. The configuration channel of a device encapsulates the
461 * MASTER Paramset that does not belong to one of its actual channels.
463 private boolean isConfigurationChannel(HmChannel channel) {
464 return channel.getNumber() == CONFIGURATION_CHANNEL_NUMBER;