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.protocol;
15 import java.io.Closeable;
16 import java.io.IOException;
17 import java.net.DatagramPacket;
18 import java.net.DatagramSocket;
19 import java.net.InetAddress;
20 import java.net.InterfaceAddress;
21 import java.net.NetworkInterface;
22 import java.net.SocketException;
23 import java.net.SocketTimeoutException;
24 import java.nio.ByteBuffer;
25 import java.time.Duration;
26 import java.time.Instant;
27 import java.util.Enumeration;
28 import java.util.Iterator;
30 import java.util.TreeMap;
31 import java.util.concurrent.CompletableFuture;
33 import org.eclipse.jdt.annotation.NonNullByDefault;
34 import org.eclipse.jdt.annotation.Nullable;
35 import org.slf4j.Logger;
36 import org.slf4j.LoggerFactory;
39 * The milightV6 protocol is stateful and needs an established session for each client.
40 * This class handles the password bytes, session bytes and sequence number.
42 * The session handshake is a 3-way handshake. First we are sending either a general
43 * search for bridges command or a search for a specific bridge command (containing the bridge ID)
44 * with our own client session bytes included.
46 * The response will assign as session bytes that we can use for subsequent commands
47 * see {@link MilightV6SessionManager#sid1} and see {@link MilightV6SessionManager#sid2}.
49 * We register ourself to the bridge now and finalise the handshake by sending a register command
50 * see {@link MilightV6SessionManager#sendRegistration()} to the bridge.
52 * From this point on we are required to send keep alive packets to the bridge every ~10sec
53 * to keep the session alive. Because each command we send is confirmed by the bridge, we know if
54 * our session is still valid and can redo the session handshake if necessary.
56 * @author David Graeff - Initial contribution
59 public class MilightV6SessionManager implements Runnable, Closeable {
60 protected final Logger logger = LoggerFactory.getLogger(MilightV6SessionManager.class);
62 // The used sequence number for a command will be present in the response of the iBox. This
63 // allows us to identify failed command deliveries.
64 private int sequenceNo = 0;
66 // Password bytes 1 and 2
67 public byte[] pw = { 0, 0 };
69 // Session bytes 1 and 2
70 public byte[] sid = { 0, 0 };
72 // Client session bytes 1 and 2. Those are fixed for now.
73 public final byte clientSID1 = (byte) 0xab;
74 public final byte clientSID2 = (byte) 0xde;
76 // We need the bridge mac (bridge ID) in many responses to the session commands.
77 private final byte[] bridgeMAC = { (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0 };
80 * The session handshake is a 3 way handshake.
82 public enum SessionState {
83 // No session established and nothing in progress
85 // Send "find bridge" and wait for response
86 SESSION_WAIT_FOR_BRIDGE,
87 // Send "get session bytes" and wait for response
88 SESSION_WAIT_FOR_SESSION_SID,
89 // Session bytes received, register session now
90 SESSION_NEED_REGISTER,
91 // Registration complete, session is valid now
93 // The session is still active, a keep alive was just received.
94 SESSION_VALID_KEEP_ALIVE,
97 public enum StateMachineInput {
107 private SessionState sessionState = SessionState.SESSION_INVALID;
109 // Implement this interface to get notifications about the current session state.
110 public interface ISessionState {
112 * Notifies about a state change of {@link MilightV6SessionManager}.
113 * SESSION_VALID_KEEP_ALIVE will be reported in the interval, given to the constructor of
114 * {@link MilightV6SessionManager}.
116 * @param state The new state
117 * @param address The remote IP address. Only guaranteed to be non null in the SESSION_VALID* states.
119 void sessionStateChanged(SessionState state, @Nullable InetAddress address);
122 private final ISessionState observer;
124 /** Used to determine if the session needs a refresh */
125 private Instant lastSessionConfirmed = Instant.now();
126 /** Quits the receive thread if set to true */
127 private volatile boolean willbeclosed = false;
128 /** Keep track of send commands and their sequence number */
129 private final Map<Integer, Instant> usedSequenceNo = new TreeMap<>();
130 /** The receive thread for all bridge responses. */
131 private final Thread sessionThread;
133 private final String bridgeId;
134 private @Nullable DatagramSocket datagramSocket;
135 private @Nullable CompletableFuture<DatagramSocket> startFuture;
138 * Usually we only send BROADCAST packets. If we know the IP address of the bridge though,
139 * we should try UNICAST packets before falling back to BROADCAST.
140 * This allows communication with the bridge even if it is in another subnet.
142 private @Nullable final InetAddress destIP;
144 * We cache the last known IP to avoid using broadcast.
146 private @Nullable InetAddress lastKnownIP;
148 private final int port;
150 /** The maximum duration for a session registration / keep alive process in milliseconds. */
151 public static final int TIMEOUT_MS = 10000;
152 /** A packet is handled as lost / not confirmed after this time */
153 public static final int MAX_PACKET_IN_FLIGHT_MS = 2000;
154 /** The keep alive interval. Must be between 100 and REG_TIMEOUT_MS milliseconds or 0 */
155 private final int keepAliveInterval;
158 * A session manager for the V6 bridge needs a way to send data (a QueuedSend object), the destination bridge ID, a
159 * scheduler for timeout timers and optionally an observer for session state changes.
161 * @param sendQueue A send queue. Never remove or change that object while the session manager is still working.
162 * @param bridgeId Destination bridge ID. If the bridge ID for whatever reason changes, you need to create a new
163 * session manager object
164 * @param scheduler A framework scheduler to create timeout events.
165 * @param observer Get notifications of state changes
166 * @param destIP If you know the bridge IP address, provide it here.
167 * @param port The bridge port
168 * @param keepAliveInterval The keep alive interval. Must be between 100 and REG_TIMEOUT_MS milliseconds.
169 * if it is equal to REG_TIMEOUT_MS, then a new session will be established instead of renewing the
171 * @param pw The two "password" bytes for the bridge
173 public MilightV6SessionManager(String bridgeId, ISessionState observer, @Nullable InetAddress destIP, int port,
174 int keepAliveInterval, byte[] pw) {
175 this.bridgeId = bridgeId;
176 this.observer = observer;
177 this.destIP = destIP;
178 this.lastKnownIP = destIP;
180 this.keepAliveInterval = keepAliveInterval;
183 for (int i = 0; i < 6; ++i) {
184 bridgeMAC[i] = Integer.valueOf(bridgeId.substring(i * 2, i * 2 + 2), 16).byteValue();
186 if (keepAliveInterval < 100 || keepAliveInterval > TIMEOUT_MS) {
187 throw new IllegalArgumentException("keepAliveInterval not within given limits!");
190 sessionThread = new Thread(this, "SessionThread");
194 * Start the session thread if it is not already running
196 public CompletableFuture<DatagramSocket> start() {
198 CompletableFuture<DatagramSocket> f = new CompletableFuture<>();
199 f.completeExceptionally(new IllegalStateException("will be closed"));
202 if (sessionThread.isAlive()) {
203 DatagramSocket s = datagramSocket;
205 return CompletableFuture.completedFuture(s);
208 CompletableFuture<DatagramSocket> f = new CompletableFuture<>();
210 sessionThread.start();
215 * You have to call that if you are done with this object. Cleans up the receive thread.
218 public void close() throws IOException {
223 final DatagramSocket socket = datagramSocket;
224 if (socket != null) {
227 sessionThread.interrupt();
229 sessionThread.join();
230 } catch (InterruptedException e) {
234 // Set the session id bytes for bridge access. Usually they are acquired automatically
235 // during the session handshake.
236 public void setSessionID(byte[] sid) {
237 this.sid[0] = sid[0];
238 this.sid[1] = sid[1];
239 sessionState = SessionState.SESSION_NEED_REGISTER;
242 // Return the session bytes as hex string
243 public String getSession() {
244 return String.format("%02X %02X", this.sid[0], this.sid[1]);
247 public Instant getLastSessionValidConfirmation() {
248 return lastSessionConfirmed;
251 // Get a new sequence number. Add that to a queue of used sequence numbers.
252 // The bridge response will remove the queued number. This method also checks
253 // for non confirmed sequence numbers older that 2 seconds and report them.
254 public int getNextSequenceNo() {
255 int currentSequenceNo = this.sequenceNo;
256 usedSequenceNo.put(currentSequenceNo, Instant.now());
258 return currentSequenceNo;
261 public static byte firstSeqByte(int seq) {
262 return (byte) (seq & 0xff);
265 public static byte secondSeqByte(int seq) {
266 return (byte) ((seq >> 8) & 0xff);
270 * Send a search for bridgeID packet on all network interfaces.
271 * This is used for the initial way to determine the IP of the bridge as well
272 * as if the IP of a bridge has changed and the session got invalid because of that.
274 * A response will assign us session bytes.
276 * @throws InterruptedException
278 @SuppressWarnings({ "null", "unused" })
279 private void sendSearchForBroadcast(DatagramSocket datagramSocket) {
280 byte[] t = new byte[] { (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x0A, (byte) 0x02,
281 clientSID1, clientSID2, (byte) 0x01, bridgeMAC[0], bridgeMAC[1], bridgeMAC[2], bridgeMAC[3],
282 bridgeMAC[4], bridgeMAC[5] };
283 if (lastKnownIP != null) {
285 datagramSocket.send(new DatagramPacket(t, t.length, lastKnownIP, port));
286 } catch (IOException e) {
287 logger.warn("Could not send discover packet! {}", e.getLocalizedMessage());
292 Enumeration<NetworkInterface> enumNetworkInterfaces;
294 enumNetworkInterfaces = NetworkInterface.getNetworkInterfaces();
295 } catch (SocketException socketException) {
296 logger.warn("Could not enumerate network interfaces for sending the discover packet!", socketException);
299 DatagramPacket packet = new DatagramPacket(t, t.length, lastKnownIP, port);
300 while (enumNetworkInterfaces.hasMoreElements()) {
301 NetworkInterface networkInterface = enumNetworkInterfaces.nextElement();
302 Iterator<InterfaceAddress> it = networkInterface.getInterfaceAddresses().iterator();
303 while (it.hasNext()) {
304 InterfaceAddress address = it.next();
305 if (address == null) {
308 InetAddress broadcast = address.getBroadcast();
309 if (broadcast != null && !address.getAddress().isLoopbackAddress()) {
310 packet.setAddress(broadcast);
312 datagramSocket.send(packet);
313 } catch (IOException e) {
314 logger.warn("Could not send discovery packet! {}", e.getLocalizedMessage());
321 // Search for a specific bridge (our bridge). A response will assign us session bytes.
322 // private void send_search_for() {
323 // sendQueue.queue(AbstractBulbInterface.CAT_SESSION, searchForPacket());
326 private void sendEstablishSession(DatagramSocket datagramSocket) throws IOException {
327 final InetAddress address = lastKnownIP;
328 if (address == null) {
331 byte unknown = (byte) 0x1E; // Either checksum or counter. Was 64 and 1e so far.
332 byte[] t = { (byte) 0x20, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x16, (byte) 0x02, (byte) 0x62,
333 (byte) 0x3A, (byte) 0xD5, (byte) 0xED, (byte) 0xA3, (byte) 0x01, (byte) 0xAE, (byte) 0x08, (byte) 0x2D,
334 (byte) 0x46, (byte) 0x61, (byte) 0x41, (byte) 0xA7, (byte) 0xF6, (byte) 0xDC, (byte) 0xAF, clientSID1,
335 clientSID2, (byte) 0x00, (byte) 0x00, unknown };
337 datagramSocket.send(new DatagramPacket(t, t.length, address, port));
340 // Some apps first send {@see send_establish_session} and with the aquired session bytes they
341 // subsequently send this command for establishing the session. This is not well documented unfortunately.
342 @SuppressWarnings("unused")
343 private void sendPreRegistration(DatagramSocket datagramSocket) throws IOException {
344 final InetAddress address = lastKnownIP;
345 if (address == null) {
348 byte[] t = { 0x30, 0, 0, 0, 3, sid[0], sid[1], 1, 0 };
349 datagramSocket.send(new DatagramPacket(t, t.length, address, port));
352 // After the bridges knows our client session bytes and we know the bridge session bytes, we do a final
353 // registration with this command. The response will again contain the bridge ID and the session should
354 // be established by then.
355 private void sendRegistration(DatagramSocket datagramSocket) throws IOException {
356 final InetAddress address = lastKnownIP;
357 if (address == null) {
361 int seq = getNextSequenceNo();
362 byte[] t = { (byte) 0x80, 0x00, 0x00, 0x00, 0x11, sid[0], sid[1], firstSeqByte(seq), secondSeqByte(seq), 0x00,
363 0x33, pw[0], pw[1], 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) (0x33 + pw[0] + pw[1]) };
364 datagramSocket.send(new DatagramPacket(t, t.length, address, port));
368 * Constructs a 0x80... command which us used for all colour,brightness,saturation,mode operations.
369 * The session ID, password and sequence number is automatically inserted from this object.
371 * Produces data like:
372 * SN: Sequence number
375 * P1/P2: Password bytes
376 * WB: Remote (08) or iBox integrated bulb (00)
377 * ZN: Zone {Zone1-4 0=All}
381 * @ 80 00 00 00 11 84 00 00 0c 00 31 00 00 08 04 01 00 00 00 01 00 3f
384 * CC: Color value (hue)
385 * 80 00 00 00 11 S1 S2 SN SN 00 31 P1 P2 WB 01 CC CC CC CC ZN 00 CK
387 * 80 00 00 00 11 D4 00 00 12 00 31 00 00 08 01 FF FF FF FF 01 00 38
391 public byte[] makeCommand(byte wb, int zone, int... data) {
392 int seq = getNextSequenceNo();
393 byte[] t = { (byte) 0x80, 0x00, 0x00, 0x00, 0x11, sid[0], sid[1], MilightV6SessionManager.firstSeqByte(seq),
394 MilightV6SessionManager.secondSeqByte(seq), 0x00, 0x31, pw[0], pw[1], wb, 0, 0, 0, 0, 0, (byte) zone, 0,
397 for (int i = 0; i < data.length; ++i) {
398 t[14 + i] = (byte) data[i];
401 byte chksum = (byte) (t[10 + 0] + t[10 + 1] + t[10 + 2] + t[10 + 3] + t[10 + 4] + t[10 + 5] + t[10 + 6]
402 + t[10 + 7] + t[10 + 8] + zone);
408 * Constructs a 0x3D or 0x3E link/unlink command.
409 * The session ID, password and sequence number is automatically inserted from this object.
411 * WB: Remote (08) or iBox integrated bulb (00)
413 public byte[] makeLink(byte wb, int zone, boolean link) {
414 int seq = getNextSequenceNo();
415 byte[] t = { (link ? (byte) 0x3D : (byte) 0x3E), 0x00, 0x00, 0x00, 0x11, sid[0], sid[1],
416 MilightV6SessionManager.firstSeqByte(seq), MilightV6SessionManager.secondSeqByte(seq), 0x00, 0x31,
417 pw[0], pw[1], wb, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) zone, 0x00, 0x00 };
419 byte chksum = (byte) (t[10 + 0] + t[10 + 1] + t[10 + 2] + t[10 + 3] + t[10 + 4] + t[10 + 5] + t[10 + 6]
420 + t[10 + 7] + t[10 + 8] + zone);
426 * The main state machine of the session handshake.
428 * @throws InterruptedException
429 * @throws IOException
431 private void sessionStateMachine(DatagramSocket datagramSocket, StateMachineInput input) throws IOException {
432 final SessionState lastSessionState = sessionState;
435 final Instant current = Instant.now();
436 final Duration timeElapsed = Duration.between(lastSessionConfirmed, current);
437 if (timeElapsed.toMillis() > TIMEOUT_MS) {
438 if (sessionState != SessionState.SESSION_WAIT_FOR_BRIDGE) {
439 logger.warn("Session timeout!");
441 // One reason we failed, might be that a last known IP is not correct anymore.
442 // Reset to the given dest IP (which might be null).
443 lastKnownIP = destIP;
444 sessionState = SessionState.SESSION_INVALID;
447 if (input == StateMachineInput.INVALID_COMMAND) {
448 sessionState = SessionState.SESSION_INVALID;
452 for (Iterator<Map.Entry<Integer, Instant>> it = usedSequenceNo.entrySet().iterator(); it.hasNext();) {
453 Map.Entry<Integer, Instant> entry = it.next();
454 if (Duration.between(entry.getValue(), current).toMillis() > MAX_PACKET_IN_FLIGHT_MS) {
455 logger.debug("Command not confirmed: {}", entry.getKey());
460 switch (sessionState) {
461 case SESSION_INVALID:
462 usedSequenceNo.clear();
463 sessionState = SessionState.SESSION_WAIT_FOR_BRIDGE;
464 lastSessionConfirmed = Instant.now();
465 case SESSION_WAIT_FOR_BRIDGE:
466 if (input == StateMachineInput.BRIDGE_CONFIRMED) {
467 sessionState = SessionState.SESSION_WAIT_FOR_SESSION_SID;
469 datagramSocket.setSoTimeout(150);
470 sendSearchForBroadcast(datagramSocket);
473 case SESSION_WAIT_FOR_SESSION_SID:
474 if (input == StateMachineInput.SESSION_ID_RECEIVED) {
475 if (ProtocolConstants.DEBUG_SESSION) {
476 logger.debug("Session ID received: {}", String.format("%02X %02X", this.sid[0], this.sid[1]));
478 sessionState = SessionState.SESSION_NEED_REGISTER;
480 datagramSocket.setSoTimeout(300);
481 sendEstablishSession(datagramSocket);
484 case SESSION_NEED_REGISTER:
485 if (input == StateMachineInput.SESSION_ESTABLISHED) {
486 sessionState = SessionState.SESSION_VALID;
487 lastSessionConfirmed = Instant.now();
488 if (ProtocolConstants.DEBUG_SESSION) {
489 logger.debug("Registration complete");
492 datagramSocket.setSoTimeout(300);
493 sendRegistration(datagramSocket);
496 case SESSION_VALID_KEEP_ALIVE:
498 if (input == StateMachineInput.KEEP_ALIVE_RECEIVED) {
499 lastSessionConfirmed = Instant.now();
500 observer.sessionStateChanged(SessionState.SESSION_VALID_KEEP_ALIVE, lastKnownIP);
502 final InetAddress address = lastKnownIP;
503 if (keepAliveInterval > 0 && timeElapsed.toMillis() > keepAliveInterval && address != null) {
505 byte[] t = { (byte) 0xD0, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x02, sid[0], sid[1] };
506 datagramSocket.send(new DatagramPacket(t, t.length, address, port));
508 // Increase socket timeout to wake up for the next keep alive interval
509 datagramSocket.setSoTimeout(keepAliveInterval);
514 if (lastSessionState != sessionState) {
515 observer.sessionStateChanged(sessionState, lastKnownIP);
519 private void logUnknownPacket(byte[] data, int len, String reason) {
520 StringBuilder s = new StringBuilder();
521 for (int i = 0; i < len; ++i) {
522 s.append(String.format("%02X ", data[i]));
525 s.append(String.format("%02X ", clientSID1));
526 s.append(String.format("%02X ", clientSID2));
527 logger.info("{} ({}): {}", reason, bridgeId, s);
531 * The session thread executes this run() method and a blocking UDP receive
532 * is performed in a loop.
534 @SuppressWarnings({ "null", "unused" })
537 try (DatagramSocket datagramSocket = new DatagramSocket(null)) {
538 this.datagramSocket = datagramSocket;
539 datagramSocket.setBroadcast(true);
540 datagramSocket.setReuseAddress(true);
541 datagramSocket.setSoTimeout(150);
542 datagramSocket.bind(null);
544 if (ProtocolConstants.DEBUG_SESSION) {
545 logger.debug("MilightCommunicationV6 receive thread ready");
548 // Inform the start future about the datagram socket
549 CompletableFuture<DatagramSocket> f = startFuture;
551 f.complete(datagramSocket);
555 byte[] buffer = new byte[1024];
556 DatagramPacket rPacket = new DatagramPacket(buffer, buffer.length);
558 sessionStateMachine(datagramSocket, StateMachineInput.NO_INPUT);
560 // Now loop forever, waiting to receive packets and printing them.
561 while (!willbeclosed) {
562 rPacket.setLength(buffer.length);
564 datagramSocket.receive(rPacket);
565 } catch (SocketTimeoutException e) {
566 sessionStateMachine(datagramSocket, StateMachineInput.TIMEOUT);
569 int len = rPacket.getLength();
571 if (len < 5 || buffer[1] != 0 || buffer[2] != 0 || buffer[3] != 0) {
572 logUnknownPacket(buffer, len, "Not an iBox response!");
576 int expectedLen = buffer[4] + 5;
578 if (expectedLen > len) {
579 logUnknownPacket(buffer, len, "Unexpected size!");
583 // 13 00 00 00 0A 03 D3 54 11 (AC CF 23 F5 7A D4)
585 boolean eq = ByteBuffer.wrap(bridgeMAC, 0, 6).equals(ByteBuffer.wrap(buffer, 9, 6));
587 logger.debug("TODO: Feedback required");
588 // I have no clue what that packet means. But the bridge is going to timeout the next
589 // keep alive and it is a good idea to start the session again.
591 logger.info("Unknown 0x13 received, but not for our bridge ({})", bridgeId);
595 // 18 00 00 00 40 02 (AC CF 23 F5 7A D4) 00 20 39 38 35 62 31 35 37 62 66 36 66 63 34 33 33 36 38 61
596 // 36 33 34 36 37 65 61 33 62 31 39 64 30 64 01 00 01 17 63 00 00 05 00 09 78 6C 69 6E 6B 5F 64 65
598 // ASCII string contained: 985b157bf6fc43368a63467ea3b19d0dc .. xlink_dev
599 // Response to the v6 SEARCH and the SEARCH FOR commands to look for new or known devices.
600 // Our session id will be transfered in this process (!= bridge session id)
602 boolean eq = ByteBuffer.wrap(bridgeMAC, 0, 6).equals(ByteBuffer.wrap(buffer, 6, 6));
604 if (ProtocolConstants.DEBUG_SESSION) {
605 logger.debug("Session ID reestablished");
607 lastKnownIP = rPacket.getAddress();
608 sessionStateMachine(datagramSocket, StateMachineInput.BRIDGE_CONFIRMED);
610 logger.info("Session ID received, but not for our bridge ({})", bridgeId);
611 logUnknownPacket(buffer, len, "ID not matching");
616 // 28 00 00 00 11 00 02 (AC CF 23 F5 7A D4) 50 AA 4D 2A 00 01 SS_ID 00
617 // Response to the keepAlive() packet if session is not valid yet.
618 // Should contain the session ids
620 boolean eq = ByteBuffer.wrap(bridgeMAC, 0, 6).equals(ByteBuffer.wrap(buffer, 7, 6));
622 this.sid[0] = buffer[19];
623 this.sid[1] = buffer[20];
624 sessionStateMachine(datagramSocket, StateMachineInput.SESSION_ID_RECEIVED);
626 logger.info("Session ID received, but not for our bridge ({})", bridgeId);
627 logUnknownPacket(buffer, len, "ID not matching");
632 // 80 00 00 00 15 (AC CF 23 F5 7A D4) 05 02 00 34 00 00 00 00 00 00 00 00 00 00 34
633 // Response to the registration packet
635 boolean eq = ByteBuffer.wrap(bridgeMAC, 0, 6).equals(ByteBuffer.wrap(buffer, 5, 6));
637 sessionStateMachine(datagramSocket, StateMachineInput.SESSION_ESTABLISHED);
639 logger.info("Registration received, but not for our bridge ({})", bridgeId);
640 logUnknownPacket(buffer, len, "ID not matching");
644 // 88 00 00 00 03 SN SN OK // two byte sequence number, we use the later one only.
645 // OK: is 00 if ok or 01 if failed
647 int seq = Byte.toUnsignedInt(buffer[6]) + Byte.toUnsignedInt(buffer[7]) * 256;
648 Instant timePacketWasSend = usedSequenceNo.remove(seq);
649 if (timePacketWasSend != null) {
650 if (ProtocolConstants.DEBUG_SESSION) {
651 logger.debug("Confirmation received for command: {}", String.valueOf(seq));
653 if (buffer[8] == 1) {
654 logger.warn("Command {} failed", seq);
657 // another participant might have established a session from the same host
658 logger.info("Confirmation received for unsend command. Sequence number: {}",
659 String.valueOf(seq));
662 // D8 00 00 00 07 (AC CF 23 F5 7A D4) 01
663 // Response to the keepAlive() packet
665 boolean eq = ByteBuffer.wrap(bridgeMAC, 0, 6).equals(ByteBuffer.wrap(buffer, 5, 6));
667 sessionStateMachine(datagramSocket, StateMachineInput.KEEP_ALIVE_RECEIVED);
669 logger.info("Keep alive received but not for our bridge ({})", bridgeId);
670 logUnknownPacket(buffer, len, "ID not matching");
675 logUnknownPacket(buffer, len, "No valid start byte");
678 } catch (IOException e) {
680 logger.warn("Session Manager receive thread failed: {}", e.getLocalizedMessage(), e);
683 this.datagramSocket = null;
685 if (ProtocolConstants.DEBUG_SESSION) {
686 logger.debug("MilightCommunicationV6 receive thread stopped");
690 // Return true if the session is established successfully
691 public boolean isValid() {
692 return sessionState == SessionState.SESSION_VALID;