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.deconz.internal.netutils;
17 import java.util.Objects;
18 import java.util.concurrent.ConcurrentHashMap;
19 import java.util.concurrent.ScheduledExecutorService;
20 import java.util.concurrent.ScheduledFuture;
21 import java.util.concurrent.TimeUnit;
22 import java.util.concurrent.atomic.AtomicInteger;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.eclipse.jetty.websocket.api.Session;
27 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
28 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
29 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
30 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
31 import org.eclipse.jetty.websocket.api.annotations.WebSocket;
32 import org.eclipse.jetty.websocket.client.WebSocketClient;
33 import org.openhab.binding.deconz.internal.dto.DeconzBaseMessage;
34 import org.openhab.binding.deconz.internal.types.ResourceType;
35 import org.openhab.core.common.ThreadPoolManager;
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
39 import com.google.gson.Gson;
42 * Establishes and keeps a websocket connection to the deCONZ software.
44 * The connection is closed by deCONZ now and then and needs to be re-established.
46 * @author David Graeff - Initial contribution
50 public class WebSocketConnection {
51 private static final AtomicInteger INSTANCE_COUNTER = new AtomicInteger();
52 private final Logger logger = LoggerFactory.getLogger(WebSocketConnection.class);
53 private final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool("thingHandler");
55 private final WebSocketClient client;
56 private final String socketName;
57 private final Gson gson;
58 private int watchdogInterval;
60 private final WebSocketConnectionListener connectionListener;
61 private final Map<String, WebSocketMessageListener> listeners = new ConcurrentHashMap<>();
63 private ConnectionState connectionState = ConnectionState.DISCONNECTED;
64 private @Nullable ScheduledFuture<?> watchdogJob;
66 private @Nullable Session session;
68 public WebSocketConnection(WebSocketConnectionListener listener, WebSocketClient client, Gson gson,
69 int watchdogInterval) {
70 this.connectionListener = listener;
72 this.client.setMaxIdleTimeout(0);
74 this.socketName = "Websocket$" + System.currentTimeMillis() + "-" + INSTANCE_COUNTER.incrementAndGet();
75 this.watchdogInterval = watchdogInterval;
78 public void setWatchdogInterval(int watchdogInterval) {
79 this.watchdogInterval = watchdogInterval;
82 public void start(String ip) {
83 if (connectionState == ConnectionState.CONNECTED) {
85 } else if (connectionState == ConnectionState.CONNECTING) {
86 logger.debug("{} already connecting", socketName);
88 } else if (connectionState == ConnectionState.DISCONNECTING) {
89 logger.warn("{} trying to re-connect while still disconnecting", socketName);
93 connectionState = ConnectionState.CONNECTING;
94 URI destUri = URI.create("ws://" + ip);
96 logger.debug("Trying to connect {} to {}", socketName, destUri);
97 client.connect(this, destUri).get();
98 } catch (Exception e) {
99 String reason = "Error while connecting: " + e.getMessage();
100 if (e.getMessage() == null) {
101 logger.warn("{}: {}", socketName, reason, e);
103 logger.warn("{}: {}", socketName, reason);
105 connectionListener.webSocketConnectionLost(reason);
109 private void startOrResetWatchdogTimer() {
110 stopWatchdogTimer(); // stop already running timer
111 watchdogJob = scheduler.schedule(
112 () -> connectionListener.webSocketConnectionLost(
113 "Watchdog timed out after " + watchdogInterval + "s. Websocket seems to be dead."),
114 watchdogInterval, TimeUnit.SECONDS);
117 private void stopWatchdogTimer() {
118 ScheduledFuture<?> watchdogTimer = this.watchdogJob;
119 if (watchdogTimer != null) {
120 watchdogTimer.cancel(false);
121 this.watchdogJob = null;
126 * dispose the websocket (close connection and destroy client)
129 public void dispose() {
132 connectionState = ConnectionState.DISCONNECTING;
134 } catch (Exception e) {
135 logger.debug("{} encountered an error while closing connection", socketName, e);
138 connectionState = ConnectionState.DISCONNECTED;
141 public void registerListener(ResourceType resourceType, String sensorID, WebSocketMessageListener listener) {
142 listeners.put(getListenerId(resourceType, sensorID), listener);
145 public void unregisterListener(ResourceType resourceType, String sensorID) {
146 listeners.remove(getListenerId(resourceType, sensorID));
149 @SuppressWarnings("unused")
151 public void onConnect(Session session) {
152 connectionState = ConnectionState.CONNECTED;
153 logger.debug("{} successfully connected to {}: {}", socketName, session.getRemoteAddress().getAddress(),
155 connectionListener.webSocketConnectionEstablished();
156 startOrResetWatchdogTimer();
157 this.session = session;
160 @SuppressWarnings("unused")
162 public void onMessage(Session session, String message) {
163 if (!session.equals(this.session)) {
164 handleWrongSession(session, message);
167 startOrResetWatchdogTimer();
168 logger.trace("{} received raw data: {}", socketName, message);
171 DeconzBaseMessage changedMessage = Objects.requireNonNull(gson.fromJson(message, DeconzBaseMessage.class));
172 if (changedMessage.r == ResourceType.UNKNOWN) {
173 logger.trace("Received message has unknown resource type. Skipping message.");
177 ResourceType resourceType = changedMessage.r;
178 String resourceId = changedMessage.id;
180 if (resourceType == ResourceType.SCENES) {
182 resourceType = ResourceType.GROUPS;
183 resourceId = changedMessage.gid;
186 WebSocketMessageListener listener = listeners.get(getListenerId(resourceType, resourceId));
187 if (listener == null) {
189 "Couldn't find listener for id {} with resource type {}. Either no thing for this id has been defined or this is a bug.",
190 changedMessage.id, changedMessage.r);
194 // we still need the original resource type here
195 Class<? extends DeconzBaseMessage> expectedMessageType = changedMessage.r.getExpectedMessageType();
196 if (expectedMessageType == null) {
198 "BUG! Could not get expected message type for resource type {}. Please report this incident.",
203 DeconzBaseMessage deconzMessage = Objects.requireNonNull(gson.fromJson(message, expectedMessageType));
204 listener.messageReceived(deconzMessage);
205 } catch (RuntimeException e) {
206 // we need to catch all processing exceptions, otherwise they could affect the connection
207 logger.warn("{} encountered an error while processing the message {}: {}", socketName, message,
212 @SuppressWarnings("unused")
214 public void onError(@Nullable Session session, Throwable cause) {
215 if (session != null && !session.equals(this.session)) {
216 handleWrongSession(session, "Connection error: " + cause.getMessage());
219 logger.warn("{} connection errored, closing: {}", socketName, cause.getMessage());
222 Session storedSession = this.session;
223 if (storedSession != null && storedSession.isOpen()) {
224 storedSession.close(-1, "Processing error");
228 @SuppressWarnings("unused")
230 public void onClose(Session session, int statusCode, String reason) {
231 if (!session.equals(this.session)) {
232 handleWrongSession(session, "Connection closed: " + statusCode + " / " + reason);
235 logger.trace("{} closed connection: {} / {}", socketName, statusCode, reason);
236 connectionState = ConnectionState.DISCONNECTED;
239 connectionListener.webSocketConnectionLost(reason);
242 private void handleWrongSession(Session session, String message) {
243 logger.warn("{}{} received and discarded message for other or session {}: {}.", socketName, session.hashCode(),
244 session.hashCode(), message);
245 if (session.isOpen()) {
246 // Close the session if it is still open. It should already be closed anyway
252 * check connection state (successfully connected)
254 * @return true if connected, false if connecting, disconnecting or disconnected
256 public boolean isConnected() {
257 return connectionState == ConnectionState.CONNECTED;
261 * create a unique identifier for a listener
263 * @param resourceType the listener resource-type (LIGHT, SENSOR, ...)
264 * @param id the listener id (same as deconz-id)
265 * @return a unique string for this listener
267 private String getListenerId(ResourceType resourceType, String id) {
268 return resourceType.name() + "$" + id;
272 * used internally to represent the connection state
274 private enum ConnectionState {