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