2 * Copyright (c) 2010-2023 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.milight.internal.test;
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;
22 import org.openhab.binding.milight.internal.MilightBindingConstants;
23 import org.slf4j.Logger;
24 import org.slf4j.LoggerFactory;
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.
30 * @author David Graeff - Initial contribution
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;
40 // These bytes are the client session bytes
41 private byte cls1 = (byte) 0xf6;
42 private byte cls2 = (byte) 0x0D;
44 private static final byte SEQ1 = 0, SEQ2 = 0;
46 private static final byte[] FAKE_MAC = { (byte) 0xAC, (byte) 0xCF, (byte) 0x23, (byte) 0xF5, (byte) 0x7A,
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 };
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] };
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 };
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,
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 };
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 };
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 };
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 };
89 private static final byte[] KEEP_ALIVE_REQUEST = { (byte) 0xD0, 0, 0, 0, 2, SID1, SID2 };
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 };
95 new Thread(this::runDiscovery).start();
96 new Thread(this::runBrigde).start();
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];
108 public void runDiscovery() {
109 final byte[] discover = "HF-A11ASSISTHREAD".getBytes();
112 byte[] a = new byte[0];
113 DatagramPacket sPacket = new DatagramPacket(a, a.length);
114 DatagramSocket datagramSocket = new DatagramSocket(MilightBindingConstants.PORT_DISCOVER);
116 debugSession("EmulatedV6Bridge discover thread ready");
117 byte[] buffer = new byte[1024];
118 DatagramPacket rPacket = new DatagramPacket(buffer, buffer.length);
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());
127 int len = rPacket.getLength();
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());
137 // logUnknownPacket(buffer, len, "No valid discovery received");
139 } catch (IOException e) {
143 logger.error("{}", e.getLocalizedMessage());
147 public void runBrigde() {
149 byte[] a = new byte[0];
150 DatagramPacket sPacket = new DatagramPacket(a, a.length);
151 DatagramSocket datagramSocket = new DatagramSocket(MilightBindingConstants.PORT_VER6);
153 debugSession("EmulatedV6Bridge control thread ready");
154 byte[] buffer = new byte[1024];
155 DatagramPacket rPacket = new DatagramPacket(buffer, buffer.length);
157 // Now loop forever, waiting to receive packets and printing them.
158 while (!willbeclosed) {
159 rPacket.setLength(buffer.length);
160 datagramSocket.receive(rPacket);
162 sPacket.setAddress(rPacket.getAddress());
163 sPacket.setPort(rPacket.getPort());
165 int len = rPacket.getLength();
167 if (len < 5 || buffer[1] != 0 || buffer[2] != 0 || buffer[3] != 0) {
168 logUnknownPacket(buffer, len, "Not an iBox request!");
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));
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);
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));
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);
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));
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);
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));
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);
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));
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);
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));
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));
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
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]
275 if (chksum != buffer[21]) {
276 logger.error("Checksum wrong:{} {}", chksum, buffer[21]);
280 StringBuilder debugStr = new StringBuilder();
281 if (buffer[13] == 0x08) {
282 debugStr.append("RGBWW ");
283 } else if (buffer[13] == 0x07) {
284 debugStr.append("RGBW ");
286 debugStr.append("iBox ");
289 debugStr.append("Zone " + buffer[19] + " ");
291 for (int i = 13; i < 19; ++i) {
292 debugStr.append(String.format("%02X ", buffer[i]));
294 logger.debug("{}", debugStr);
298 byte[] response = { (byte) 0x88, 0, 0, 0, (byte) 0x03, 0, seq, 0 };
299 sendMessage(sPacket, datagramSocket, response);
303 logUnknownPacket(buffer, len, "Not recognised command");
311 logger.error("{}", e.getLocalizedMessage());
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]));
320 logger.error("{}: {}", reason, s);
323 protected void sendMessage(DatagramPacket packet, DatagramSocket datagramSocket, byte[] buffer) {
324 packet.setData(buffer);
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() });
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]));
338 // logger.debug("Sent packet '{}' to ({})", new Object[] { s.toString(), address.getHostAddress() });
341 private void debugSession(String msg) {
342 // logger.debug(msg);