]> git.basschouten.com Git - openhab-addons.git/blob
5fa35e406d67124953ee0febcb6cfc607e8082f3
[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.milight.internal.test;
14
15 import java.io.IOException;
16 import java.net.DatagramPacket;
17 import java.net.DatagramSocket;
18 import java.net.InetAddress;
19 import java.nio.ByteBuffer;
20 import java.util.Arrays;
21
22 import org.openhab.binding.milight.internal.MilightBindingConstants;
23 import org.slf4j.Logger;
24 import org.slf4j.LoggerFactory;
25
26 /**
27  * Emulates a Milight V6 iBox bridge to test and intercept communication with the official apps,
28  * as well as test the binding to be conformant to the protocol.
29  *
30  * @author David Graeff - Initial contribution
31  */
32 public class EmulatedV6Bridge {
33     protected final Logger logger = LoggerFactory.getLogger(EmulatedV6Bridge.class);
34     private boolean willbeclosed = false;
35     private static final byte SID1 = (byte) 0xed;
36     private static final byte SID2 = (byte) 0xab;
37     private static final byte PW1 = 0;
38     private static final byte PW2 = 0;
39
40     // These bytes are the client session bytes
41     private byte cls1 = (byte) 0xf6;
42     private byte cls2 = (byte) 0x0D;
43
44     private static final byte SEQ1 = 0, SEQ2 = 0;
45
46     private static final byte[] FAKE_MAC = { (byte) 0xAC, (byte) 0xCF, (byte) 0x23, (byte) 0xF5, (byte) 0x7A,
47             (byte) 0xD4 };
48
49     // Send to the network by clients to find V6 bridges
50     private byte[] searchBroadcast = new byte[] { 0x10, 0, 0, 0, 0x24, 0x02, cls1, cls2, 0x02, 0x39, 0x38, 0x35, 0x62,
51             0x31, 0x35, 0x37, 0x62, 0x66, 0x36, 0x66, 0x63, 0x34, 0x33, 0x33, 0x36, 0x38, 0x61, 0x36, 0x33, 0x34, 0x36,
52             0x37, 0x65, 0x61, 0x33, 0x62, 0x31, 0x39, 0x64, 0x30, 0x64 };
53
54     // Send to broadcast address by the client usually and used to test if the client with the contained bridge id
55     // is present on the network. If the IP of the bridge is known already, then SESSION_REQUEST is used usually.
56     private byte[] sessionRequestFindBroadcast = new byte[] { 0x10, 0, 0, 0, 0x0A, 2, cls1, cls2, 1, FAKE_MAC[0],
57             FAKE_MAC[1], FAKE_MAC[2], FAKE_MAC[3], FAKE_MAC[4], FAKE_MAC[5] };
58
59     // Some clients send this as first command to get a session id, especially if the bridge IP is already known.
60     private byte[] sessionRequest = new byte[] { (byte) 0x20, 0, 0, 0, (byte) 0x16, 2, (byte) 0x62, (byte) 0x3A,
61             (byte) 0xD5, (byte) 0xED, (byte) 0xA3, 1, (byte) 0xAE, (byte) 0x08, (byte) 0x2D, (byte) 0x46, (byte) 0x61,
62             (byte) 0x41, (byte) 0xA7, (byte) 0xF6, (byte) 0xDC, (byte) 0xAF, cls1, cls2, 0, 0, (byte) 0x1E };
63
64     private byte[] sessionResponse = { (byte) 0x28, 0, 0, 0, (byte) 0x11, 0, 2, (byte) 0xAC, (byte) 0xCF, (byte) 0x23,
65             (byte) 0xF5, (byte) 0x7A, (byte) 0xD4, (byte) 0x69, (byte) 0xF0, (byte) 0x3C, (byte) 0x23, 0, 1, SID1, SID2,
66             0 };
67
68     // Some clients call this as second command to establish a session.
69     private static final byte[] ESTABLISH_SESSION_REQUEST = new byte[] { (byte) 0x30, 0, 0, 0, 3, SID1, SID2, 0 };
70
71     // In response to SEARCH, ESTABLISH_SESSION_REQUEST but also to SESSION_REQUEST_FIND_BROADCAST
72     private static final byte[] REESTABLISH_SESSION_RESPONSE = new byte[] { (byte) 0x18, 0, 0, 0, (byte) 0x40, 2,
73             FAKE_MAC[0], FAKE_MAC[1], FAKE_MAC[2], FAKE_MAC[3], FAKE_MAC[4], FAKE_MAC[5], 0, (byte) 0x20, (byte) 0x39,
74             (byte) 0x38, (byte) 0x35, (byte) 0x62, (byte) 0x31, (byte) 0x35, (byte) 0x37, (byte) 0x62, (byte) 0x66,
75             (byte) 0x36, (byte) 0x66, (byte) 0x63, (byte) 0x34, (byte) 0x33, (byte) 0x33, (byte) 0x36, (byte) 0x38,
76             (byte) 0x61, (byte) 0x36, (byte) 0x33, (byte) 0x34, (byte) 0x36, (byte) 0x37, (byte) 0x65, (byte) 0x61,
77             (byte) 0x33, (byte) 0x62, (byte) 0x31, (byte) 0x39, (byte) 0x64, (byte) 0x30, (byte) 0x64, 1, 0, 1,
78             (byte) 0x17, (byte) 0x63, 0, 0, (byte) 0x05, 0, (byte) 0x09, (byte) 0x78, (byte) 0x6C, (byte) 0x69,
79             (byte) 0x6E, (byte) 0x6B, (byte) 0x5F, (byte) 0x64, (byte) 0x65, (byte) 0x76, (byte) 0x07, (byte) 0x5B,
80             (byte) 0xCD, (byte) 0x15 };
81
82     private static final byte[] REGISTRATION_REQUEST = { (byte) 0x80, 0, 0, 0, 0x11, SID1, SID2, SEQ1, SEQ2, 0, 0x33,
83             PW1, PW2, 0, 0, 0, 0, 0, 0, 0, 0, 0x33 };
84
85     // 80:00:00:00:15:(f0:fe:6b:16:b0:8a):05:02:00:34:00:00:00:00:00:00:00:00:00:00:34
86     private static final byte[] REGISTRATION_REQUEST_RESPONSE = { (byte) 0x80, 0, 0, 0, 0x15, FAKE_MAC[0], FAKE_MAC[1],
87             FAKE_MAC[2], FAKE_MAC[3], FAKE_MAC[4], FAKE_MAC[5], 5, 2, 0, 0x34, PW1, PW2, 0, 0, 0, 0, 0, 0, 0, 0, 0x34 };
88
89     private static final byte[] KEEP_ALIVE_REQUEST = { (byte) 0xD0, 0, 0, 0, 2, SID1, SID2 };
90
91     private static final byte[] KEEP_ALIVE_RESPONSE = { (byte) 0xD8, 0, 0, 0, (byte) 0x07, FAKE_MAC[0], FAKE_MAC[1],
92             FAKE_MAC[2], FAKE_MAC[3], FAKE_MAC[4], FAKE_MAC[5], 1 };
93
94     EmulatedV6Bridge() {
95         new Thread(this::runDiscovery).start();
96         new Thread(this::runBrigde).start();
97     }
98
99     private void replaceWithMac(byte[] data, int offset) {
100         data[offset + 0] = FAKE_MAC[0];
101         data[offset + 1] = FAKE_MAC[1];
102         data[offset + 2] = FAKE_MAC[2];
103         data[offset + 3] = FAKE_MAC[3];
104         data[offset + 4] = FAKE_MAC[4];
105         data[offset + 5] = FAKE_MAC[5];
106     }
107
108     public void runDiscovery() {
109         final byte[] discover = "HF-A11ASSISTHREAD".getBytes();
110
111         try {
112             byte[] a = new byte[0];
113             DatagramPacket sPacket = new DatagramPacket(a, a.length);
114             DatagramSocket datagramSocket = new DatagramSocket(MilightBindingConstants.PORT_DISCOVER);
115
116             debugSession("EmulatedV6Bridge discover thread ready");
117             byte[] buffer = new byte[1024];
118             DatagramPacket rPacket = new DatagramPacket(buffer, buffer.length);
119
120             // Now loop forever, waiting to receive packets and printing them.
121             while (!willbeclosed) {
122                 rPacket.setLength(buffer.length);
123                 datagramSocket.receive(rPacket);
124                 sPacket.setAddress(rPacket.getAddress());
125                 sPacket.setPort(rPacket.getPort());
126
127                 int len = rPacket.getLength();
128
129                 if (len >= discover.length) {
130                     if (Arrays.equals(discover, Arrays.copyOf(buffer, discover.length))) {
131                         String data = rPacket.getAddress().getHostAddress() + ",ACCF23F57AD4,HF-LPB100";
132                         debugSession("Discover message received. Send: " + data);
133                         sendMessage(sPacket, datagramSocket, data.getBytes());
134                         continue;
135                     }
136                 }
137                 // logUnknownPacket(buffer, len, "No valid discovery received");
138             }
139         } catch (IOException e) {
140             if (willbeclosed) {
141                 return;
142             }
143             logger.error("{}", e.getLocalizedMessage());
144         }
145     }
146
147     public void runBrigde() {
148         try {
149             byte[] a = new byte[0];
150             DatagramPacket sPacket = new DatagramPacket(a, a.length);
151             DatagramSocket datagramSocket = new DatagramSocket(MilightBindingConstants.PORT_VER6);
152
153             debugSession("EmulatedV6Bridge control thread ready");
154             byte[] buffer = new byte[1024];
155             DatagramPacket rPacket = new DatagramPacket(buffer, buffer.length);
156
157             // Now loop forever, waiting to receive packets and printing them.
158             while (!willbeclosed) {
159                 rPacket.setLength(buffer.length);
160                 datagramSocket.receive(rPacket);
161
162                 sPacket.setAddress(rPacket.getAddress());
163                 sPacket.setPort(rPacket.getPort());
164
165                 int len = rPacket.getLength();
166
167                 if (len < 5 || buffer[1] != 0 || buffer[2] != 0 || buffer[3] != 0) {
168                     logUnknownPacket(buffer, len, "Not an iBox request!");
169                     continue;
170                 }
171
172                 if (len >= sessionRequest.length && buffer[0] == sessionRequest[0]) {
173                     sessionRequest[22] = buffer[22];
174                     sessionRequest[23] = buffer[23];
175                     boolean eq = ByteBuffer.wrap(sessionRequest, 0, sessionRequest.length)
176                             .equals(ByteBuffer.wrap(buffer, 0, sessionRequest.length));
177                     if (eq) {
178                         debugSession("session get message received");
179                         sessionResponse[19] = SID1;
180                         sessionResponse[20] = SID2;
181                         replaceWithMac(sessionResponse, 7);
182                         debugSessionSend(sessionResponse, sPacket.getAddress());
183                         sendMessage(sPacket, datagramSocket, sessionResponse);
184                         continue;
185                     }
186                 }
187
188                 if (len >= sessionRequestFindBroadcast.length && buffer[0] == sessionRequestFindBroadcast[0]) {
189                     sessionRequestFindBroadcast[6] = buffer[6];
190                     sessionRequestFindBroadcast[7] = buffer[7];
191                     boolean eq = ByteBuffer.wrap(sessionRequestFindBroadcast, 0, 6)
192                             .equals(ByteBuffer.wrap(buffer, 0, 6));
193                     if (eq) {
194                         debugSession("init session received");
195                         cls1 = sessionRequestFindBroadcast[6];
196                         cls2 = sessionRequestFindBroadcast[7];
197                         replaceWithMac(REESTABLISH_SESSION_RESPONSE, 6);
198                         debugSessionSend(REESTABLISH_SESSION_RESPONSE, sPacket.getAddress());
199                         sendMessage(sPacket, datagramSocket, REESTABLISH_SESSION_RESPONSE);
200                         continue;
201                     }
202                 }
203
204                 if (len >= searchBroadcast.length && buffer[0] == searchBroadcast[0]) {
205                     searchBroadcast[6] = buffer[6];
206                     searchBroadcast[7] = buffer[7];
207                     boolean eq = ByteBuffer.wrap(searchBroadcast, 0, searchBroadcast.length)
208                             .equals(ByteBuffer.wrap(buffer, 0, searchBroadcast.length));
209                     if (eq) {
210                         debugSession("Search request");
211                         cls1 = searchBroadcast[6];
212                         cls2 = searchBroadcast[7];
213                         replaceWithMac(REESTABLISH_SESSION_RESPONSE, 6);
214                         debugSessionSend(REESTABLISH_SESSION_RESPONSE, sPacket.getAddress());
215                         sendMessage(sPacket, datagramSocket, REESTABLISH_SESSION_RESPONSE);
216                         continue;
217                     }
218                 }
219
220                 if (len >= ESTABLISH_SESSION_REQUEST.length && buffer[0] == ESTABLISH_SESSION_REQUEST[0]) {
221                     ESTABLISH_SESSION_REQUEST[5] = SID1;
222                     ESTABLISH_SESSION_REQUEST[6] = SID2;
223                     boolean eq = ByteBuffer.wrap(ESTABLISH_SESSION_REQUEST, 0, ESTABLISH_SESSION_REQUEST.length)
224                             .equals(ByteBuffer.wrap(buffer, 0, ESTABLISH_SESSION_REQUEST.length));
225                     if (eq) {
226                         debugSession("Session establish request");
227                         replaceWithMac(REESTABLISH_SESSION_RESPONSE, 6);
228                         debugSessionSend(REESTABLISH_SESSION_RESPONSE, sPacket.getAddress());
229                         sendMessage(sPacket, datagramSocket, REESTABLISH_SESSION_RESPONSE);
230                         continue;
231                     }
232                 }
233
234                 if (len >= KEEP_ALIVE_REQUEST.length && buffer[0] == KEEP_ALIVE_REQUEST[0]) {
235                     KEEP_ALIVE_REQUEST[5] = SID1;
236                     KEEP_ALIVE_REQUEST[6] = SID2;
237                     boolean eq = ByteBuffer.wrap(KEEP_ALIVE_REQUEST, 0, KEEP_ALIVE_REQUEST.length)
238                             .equals(ByteBuffer.wrap(buffer, 0, KEEP_ALIVE_REQUEST.length));
239                     if (eq) {
240                         debugSession("keep alive received");
241                         replaceWithMac(KEEP_ALIVE_RESPONSE, 5);
242                         debugSessionSend(KEEP_ALIVE_RESPONSE, sPacket.getAddress());
243                         sendMessage(sPacket, datagramSocket, KEEP_ALIVE_RESPONSE);
244                         continue;
245                     }
246                 }
247
248                 if (len >= REGISTRATION_REQUEST.length && buffer[0] == REGISTRATION_REQUEST[0]) {
249                     byte seq = buffer[8];
250                     if (buffer[5] != SID1 || buffer[6] != SID2) {
251                         logUnknownPacket(buffer, len,
252                                 "No valid ssid. Current ssid is " + String.format("%02X %02X", SID1, SID2));
253                         continue;
254                     }
255                     if (buffer[11] != PW1 || buffer[12] != PW2) {
256                         logUnknownPacket(buffer, len,
257                                 "No valid password. Current password is " + String.format("%02X %02X", PW1, PW2));
258                         continue;
259                     }
260
261                     if (buffer[4] == 0x11) {
262                         if (buffer[10] == 0x33) {
263                             replaceWithMac(REGISTRATION_REQUEST_RESPONSE, 5);
264                             sendMessage(sPacket, datagramSocket, REGISTRATION_REQUEST_RESPONSE);
265                         } else if (buffer[10] == 0x31) {
266                             // 80 00 00 00 11 {WifiBridgeSessionID1} {WifiBridgeSessionID2} 00 {SequenceNumber} 00 0x31
267                             // {PasswordByte1 default 00} {PasswordByte2 default 00} {remoteStyle 08 for RGBW/WW/CW or
268                             // 00 for bridge lamp} {LightCommandByte1} {LightCommandByte2} 0 0 0 {Zone1-4 0=All} 0
269                             // {Checksum}
270
271                             byte chksum = (byte) (buffer[10 + 0] + buffer[10 + 1] + buffer[10 + 2] + buffer[10 + 3]
272                                     + buffer[10 + 4] + buffer[10 + 5] + buffer[10 + 6] + buffer[10 + 7] + buffer[10 + 8]
273                                     + buffer[19]);
274
275                             if (chksum != buffer[21]) {
276                                 logger.error("Checksum wrong:{} {}", chksum, buffer[21]);
277                                 continue;
278                             }
279
280                             StringBuilder debugStr = new StringBuilder();
281                             if (buffer[13] == 0x08) {
282                                 debugStr.append("RGBWW ");
283                             } else if (buffer[13] == 0x07) {
284                                 debugStr.append("RGBW ");
285                             } else {
286                                 debugStr.append("iBox ");
287                             }
288
289                             debugStr.append("Zone " + buffer[19] + " ");
290
291                             for (int i = 13; i < 19; ++i) {
292                                 debugStr.append(String.format("%02X ", buffer[i]));
293                             }
294                             logger.debug("{}", debugStr);
295                         }
296                     }
297
298                     byte[] response = { (byte) 0x88, 0, 0, 0, (byte) 0x03, 0, seq, 0 };
299                     sendMessage(sPacket, datagramSocket, response);
300                     continue;
301                 }
302
303                 logUnknownPacket(buffer, len, "Not recognised command");
304             }
305         } catch (
306
307         IOException e) {
308             if (willbeclosed) {
309                 return;
310             }
311             logger.error("{}", e.getLocalizedMessage());
312         }
313     }
314
315     protected void logUnknownPacket(byte[] data, int len, String reason) {
316         StringBuilder s = new StringBuilder();
317         for (int i = 0; i < len; ++i) {
318             s.append(String.format("%02X ", data[i]));
319         }
320         logger.error("{}: {}", reason, s);
321     }
322
323     protected void sendMessage(DatagramPacket packet, DatagramSocket datagramSocket, byte[] buffer) {
324         packet.setData(buffer);
325         try {
326             datagramSocket.send(packet);
327         } catch (IOException e) {
328             logger.error("Failed to send Message to '{}', Error message: {}",
329                     new Object[] { packet.getAddress().getHostAddress(), e.getMessage() });
330         }
331     }
332
333     private void debugSessionSend(byte[] buffer, InetAddress address) {
334         StringBuilder s = new StringBuilder();
335         for (int i = 0; i < buffer.length; ++i) {
336             s.append(String.format("%02X ", buffer[i]));
337         }
338         // logger.debug("Sent packet '{}' to ({})", new Object[] { s.toString(), address.getHostAddress() });
339     }
340
341     private void debugSession(String msg) {
342         // logger.debug(msg);
343     }
344 }