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#clientSID1} and see {@link MilightV6SessionManager#clientSID2}.
49 * We register ourself to the bridge now and finalise the handshake by sending a register command
50 * see {@link #sendRegistration(DatagramSocket)} 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 bridgeId Destination bridge ID. If the bridge ID for whatever reason changes, you need to create a new
162 * session manager object
163 * @param observer Get notifications of state changes
164 * @param destIP If you know the bridge IP address, provide it here.
165 * @param port The bridge port
166 * @param keepAliveInterval The keep alive interval. Must be between 100 and REG_TIMEOUT_MS milliseconds.
167 * if it is equal to REG_TIMEOUT_MS, then a new session will be established instead of renewing the
169 * @param pw The two "password" bytes for the bridge
171 public MilightV6SessionManager(String bridgeId, ISessionState observer, @Nullable InetAddress destIP, int port,
172 int keepAliveInterval, byte[] pw) {
173 this.bridgeId = bridgeId;
174 this.observer = observer;
175 this.destIP = destIP;
176 this.lastKnownIP = destIP;
178 this.keepAliveInterval = keepAliveInterval;
181 for (int i = 0; i < 6; ++i) {
182 bridgeMAC[i] = Integer.valueOf(bridgeId.substring(i * 2, i * 2 + 2), 16).byteValue();
184 if (keepAliveInterval < 100 || keepAliveInterval > TIMEOUT_MS) {
185 throw new IllegalArgumentException("keepAliveInterval not within given limits!");
188 sessionThread = new Thread(this, "SessionThread");
192 * Start the session thread if it is not already running
194 public CompletableFuture<DatagramSocket> start() {
196 CompletableFuture<DatagramSocket> f = new CompletableFuture<>();
197 f.completeExceptionally(new IllegalStateException("will be closed"));
200 if (sessionThread.isAlive()) {
201 DatagramSocket s = datagramSocket;
203 return CompletableFuture.completedFuture(s);
206 CompletableFuture<DatagramSocket> f = new CompletableFuture<>();
208 sessionThread.start();
213 * You have to call that if you are done with this object. Cleans up the receive thread.
216 public void close() throws IOException {
221 final DatagramSocket socket = datagramSocket;
222 if (socket != null) {
225 sessionThread.interrupt();
227 sessionThread.join();
228 } catch (InterruptedException e) {
232 // Set the session id bytes for bridge access. Usually they are acquired automatically
233 // during the session handshake.
234 public void setSessionID(byte[] sid) {
235 this.sid[0] = sid[0];
236 this.sid[1] = sid[1];
237 sessionState = SessionState.SESSION_NEED_REGISTER;
240 // Return the session bytes as hex string
241 public String getSession() {
242 return String.format("%02X %02X", this.sid[0], this.sid[1]);
245 public Instant getLastSessionValidConfirmation() {
246 return lastSessionConfirmed;
249 // Get a new sequence number. Add that to a queue of used sequence numbers.
250 // The bridge response will remove the queued number. This method also checks
251 // for non confirmed sequence numbers older that 2 seconds and report them.
252 public int getNextSequenceNo() {
253 int currentSequenceNo = this.sequenceNo;
254 usedSequenceNo.put(currentSequenceNo, Instant.now());
256 return currentSequenceNo;
259 public static byte firstSeqByte(int seq) {
260 return (byte) (seq & 0xff);
263 public static byte secondSeqByte(int seq) {
264 return (byte) ((seq >> 8) & 0xff);
268 * Send a search for bridgeID packet on all network interfaces.
269 * This is used for the initial way to determine the IP of the bridge as well
270 * as if the IP of a bridge has changed and the session got invalid because of that.
272 * A response will assign us session bytes.
274 * @throws InterruptedException
276 @SuppressWarnings({ "null", "unused" })
277 private void sendSearchForBroadcast(DatagramSocket datagramSocket) {
278 byte[] t = new byte[] { (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x0A, (byte) 0x02,
279 clientSID1, clientSID2, (byte) 0x01, bridgeMAC[0], bridgeMAC[1], bridgeMAC[2], bridgeMAC[3],
280 bridgeMAC[4], bridgeMAC[5] };
281 if (lastKnownIP != null) {
283 datagramSocket.send(new DatagramPacket(t, t.length, lastKnownIP, port));
284 } catch (IOException e) {
285 logger.warn("Could not send discover packet! {}", e.getLocalizedMessage());
290 Enumeration<NetworkInterface> enumNetworkInterfaces;
292 enumNetworkInterfaces = NetworkInterface.getNetworkInterfaces();
293 } catch (SocketException socketException) {
294 logger.warn("Could not enumerate network interfaces for sending the discover packet!", socketException);
297 DatagramPacket packet = new DatagramPacket(t, t.length, lastKnownIP, port);
298 while (enumNetworkInterfaces.hasMoreElements()) {
299 NetworkInterface networkInterface = enumNetworkInterfaces.nextElement();
300 Iterator<InterfaceAddress> it = networkInterface.getInterfaceAddresses().iterator();
301 while (it.hasNext()) {
302 InterfaceAddress address = it.next();
303 if (address == null) {
306 InetAddress broadcast = address.getBroadcast();
307 if (broadcast != null && !address.getAddress().isLoopbackAddress()) {
308 packet.setAddress(broadcast);
310 datagramSocket.send(packet);
311 } catch (IOException e) {
312 logger.warn("Could not send discovery packet! {}", e.getLocalizedMessage());
319 // Search for a specific bridge (our bridge). A response will assign us session bytes.
320 // private void send_search_for() {
321 // sendQueue.queue(AbstractBulbInterface.CAT_SESSION, searchForPacket());
324 private void sendEstablishSession(DatagramSocket datagramSocket) throws IOException {
325 final InetAddress address = lastKnownIP;
326 if (address == null) {
329 byte unknown = (byte) 0x1E; // Either checksum or counter. Was 64 and 1e so far.
330 byte[] t = { (byte) 0x20, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x16, (byte) 0x02, (byte) 0x62,
331 (byte) 0x3A, (byte) 0xD5, (byte) 0xED, (byte) 0xA3, (byte) 0x01, (byte) 0xAE, (byte) 0x08, (byte) 0x2D,
332 (byte) 0x46, (byte) 0x61, (byte) 0x41, (byte) 0xA7, (byte) 0xF6, (byte) 0xDC, (byte) 0xAF, clientSID1,
333 clientSID2, (byte) 0x00, (byte) 0x00, unknown };
335 datagramSocket.send(new DatagramPacket(t, t.length, address, port));
338 // Some apps first send {@see send_establish_session} and with the aquired session bytes they
339 // subsequently send this command for establishing the session. This is not well documented unfortunately.
340 @SuppressWarnings("unused")
341 private void sendPreRegistration(DatagramSocket datagramSocket) throws IOException {
342 final InetAddress address = lastKnownIP;
343 if (address == null) {
346 byte[] t = { 0x30, 0, 0, 0, 3, sid[0], sid[1], 1, 0 };
347 datagramSocket.send(new DatagramPacket(t, t.length, address, port));
350 // After the bridges knows our client session bytes and we know the bridge session bytes, we do a final
351 // registration with this command. The response will again contain the bridge ID and the session should
352 // be established by then.
353 private void sendRegistration(DatagramSocket datagramSocket) throws IOException {
354 final InetAddress address = lastKnownIP;
355 if (address == null) {
359 int seq = getNextSequenceNo();
360 byte[] t = { (byte) 0x80, 0x00, 0x00, 0x00, 0x11, sid[0], sid[1], firstSeqByte(seq), secondSeqByte(seq), 0x00,
361 0x33, pw[0], pw[1], 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) (0x33 + pw[0] + pw[1]) };
362 datagramSocket.send(new DatagramPacket(t, t.length, address, port));
366 * Constructs a 0x80... command which us used for all colour,brightness,saturation,mode operations.
367 * The session ID, password and sequence number is automatically inserted from this object.
369 * Produces data like:
370 * SN: Sequence number
373 * P1/P2: Password bytes
374 * WB: Remote (08) or iBox integrated bulb (00)
375 * ZN: Zone {Zone1-4 0=All}
379 * @ 80 00 00 00 11 84 00 00 0c 00 31 00 00 08 04 01 00 00 00 01 00 3f
382 * CC: Color value (hue)
383 * 80 00 00 00 11 S1 S2 SN SN 00 31 P1 P2 WB 01 CC CC CC CC ZN 00 CK
385 * 80 00 00 00 11 D4 00 00 12 00 31 00 00 08 01 FF FF FF FF 01 00 38
389 public byte[] makeCommand(byte wb, int zone, int... data) {
390 int seq = getNextSequenceNo();
391 byte[] t = { (byte) 0x80, 0x00, 0x00, 0x00, 0x11, sid[0], sid[1], MilightV6SessionManager.firstSeqByte(seq),
392 MilightV6SessionManager.secondSeqByte(seq), 0x00, 0x31, pw[0], pw[1], wb, 0, 0, 0, 0, 0, (byte) zone, 0,
395 for (int i = 0; i < data.length; ++i) {
396 t[14 + i] = (byte) data[i];
399 byte chksum = (byte) (t[10 + 0] + t[10 + 1] + t[10 + 2] + t[10 + 3] + t[10 + 4] + t[10 + 5] + t[10 + 6]
400 + t[10 + 7] + t[10 + 8] + zone);
406 * Constructs a 0x3D or 0x3E link/unlink command.
407 * The session ID, password and sequence number is automatically inserted from this object.
409 * WB: Remote (08) or iBox integrated bulb (00)
411 public byte[] makeLink(byte wb, int zone, boolean link) {
412 int seq = getNextSequenceNo();
413 byte[] t = { (link ? (byte) 0x3D : (byte) 0x3E), 0x00, 0x00, 0x00, 0x11, sid[0], sid[1],
414 MilightV6SessionManager.firstSeqByte(seq), MilightV6SessionManager.secondSeqByte(seq), 0x00, 0x31,
415 pw[0], pw[1], wb, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) zone, 0x00, 0x00 };
417 byte chksum = (byte) (t[10 + 0] + t[10 + 1] + t[10 + 2] + t[10 + 3] + t[10 + 4] + t[10 + 5] + t[10 + 6]
418 + t[10 + 7] + t[10 + 8] + zone);
424 * The main state machine of the session handshake.
426 * @throws InterruptedException
427 * @throws IOException
429 private void sessionStateMachine(DatagramSocket datagramSocket, StateMachineInput input) throws IOException {
430 final SessionState lastSessionState = sessionState;
433 final Instant current = Instant.now();
434 final Duration timeElapsed = Duration.between(lastSessionConfirmed, current);
435 if (timeElapsed.toMillis() > TIMEOUT_MS) {
436 if (sessionState != SessionState.SESSION_WAIT_FOR_BRIDGE) {
437 logger.warn("Session timeout!");
439 // One reason we failed, might be that a last known IP is not correct anymore.
440 // Reset to the given dest IP (which might be null).
441 lastKnownIP = destIP;
442 sessionState = SessionState.SESSION_INVALID;
445 if (input == StateMachineInput.INVALID_COMMAND) {
446 sessionState = SessionState.SESSION_INVALID;
450 for (Iterator<Map.Entry<Integer, Instant>> it = usedSequenceNo.entrySet().iterator(); it.hasNext();) {
451 Map.Entry<Integer, Instant> entry = it.next();
452 if (Duration.between(entry.getValue(), current).toMillis() > MAX_PACKET_IN_FLIGHT_MS) {
453 logger.debug("Command not confirmed: {}", entry.getKey());
458 switch (sessionState) {
459 case SESSION_INVALID:
460 usedSequenceNo.clear();
461 sessionState = SessionState.SESSION_WAIT_FOR_BRIDGE;
462 lastSessionConfirmed = Instant.now();
463 case SESSION_WAIT_FOR_BRIDGE:
464 if (input == StateMachineInput.BRIDGE_CONFIRMED) {
465 sessionState = SessionState.SESSION_WAIT_FOR_SESSION_SID;
467 datagramSocket.setSoTimeout(150);
468 sendSearchForBroadcast(datagramSocket);
471 case SESSION_WAIT_FOR_SESSION_SID:
472 if (input == StateMachineInput.SESSION_ID_RECEIVED) {
473 if (ProtocolConstants.DEBUG_SESSION) {
474 logger.debug("Session ID received: {}", String.format("%02X %02X", this.sid[0], this.sid[1]));
476 sessionState = SessionState.SESSION_NEED_REGISTER;
478 datagramSocket.setSoTimeout(300);
479 sendEstablishSession(datagramSocket);
482 case SESSION_NEED_REGISTER:
483 if (input == StateMachineInput.SESSION_ESTABLISHED) {
484 sessionState = SessionState.SESSION_VALID;
485 lastSessionConfirmed = Instant.now();
486 if (ProtocolConstants.DEBUG_SESSION) {
487 logger.debug("Registration complete");
490 datagramSocket.setSoTimeout(300);
491 sendRegistration(datagramSocket);
494 case SESSION_VALID_KEEP_ALIVE:
496 if (input == StateMachineInput.KEEP_ALIVE_RECEIVED) {
497 lastSessionConfirmed = Instant.now();
498 observer.sessionStateChanged(SessionState.SESSION_VALID_KEEP_ALIVE, lastKnownIP);
500 final InetAddress address = lastKnownIP;
501 if (keepAliveInterval > 0 && timeElapsed.toMillis() > keepAliveInterval && address != null) {
503 byte[] t = { (byte) 0xD0, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x02, sid[0], sid[1] };
504 datagramSocket.send(new DatagramPacket(t, t.length, address, port));
506 // Increase socket timeout to wake up for the next keep alive interval
507 datagramSocket.setSoTimeout(keepAliveInterval);
512 if (lastSessionState != sessionState) {
513 observer.sessionStateChanged(sessionState, lastKnownIP);
517 private void logUnknownPacket(byte[] data, int len, String reason) {
518 StringBuilder s = new StringBuilder();
519 for (int i = 0; i < len; ++i) {
520 s.append(String.format("%02X ", data[i]));
523 s.append(String.format("%02X ", clientSID1));
524 s.append(String.format("%02X ", clientSID2));
525 logger.info("{} ({}): {}", reason, bridgeId, s);
529 * The session thread executes this run() method and a blocking UDP receive
530 * is performed in a loop.
532 @SuppressWarnings({ "null", "unused" })
535 try (DatagramSocket datagramSocket = new DatagramSocket(null)) {
536 this.datagramSocket = datagramSocket;
537 datagramSocket.setBroadcast(true);
538 datagramSocket.setReuseAddress(true);
539 datagramSocket.setSoTimeout(150);
540 datagramSocket.bind(null);
542 if (ProtocolConstants.DEBUG_SESSION) {
543 logger.debug("MilightCommunicationV6 receive thread ready");
546 // Inform the start future about the datagram socket
547 CompletableFuture<DatagramSocket> f = startFuture;
549 f.complete(datagramSocket);
553 byte[] buffer = new byte[1024];
554 DatagramPacket rPacket = new DatagramPacket(buffer, buffer.length);
556 sessionStateMachine(datagramSocket, StateMachineInput.NO_INPUT);
558 // Now loop forever, waiting to receive packets and printing them.
559 while (!willbeclosed) {
560 rPacket.setLength(buffer.length);
562 datagramSocket.receive(rPacket);
563 } catch (SocketTimeoutException e) {
564 sessionStateMachine(datagramSocket, StateMachineInput.TIMEOUT);
567 int len = rPacket.getLength();
569 if (len < 5 || buffer[1] != 0 || buffer[2] != 0 || buffer[3] != 0) {
570 logUnknownPacket(buffer, len, "Not an iBox response!");
574 int expectedLen = buffer[4] + 5;
576 if (expectedLen > len) {
577 logUnknownPacket(buffer, len, "Unexpected size!");
581 // 13 00 00 00 0A 03 D3 54 11 (AC CF 23 F5 7A D4)
583 boolean eq = ByteBuffer.wrap(bridgeMAC, 0, 6).equals(ByteBuffer.wrap(buffer, 9, 6));
585 logger.debug("TODO: Feedback required");
586 // I have no clue what that packet means. But the bridge is going to timeout the next
587 // keep alive and it is a good idea to start the session again.
589 logger.info("Unknown 0x13 received, but not for our bridge ({})", bridgeId);
593 // 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
594 // 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
596 // ASCII string contained: 985b157bf6fc43368a63467ea3b19d0dc .. xlink_dev
597 // Response to the v6 SEARCH and the SEARCH FOR commands to look for new or known devices.
598 // Our session id will be transfered in this process (!= bridge session id)
600 boolean eq = ByteBuffer.wrap(bridgeMAC, 0, 6).equals(ByteBuffer.wrap(buffer, 6, 6));
602 if (ProtocolConstants.DEBUG_SESSION) {
603 logger.debug("Session ID reestablished");
605 lastKnownIP = rPacket.getAddress();
606 sessionStateMachine(datagramSocket, StateMachineInput.BRIDGE_CONFIRMED);
608 logger.info("Session ID received, but not for our bridge ({})", bridgeId);
609 logUnknownPacket(buffer, len, "ID not matching");
614 // 28 00 00 00 11 00 02 (AC CF 23 F5 7A D4) 50 AA 4D 2A 00 01 SS_ID 00
615 // Response to the keepAlive() packet if session is not valid yet.
616 // Should contain the session ids
618 boolean eq = ByteBuffer.wrap(bridgeMAC, 0, 6).equals(ByteBuffer.wrap(buffer, 7, 6));
620 this.sid[0] = buffer[19];
621 this.sid[1] = buffer[20];
622 sessionStateMachine(datagramSocket, StateMachineInput.SESSION_ID_RECEIVED);
624 logger.info("Session ID received, but not for our bridge ({})", bridgeId);
625 logUnknownPacket(buffer, len, "ID not matching");
630 // 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
631 // Response to the registration packet
633 boolean eq = ByteBuffer.wrap(bridgeMAC, 0, 6).equals(ByteBuffer.wrap(buffer, 5, 6));
635 sessionStateMachine(datagramSocket, StateMachineInput.SESSION_ESTABLISHED);
637 logger.info("Registration received, but not for our bridge ({})", bridgeId);
638 logUnknownPacket(buffer, len, "ID not matching");
642 // 88 00 00 00 03 SN SN OK // two byte sequence number, we use the later one only.
643 // OK: is 00 if ok or 01 if failed
645 int seq = Byte.toUnsignedInt(buffer[6]) + Byte.toUnsignedInt(buffer[7]) * 256;
646 Instant timePacketWasSend = usedSequenceNo.remove(seq);
647 if (timePacketWasSend != null) {
648 if (ProtocolConstants.DEBUG_SESSION) {
649 logger.debug("Confirmation received for command: {}", String.valueOf(seq));
651 if (buffer[8] == 1) {
652 logger.warn("Command {} failed", seq);
655 // another participant might have established a session from the same host
656 logger.info("Confirmation received for unsend command. Sequence number: {}",
657 String.valueOf(seq));
660 // D8 00 00 00 07 (AC CF 23 F5 7A D4) 01
661 // Response to the keepAlive() packet
663 boolean eq = ByteBuffer.wrap(bridgeMAC, 0, 6).equals(ByteBuffer.wrap(buffer, 5, 6));
665 sessionStateMachine(datagramSocket, StateMachineInput.KEEP_ALIVE_RECEIVED);
667 logger.info("Keep alive received but not for our bridge ({})", bridgeId);
668 logUnknownPacket(buffer, len, "ID not matching");
673 logUnknownPacket(buffer, len, "No valid start byte");
676 } catch (IOException e) {
678 logger.warn("Session Manager receive thread failed: {}", e.getLocalizedMessage(), e);
681 this.datagramSocket = null;
683 if (ProtocolConstants.DEBUG_SESSION) {
684 logger.debug("MilightCommunicationV6 receive thread stopped");
688 // Return true if the session is established successfully
689 public boolean isValid() {
690 return sessionState == SessionState.SESSION_VALID;