]> git.basschouten.com Git - openhab-addons.git/blob
72ea73b14e24ee518b57a6e821a5cea0dc705392
[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.bondhome.internal.api;
14
15 import static java.nio.charset.StandardCharsets.*;
16 import static org.openhab.binding.bondhome.internal.BondHomeBindingConstants.*;
17
18 import java.io.IOException;
19 import java.net.DatagramPacket;
20 import java.net.DatagramSocket;
21 import java.net.InetAddress;
22 import java.net.SocketException;
23 import java.net.SocketTimeoutException;
24 import java.util.concurrent.Executor;
25
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.bondhome.internal.handler.BondBridgeHandler;
29 import org.openhab.core.thing.ThingStatusDetail;
30 import org.slf4j.Logger;
31 import org.slf4j.LoggerFactory;
32
33 import com.google.gson.Gson;
34 import com.google.gson.GsonBuilder;
35 import com.google.gson.JsonParseException;
36
37 /**
38  * This Thread is responsible maintaining the Bond Push UDP Protocol
39  *
40  * @author Sara Geleskie Damiano - Initial contribution
41  *
42  */
43 @NonNullByDefault
44 public class BPUPListener implements Runnable {
45
46     private static final int SOCKET_TIMEOUT_MILLISECONDS = 3000;
47     private static final int SOCKET_RETRY_TIMEOUT_MILLISECONDS = 3000;
48
49     private final Logger logger = LoggerFactory.getLogger(BPUPListener.class);
50
51     // To parse the JSON responses
52     private final Gson gsonBuilder;
53
54     // Used for callbacks to handler
55     private final BondBridgeHandler bridgeHandler;
56
57     // UDP socket used to receive status events
58     private @Nullable DatagramSocket socket;
59
60     public @Nullable String lastRequestId;
61     private long timeOfLastKeepAlivePacket;
62     private boolean shutdown;
63
64     private int numberOfKeepAliveTimeouts;
65
66     /**
67      * Constructor of the receiver runnable thread.
68      *
69      * @param bridgeHandler The handler of the Bond Bridge
70      * @throws SocketException is some problem occurs opening the socket.
71      */
72     public BPUPListener(BondBridgeHandler bridgeHandler) {
73         logger.debug("Starting BPUP Listener...");
74
75         this.bridgeHandler = bridgeHandler;
76         this.timeOfLastKeepAlivePacket = -1;
77         this.numberOfKeepAliveTimeouts = 0;
78
79         GsonBuilder gsonBuilder = new GsonBuilder();
80         gsonBuilder.excludeFieldsWithoutExposeAnnotation();
81         Gson gson = gsonBuilder.create();
82         this.gsonBuilder = gson;
83         this.shutdown = true;
84     }
85
86     public boolean isRunning() {
87         return !shutdown;
88     }
89
90     public void start(Executor executor) {
91         shutdown = false;
92         executor.execute(this);
93     }
94
95     /**
96      * Send keep-alive as necessary and listen for push messages
97      */
98     @Override
99     public void run() {
100         receivePackets();
101     }
102
103     /**
104      * Gracefully shutdown thread. Worst case takes TIMEOUT_TO_DATAGRAM_RECEPTION to
105      * shutdown.
106      */
107     public void shutdown() {
108         this.shutdown = true;
109         DatagramSocket s = this.socket;
110         if (s != null) {
111             s.close();
112             logger.debug("Listener closed socket");
113             this.socket = null;
114         }
115     }
116
117     private void sendBPUPKeepAlive() {
118         // Create a buffer and packet for the response
119         byte[] buffer = new byte[256];
120         DatagramPacket inPacket = new DatagramPacket(buffer, buffer.length);
121
122         DatagramSocket sock = this.socket;
123         if (sock != null) {
124             logger.trace("Sending keep-alive request ('\\n')");
125             try {
126                 byte[] outBuffer = { (byte) '\n' };
127                 InetAddress inetAddress = InetAddress.getByName(bridgeHandler.getBridgeIpAddress());
128                 DatagramPacket outPacket = new DatagramPacket(outBuffer, 1, inetAddress, BOND_BPUP_PORT);
129                 sock.send(outPacket);
130                 sock.receive(inPacket);
131                 BPUPUpdate response = transformUpdatePacket(inPacket);
132                 if (response != null) {
133                     @Nullable
134                     String bondId = response.bondId;
135                     if (bondId == null || !bondId.equalsIgnoreCase(bridgeHandler.getBridgeId())) {
136                         logger.warn("Response isn't from expected Bridge!  Expected: {}  Got: {}",
137                                 bridgeHandler.getBridgeId(), bondId);
138                     } else {
139                         bridgeHandler.setBridgeOnline(inPacket.getAddress().getHostAddress());
140                         numberOfKeepAliveTimeouts = 0;
141                     }
142                 }
143             } catch (SocketTimeoutException e) {
144                 numberOfKeepAliveTimeouts++;
145                 logger.trace("BPUP Socket timeout, number of timeouts: {}", numberOfKeepAliveTimeouts);
146                 if (numberOfKeepAliveTimeouts > 10) {
147                     bridgeHandler.setBridgeOffline(ThingStatusDetail.COMMUNICATION_ERROR,
148                             "@text/offline.comm-error.timeout");
149                 }
150             } catch (IOException e) {
151                 logger.debug("One exception has occurred", e);
152             }
153         }
154     }
155
156     private void receivePackets() {
157         try {
158             DatagramSocket s = new DatagramSocket(null);
159             s.setSoTimeout(SOCKET_TIMEOUT_MILLISECONDS);
160             s.bind(null);
161             socket = s;
162             logger.debug("Listener created UDP socket on port {} with timeout {}", s.getPort(),
163                     SOCKET_TIMEOUT_MILLISECONDS);
164         } catch (SocketException e) {
165             logger.debug("Listener got SocketException", e);
166             datagramSocketHealthRoutine();
167         }
168
169         // Create a buffer and packet for the response
170         byte[] buffer = new byte[256];
171         DatagramPacket inPacket = new DatagramPacket(buffer, buffer.length);
172
173         DatagramSocket sock = this.socket;
174         while (sock != null && !this.shutdown) {
175             // Check if we're due to send something to keep the connection
176             long now = System.nanoTime() / 1000000L;
177             long timePassedFromLastKeepAlive = now - timeOfLastKeepAlivePacket;
178
179             if (timeOfLastKeepAlivePacket == -1 || timePassedFromLastKeepAlive >= 60000L) {
180                 sendBPUPKeepAlive();
181                 timeOfLastKeepAlivePacket = now;
182             }
183
184             try {
185                 sock.receive(inPacket);
186                 processPacket(inPacket);
187             } catch (SocketTimeoutException e) {
188                 // Ignore. Means there was no updates while we waited.
189                 // We'll just loop around and try again after sending a keep alive.
190             } catch (IOException e) {
191                 logger.debug("Listener got IOException waiting for datagram: {}", e.getMessage());
192                 datagramSocketHealthRoutine();
193             }
194         }
195         logger.debug("Listener exiting");
196     }
197
198     private void processPacket(DatagramPacket packet) {
199         logger.trace("Got datagram of length {} from {}", packet.getLength(), packet.getAddress().getHostAddress());
200
201         BPUPUpdate update = transformUpdatePacket(packet);
202         if (update != null) {
203             if (!update.bondId.equalsIgnoreCase(bridgeHandler.getBridgeId())) {
204                 logger.warn("Response isn't from expected Bridge!  Expected: {}  Got: {}", bridgeHandler.getBridgeId(),
205                         update.bondId);
206             }
207
208             // Check for duplicate packet
209             if (isDuplicate(update)) {
210                 logger.trace("Dropping duplicate packet");
211                 return;
212             }
213
214             // Send the update the the bridge for it to pass on to the devices
215             if (update.topic != null) {
216                 logger.trace("Forwarding message to bridge handler");
217                 bridgeHandler.forwardUpdateToThing(update);
218             } else {
219                 logger.debug("No topic in incoming message!");
220             }
221         }
222     }
223
224     /**
225      * Method that transforms {@link DatagramPacket} to a {@link BPUPUpdate} Object
226      *
227      * @param packet the {@link DatagramPacket}
228      * @return the {@link BPUPUpdate}
229      */
230     public @Nullable BPUPUpdate transformUpdatePacket(final DatagramPacket packet) {
231         String responseJson = new String(packet.getData(), 0, packet.getLength(), UTF_8);
232         logger.debug("Message from {}:{} -> {}", packet.getAddress().getHostAddress(), packet.getPort(), responseJson);
233
234         @Nullable
235         BPUPUpdate response = null;
236         try {
237             response = this.gsonBuilder.fromJson(responseJson, BPUPUpdate.class);
238         } catch (JsonParseException e) {
239             logger.warn("Error parsing json! {}", e.getMessage());
240         }
241         return response;
242     }
243
244     private boolean isDuplicate(BPUPUpdate update) {
245         boolean packetIsDuplicate = false;
246         String newReqestId = update.requestId;
247         String lastRequestId = this.lastRequestId;
248         if (lastRequestId != null && newReqestId != null) {
249             if (lastRequestId.equalsIgnoreCase(newReqestId)) {
250                 packetIsDuplicate = true;
251             }
252         }
253         // Remember this packet for duplicate check
254         lastRequestId = newReqestId;
255         return packetIsDuplicate;
256     }
257
258     private void datagramSocketHealthRoutine() {
259         @Nullable
260         DatagramSocket datagramSocket = this.socket;
261         if (datagramSocket == null || (datagramSocket.isClosed() || !datagramSocket.isConnected())) {
262             logger.trace("Datagram Socket is disconnected or has been closed, reconnecting...");
263             try {
264                 // close the socket before trying to reopen
265                 if (datagramSocket != null) {
266                     datagramSocket.close();
267                 }
268                 logger.trace("Old socket closed.");
269                 try {
270                     Thread.sleep(SOCKET_RETRY_TIMEOUT_MILLISECONDS);
271                 } catch (InterruptedException e) {
272                     Thread.currentThread().interrupt();
273                 }
274                 DatagramSocket s = new DatagramSocket(null);
275                 s.setSoTimeout(SOCKET_TIMEOUT_MILLISECONDS);
276                 s.bind(null);
277                 this.socket = s;
278                 logger.trace("Datagram Socket reconnected using port {}.", s.getPort());
279             } catch (SocketException exception) {
280                 logger.warn("Problem creating new socket : {}", exception.getLocalizedMessage());
281             }
282         }
283     }
284 }