]> git.basschouten.com Git - openhab-addons.git/blob
a94f38d2d4490574ee26d59dfcb68f5ec512e728
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.homematic.internal.communicator.message;
14
15 import java.io.EOFException;
16 import java.io.IOException;
17 import java.io.InputStream;
18 import java.io.UnsupportedEncodingException;
19 import java.math.BigDecimal;
20 import java.math.BigInteger;
21 import java.math.RoundingMode;
22 import java.text.ParseException;
23 import java.util.ArrayList;
24 import java.util.Arrays;
25 import java.util.Collection;
26 import java.util.Collections;
27 import java.util.Date;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.TreeMap;
31
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
34
35 /**
36  * Handles BIN-RPC request and response messages to communicate with a Homematic gateway.
37  *
38  * @author Gerhard Riegler - Initial contribution
39  */
40 public class BinRpcMessage implements RpcRequest<byte[]>, RpcResponse {
41     private final Logger logger = LoggerFactory.getLogger(BinRpcMessage.class);
42
43     public enum TYPE {
44         REQUEST,
45         RESPONSE
46     }
47
48     private Object[] messageData;
49     private byte binRpcData[];
50     private int offset;
51
52     private String methodName;
53     private TYPE type;
54     private int args;
55     private String encoding;
56
57     public BinRpcMessage(String methodName, String encoding) {
58         this(methodName, TYPE.REQUEST, encoding);
59     }
60
61     /**
62      * Creates a new request with the specified methodName.
63      */
64     public BinRpcMessage(String methodName, TYPE type, String encoding) {
65         this.methodName = methodName;
66         this.type = type;
67         this.encoding = encoding;
68         createHeader();
69     }
70
71     /**
72      * Decodes a BIN-RPC message from the given InputStream.
73      */
74     public BinRpcMessage(InputStream is, boolean methodHeader, String encoding) throws IOException {
75         this.encoding = encoding;
76         byte sig[] = new byte[8];
77         int length = is.read(sig, 0, 4);
78         if (length != 4) {
79             throw new EOFException("Only " + length + " bytes received reading signature");
80         }
81         validateBinXSignature(sig);
82         length = is.read(sig, 4, 4);
83         if (length != 4) {
84             throw new EOFException("Only " + length + " bytes received reading message length");
85         }
86         int datasize = (new BigInteger(Arrays.copyOfRange(sig, 4, 8))).intValue();
87         byte payload[] = new byte[datasize];
88         int offset = 0;
89         int currentLength;
90
91         while (offset < datasize && (currentLength = is.read(payload, offset, datasize - offset)) != -1) {
92             offset += currentLength;
93         }
94         if (offset != datasize) {
95             throw new EOFException("Only " + offset + " bytes received while reading message payload, expected "
96                     + datasize + " bytes");
97         }
98         byte[] message = new byte[sig.length + payload.length];
99         System.arraycopy(sig, 0, message, 0, sig.length);
100         System.arraycopy(payload, 0, message, sig.length, payload.length);
101
102         decodeMessage(message, methodHeader);
103     }
104
105     private void validateBinXSignature(byte[] sig) throws UnsupportedEncodingException {
106         if (sig[0] != 'B' || sig[1] != 'i' || sig[2] != 'n') {
107             throw new UnsupportedEncodingException("No BinX signature");
108         }
109     }
110
111     /**
112      * Decodes a BIN-RPC message from the given byte array.
113      */
114     public BinRpcMessage(byte[] message, boolean methodHeader, String encoding) throws IOException, ParseException {
115         this.encoding = encoding;
116         if (message.length < 8) {
117             throw new EOFException("Only " + message.length + " bytes received");
118         }
119         validateBinXSignature(message);
120         decodeMessage(message, methodHeader);
121     }
122
123     private void decodeMessage(byte[] message, boolean methodHeader) throws IOException {
124         binRpcData = message;
125
126         offset = 8;
127
128         if (methodHeader) {
129             methodName = readString();
130             readInt();
131         }
132         generateResponseData();
133     }
134
135     public void setType(TYPE type) {
136         binRpcData[3] = type == TYPE.RESPONSE ? (byte) 1 : (byte) 0;
137     }
138
139     private void generateResponseData() throws IOException {
140         offset = 8 + (methodName != null ? methodName.length() + 8 : 0);
141         List<Object> values = new ArrayList<>();
142         while (offset < binRpcData.length) {
143             values.add(readRpcValue());
144         }
145         messageData = values.toArray();
146         values.clear();
147     }
148
149     private void createHeader() {
150         binRpcData = new byte[256];
151         addString("Bin ");
152         setType(type);
153         addInt(0); // placeholder content length
154         if (methodName != null) {
155             addInt(methodName.length());
156             addString(methodName);
157             addInt(0); // placeholder arguments
158         }
159         setInt(4, offset - 8);
160     }
161
162     /**
163      * Adds arguments to the method.
164      */
165     @Override
166     public void addArg(Object argument) {
167         addObject(argument);
168         setInt(4, offset - 8);
169
170         if (methodName != null) {
171             setInt(12 + methodName.length(), ++args);
172         }
173     }
174
175     public int getArgCount() {
176         return args;
177     }
178
179     @Override
180     public String getMethodName() {
181         return methodName;
182     }
183
184     @Override
185     public byte[] createMessage() {
186         trimBinRpcData();
187         return binRpcData;
188     }
189
190     private void trimBinRpcData() {
191         byte[] trimmed = new byte[offset];
192         System.arraycopy(binRpcData, 0, trimmed, 0, offset);
193         binRpcData = trimmed;
194     }
195
196     @Override
197     public Object[] getResponseData() {
198         return messageData;
199     }
200
201     // read rpc values
202     private int readInt() {
203         byte bi[] = new byte[4];
204         System.arraycopy(binRpcData, offset, bi, 0, 4);
205         offset += 4;
206         return (new BigInteger(bi)).intValue();
207     }
208
209     private long readInt64() {
210         byte bi[] = new byte[8];
211         System.arraycopy(binRpcData, offset, bi, 0, 8);
212         offset += 8;
213         return (new BigInteger(bi)).longValue();
214     }
215
216     private String readString() throws UnsupportedEncodingException {
217         int len = readInt();
218         offset += len;
219         return new String(binRpcData, offset - len, len, encoding);
220     }
221
222     private Object readRpcValue() throws IOException {
223         int type = readInt();
224         switch (type) {
225             case 1:
226                 return Integer.valueOf(readInt());
227             case 2:
228                 return binRpcData[offset++] != 0 ? Boolean.TRUE : Boolean.FALSE;
229             case 3:
230                 return readString();
231             case 4:
232                 int mantissa = readInt();
233                 int exponent = readInt();
234                 BigDecimal bd = new BigDecimal((double) mantissa / (double) (1 << 30) * Math.pow(2, exponent));
235                 return bd.setScale(6, RoundingMode.HALF_DOWN).doubleValue();
236             case 5:
237                 return new Date(readInt() * 1000);
238             case 0xD1:
239                 // Int64
240                 return Long.valueOf(readInt64());
241             case 0x100:
242                 // Array
243                 int numElements = readInt();
244                 Collection<Object> array = new ArrayList<>();
245                 while (numElements-- > 0) {
246                     array.add(readRpcValue());
247                 }
248                 return array.toArray();
249             case 0x101:
250                 // Struct
251                 numElements = readInt();
252                 Map<String, Object> struct = new TreeMap<>();
253                 while (numElements-- > 0) {
254                     String name = readString();
255                     struct.put(name, readRpcValue());
256                 }
257                 return struct;
258
259             default:
260                 for (int i = 0; i < binRpcData.length; i++) {
261                     logger.info("{} {}", Integer.toHexString(binRpcData[i]), (char) binRpcData[i]);
262                 }
263                 throw new IOException("Unknown data type " + type);
264         }
265     }
266
267     private void setInt(int position, int value) {
268         int temp = offset;
269         offset = position;
270         addInt(value);
271         offset = temp;
272     }
273
274     private void addByte(byte b) {
275         if (offset == binRpcData.length) {
276             byte newdata[] = new byte[binRpcData.length * 2];
277             System.arraycopy(binRpcData, 0, newdata, 0, binRpcData.length);
278             binRpcData = newdata;
279         }
280         binRpcData[offset++] = b;
281     }
282
283     private void addInt(int value) {
284         addByte((byte) (value >> 24));
285         addByte((byte) (value >> 16));
286         addByte((byte) (value >> 8));
287         addByte((byte) (value));
288     }
289
290     private void addDouble(double value) {
291         double tmp = Math.abs(value);
292         int exp = 0;
293         if (tmp != 0 && tmp < 0.5) {
294             while (tmp < 0.5) {
295                 tmp *= 2;
296                 exp--;
297             }
298         } else {
299             while (tmp >= 1) {
300                 tmp /= 2;
301                 exp++;
302             }
303         }
304         if (value < 0) {
305             tmp *= -1;
306         }
307         int mantissa = (int) Math.round(tmp * 0x40000000);
308         addInt(mantissa);
309         addInt(exp);
310     }
311
312     private void addString(String string) {
313         byte sd[];
314         try {
315             sd = string.getBytes(encoding);
316         } catch (UnsupportedEncodingException use) {
317             sd = string.getBytes();
318         }
319         for (byte ch : sd) {
320             addByte(ch);
321         }
322     }
323
324     private void addList(Collection<?> collection) {
325         for (Object object : collection) {
326             addObject(object);
327         }
328     }
329
330     private void addObject(Object object) {
331         if (object.getClass() == String.class) {
332             addInt(3);
333             String string = (String) object;
334             addInt(string.length());
335             addString(string);
336         } else if (object.getClass() == Boolean.class) {
337             addInt(2);
338             addByte(((Boolean) object).booleanValue() ? (byte) 1 : (byte) 0);
339         } else if (object.getClass() == Integer.class) {
340             addInt(1);
341             addInt(((Integer) object).intValue());
342         } else if (object.getClass() == Double.class) {
343             addInt(4);
344             addDouble(((Double) object).doubleValue());
345         } else if (object.getClass() == Float.class) {
346             addInt(4);
347             BigDecimal bd = new BigDecimal((Float) object);
348             addDouble(bd.setScale(6, RoundingMode.HALF_DOWN).doubleValue());
349         } else if (object.getClass() == BigDecimal.class) {
350             addInt(4);
351             addDouble(((BigDecimal) object).setScale(6, RoundingMode.HALF_DOWN).doubleValue());
352         } else if (object.getClass() == BigInteger.class) {
353             addInt(4);
354             addDouble(((BigInteger) object).doubleValue());
355         } else if (object.getClass() == Date.class) {
356             addInt(5);
357             addInt((int) ((Date) object).getTime() / 1000);
358         } else if (object instanceof List<?>) {
359             Collection<?> list = (Collection<?>) object;
360             addInt(0x100);
361             addInt(list.size());
362             addList(list);
363         } else if (object instanceof Map<?, ?>) {
364             Map<?, ?> map = (Map<?, ?>) object;
365             addInt(0x101);
366             addInt(map.size());
367             for (Map.Entry<?, ?> entry : map.entrySet()) {
368                 String key = (String) entry.getKey();
369                 if (key != null) {
370                     addInt(key.length());
371                     addString(key);
372                     addList(Collections.singleton(entry.getValue()));
373                 }
374             }
375         }
376     }
377
378     public String toBinString() {
379         return Arrays.toString(createMessage());
380     }
381
382     @Override
383     public String toString() {
384         try {
385             trimBinRpcData();
386             generateResponseData();
387             return RpcUtils.dumpRpcMessage(methodName, messageData);
388         } catch (Exception e) {
389             throw new RuntimeException(e.getMessage(), e);
390         }
391     }
392 }