2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.bondhome.internal.api;
15 import static java.nio.charset.StandardCharsets.*;
16 import static org.openhab.binding.bondhome.internal.BondHomeBindingConstants.*;
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;
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;
33 import com.google.gson.Gson;
34 import com.google.gson.GsonBuilder;
35 import com.google.gson.JsonParseException;
38 * This Thread is responsible maintaining the Bond Push UDP Protocol
40 * @author Sara Geleskie Damiano - Initial contribution
44 public class BPUPListener implements Runnable {
46 private static final int SOCKET_TIMEOUT_MILLISECONDS = 3000;
47 private static final int SOCKET_RETRY_TIMEOUT_MILLISECONDS = 3000;
49 private final Logger logger = LoggerFactory.getLogger(BPUPListener.class);
51 // To parse the JSON responses
52 private final Gson gsonBuilder;
54 // Used for callbacks to handler
55 private final BondBridgeHandler bridgeHandler;
57 // UDP socket used to receive status events
58 private @Nullable DatagramSocket socket;
60 public @Nullable String lastRequestId;
61 private long timeOfLastKeepAlivePacket;
62 private boolean shutdown;
64 private int numberOfKeepAliveTimeouts;
67 * Constructor of the receiver runnable thread.
69 * @param bridgeHandler The handler of the Bond Bridge
70 * @throws SocketException is some problem occurs opening the socket.
72 public BPUPListener(BondBridgeHandler bridgeHandler) {
73 logger.debug("Starting BPUP Listener...");
75 this.bridgeHandler = bridgeHandler;
76 this.timeOfLastKeepAlivePacket = -1;
77 this.numberOfKeepAliveTimeouts = 0;
79 GsonBuilder gsonBuilder = new GsonBuilder();
80 gsonBuilder.excludeFieldsWithoutExposeAnnotation();
81 Gson gson = gsonBuilder.create();
82 this.gsonBuilder = gson;
86 public boolean isRunning() {
90 public void start(Executor executor) {
92 executor.execute(this);
96 * Send keep-alive as necessary and listen for push messages
104 * Gracefully shutdown thread. Worst case takes TIMEOUT_TO_DATAGRAM_RECEPTION to
107 public void shutdown() {
108 this.shutdown = true;
109 DatagramSocket s = this.socket;
112 logger.debug("Listener closed socket");
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);
122 DatagramSocket sock = this.socket;
124 logger.trace("Sending keep-alive request ('\\n')");
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) {
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);
139 bridgeHandler.setBridgeOnline(inPacket.getAddress().getHostAddress());
140 numberOfKeepAliveTimeouts = 0;
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");
150 } catch (IOException e) {
151 logger.debug("One exception has occurred", e);
156 private void receivePackets() {
158 DatagramSocket s = new DatagramSocket(null);
159 s.setSoTimeout(SOCKET_TIMEOUT_MILLISECONDS);
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();
169 // Create a buffer and packet for the response
170 byte[] buffer = new byte[256];
171 DatagramPacket inPacket = new DatagramPacket(buffer, buffer.length);
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;
179 if (timeOfLastKeepAlivePacket == -1 || timePassedFromLastKeepAlive >= 60000L) {
181 timeOfLastKeepAlivePacket = now;
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();
195 logger.debug("Listener exiting");
198 private void processPacket(DatagramPacket packet) {
199 logger.trace("Got datagram of length {} from {}", packet.getLength(), packet.getAddress().getHostAddress());
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(),
208 // Check for duplicate packet
209 if (isDuplicate(update)) {
210 logger.trace("Dropping duplicate packet");
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);
219 logger.debug("No topic in incoming message!");
225 * Method that transforms {@link DatagramPacket} to a {@link BPUPUpdate} Object
227 * @param packet the {@link DatagramPacket}
228 * @return the {@link BPUPUpdate}
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);
235 BPUPUpdate response = null;
237 response = this.gsonBuilder.fromJson(responseJson, BPUPUpdate.class);
238 } catch (JsonParseException e) {
239 logger.warn("Error parsing json! {}", e.getMessage());
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;
253 // Remember this packet for duplicate check
254 lastRequestId = newReqestId;
255 return packetIsDuplicate;
258 private void datagramSocketHealthRoutine() {
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...");
264 // close the socket before trying to reopen
265 if (datagramSocket != null) {
266 datagramSocket.close();
268 logger.trace("Old socket closed.");
270 Thread.sleep(SOCKET_RETRY_TIMEOUT_MILLISECONDS);
271 } catch (InterruptedException e) {
272 Thread.currentThread().interrupt();
274 DatagramSocket s = new DatagramSocket(null);
275 s.setSoTimeout(SOCKET_TIMEOUT_MILLISECONDS);
278 logger.trace("Datagram Socket reconnected using port {}.", s.getPort());
279 } catch (SocketException exception) {
280 logger.warn("Problem creating new socket : {}", exception.getLocalizedMessage());