]> git.basschouten.com Git - openhab-addons.git/blob
13d1e06b154ce173b4c9be65677f64a8b74bbd76
[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      * SN: Sequence number
371      * S1: SessionID1
372      * S2: SessionID2
373      * P1/P2: Password bytes
374      * WB: Remote (08) or iBox integrated bulb (00)
375      * ZN: Zone {Zone1-4 0=All}
376      * CK: Checksum
377      *
378      * #zone 1 on
379      * @ 80 00 00 00 11 84 00 00 0c 00 31 00 00 08 04 01 00 00 00 01 00 3f
380      *
381      * Colors:
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
384      *
385      * 80 00 00 00 11 D4 00 00 12 00 31 00 00 08 01 FF FF FF FF 01 00 38
386      *
387      * @return
388      */
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,
393                 0 };
394
395         for (int i = 0; i < data.length; ++i) {
396             t[14 + i] = (byte) data[i];
397         }
398
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);
401         t[21] = chksum;
402         return t;
403     }
404
405     /**
406      * Constructs a 0x3D or 0x3E link/unlink command.
407      * The session ID, password and sequence number is automatically inserted from this object.
408      *
409      * WB: Remote (08) or iBox integrated bulb (00)
410      */
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 };
416
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);
419         t[21] = chksum;
420         return t;
421     }
422
423     /**
424      * The main state machine of the session handshake.
425      *
426      * @throws InterruptedException
427      * @throws IOException
428      */
429     private void sessionStateMachine(DatagramSocket datagramSocket, StateMachineInput input) throws IOException {
430         final SessionState lastSessionState = sessionState;
431
432         // Check for timeout
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!");
438             }
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;
443         }
444
445         if (input == StateMachineInput.INVALID_COMMAND) {
446             sessionState = SessionState.SESSION_INVALID;
447         }
448
449         // Check old seq no:
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());
454                 it.remove();
455             }
456         }
457
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;
466                 } else {
467                     datagramSocket.setSoTimeout(150);
468                     sendSearchForBroadcast(datagramSocket);
469                     break;
470                 }
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]));
475                     }
476                     sessionState = SessionState.SESSION_NEED_REGISTER;
477                 } else {
478                     datagramSocket.setSoTimeout(300);
479                     sendEstablishSession(datagramSocket);
480                     break;
481                 }
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");
488                     }
489                 } else {
490                     datagramSocket.setSoTimeout(300);
491                     sendRegistration(datagramSocket);
492                     break;
493                 }
494             case SESSION_VALID_KEEP_ALIVE:
495             case SESSION_VALID:
496                 if (input == StateMachineInput.KEEP_ALIVE_RECEIVED) {
497                     lastSessionConfirmed = Instant.now();
498                     observer.sessionStateChanged(SessionState.SESSION_VALID_KEEP_ALIVE, lastKnownIP);
499                 } else {
500                     final InetAddress address = lastKnownIP;
501                     if (keepAliveInterval > 0 && timeElapsed.toMillis() > keepAliveInterval && address != null) {
502                         // Send keep alive
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));
505                     }
506                     // Increase socket timeout to wake up for the next keep alive interval
507                     datagramSocket.setSoTimeout(keepAliveInterval);
508                 }
509                 break;
510         }
511
512         if (lastSessionState != sessionState) {
513             observer.sessionStateChanged(sessionState, lastKnownIP);
514         }
515     }
516
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]));
521         }
522         s.append("Sid: ");
523         s.append(String.format("%02X ", clientSID1));
524         s.append(String.format("%02X ", clientSID2));
525         logger.info("{} ({}): {}", reason, bridgeId, s);
526     }
527
528     /**
529      * The session thread executes this run() method and a blocking UDP receive
530      * is performed in a loop.
531      */
532     @SuppressWarnings({ "null", "unused" })
533     @Override
534     public void run() {
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);
541
542             if (ProtocolConstants.DEBUG_SESSION) {
543                 logger.debug("MilightCommunicationV6 receive thread ready");
544             }
545
546             // Inform the start future about the datagram socket
547             CompletableFuture<DatagramSocket> f = startFuture;
548             if (f != null) {
549                 f.complete(datagramSocket);
550                 startFuture = null;
551             }
552
553             byte[] buffer = new byte[1024];
554             DatagramPacket rPacket = new DatagramPacket(buffer, buffer.length);
555
556             sessionStateMachine(datagramSocket, StateMachineInput.NO_INPUT);
557
558             // Now loop forever, waiting to receive packets and printing them.
559             while (!willbeclosed) {
560                 rPacket.setLength(buffer.length);
561                 try {
562                     datagramSocket.receive(rPacket);
563                 } catch (SocketTimeoutException e) {
564                     sessionStateMachine(datagramSocket, StateMachineInput.TIMEOUT);
565                     continue;
566                 }
567                 int len = rPacket.getLength();
568
569                 if (len < 5 || buffer[1] != 0 || buffer[2] != 0 || buffer[3] != 0) {
570                     logUnknownPacket(buffer, len, "Not an iBox response!");
571                     continue;
572                 }
573
574                 int expectedLen = buffer[4] + 5;
575
576                 if (expectedLen > len) {
577                     logUnknownPacket(buffer, len, "Unexpected size!");
578                     continue;
579                 }
580                 switch (buffer[0]) {
581                     // 13 00 00 00 0A 03 D3 54 11 (AC CF 23 F5 7A D4)
582                     case (byte) 0x13: {
583                         boolean eq = ByteBuffer.wrap(bridgeMAC, 0, 6).equals(ByteBuffer.wrap(buffer, 9, 6));
584                         if (eq) {
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.
588                         } else {
589                             logger.info("Unknown 0x13 received, but not for our bridge ({})", bridgeId);
590                         }
591                         break;
592                     }
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
595                     // 76 07 5B CD 15
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)
599                     case (byte) 0x18: {
600                         boolean eq = ByteBuffer.wrap(bridgeMAC, 0, 6).equals(ByteBuffer.wrap(buffer, 6, 6));
601                         if (eq) {
602                             if (ProtocolConstants.DEBUG_SESSION) {
603                                 logger.debug("Session ID reestablished");
604                             }
605                             lastKnownIP = rPacket.getAddress();
606                             sessionStateMachine(datagramSocket, StateMachineInput.BRIDGE_CONFIRMED);
607                         } else {
608                             logger.info("Session ID received, but not for our bridge ({})", bridgeId);
609                             logUnknownPacket(buffer, len, "ID not matching");
610                         }
611
612                         break;
613                     }
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
617                     case (byte) 0x28: {
618                         boolean eq = ByteBuffer.wrap(bridgeMAC, 0, 6).equals(ByteBuffer.wrap(buffer, 7, 6));
619                         if (eq) {
620                             this.sid[0] = buffer[19];
621                             this.sid[1] = buffer[20];
622                             sessionStateMachine(datagramSocket, StateMachineInput.SESSION_ID_RECEIVED);
623                         } else {
624                             logger.info("Session ID received, but not for our bridge ({})", bridgeId);
625                             logUnknownPacket(buffer, len, "ID not matching");
626                         }
627
628                         break;
629                     }
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
632                     case (byte) 0x80: {
633                         boolean eq = ByteBuffer.wrap(bridgeMAC, 0, 6).equals(ByteBuffer.wrap(buffer, 5, 6));
634                         if (eq) {
635                             sessionStateMachine(datagramSocket, StateMachineInput.SESSION_ESTABLISHED);
636                         } else {
637                             logger.info("Registration received, but not for our bridge ({})", bridgeId);
638                             logUnknownPacket(buffer, len, "ID not matching");
639                         }
640                         break;
641                     }
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
644                     case (byte) 0x88:
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));
650                             }
651                             if (buffer[8] == 1) {
652                                 logger.warn("Command {} failed", seq);
653                             }
654                         } else {
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));
658                         }
659                         break;
660                     // D8 00 00 00 07 (AC CF 23 F5 7A D4) 01
661                     // Response to the keepAlive() packet
662                     case (byte) 0xD8: {
663                         boolean eq = ByteBuffer.wrap(bridgeMAC, 0, 6).equals(ByteBuffer.wrap(buffer, 5, 6));
664                         if (eq) {
665                             sessionStateMachine(datagramSocket, StateMachineInput.KEEP_ALIVE_RECEIVED);
666                         } else {
667                             logger.info("Keep alive received but not for our bridge ({})", bridgeId);
668                             logUnknownPacket(buffer, len, "ID not matching");
669                         }
670                         break;
671                     }
672                     default:
673                         logUnknownPacket(buffer, len, "No valid start byte");
674                 }
675             }
676         } catch (IOException e) {
677             if (!willbeclosed) {
678                 logger.warn("Session Manager receive thread failed: {}", e.getLocalizedMessage(), e);
679             }
680         } finally {
681             this.datagramSocket = null;
682         }
683         if (ProtocolConstants.DEBUG_SESSION) {
684             logger.debug("MilightCommunicationV6 receive thread stopped");
685         }
686     }
687
688     // Return true if the session is established successfully
689     public boolean isValid() {
690         return sessionState == SessionState.SESSION_VALID;
691     }
692 }