]> git.basschouten.com Git - openhab-addons.git/blob
fe3f274fd3739c3709da0edd2c43e474d4708c13
[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#clientSID1} and see {@link MilightV6SessionManager#clientSID2}.
48  *
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.
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 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
168      *            current one.
169      * @param pw The two "password" bytes for the bridge
170      */
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;
177         this.port = port;
178         this.keepAliveInterval = keepAliveInterval;
179         this.pw[0] = pw[0];
180         this.pw[1] = pw[1];
181         for (int i = 0; i < 6; ++i) {
182             bridgeMAC[i] = Integer.valueOf(bridgeId.substring(i * 2, i * 2 + 2), 16).byteValue();
183         }
184         if (keepAliveInterval < 100 || keepAliveInterval > TIMEOUT_MS) {
185             throw new IllegalArgumentException("keepAliveInterval not within given limits!");
186         }
187
188         sessionThread = new Thread(this, "SessionThread");
189     }
190
191     /**
192      * Start the session thread if it is not already running
193      */
194     public CompletableFuture<DatagramSocket> start() {
195         if (willbeclosed) {
196             CompletableFuture<DatagramSocket> f = new CompletableFuture<>();
197             f.completeExceptionally(new IllegalStateException("will be closed"));
198             return f;
199         }
200         if (sessionThread.isAlive()) {
201             DatagramSocket s = datagramSocket;
202             assert s != null;
203             return CompletableFuture.completedFuture(s);
204         }
205
206         CompletableFuture<DatagramSocket> f = new CompletableFuture<>();
207         startFuture = f;
208         sessionThread.start();
209         return f;
210     }
211
212     /**
213      * You have to call that if you are done with this object. Cleans up the receive thread.
214      */
215     @Override
216     public void close() throws IOException {
217         if (willbeclosed) {
218             return;
219         }
220         willbeclosed = true;
221         final DatagramSocket socket = datagramSocket;
222         if (socket != null) {
223             socket.close();
224         }
225         sessionThread.interrupt();
226         try {
227             sessionThread.join();
228         } catch (InterruptedException e) {
229         }
230     }
231
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;
238     }
239
240     // Return the session bytes as hex string
241     public String getSession() {
242         return String.format("%02X %02X", this.sid[0], this.sid[1]);
243     }
244
245     public Instant getLastSessionValidConfirmation() {
246         return lastSessionConfirmed;
247     }
248
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());
255         ++sequenceNo;
256         return currentSequenceNo;
257     }
258
259     public static byte firstSeqByte(int seq) {
260         return (byte) (seq & 0xff);
261     }
262
263     public static byte secondSeqByte(int seq) {
264         return (byte) ((seq >> 8) & 0xff);
265     }
266
267     /**
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.
271      *
272      * A response will assign us session bytes.
273      *
274      * @throws InterruptedException
275      */
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) {
282             try {
283                 datagramSocket.send(new DatagramPacket(t, t.length, lastKnownIP, port));
284             } catch (IOException e) {
285                 logger.warn("Could not send discover packet! {}", e.getLocalizedMessage());
286             }
287             return;
288         }
289
290         Enumeration<NetworkInterface> enumNetworkInterfaces;
291         try {
292             enumNetworkInterfaces = NetworkInterface.getNetworkInterfaces();
293         } catch (SocketException socketException) {
294             logger.warn("Could not enumerate network interfaces for sending the discover packet!", socketException);
295             return;
296         }
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) {
304                     continue;
305                 }
306                 InetAddress broadcast = address.getBroadcast();
307                 if (broadcast != null && !address.getAddress().isLoopbackAddress()) {
308                     packet.setAddress(broadcast);
309                     try {
310                         datagramSocket.send(packet);
311                     } catch (IOException e) {
312                         logger.warn("Could not send discovery packet! {}", e.getLocalizedMessage());
313                     }
314                 }
315             }
316         }
317     }
318
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());
322     // }
323
324     private void sendEstablishSession(DatagramSocket datagramSocket) throws IOException {
325         final InetAddress address = lastKnownIP;
326         if (address == null) {
327             return;
328         }
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 };
334
335         datagramSocket.send(new DatagramPacket(t, t.length, address, port));
336     }
337
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) {
344             return;
345         }
346         byte[] t = { 0x30, 0, 0, 0, 3, sid[0], sid[1], 1, 0 };
347         datagramSocket.send(new DatagramPacket(t, t.length, address, port));
348     }
349
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) {
356             return;
357         }
358
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));
363     }
364
365     /**
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.
368      *
369      * Produces data like:
370      * 
371      * <pre>
372      * {@code
373      * SN: Sequence number
374      * S1: SessionID1
375      * S2: SessionID2
376      * P1/P2: Password bytes
377      * WB: Remote (08) or iBox integrated bulb (00)
378      * ZN: Zone {Zone1-4 0=All}
379      * CK: Checksum
380      *
381      * #zone 1 on
382      * &#64; 80 00 00 00 11 84 00 00 0c 00 31 00 00 08 04 01 00 00 00 01 00 3f
383      *
384      * Colors:
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
387      *
388      * 80 00 00 00 11 D4 00 00 12 00 31 00 00 08 01 FF FF FF FF 01 00 38
389      * }
390      * </pre>
391      *
392      * @return
393      */
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,
398                 0 };
399
400         for (int i = 0; i < data.length; ++i) {
401             t[14 + i] = (byte) data[i];
402         }
403
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);
406         t[21] = chksum;
407         return t;
408     }
409
410     /**
411      * Constructs a 0x3D or 0x3E link/unlink command.
412      * The session ID, password and sequence number is automatically inserted from this object.
413      *
414      * WB: Remote (08) or iBox integrated bulb (00)
415      */
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 };
421
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);
424         t[21] = chksum;
425         return t;
426     }
427
428     /**
429      * The main state machine of the session handshake.
430      *
431      * @throws InterruptedException
432      * @throws IOException
433      */
434     private void sessionStateMachine(DatagramSocket datagramSocket, StateMachineInput input) throws IOException {
435         final SessionState lastSessionState = sessionState;
436
437         // Check for timeout
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!");
443             }
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;
448         }
449
450         if (input == StateMachineInput.INVALID_COMMAND) {
451             sessionState = SessionState.SESSION_INVALID;
452         }
453
454         // Check old seq no:
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());
459                 it.remove();
460             }
461         }
462
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;
471                 } else {
472                     datagramSocket.setSoTimeout(150);
473                     sendSearchForBroadcast(datagramSocket);
474                     break;
475                 }
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]));
480                     }
481                     sessionState = SessionState.SESSION_NEED_REGISTER;
482                 } else {
483                     datagramSocket.setSoTimeout(300);
484                     sendEstablishSession(datagramSocket);
485                     break;
486                 }
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");
493                     }
494                 } else {
495                     datagramSocket.setSoTimeout(300);
496                     sendRegistration(datagramSocket);
497                     break;
498                 }
499             case SESSION_VALID_KEEP_ALIVE:
500             case SESSION_VALID:
501                 if (input == StateMachineInput.KEEP_ALIVE_RECEIVED) {
502                     lastSessionConfirmed = Instant.now();
503                     observer.sessionStateChanged(SessionState.SESSION_VALID_KEEP_ALIVE, lastKnownIP);
504                 } else {
505                     final InetAddress address = lastKnownIP;
506                     if (keepAliveInterval > 0 && timeElapsed.toMillis() > keepAliveInterval && address != null) {
507                         // Send keep alive
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));
510                     }
511                     // Increase socket timeout to wake up for the next keep alive interval
512                     datagramSocket.setSoTimeout(keepAliveInterval);
513                 }
514                 break;
515         }
516
517         if (lastSessionState != sessionState) {
518             observer.sessionStateChanged(sessionState, lastKnownIP);
519         }
520     }
521
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]));
526         }
527         s.append("Sid: ");
528         s.append(String.format("%02X ", clientSID1));
529         s.append(String.format("%02X ", clientSID2));
530         logger.info("{} ({}): {}", reason, bridgeId, s);
531     }
532
533     /**
534      * The session thread executes this run() method and a blocking UDP receive
535      * is performed in a loop.
536      */
537     @SuppressWarnings({ "null", "unused" })
538     @Override
539     public void run() {
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);
546
547             if (ProtocolConstants.DEBUG_SESSION) {
548                 logger.debug("MilightCommunicationV6 receive thread ready");
549             }
550
551             // Inform the start future about the datagram socket
552             CompletableFuture<DatagramSocket> f = startFuture;
553             if (f != null) {
554                 f.complete(datagramSocket);
555                 startFuture = null;
556             }
557
558             byte[] buffer = new byte[1024];
559             DatagramPacket rPacket = new DatagramPacket(buffer, buffer.length);
560
561             sessionStateMachine(datagramSocket, StateMachineInput.NO_INPUT);
562
563             // Now loop forever, waiting to receive packets and printing them.
564             while (!willbeclosed) {
565                 rPacket.setLength(buffer.length);
566                 try {
567                     datagramSocket.receive(rPacket);
568                 } catch (SocketTimeoutException e) {
569                     sessionStateMachine(datagramSocket, StateMachineInput.TIMEOUT);
570                     continue;
571                 }
572                 int len = rPacket.getLength();
573
574                 if (len < 5 || buffer[1] != 0 || buffer[2] != 0 || buffer[3] != 0) {
575                     logUnknownPacket(buffer, len, "Not an iBox response!");
576                     continue;
577                 }
578
579                 int expectedLen = buffer[4] + 5;
580
581                 if (expectedLen > len) {
582                     logUnknownPacket(buffer, len, "Unexpected size!");
583                     continue;
584                 }
585                 switch (buffer[0]) {
586                     // 13 00 00 00 0A 03 D3 54 11 (AC CF 23 F5 7A D4)
587                     case (byte) 0x13: {
588                         boolean eq = ByteBuffer.wrap(bridgeMAC, 0, 6).equals(ByteBuffer.wrap(buffer, 9, 6));
589                         if (eq) {
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.
593                         } else {
594                             logger.info("Unknown 0x13 received, but not for our bridge ({})", bridgeId);
595                         }
596                         break;
597                     }
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
600                     // 76 07 5B CD 15
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)
604                     case (byte) 0x18: {
605                         boolean eq = ByteBuffer.wrap(bridgeMAC, 0, 6).equals(ByteBuffer.wrap(buffer, 6, 6));
606                         if (eq) {
607                             if (ProtocolConstants.DEBUG_SESSION) {
608                                 logger.debug("Session ID reestablished");
609                             }
610                             lastKnownIP = rPacket.getAddress();
611                             sessionStateMachine(datagramSocket, StateMachineInput.BRIDGE_CONFIRMED);
612                         } else {
613                             logger.info("Session ID received, but not for our bridge ({})", bridgeId);
614                             logUnknownPacket(buffer, len, "ID not matching");
615                         }
616
617                         break;
618                     }
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
622                     case (byte) 0x28: {
623                         boolean eq = ByteBuffer.wrap(bridgeMAC, 0, 6).equals(ByteBuffer.wrap(buffer, 7, 6));
624                         if (eq) {
625                             this.sid[0] = buffer[19];
626                             this.sid[1] = buffer[20];
627                             sessionStateMachine(datagramSocket, StateMachineInput.SESSION_ID_RECEIVED);
628                         } else {
629                             logger.info("Session ID received, but not for our bridge ({})", bridgeId);
630                             logUnknownPacket(buffer, len, "ID not matching");
631                         }
632
633                         break;
634                     }
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
637                     case (byte) 0x80: {
638                         boolean eq = ByteBuffer.wrap(bridgeMAC, 0, 6).equals(ByteBuffer.wrap(buffer, 5, 6));
639                         if (eq) {
640                             sessionStateMachine(datagramSocket, StateMachineInput.SESSION_ESTABLISHED);
641                         } else {
642                             logger.info("Registration received, but not for our bridge ({})", bridgeId);
643                             logUnknownPacket(buffer, len, "ID not matching");
644                         }
645                         break;
646                     }
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
649                     case (byte) 0x88:
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));
655                             }
656                             if (buffer[8] == 1) {
657                                 logger.warn("Command {} failed", seq);
658                             }
659                         } else {
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));
663                         }
664                         break;
665                     // D8 00 00 00 07 (AC CF 23 F5 7A D4) 01
666                     // Response to the keepAlive() packet
667                     case (byte) 0xD8: {
668                         boolean eq = ByteBuffer.wrap(bridgeMAC, 0, 6).equals(ByteBuffer.wrap(buffer, 5, 6));
669                         if (eq) {
670                             sessionStateMachine(datagramSocket, StateMachineInput.KEEP_ALIVE_RECEIVED);
671                         } else {
672                             logger.info("Keep alive received but not for our bridge ({})", bridgeId);
673                             logUnknownPacket(buffer, len, "ID not matching");
674                         }
675                         break;
676                     }
677                     default:
678                         logUnknownPacket(buffer, len, "No valid start byte");
679                 }
680             }
681         } catch (IOException e) {
682             if (!willbeclosed) {
683                 logger.warn("Session Manager receive thread failed: {}", e.getLocalizedMessage(), e);
684             }
685         } finally {
686             this.datagramSocket = null;
687         }
688         if (ProtocolConstants.DEBUG_SESSION) {
689             logger.debug("MilightCommunicationV6 receive thread stopped");
690         }
691     }
692
693     // Return true if the session is established successfully
694     public boolean isValid() {
695         return sessionState == SessionState.SESSION_VALID;
696     }
697 }