2 * Copyright (c) 2010-2020 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.concurrent.ConcurrentHashMap;
19 import org.eclipse.jdt.annotation.NonNullByDefault;
20 import org.eclipse.jetty.util.thread.QueuedThreadPool;
21 import org.eclipse.jetty.websocket.api.Session;
22 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
23 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
24 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
25 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
26 import org.eclipse.jetty.websocket.api.annotations.WebSocket;
27 import org.eclipse.jetty.websocket.client.WebSocketClient;
28 import org.openhab.binding.deconz.internal.dto.DeconzBaseMessage;
29 import org.openhab.binding.deconz.internal.dto.GroupMessage;
30 import org.openhab.binding.deconz.internal.dto.LightMessage;
31 import org.openhab.binding.deconz.internal.dto.SensorMessage;
32 import org.openhab.binding.deconz.internal.types.ResourceType;
33 import org.slf4j.Logger;
34 import org.slf4j.LoggerFactory;
36 import com.google.gson.Gson;
39 * Establishes and keeps a websocket connection to the deCONZ software.
41 * The connection is closed by deCONZ now and then and needs to be re-established.
43 * @author David Graeff - Initial contribution
47 public class WebSocketConnection {
48 private static final Map<ResourceType, Class<? extends DeconzBaseMessage>> EXPECTED_MESSAGE_TYPES = Map.of(
49 ResourceType.GROUPS, GroupMessage.class, ResourceType.LIGHTS, LightMessage.class, ResourceType.SENSORS,
52 private final Logger logger = LoggerFactory.getLogger(WebSocketConnection.class);
54 private final WebSocketClient client;
55 private final String socketName;
56 private final Gson gson;
58 private final WebSocketConnectionListener connectionListener;
59 private final Map<Map.Entry<ResourceType, String>, WebSocketMessageListener> listeners = new ConcurrentHashMap<>();
61 private ConnectionState connectionState = ConnectionState.DISCONNECTED;
63 public WebSocketConnection(WebSocketConnectionListener listener, WebSocketClient client, Gson gson) {
64 this.connectionListener = listener;
66 this.client.setMaxIdleTimeout(0);
68 this.socketName = ((QueuedThreadPool) client.getExecutor()).getName() + "$" + this.hashCode();
71 public void start(String ip) {
72 if (connectionState == ConnectionState.CONNECTED) {
74 } else if (connectionState == ConnectionState.CONNECTING) {
75 logger.debug("{} already connecting", socketName);
79 URI destUri = URI.create("ws://" + ip);
81 logger.debug("Trying to connect {} to {}", socketName, destUri);
82 client.connect(this, destUri).get();
83 } catch (Exception e) {
84 connectionListener.connectionError(e);
90 connectionState = ConnectionState.DISCONNECTING;
92 } catch (Exception e) {
93 logger.debug("{} encountered an error while closing connection", socketName, e);
98 public void registerListener(ResourceType resourceType, String sensorID, WebSocketMessageListener listener) {
99 listeners.put(Map.entry(resourceType, sensorID), listener);
102 public void unregisterListener(ResourceType resourceType, String sensorID) {
103 listeners.remove(Map.entry(resourceType, sensorID));
106 @SuppressWarnings("unused")
108 public void onConnect(Session session) {
109 connectionState = ConnectionState.CONNECTED;
110 logger.debug("{} successfully connected to {}", socketName, session.getRemoteAddress().getAddress());
111 connectionListener.connectionEstablished();
114 @SuppressWarnings("null, unused")
116 public void onMessage(String message) {
117 logger.trace("Raw data received by websocket {}: {}", socketName, message);
119 DeconzBaseMessage changedMessage = gson.fromJson(message, DeconzBaseMessage.class);
120 if (changedMessage.r == ResourceType.UNKNOWN) {
121 logger.trace("Received message has unknown resource type. Skipping message.");
125 WebSocketMessageListener listener = listeners.get(Map.entry(changedMessage.r, changedMessage.id));
126 if (listener == null) {
128 "Couldn't find listener for id {} with resource type {}. Either no thing for this id has been defined or this is a bug.",
129 changedMessage.id, changedMessage.r);
133 Class<? extends DeconzBaseMessage> expectedMessageType = EXPECTED_MESSAGE_TYPES.get(changedMessage.r);
134 if (expectedMessageType == null) {
135 logger.warn("BUG! Could not get expected message type for resource type {}. Please report this incident.",
140 DeconzBaseMessage deconzMessage = gson.fromJson(message, expectedMessageType);
141 if (deconzMessage != null) {
142 listener.messageReceived(changedMessage.id, deconzMessage);
146 @SuppressWarnings("unused")
148 public void onError(Throwable cause) {
149 connectionState = ConnectionState.DISCONNECTED;
150 connectionListener.connectionError(cause);
153 @SuppressWarnings("unused")
155 public void onClose(int statusCode, String reason) {
156 connectionState = ConnectionState.DISCONNECTED;
157 connectionListener.connectionLost(reason);
161 * check connection state (successfully connected)
163 * @return true if connected, false if connecting, disconnecting or disconnected
165 public boolean isConnected() {
166 return connectionState == ConnectionState.CONNECTED;
170 * used internally to represent the connection state
172 private enum ConnectionState {