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