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.shelly.internal.api2;
15 import static org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.*;
16 import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
18 import java.io.IOException;
20 import java.util.concurrent.CountDownLatch;
22 import javax.ws.rs.core.HttpHeaders;
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.StatusCode;
28 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
29 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
30 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
31 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
32 import org.eclipse.jetty.websocket.api.annotations.WebSocket;
33 import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
34 import org.eclipse.jetty.websocket.client.WebSocketClient;
35 import org.openhab.binding.shelly.internal.api.ShellyApiException;
36 import org.openhab.binding.shelly.internal.api1.Shelly1HttpApi;
37 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcBaseMessage;
38 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcNotifyEvent;
39 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcNotifyStatus;
40 import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
41 import org.openhab.binding.shelly.internal.handler.ShellyThingTable;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
45 import com.google.gson.Gson;
48 * {@link Shelly1HttpApi} wraps the Shelly REST API and provides various low level function to access the device api
52 * @author Markus Michels - Initial contribution
55 @WebSocket(maxIdleTime = Integer.MAX_VALUE)
56 public class Shelly2RpcSocket {
57 private final Logger logger = LoggerFactory.getLogger(Shelly2RpcSocket.class);
58 private final Gson gson = new Gson();
60 private String thingName = "";
61 private String deviceIp = "";
62 private boolean inbound = false;
63 private CountDownLatch connectLatch = new CountDownLatch(1);
65 private @Nullable Session session;
66 private @Nullable Shelly2RpctInterface websocketHandler;
67 private WebSocketClient client = new WebSocketClient();
68 private @Nullable ShellyThingTable thingTable;
70 public Shelly2RpcSocket() {
74 * Regular constructor for Thing and Discover handler
76 * @param thingName Thing/Service name
78 * @param deviceIp IP address for the device
80 public Shelly2RpcSocket(String thingName, @Nullable ShellyThingTable thingTable, String deviceIp) {
81 this.thingName = thingName;
82 this.deviceIp = deviceIp;
83 this.thingTable = thingTable;
87 * Constructor called from Servlet handler
92 public Shelly2RpcSocket(ShellyThingTable thingTable, boolean inbound) {
93 this.thingTable = thingTable;
94 this.inbound = inbound;
98 * Add listener for inbound messages implementing Shelly2RpctInterface
100 * @param interfacehandler
102 public void addMessageHandler(Shelly2RpctInterface interfacehandler) {
103 this.websocketHandler = interfacehandler;
107 * Connect outbound Web Socket
109 * @throws ShellyApiException
111 public void connect() throws ShellyApiException {
113 disconnect(); // for safety
115 URI uri = new URI("ws://" + deviceIp + "/rpc");
116 ClientUpgradeRequest request = new ClientUpgradeRequest();
117 request.setHeader(HttpHeaders.HOST, deviceIp);
118 request.setHeader("Origin", "http://" + deviceIp);
119 request.setHeader("Pragma", "no-cache");
120 request.setHeader("Cache-Control", "no-cache");
122 logger.debug("{}: Connect WebSocket, URI={}", thingName, uri);
123 client = new WebSocketClient();
124 connectLatch = new CountDownLatch(1);
126 client.setConnectTimeout(5000);
127 client.setStopTimeout(0);
128 client.connect(this, uri, request);
129 } catch (Exception e) {
130 throw new ShellyApiException("Unable to initialize WebSocket", e);
135 * Web Socket is connected, lookup thing and create connectLatch to synchronize first sendMessage()
137 * @param session Newly created WebSocket connection
140 public void onConnect(Session session) {
142 if (session.getRemoteAddress() == null) {
143 logger.debug("{}: Invalid inbound WebSocket connect", thingName);
144 session.close(StatusCode.ABNORMAL, "Invalid remote IP");
147 this.session = session;
148 if (deviceIp.isEmpty()) {
149 // This is the inbound event web socket
150 deviceIp = session.getRemoteAddress().getAddress().getHostAddress();
152 if (websocketHandler == null) {
153 if (thingTable != null) {
154 ShellyThingInterface thing = thingTable.getThing(deviceIp);
155 Shelly2ApiRpc api = (Shelly2ApiRpc) thing.getApi();
156 websocketHandler = api.getRpcHandler();
159 connectLatch.countDown();
161 logger.debug("{}: WebSocket connected {}<-{}, Idle Timeout={}", thingName, session.getLocalAddress(),
162 session.getRemoteAddress(), session.getIdleTimeout());
163 if (websocketHandler != null) {
164 websocketHandler.onConnect(deviceIp, true);
167 } catch (IllegalArgumentException e) { // unknown thing
171 if (websocketHandler == null && thingTable != null) {
172 logger.debug("Rpc: Unable to handle connection from {} (unknown/disabled thing), closing socket", deviceIp);
173 session.close(StatusCode.SHUTDOWN, "Thing not active");
178 * Send request over WebSocket
180 * @param str API request message
181 * @throws ShellyApiException
183 @SuppressWarnings("null")
184 public void sendMessage(String str) throws ShellyApiException {
185 if (session != null) {
187 connectLatch.await();
188 session.getRemote().sendString(str);
190 } catch (IOException | InterruptedException e) {
191 throw new ShellyApiException("Error RpcSend failed", e);
194 throw new ShellyApiException("Unable to send API request (No Rpc session)");
198 * Close WebSocket session
200 public void disconnect() {
202 if (session != null) {
205 logger.debug("{}: Disconnecting WebSocket ({} -> {})", thingName, s.getLocalAddress(),
206 s.getRemoteAddress());
209 s.close(StatusCode.NORMAL, "Socket closed");
212 if (client.isStarted()) {
215 } catch (Exception e) {
216 if (e.getCause() instanceof InterruptedException) {
217 logger.debug("{}: Unable to close socket - interrupted", thingName); // e.g. device was rebooted
219 logger.debug("{}: Unable to close socket", thingName, e);
225 * Inbound WebSocket message
227 * @param session WebSpcket session
228 * @param receivedMessage Textial API message
231 public void onText(Session session, String receivedMessage) {
233 Shelly2RpctInterface handler = websocketHandler;
234 Shelly2RpcBaseMessage message = fromJson(gson, receivedMessage, Shelly2RpcBaseMessage.class);
235 logger.trace("{}: Inbound Rpc message: {}", thingName, receivedMessage);
236 if (handler != null) {
237 if (thingName.isEmpty()) {
238 thingName = getString(message.src);
240 if (message.method == null) {
241 message.method = SHELLYRPC_METHOD_NOTIFYFULLSTATUS;
243 switch (getString(message.method)) {
244 case SHELLYRPC_METHOD_NOTIFYSTATUS:
245 case SHELLYRPC_METHOD_NOTIFYFULLSTATUS:
246 Shelly2RpcNotifyStatus status = fromJson(gson, receivedMessage, Shelly2RpcNotifyStatus.class);
247 if (status.params == null) {
248 status.params = status.result;
250 handler.onNotifyStatus(status);
252 case SHELLYRPC_METHOD_NOTIFYEVENT:
253 handler.onNotifyEvent(fromJson(gson, receivedMessage, Shelly2RpcNotifyEvent.class));
256 handler.onMessage(receivedMessage);
259 logger.debug("{}: No Rpc listener registered for device {}, skip message: {}", thingName,
260 getString(message.src), receivedMessage);
262 } catch (ShellyApiException | IllegalArgumentException | NullPointerException e) {
263 logger.debug("{}: Unable to process Rpc message: {}", thingName, receivedMessage, e);
267 public boolean isConnected() {
268 return session != null && session.isOpen();
271 public boolean isInbound() {
276 * Web Socket closed, notify thing handler
278 * @param statusCode StatusCode
279 * @param reason Textual reason
282 public void onClose(int statusCode, String reason) {
283 if (statusCode != StatusCode.NORMAL) {
284 logger.trace("{}: Rpc connection closed: {} - {}", thingName, statusCode, getString(reason));
287 // Ignore disconnect: Device establishes the socket, sends NotifyxFullStatus and disconnects
291 if (websocketHandler != null) {
292 websocketHandler.onClose(statusCode, reason);
297 * WebSocket error handler
299 * @param cause WebSocket error/Exception
302 public void onError(Throwable cause) {
304 // Ignore disconnect: Device establishes the socket, sends NotifyxFullStatus and disconnects
307 if (websocketHandler != null) {
308 websocketHandler.onError(cause);