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.Shelly2NotifyEvent;
38 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcBaseMessage;
39 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcNotifyEvent;
40 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcNotifyStatus;
41 import org.openhab.binding.shelly.internal.handler.ShellyBluSensorHandler;
42 import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
43 import org.openhab.binding.shelly.internal.handler.ShellyThingTable;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
47 import com.google.gson.Gson;
50 * {@link Shelly1HttpApi} wraps the Shelly REST API and provides various low level function to access the device api
54 * @author Markus Michels - Initial contribution
57 @WebSocket(maxIdleTime = Integer.MAX_VALUE)
58 public class Shelly2RpcSocket {
59 private final Logger logger = LoggerFactory.getLogger(Shelly2RpcSocket.class);
60 private final Gson gson = new Gson();
62 private String thingName = "";
63 private String deviceIp = "";
64 private boolean inbound = false;
65 private CountDownLatch connectLatch = new CountDownLatch(1);
67 private @Nullable Session session;
68 private @Nullable Shelly2RpctInterface websocketHandler;
69 private WebSocketClient client = new WebSocketClient();
70 private @Nullable ShellyThingTable thingTable;
72 public Shelly2RpcSocket() {
76 * Regular constructor for Thing and Discover handler
78 * @param thingName Thing/Service name
80 * @param deviceIp IP address for the device
82 public Shelly2RpcSocket(String thingName, @Nullable ShellyThingTable thingTable, String deviceIp) {
83 this.thingName = thingName;
84 this.deviceIp = deviceIp;
85 this.thingTable = thingTable;
89 * Constructor called from Servlet handler
94 public Shelly2RpcSocket(ShellyThingTable thingTable, boolean inbound) {
95 this.thingTable = thingTable;
96 this.inbound = inbound;
100 * Add listener for inbound messages implementing Shelly2RpctInterface
102 * @param interfacehandler
104 public void addMessageHandler(Shelly2RpctInterface interfacehandler) {
105 this.websocketHandler = interfacehandler;
109 * Connect outbound Web Socket
111 * @throws ShellyApiException
113 public void connect() throws ShellyApiException {
115 disconnect(); // for safety
117 URI uri = new URI("ws://" + deviceIp + SHELLYRPC_ENDPOINT);
118 ClientUpgradeRequest request = new ClientUpgradeRequest();
119 request.setHeader(HttpHeaders.HOST, deviceIp);
120 request.setHeader("Origin", "http://" + deviceIp);
121 request.setHeader("Pragma", "no-cache");
122 request.setHeader("Cache-Control", "no-cache");
124 logger.debug("{}: Connect WebSocket, URI={}", thingName, uri);
125 client = new WebSocketClient();
126 connectLatch = new CountDownLatch(1);
128 client.setConnectTimeout(5000);
129 client.setStopTimeout(0);
130 client.connect(this, uri, request);
131 } catch (Exception e) {
132 throw new ShellyApiException("Unable to initialize WebSocket", e);
137 * Web Socket is connected, lookup thing and create connectLatch to synchronize first sendMessage()
139 * @param session Newly created WebSocket connection
142 public void onConnect(Session session) {
144 if (session.getRemoteAddress() == null) {
145 logger.debug("{}: Invalid inbound WebSocket connect", thingName);
146 session.close(StatusCode.ABNORMAL, "Invalid remote IP");
149 this.session = session;
150 if (deviceIp.isEmpty()) {
151 // This is the inbound event web socket
152 deviceIp = session.getRemoteAddress().getAddress().getHostAddress();
154 if (websocketHandler == null) {
155 if (thingTable != null) {
156 ShellyThingInterface thing = thingTable.getThing(deviceIp);
157 Shelly2ApiRpc api = (Shelly2ApiRpc) thing.getApi();
158 websocketHandler = api.getRpcHandler();
161 connectLatch.countDown();
163 logger.debug("{}: WebSocket connected {}<-{}, Idle Timeout={}", thingName, session.getLocalAddress(),
164 session.getRemoteAddress(), session.getIdleTimeout());
165 if (websocketHandler != null) {
166 websocketHandler.onConnect(deviceIp, true);
169 } catch (IllegalArgumentException e) { // unknown thing
173 if (websocketHandler == null && thingTable != null) {
174 logger.debug("Rpc: Unable to handle connection from {} (unknown/disabled thing), closing socket", deviceIp);
175 session.close(StatusCode.SHUTDOWN, "Thing not active");
180 * Send request over WebSocket
182 * @param str API request message
183 * @throws ShellyApiException
185 @SuppressWarnings("null")
186 public void sendMessage(String str) throws ShellyApiException {
187 if (session != null) {
189 connectLatch.await();
190 session.getRemote().sendString(str);
192 } catch (IOException | InterruptedException e) {
193 throw new ShellyApiException("Error RpcSend failed", e);
196 throw new ShellyApiException("Unable to send API request (No Rpc session)");
200 * Close WebSocket session
202 public void disconnect() {
204 if (session != null) {
207 logger.debug("{}: Disconnecting WebSocket ({} -> {})", thingName, s.getLocalAddress(),
208 s.getRemoteAddress());
211 s.close(StatusCode.NORMAL, "Socket closed");
214 if (client.isStarted()) {
217 } catch (Exception e) {
218 if (e.getCause() instanceof InterruptedException) {
219 logger.debug("{}: Unable to close socket - interrupted", thingName); // e.g. device was rebooted
221 logger.debug("{}: Unable to close socket", thingName, e);
227 * Inbound WebSocket message
229 * @param session WebSpcket session
230 * @param receivedMessage Textial API message
233 public void onText(Session session, String receivedMessage) {
235 Shelly2RpctInterface handler = websocketHandler;
236 Shelly2RpcBaseMessage message = fromJson(gson, receivedMessage, Shelly2RpcBaseMessage.class);
237 logger.trace("{}: Inbound Rpc message: {}", thingName, receivedMessage);
238 if (handler != null) {
239 if (thingName.isEmpty()) {
240 thingName = getString(message.src);
242 if (message.method == null) {
243 message.method = SHELLYRPC_METHOD_NOTIFYFULLSTATUS;
245 switch (getString(message.method)) {
246 case SHELLYRPC_METHOD_NOTIFYSTATUS:
247 case SHELLYRPC_METHOD_NOTIFYFULLSTATUS:
248 Shelly2RpcNotifyStatus status = fromJson(gson, receivedMessage, Shelly2RpcNotifyStatus.class);
249 if (status.params == null) {
250 status.params = status.result;
252 handler.onNotifyStatus(status);
254 case SHELLYRPC_METHOD_NOTIFYEVENT:
255 Shelly2RpcNotifyEvent events = fromJson(gson, receivedMessage, Shelly2RpcNotifyEvent.class);
256 events.src = message.src;
257 if (events.params == null || events.params.events == null) {
258 logger.debug("{}: Malformed event data: {}", thingName, receivedMessage);
260 for (Shelly2NotifyEvent e : events.params.events) {
261 if (getString(e.event).startsWith(SHELLY2_EVENT_BLUPREFIX)) {
262 String address = getString(e.data.addr).replace(":", "");
263 if (thingTable != null && thingTable.findThing(address) != null) {
264 if (thingTable != null) { // known device
265 ShellyThingInterface thing = thingTable.getThing(address);
266 Shelly2ApiRpc api = (Shelly2ApiRpc) thing.getApi();
267 handler = api.getRpcHandler();
268 handler.onNotifyEvent(
269 fromJson(gson, receivedMessage, Shelly2RpcNotifyEvent.class));
271 } else { // new device
272 if (e.event.equals(SHELLY2_EVENT_BLUSCAN)) {
273 ShellyBluSensorHandler.addBluThing(message.src, e, thingTable);
275 logger.debug("{}: NotifyEvent {} for unknown device {}", message.src,
276 e.event, e.data.name);
280 handler.onNotifyEvent(fromJson(gson, receivedMessage, Shelly2RpcNotifyEvent.class));
286 handler.onMessage(receivedMessage);
289 logger.debug("{}: No Rpc listener registered for device {}, skip message: {}", thingName,
290 getString(message.src), receivedMessage);
292 } catch (ShellyApiException | IllegalArgumentException e) {
293 logger.debug("{}: Unable to process Rpc message ({}): {}", thingName, e.getMessage(), receivedMessage);
294 } catch (NullPointerException e) {
295 logger.debug("{}: Unable to process Rpc message: {}", thingName, receivedMessage, e);
299 public boolean isConnected() {
300 return session != null && session.isOpen();
303 public boolean isInbound() {
308 * Web Socket closed, notify thing handler
310 * @param statusCode StatusCode
311 * @param reason Textual reason
314 public void onClose(int statusCode, String reason) {
315 if (statusCode != StatusCode.NORMAL) {
316 logger.trace("{}: Rpc connection closed: {} - {}", thingName, statusCode, getString(reason));
319 // Ignore disconnect: Device establishes the socket, sends NotifyxFullStatus and disconnects
323 if (websocketHandler != null) {
324 websocketHandler.onClose(statusCode, reason);
329 * WebSocket error handler
331 * @param cause WebSocket error/Exception
334 public void onError(Throwable cause) {
336 // Ignore disconnect: Device establishes the socket, sends NotifyxFullStatus and disconnects
339 if (websocketHandler != null) {
340 websocketHandler.onError(cause);