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:
373 * SN: Sequence number
376 * P1/P2: Password bytes
377 * WB: Remote (08) or iBox integrated bulb (00)
378 * ZN: Zone {Zone1-4 0=All}
382 * @ 80 00 00 00 11 84 00 00 0c 00 31 00 00 08 04 01 00 00 00 01 00 3f
385 * CC: Color value (hue)
386 * 80 00 00 00 11 S1 S2 SN SN 00 31 P1 P2 WB 01 CC CC CC CC ZN 00 CK
388 * 80 00 00 00 11 D4 00 00 12 00 31 00 00 08 01 FF FF FF FF 01 00 38
394 public byte[] makeCommand(byte wb, int zone, int... data) {
395 int seq = getNextSequenceNo();
396 byte[] t = { (byte) 0x80, 0x00, 0x00, 0x00, 0x11, sid[0], sid[1], MilightV6SessionManager.firstSeqByte(seq),
397 MilightV6SessionManager.secondSeqByte(seq), 0x00, 0x31, pw[0], pw[1], wb, 0, 0, 0, 0, 0, (byte) zone, 0,
400 for (int i = 0; i < data.length; ++i) {
401 t[14 + i] = (byte) data[i];
404 byte chksum = (byte) (t[10 + 0] + t[10 + 1] + t[10 + 2] + t[10 + 3] + t[10 + 4] + t[10 + 5] + t[10 + 6]
405 + t[10 + 7] + t[10 + 8] + zone);
411 * Constructs a 0x3D or 0x3E link/unlink command.
412 * The session ID, password and sequence number is automatically inserted from this object.
414 * WB: Remote (08) or iBox integrated bulb (00)
416 public byte[] makeLink(byte wb, int zone, boolean link) {
417 int seq = getNextSequenceNo();
418 byte[] t = { (link ? (byte) 0x3D : (byte) 0x3E), 0x00, 0x00, 0x00, 0x11, sid[0], sid[1],
419 MilightV6SessionManager.firstSeqByte(seq), MilightV6SessionManager.secondSeqByte(seq), 0x00, 0x31,
420 pw[0], pw[1], wb, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) zone, 0x00, 0x00 };
422 byte chksum = (byte) (t[10 + 0] + t[10 + 1] + t[10 + 2] + t[10 + 3] + t[10 + 4] + t[10 + 5] + t[10 + 6]
423 + t[10 + 7] + t[10 + 8] + zone);
429 * The main state machine of the session handshake.
431 * @throws InterruptedException
432 * @throws IOException
434 private void sessionStateMachine(DatagramSocket datagramSocket, StateMachineInput input) throws IOException {
435 final SessionState lastSessionState = sessionState;
438 final Instant current = Instant.now();
439 final Duration timeElapsed = Duration.between(lastSessionConfirmed, current);
440 if (timeElapsed.toMillis() > TIMEOUT_MS) {
441 if (sessionState != SessionState.SESSION_WAIT_FOR_BRIDGE) {
442 logger.warn("Session timeout!");
444 // One reason we failed, might be that a last known IP is not correct anymore.
445 // Reset to the given dest IP (which might be null).
446 lastKnownIP = destIP;
447 sessionState = SessionState.SESSION_INVALID;
450 if (input == StateMachineInput.INVALID_COMMAND) {
451 sessionState = SessionState.SESSION_INVALID;
455 for (Iterator<Map.Entry<Integer, Instant>> it = usedSequenceNo.entrySet().iterator(); it.hasNext();) {
456 Map.Entry<Integer, Instant> entry = it.next();
457 if (Duration.between(entry.getValue(), current).toMillis() > MAX_PACKET_IN_FLIGHT_MS) {
458 logger.debug("Command not confirmed: {}", entry.getKey());
463 switch (sessionState) {
464 case SESSION_INVALID:
465 usedSequenceNo.clear();
466 sessionState = SessionState.SESSION_WAIT_FOR_BRIDGE;
467 lastSessionConfirmed = Instant.now();
468 case SESSION_WAIT_FOR_BRIDGE:
469 if (input == StateMachineInput.BRIDGE_CONFIRMED) {
470 sessionState = SessionState.SESSION_WAIT_FOR_SESSION_SID;
472 datagramSocket.setSoTimeout(150);
473 sendSearchForBroadcast(datagramSocket);
476 case SESSION_WAIT_FOR_SESSION_SID:
477 if (input == StateMachineInput.SESSION_ID_RECEIVED) {
478 if (ProtocolConstants.DEBUG_SESSION) {
479 logger.debug("Session ID received: {}", String.format("%02X %02X", this.sid[0], this.sid[1]));
481 sessionState = SessionState.SESSION_NEED_REGISTER;
483 datagramSocket.setSoTimeout(300);
484 sendEstablishSession(datagramSocket);
487 case SESSION_NEED_REGISTER:
488 if (input == StateMachineInput.SESSION_ESTABLISHED) {
489 sessionState = SessionState.SESSION_VALID;
490 lastSessionConfirmed = Instant.now();
491 if (ProtocolConstants.DEBUG_SESSION) {
492 logger.debug("Registration complete");
495 datagramSocket.setSoTimeout(300);
496 sendRegistration(datagramSocket);
499 case SESSION_VALID_KEEP_ALIVE:
501 if (input == StateMachineInput.KEEP_ALIVE_RECEIVED) {
502 lastSessionConfirmed = Instant.now();
503 observer.sessionStateChanged(SessionState.SESSION_VALID_KEEP_ALIVE, lastKnownIP);
505 final InetAddress address = lastKnownIP;
506 if (keepAliveInterval > 0 && timeElapsed.toMillis() > keepAliveInterval && address != null) {
508 byte[] t = { (byte) 0xD0, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x02, sid[0], sid[1] };
509 datagramSocket.send(new DatagramPacket(t, t.length, address, port));
511 // Increase socket timeout to wake up for the next keep alive interval
512 datagramSocket.setSoTimeout(keepAliveInterval);
517 if (lastSessionState != sessionState) {
518 observer.sessionStateChanged(sessionState, lastKnownIP);
522 private void logUnknownPacket(byte[] data, int len, String reason) {
523 StringBuilder s = new StringBuilder();
524 for (int i = 0; i < len; ++i) {
525 s.append(String.format("%02X ", data[i]));
528 s.append(String.format("%02X ", clientSID1));
529 s.append(String.format("%02X ", clientSID2));
530 logger.info("{} ({}): {}", reason, bridgeId, s);
534 * The session thread executes this run() method and a blocking UDP receive
535 * is performed in a loop.
537 @SuppressWarnings({ "null", "unused" })
540 try (DatagramSocket datagramSocket = new DatagramSocket(null)) {
541 this.datagramSocket = datagramSocket;
542 datagramSocket.setBroadcast(true);
543 datagramSocket.setReuseAddress(true);
544 datagramSocket.setSoTimeout(150);
545 datagramSocket.bind(null);
547 if (ProtocolConstants.DEBUG_SESSION) {
548 logger.debug("MilightCommunicationV6 receive thread ready");
551 // Inform the start future about the datagram socket
552 CompletableFuture<DatagramSocket> f = startFuture;
554 f.complete(datagramSocket);
558 byte[] buffer = new byte[1024];
559 DatagramPacket rPacket = new DatagramPacket(buffer, buffer.length);
561 sessionStateMachine(datagramSocket, StateMachineInput.NO_INPUT);
563 // Now loop forever, waiting to receive packets and printing them.
564 while (!willbeclosed) {
565 rPacket.setLength(buffer.length);
567 datagramSocket.receive(rPacket);
568 } catch (SocketTimeoutException e) {
569 sessionStateMachine(datagramSocket, StateMachineInput.TIMEOUT);
572 int len = rPacket.getLength();
574 if (len < 5 || buffer[1] != 0 || buffer[2] != 0 || buffer[3] != 0) {
575 logUnknownPacket(buffer, len, "Not an iBox response!");
579 int expectedLen = buffer[4] + 5;
581 if (expectedLen > len) {
582 logUnknownPacket(buffer, len, "Unexpected size!");
586 // 13 00 00 00 0A 03 D3 54 11 (AC CF 23 F5 7A D4)
588 boolean eq = ByteBuffer.wrap(bridgeMAC, 0, 6).equals(ByteBuffer.wrap(buffer, 9, 6));
590 logger.debug("TODO: Feedback required");
591 // I have no clue what that packet means. But the bridge is going to timeout the next
592 // keep alive and it is a good idea to start the session again.
594 logger.info("Unknown 0x13 received, but not for our bridge ({})", bridgeId);
598 // 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
599 // 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
601 // ASCII string contained: 985b157bf6fc43368a63467ea3b19d0dc .. xlink_dev
602 // Response to the v6 SEARCH and the SEARCH FOR commands to look for new or known devices.
603 // Our session id will be transfered in this process (!= bridge session id)
605 boolean eq = ByteBuffer.wrap(bridgeMAC, 0, 6).equals(ByteBuffer.wrap(buffer, 6, 6));
607 if (ProtocolConstants.DEBUG_SESSION) {
608 logger.debug("Session ID reestablished");
610 lastKnownIP = rPacket.getAddress();
611 sessionStateMachine(datagramSocket, StateMachineInput.BRIDGE_CONFIRMED);
613 logger.info("Session ID received, but not for our bridge ({})", bridgeId);
614 logUnknownPacket(buffer, len, "ID not matching");
619 // 28 00 00 00 11 00 02 (AC CF 23 F5 7A D4) 50 AA 4D 2A 00 01 SS_ID 00
620 // Response to the keepAlive() packet if session is not valid yet.
621 // Should contain the session ids
623 boolean eq = ByteBuffer.wrap(bridgeMAC, 0, 6).equals(ByteBuffer.wrap(buffer, 7, 6));
625 this.sid[0] = buffer[19];
626 this.sid[1] = buffer[20];
627 sessionStateMachine(datagramSocket, StateMachineInput.SESSION_ID_RECEIVED);
629 logger.info("Session ID received, but not for our bridge ({})", bridgeId);
630 logUnknownPacket(buffer, len, "ID not matching");
635 // 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
636 // Response to the registration packet
638 boolean eq = ByteBuffer.wrap(bridgeMAC, 0, 6).equals(ByteBuffer.wrap(buffer, 5, 6));
640 sessionStateMachine(datagramSocket, StateMachineInput.SESSION_ESTABLISHED);
642 logger.info("Registration received, but not for our bridge ({})", bridgeId);
643 logUnknownPacket(buffer, len, "ID not matching");
647 // 88 00 00 00 03 SN SN OK // two byte sequence number, we use the later one only.
648 // OK: is 00 if ok or 01 if failed
650 int seq = Byte.toUnsignedInt(buffer[6]) + Byte.toUnsignedInt(buffer[7]) * 256;
651 Instant timePacketWasSend = usedSequenceNo.remove(seq);
652 if (timePacketWasSend != null) {
653 if (ProtocolConstants.DEBUG_SESSION) {
654 logger.debug("Confirmation received for command: {}", String.valueOf(seq));
656 if (buffer[8] == 1) {
657 logger.warn("Command {} failed", seq);
660 // another participant might have established a session from the same host
661 logger.info("Confirmation received for unsend command. Sequence number: {}",
662 String.valueOf(seq));
665 // D8 00 00 00 07 (AC CF 23 F5 7A D4) 01
666 // Response to the keepAlive() packet
668 boolean eq = ByteBuffer.wrap(bridgeMAC, 0, 6).equals(ByteBuffer.wrap(buffer, 5, 6));
670 sessionStateMachine(datagramSocket, StateMachineInput.KEEP_ALIVE_RECEIVED);
672 logger.info("Keep alive received but not for our bridge ({})", bridgeId);
673 logUnknownPacket(buffer, len, "ID not matching");
678 logUnknownPacket(buffer, len, "No valid start byte");
681 } catch (IOException e) {
683 logger.warn("Session Manager receive thread failed: {}", e.getLocalizedMessage(), e);
686 this.datagramSocket = null;
688 if (ProtocolConstants.DEBUG_SESSION) {
689 logger.debug("MilightCommunicationV6 receive thread stopped");
693 // Return true if the session is established successfully
694 public boolean isValid() {
695 return sessionState == SessionState.SESSION_VALID;