2 * Copyright (c) 2010-2024 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;
15 import java.io.IOException;
16 import java.io.InputStream;
17 import java.math.BigDecimal;
18 import java.util.ArrayList;
19 import java.util.Collection;
20 import java.util.HashMap;
22 import java.util.Objects;
23 import java.util.concurrent.TimeUnit;
25 import org.eclipse.jetty.client.HttpClient;
26 import org.eclipse.jetty.client.api.ContentResponse;
27 import org.eclipse.jetty.client.util.StringContentProvider;
28 import org.eclipse.jetty.http.HttpHeader;
29 import org.openhab.binding.homematic.internal.common.HomematicConfig;
30 import org.openhab.binding.homematic.internal.communicator.client.UnknownParameterSetException;
31 import org.openhab.binding.homematic.internal.communicator.client.UnknownRpcFailureException;
32 import org.openhab.binding.homematic.internal.communicator.parser.CcuLoadDeviceNamesParser;
33 import org.openhab.binding.homematic.internal.communicator.parser.CcuParamsetDescriptionParser;
34 import org.openhab.binding.homematic.internal.communicator.parser.CcuValueParser;
35 import org.openhab.binding.homematic.internal.communicator.parser.CcuVariablesAndScriptsParser;
36 import org.openhab.binding.homematic.internal.model.HmChannel;
37 import org.openhab.binding.homematic.internal.model.HmDatapoint;
38 import org.openhab.binding.homematic.internal.model.HmDevice;
39 import org.openhab.binding.homematic.internal.model.HmParamsetType;
40 import org.openhab.binding.homematic.internal.model.HmResult;
41 import org.openhab.binding.homematic.internal.model.TclScript;
42 import org.openhab.binding.homematic.internal.model.TclScriptDataList;
43 import org.openhab.binding.homematic.internal.model.TclScriptList;
44 import org.osgi.framework.Bundle;
45 import org.osgi.framework.FrameworkUtil;
46 import org.slf4j.Logger;
47 import org.slf4j.LoggerFactory;
49 import com.thoughtworks.xstream.XStream;
50 import com.thoughtworks.xstream.io.xml.StaxDriver;
53 * HomematicGateway implementation for a CCU.
55 * @author Gerhard Riegler - Initial contribution
57 public class CcuGateway extends AbstractHomematicGateway {
58 private final Logger logger = LoggerFactory.getLogger(CcuGateway.class);
60 private Map<String, String> tclregaScripts;
61 private XStream xStream = new XStream(new StaxDriver());
63 protected CcuGateway(String id, HomematicConfig config, HomematicGatewayAdapter gatewayAdapter,
64 HttpClient httpClient) {
65 super(id, config, gatewayAdapter, httpClient);
67 xStream.allowTypesByWildcard(new String[] { HmDevice.class.getPackageName() + ".**" });
68 xStream.setClassLoader(CcuGateway.class.getClassLoader());
69 xStream.autodetectAnnotations(true);
70 xStream.alias("scripts", TclScriptList.class);
71 xStream.alias("list", TclScriptDataList.class);
72 xStream.alias("result", HmResult.class);
76 protected void startClients() throws IOException {
79 tclregaScripts = loadTclRegaScripts();
83 protected void stopClients() {
85 tclregaScripts = null;
89 protected void loadVariables(HmChannel channel) throws IOException {
90 TclScriptDataList resultList = sendScriptByName("getAllVariables", TclScriptDataList.class);
91 new CcuVariablesAndScriptsParser(channel).parse(resultList);
95 protected void loadScripts(HmChannel channel) throws IOException {
96 TclScriptDataList resultList = sendScriptByName("getAllPrograms", TclScriptDataList.class);
97 new CcuVariablesAndScriptsParser(channel).parse(resultList);
101 protected void loadDeviceNames(Collection<HmDevice> devices) throws IOException {
102 TclScriptDataList resultList = sendScriptByName("getAllDeviceNames", TclScriptDataList.class);
103 new CcuLoadDeviceNamesParser(devices).parse(resultList);
107 protected void setChannelDatapointValues(HmChannel channel, HmParamsetType paramsetType) throws IOException {
109 super.setChannelDatapointValues(channel, paramsetType);
110 } catch (UnknownRpcFailureException ex) {
112 "RpcMessage unknown RPC failure (-1 Failure), fetching values with TclRega script for device {}, channel: {}, paramset: {}",
113 channel.getDevice().getAddress(), channel.getNumber(), paramsetType);
115 Collection<String> dpNames = new ArrayList<>();
116 for (HmDatapoint dp : channel.getDatapoints()) {
117 if (!dp.isVirtual() && dp.isReadable() && dp.getParamsetType() == HmParamsetType.VALUES) {
118 dpNames.add(dp.getName());
121 if (!dpNames.isEmpty()) {
122 HmDevice device = channel.getDevice();
123 String channelName = String.format("%s.%s:%s.", device.getHmInterface().getName(), device.getAddress(),
124 channel.getNumber());
125 String datapointNames = String.join("\\t", dpNames);
126 TclScriptDataList resultList = sendScriptByName("getAllChannelValues", TclScriptDataList.class,
127 new String[] { "channel_name", "datapoint_names" },
128 new String[] { channelName, datapointNames });
129 new CcuValueParser(channel).parse(resultList);
130 channel.setInitialized(true);
136 protected void addChannelDatapoints(HmChannel channel, HmParamsetType paramsetType) throws IOException {
138 getRpcClient(channel.getDevice().getHmInterface()).addChannelDatapoints(channel, paramsetType);
139 } catch (UnknownParameterSetException ex) {
141 "RpcMessage RPC failure (-3 Unknown paramset), fetching metadata with TclRega script for device: {}, channel: {}, paramset: {}",
142 channel.getDevice().getAddress(), channel.getNumber(), paramsetType);
144 TclScriptDataList resultList = sendScriptByName("getParamsetDescription", TclScriptDataList.class,
145 new String[] { "device_address", "channel_number" },
146 new String[] { channel.getDevice().getAddress(), channel.getNumber().toString() });
147 new CcuParamsetDescriptionParser(channel, paramsetType).parse(resultList);
152 protected void setVariable(HmDatapoint dp, Object value) throws IOException {
154 if (value instanceof Double) {
155 strValue = new BigDecimal((Double) value).stripTrailingZeros().toPlainString();
157 strValue = Objects.toString(value, "").replace("\"", "\\\"");
159 if (dp.isStringType()) {
160 strValue = "\"" + strValue + "\"";
162 HmResult result = sendScriptByName("setVariable", HmResult.class,
163 new String[] { "variable_name", "variable_state" }, new String[] { dp.getInfo(), strValue });
164 if (!result.isValid()) {
165 throw new IOException("Unable to set CCU variable " + dp.getInfo());
170 protected void executeScript(HmDatapoint dp) throws IOException {
171 HmResult result = sendScriptByName("executeProgram", HmResult.class, new String[] { "program_name" },
172 new String[] { dp.getInfo() });
173 if (!result.isValid()) {
174 throw new IOException("Unable to start CCU program: " + dp.getInfo());
179 * Sends a TclRega script to the CCU.
181 private <T> T sendScriptByName(String scriptName, Class<T> clazz) throws IOException {
182 return sendScriptByName(scriptName, clazz, new String[] {}, null);
186 * Sends a TclRega script with the specified variables to the CCU.
188 private <T> T sendScriptByName(String scriptName, Class<T> clazz, String[] variableNames, String[] values)
190 String script = tclregaScripts.get(scriptName);
191 if (script != null) {
192 for (int i = 0; i < variableNames.length; i++) {
193 script = script.replace("{" + variableNames[i] + "}", values[i]);
196 return sendScript(script, clazz);
200 * Main method for sending a TclRega script and parsing the XML result.
202 @SuppressWarnings("unchecked")
203 private synchronized <T> T sendScript(String script, Class<T> clazz) throws IOException {
205 script = script == null ? null : script.trim();
206 if (script == null || script.isEmpty()) {
207 throw new RuntimeException("Homematic TclRegaScript is empty!");
209 if (logger.isTraceEnabled()) {
210 logger.trace("TclRegaScript: {}", script);
213 StringContentProvider content = new StringContentProvider(script, config.getEncoding());
214 ContentResponse response = httpClient.POST(config.getTclRegaUrl()).content(content)
215 .timeout(config.getTimeout(), TimeUnit.SECONDS)
216 .header(HttpHeader.CONTENT_TYPE, "text/plain;charset=" + config.getEncoding()).send();
218 String result = new String(response.getContent(), config.getEncoding());
219 int lastPos = result.lastIndexOf("<xml><exec>");
221 result = result.substring(0, lastPos);
223 if (logger.isTraceEnabled()) {
224 logger.trace("Result TclRegaScript: {}", result);
227 return (T) xStream.fromXML(result);
228 } catch (Exception ex) {
229 throw new IOException(ex.getMessage(), ex);
234 * Load predefined scripts from an XML file.
236 private Map<String, String> loadTclRegaScripts() throws IOException {
237 Bundle bundle = FrameworkUtil.getBundle(getClass());
238 try (InputStream stream = bundle.getResource("homematic/tclrega-scripts.xml").openStream()) {
239 TclScriptList scriptList = (TclScriptList) xStream.fromXML(stream);
240 Map<String, String> result = new HashMap<>();
241 if (scriptList.getScripts() != null) {
242 for (TclScript script : scriptList.getScripts()) {
243 String value = script.data.trim();
244 result.put(script.name, value.isEmpty() ? null : value);
248 } catch (IllegalStateException | IOException e) {
249 throw new IOException("The resource homematic/tclrega-scripts.xml could not be loaded!", e);