]> git.basschouten.com Git - openhab-addons.git/blob
0eb77637629d4bd0cf16c4150d9d7a5cdb3c2ec4
[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.protocol;
14
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;
29 import java.util.Map;
30 import java.util.TreeMap;
31 import java.util.concurrent.CompletableFuture;
32
33 import org.eclipse.jdt.annotation.NonNullByDefault;
34 import org.eclipse.jdt.annotation.Nullable;
35 import org.slf4j.Logger;
36 import org.slf4j.LoggerFactory;
37
38 /**
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.
41  *
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.
45  *
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}.
48  *
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.
51  *
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.
55  *
56  * @author David Graeff - Initial contribution
57  */
58 @NonNullByDefault
59 public class MilightV6SessionManager implements Runnable, Closeable {
60     protected final Logger logger = LoggerFactory.getLogger(MilightV6SessionManager.class);
61
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;
65
66     // Password bytes 1 and 2
67     public byte[] pw = { 0, 0 };
68
69     // Session bytes 1 and 2
70     public byte[] sid = { 0, 0 };
71
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;
75
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 };
78
79     /**
80      * The session handshake is a 3 way handshake.
81      */
82     public enum SessionState {
83         // No session established and nothing in progress
84         SESSION_INVALID,
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
92         SESSION_VALID,
93         // The session is still active, a keep alive was just received.
94         SESSION_VALID_KEEP_ALIVE,
95     }
96
97     public enum StateMachineInput {
98         NO_INPUT,
99         TIMEOUT,
100         INVALID_COMMAND,
101         KEEP_ALIVE_RECEIVED,
102         BRIDGE_CONFIRMED,
103         SESSION_ID_RECEIVED,
104         SESSION_ESTABLISHED,
105     }
106
107     private SessionState sessionState = SessionState.SESSION_INVALID;
108
109     // Implement this interface to get notifications about the current session state.
110     public interface ISessionState {
111         /**
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}.
115          *
116          * @param state The new state
117          * @param address The remote IP address. Only guaranteed to be non null in the SESSION_VALID* states.
118          */
119         void sessionStateChanged(SessionState state, @Nullable InetAddress address);
120     }
121
122     private final ISessionState observer;
123
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;
132
133     private final String bridgeId;
134     private @Nullable DatagramSocket datagramSocket;
135     private @Nullable CompletableFuture<DatagramSocket> startFuture;
136
137     /**
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.
141      */
142     private @Nullable final InetAddress destIP;
143     /**
144      * We cache the last known IP to avoid using broadcast.
145      */
146     private @Nullable InetAddress lastKnownIP;
147
148     private final int port;
149
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;
156
157     /**
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.
160      *
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
170      *            current one.
171      * @param pw The two "password" bytes for the bridge
172      */
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;
179         this.port = port;
180         this.keepAliveInterval = keepAliveInterval;
181         this.pw[0] = pw[0];
182         this.pw[1] = pw[1];
183         for (int i = 0; i < 6; ++i) {
184             bridgeMAC[i] = Integer.valueOf(bridgeId.substring(i * 2, i * 2 + 2), 16).byteValue();
185         }
186         if (keepAliveInterval < 100 || keepAliveInterval > TIMEOUT_MS) {
187             throw new IllegalArgumentException("keepAliveInterval not within given limits!");
188         }
189
190         sessionThread = new Thread(this, "SessionThread");
191     }
192
193     /**
194      * Start the session thread if it is not already running
195      */
196     public CompletableFuture<DatagramSocket> start() {
197         if (willbeclosed) {
198             CompletableFuture<DatagramSocket> f = new CompletableFuture<>();
199             f.completeExceptionally(new IllegalStateException("will be closed"));
200             return f;
201         }
202         if (sessionThread.isAlive()) {
203             DatagramSocket s = datagramSocket;
204             assert s != null;
205             return CompletableFuture.completedFuture(s);
206         }
207
208         CompletableFuture<DatagramSocket> f = new CompletableFuture<>();
209         startFuture = f;
210         sessionThread.start();
211         return f;
212     }
213
214     /**
215      * You have to call that if you are done with this object. Cleans up the receive thread.
216      */
217     @Override
218     public void close() throws IOException {
219         if (willbeclosed) {
220             return;
221         }
222         willbeclosed = true;
223         final DatagramSocket socket = datagramSocket;
224         if (socket != null) {
225             socket.close();
226         }
227         sessionThread.interrupt();
228         try {
229             sessionThread.join();
230         } catch (InterruptedException e) {
231         }
232     }
233
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;
240     }
241
242     // Return the session bytes as hex string
243     public String getSession() {
244         return String.format("%02X %02X", this.sid[0], this.sid[1]);
245     }
246
247     public Instant getLastSessionValidConfirmation() {
248         return lastSessionConfirmed;
249     }
250
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());
257         ++sequenceNo;
258         return currentSequenceNo;
259     }
260
261     public static byte firstSeqByte(int seq) {
262         return (byte) (seq & 0xff);
263     }
264
265     public static byte secondSeqByte(int seq) {
266         return (byte) ((seq >> 8) & 0xff);
267     }
268
269     /**
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.
273      *
274      * A response will assign us session bytes.
275      *
276      * @throws InterruptedException
277      */
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) {
284             try {
285                 datagramSocket.send(new DatagramPacket(t, t.length, lastKnownIP, port));
286             } catch (IOException e) {
287                 logger.warn("Could not send discover packet! {}", e.getLocalizedMessage());
288             }
289             return;
290         }
291
292         Enumeration<NetworkInterface> enumNetworkInterfaces;
293         try {
294             enumNetworkInterfaces = NetworkInterface.getNetworkInterfaces();
295         } catch (SocketException socketException) {
296             logger.warn("Could not enumerate network interfaces for sending the discover packet!", socketException);
297             return;
298         }
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) {
306                     continue;
307                 }
308                 InetAddress broadcast = address.getBroadcast();
309                 if (broadcast != null && !address.getAddress().isLoopbackAddress()) {
310                     packet.setAddress(broadcast);
311                     try {
312                         datagramSocket.send(packet);
313                     } catch (IOException e) {
314                         logger.warn("Could not send discovery packet! {}", e.getLocalizedMessage());
315                     }
316                 }
317             }
318         }
319     }
320
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());
324     // }
325
326     private void sendEstablishSession(DatagramSocket datagramSocket) throws IOException {
327         final InetAddress address = lastKnownIP;
328         if (address == null) {
329             return;
330         }
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 };
336
337         datagramSocket.send(new DatagramPacket(t, t.length, address, port));
338     }
339
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) {
346             return;
347         }
348         byte[] t = { 0x30, 0, 0, 0, 3, sid[0], sid[1], 1, 0 };
349         datagramSocket.send(new DatagramPacket(t, t.length, address, port));
350     }
351
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) {
358             return;
359         }
360
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));
365     }
366
367     /**
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.
370      *
371      * Produces data like:
372      * SN: Sequence number
373      * S1: SessionID1
374      * S2: SessionID2
375      * P1/P2: Password bytes
376      * WB: Remote (08) or iBox integrated bulb (00)
377      * ZN: Zone {Zone1-4 0=All}
378      * CK: Checksum
379      *
380      * #zone 1 on
381      * @ 80 00 00 00 11 84 00 00 0c 00 31 00 00 08 04 01 00 00 00 01 00 3f
382      *
383      * Colors:
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
386      *
387      * 80 00 00 00 11 D4 00 00 12 00 31 00 00 08 01 FF FF FF FF 01 00 38
388      *
389      * @return
390      */
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,
395                 0 };
396
397         for (int i = 0; i < data.length; ++i) {
398             t[14 + i] = (byte) data[i];
399         }
400
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);
403         t[21] = chksum;
404         return t;
405     }
406
407     /**
408      * Constructs a 0x3D or 0x3E link/unlink command.
409      * The session ID, password and sequence number is automatically inserted from this object.
410      *
411      * WB: Remote (08) or iBox integrated bulb (00)
412      */
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 };
418
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);
421         t[21] = chksum;
422         return t;
423     }
424
425     /**
426      * The main state machine of the session handshake.
427      *
428      * @throws InterruptedException
429      * @throws IOException
430      */
431     private void sessionStateMachine(DatagramSocket datagramSocket, StateMachineInput input) throws IOException {
432         final SessionState lastSessionState = sessionState;
433
434         // Check for timeout
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!");
440             }
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;
445         }
446
447         if (input == StateMachineInput.INVALID_COMMAND) {
448             sessionState = SessionState.SESSION_INVALID;
449         }
450
451         // Check old seq no:
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());
456                 it.remove();
457             }
458         }
459
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;
468                 } else {
469                     datagramSocket.setSoTimeout(150);
470                     sendSearchForBroadcast(datagramSocket);
471                     break;
472                 }
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]));
477                     }
478                     sessionState = SessionState.SESSION_NEED_REGISTER;
479                 } else {
480                     datagramSocket.setSoTimeout(300);
481                     sendEstablishSession(datagramSocket);
482                     break;
483                 }
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");
490                     }
491                 } else {
492                     datagramSocket.setSoTimeout(300);
493                     sendRegistration(datagramSocket);
494                     break;
495                 }
496             case SESSION_VALID_KEEP_ALIVE:
497             case SESSION_VALID:
498                 if (input == StateMachineInput.KEEP_ALIVE_RECEIVED) {
499                     lastSessionConfirmed = Instant.now();
500                     observer.sessionStateChanged(SessionState.SESSION_VALID_KEEP_ALIVE, lastKnownIP);
501                 } else {
502                     final InetAddress address = lastKnownIP;
503                     if (keepAliveInterval > 0 && timeElapsed.toMillis() > keepAliveInterval && address != null) {
504                         // Send keep alive
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));
507                     }
508                     // Increase socket timeout to wake up for the next keep alive interval
509                     datagramSocket.setSoTimeout(keepAliveInterval);
510                 }
511                 break;
512         }
513
514         if (lastSessionState != sessionState) {
515             observer.sessionStateChanged(sessionState, lastKnownIP);
516         }
517     }
518
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]));
523         }
524         s.append("Sid: ");
525         s.append(String.format("%02X ", clientSID1));
526         s.append(String.format("%02X ", clientSID2));
527         logger.info("{} ({}): {}", reason, bridgeId, s);
528     }
529
530     /**
531      * The session thread executes this run() method and a blocking UDP receive
532      * is performed in a loop.
533      */
534     @SuppressWarnings({ "null", "unused" })
535     @Override
536     public void run() {
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);
543
544             if (ProtocolConstants.DEBUG_SESSION) {
545                 logger.debug("MilightCommunicationV6 receive thread ready");
546             }
547
548             // Inform the start future about the datagram socket
549             CompletableFuture<DatagramSocket> f = startFuture;
550             if (f != null) {
551                 f.complete(datagramSocket);
552                 startFuture = null;
553             }
554
555             byte[] buffer = new byte[1024];
556             DatagramPacket rPacket = new DatagramPacket(buffer, buffer.length);
557
558             sessionStateMachine(datagramSocket, StateMachineInput.NO_INPUT);
559
560             // Now loop forever, waiting to receive packets and printing them.
561             while (!willbeclosed) {
562                 rPacket.setLength(buffer.length);
563                 try {
564                     datagramSocket.receive(rPacket);
565                 } catch (SocketTimeoutException e) {
566                     sessionStateMachine(datagramSocket, StateMachineInput.TIMEOUT);
567                     continue;
568                 }
569                 int len = rPacket.getLength();
570
571                 if (len < 5 || buffer[1] != 0 || buffer[2] != 0 || buffer[3] != 0) {
572                     logUnknownPacket(buffer, len, "Not an iBox response!");
573                     continue;
574                 }
575
576                 int expectedLen = buffer[4] + 5;
577
578                 if (expectedLen > len) {
579                     logUnknownPacket(buffer, len, "Unexpected size!");
580                     continue;
581                 }
582                 switch (buffer[0]) {
583                     // 13 00 00 00 0A 03 D3 54 11 (AC CF 23 F5 7A D4)
584                     case (byte) 0x13: {
585                         boolean eq = ByteBuffer.wrap(bridgeMAC, 0, 6).equals(ByteBuffer.wrap(buffer, 9, 6));
586                         if (eq) {
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.
590                         } else {
591                             logger.info("Unknown 0x13 received, but not for our bridge ({})", bridgeId);
592                         }
593                         break;
594                     }
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
597                     // 76 07 5B CD 15
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)
601                     case (byte) 0x18: {
602                         boolean eq = ByteBuffer.wrap(bridgeMAC, 0, 6).equals(ByteBuffer.wrap(buffer, 6, 6));
603                         if (eq) {
604                             if (ProtocolConstants.DEBUG_SESSION) {
605                                 logger.debug("Session ID reestablished");
606                             }
607                             lastKnownIP = rPacket.getAddress();
608                             sessionStateMachine(datagramSocket, StateMachineInput.BRIDGE_CONFIRMED);
609                         } else {
610                             logger.info("Session ID received, but not for our bridge ({})", bridgeId);
611                             logUnknownPacket(buffer, len, "ID not matching");
612                         }
613
614                         break;
615                     }
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
619                     case (byte) 0x28: {
620                         boolean eq = ByteBuffer.wrap(bridgeMAC, 0, 6).equals(ByteBuffer.wrap(buffer, 7, 6));
621                         if (eq) {
622                             this.sid[0] = buffer[19];
623                             this.sid[1] = buffer[20];
624                             sessionStateMachine(datagramSocket, StateMachineInput.SESSION_ID_RECEIVED);
625                         } else {
626                             logger.info("Session ID received, but not for our bridge ({})", bridgeId);
627                             logUnknownPacket(buffer, len, "ID not matching");
628                         }
629
630                         break;
631                     }
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
634                     case (byte) 0x80: {
635                         boolean eq = ByteBuffer.wrap(bridgeMAC, 0, 6).equals(ByteBuffer.wrap(buffer, 5, 6));
636                         if (eq) {
637                             sessionStateMachine(datagramSocket, StateMachineInput.SESSION_ESTABLISHED);
638                         } else {
639                             logger.info("Registration received, but not for our bridge ({})", bridgeId);
640                             logUnknownPacket(buffer, len, "ID not matching");
641                         }
642                         break;
643                     }
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
646                     case (byte) 0x88:
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));
652                             }
653                             if (buffer[8] == 1) {
654                                 logger.warn("Command {} failed", seq);
655                             }
656                         } else {
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));
660                         }
661                         break;
662                     // D8 00 00 00 07 (AC CF 23 F5 7A D4) 01
663                     // Response to the keepAlive() packet
664                     case (byte) 0xD8: {
665                         boolean eq = ByteBuffer.wrap(bridgeMAC, 0, 6).equals(ByteBuffer.wrap(buffer, 5, 6));
666                         if (eq) {
667                             sessionStateMachine(datagramSocket, StateMachineInput.KEEP_ALIVE_RECEIVED);
668                         } else {
669                             logger.info("Keep alive received but not for our bridge ({})", bridgeId);
670                             logUnknownPacket(buffer, len, "ID not matching");
671                         }
672                         break;
673                     }
674                     default:
675                         logUnknownPacket(buffer, len, "No valid start byte");
676                 }
677             }
678         } catch (IOException e) {
679             if (!willbeclosed) {
680                 logger.warn("Session Manager receive thread failed: {}", e.getLocalizedMessage(), e);
681             }
682         } finally {
683             this.datagramSocket = null;
684         }
685         if (ProtocolConstants.DEBUG_SESSION) {
686             logger.debug("MilightCommunicationV6 receive thread stopped");
687         }
688     }
689
690     // Return true if the session is established successfully
691     public boolean isValid() {
692         return sessionState == SessionState.SESSION_VALID;
693     }
694 }