]> git.basschouten.com Git - openhab-addons.git/blob
c64684e6b53dc5321c00566bb68b616c9d8879e6
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.shelly.internal.api2;
14
15 import static org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.*;
16 import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
17
18 import java.io.IOException;
19 import java.net.URI;
20 import java.util.concurrent.CountDownLatch;
21
22 import javax.ws.rs.core.HttpHeaders;
23
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;
44
45 import com.google.gson.Gson;
46
47 /**
48  * {@link Shelly1HttpApi} wraps the Shelly REST API and provides various low level function to access the device api
49  * (not
50  * cloud api).
51  *
52  * @author Markus Michels - Initial contribution
53  */
54 @NonNullByDefault
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();
59
60     private String thingName = "";
61     private String deviceIp = "";
62     private boolean inbound = false;
63     private CountDownLatch connectLatch = new CountDownLatch(1);
64
65     private @Nullable Session session;
66     private @Nullable Shelly2RpctInterface websocketHandler;
67     private WebSocketClient client = new WebSocketClient();
68     private @Nullable ShellyThingTable thingTable;
69
70     public Shelly2RpcSocket() {
71     }
72
73     /**
74      * Regular constructor for Thing and Discover handler
75      *
76      * @param thingName Thing/Service name
77      * @param thingTable
78      * @param deviceIp IP address for the device
79      */
80     public Shelly2RpcSocket(String thingName, @Nullable ShellyThingTable thingTable, String deviceIp) {
81         this.thingName = thingName;
82         this.deviceIp = deviceIp;
83         this.thingTable = thingTable;
84     }
85
86     /**
87      * Constructor called from Servlet handler
88      *
89      * @param thingTable
90      * @param inbound
91      */
92     public Shelly2RpcSocket(ShellyThingTable thingTable, boolean inbound) {
93         this.thingTable = thingTable;
94         this.inbound = inbound;
95     }
96
97     /**
98      * Add listener for inbound messages implementing Shelly2RpctInterface
99      *
100      * @param interfacehandler
101      */
102     public void addMessageHandler(Shelly2RpctInterface interfacehandler) {
103         this.websocketHandler = interfacehandler;
104     }
105
106     /**
107      * Connect outbound Web Socket
108      *
109      * @throws ShellyApiException
110      */
111     public void connect() throws ShellyApiException {
112         try {
113             disconnect(); // for safety
114
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");
121
122             logger.debug("{}: Connect WebSocket, URI={}", thingName, uri);
123             client = new WebSocketClient();
124             connectLatch = new CountDownLatch(1);
125             client.start();
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);
131         }
132     }
133
134     /**
135      * Web Socket is connected, lookup thing and create connectLatch to synchronize first sendMessage()
136      *
137      * @param session Newly created WebSocket connection
138      */
139     @OnWebSocketConnect
140     public void onConnect(Session session) {
141         try {
142             if (session.getRemoteAddress() == null) {
143                 logger.debug("{}: Invalid inbound WebSocket connect", thingName);
144                 session.close(StatusCode.ABNORMAL, "Invalid remote IP");
145                 return;
146             }
147             this.session = session;
148             if (deviceIp.isEmpty()) {
149                 // This is the inbound event web socket
150                 deviceIp = session.getRemoteAddress().getAddress().getHostAddress();
151             }
152             if (websocketHandler == null) {
153                 if (thingTable != null) {
154                     ShellyThingInterface thing = thingTable.getThing(deviceIp);
155                     Shelly2ApiRpc api = (Shelly2ApiRpc) thing.getApi();
156                     websocketHandler = api.getRpcHandler();
157                 }
158             }
159             connectLatch.countDown();
160
161             logger.debug("{}: WebSocket connected {}<-{}, Idle Timeout={}", thingName, session.getLocalAddress(),
162                     session.getRemoteAddress(), session.getIdleTimeout());
163             if (websocketHandler != null) {
164                 websocketHandler.onConnect(deviceIp, true);
165                 return;
166             }
167         } catch (IllegalArgumentException e) { // unknown thing
168             // debug is below
169         }
170
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");
174         }
175     }
176
177     /**
178      * Send request over WebSocket
179      *
180      * @param str API request message
181      * @throws ShellyApiException
182      */
183     @SuppressWarnings("null")
184     public void sendMessage(String str) throws ShellyApiException {
185         if (session != null) {
186             try {
187                 connectLatch.await();
188                 session.getRemote().sendString(str);
189                 return;
190             } catch (IOException | InterruptedException e) {
191                 throw new ShellyApiException("Error RpcSend failed", e);
192             }
193         }
194         throw new ShellyApiException("Unable to send API request (No Rpc session)");
195     }
196
197     /**
198      * Close WebSocket session
199      */
200     public void disconnect() {
201         try {
202             if (session != null) {
203                 Session s = session;
204                 if (s.isOpen()) {
205                     logger.debug("{}: Disconnecting WebSocket ({} -> {})", thingName, s.getLocalAddress(),
206                             s.getRemoteAddress());
207                     s.disconnect();
208                 }
209                 s.close(StatusCode.NORMAL, "Socket closed");
210                 session = null;
211             }
212             if (client.isStarted()) {
213                 client.stop();
214             }
215         } catch (Exception e) {
216             if (e.getCause() instanceof InterruptedException) {
217                 logger.debug("{}: Unable to close socket - interrupted", thingName); // e.g. device was rebooted
218             } else {
219                 logger.debug("{}: Unable to close socket", thingName, e);
220             }
221         }
222     }
223
224     /**
225      * Inbound WebSocket message
226      *
227      * @param session WebSpcket session
228      * @param receivedMessage Textial API message
229      */
230     @OnWebSocketMessage
231     public void onText(Session session, String receivedMessage) {
232         try {
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);
239                 }
240                 if (message.method == null) {
241                     message.method = SHELLYRPC_METHOD_NOTIFYFULLSTATUS;
242                 }
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;
249                         }
250                         handler.onNotifyStatus(status);
251                         return;
252                     case SHELLYRPC_METHOD_NOTIFYEVENT:
253                         handler.onNotifyEvent(fromJson(gson, receivedMessage, Shelly2RpcNotifyEvent.class));
254                         return;
255                     default:
256                         handler.onMessage(receivedMessage);
257                 }
258             } else {
259                 logger.debug("{}: No Rpc listener registered for device {}, skip message: {}", thingName,
260                         getString(message.src), receivedMessage);
261             }
262         } catch (ShellyApiException | IllegalArgumentException | NullPointerException e) {
263             logger.debug("{}: Unable to process Rpc message: {}", thingName, receivedMessage, e);
264         }
265     }
266
267     public boolean isConnected() {
268         return session != null && session.isOpen();
269     }
270
271     public boolean isInbound() {
272         return inbound;
273     }
274
275     /**
276      * Web Socket closed, notify thing handler
277      *
278      * @param statusCode StatusCode
279      * @param reason Textual reason
280      */
281     @OnWebSocketClose
282     public void onClose(int statusCode, String reason) {
283         if (statusCode != StatusCode.NORMAL) {
284             logger.trace("{}: Rpc connection closed: {} - {}", thingName, statusCode, getString(reason));
285         }
286         if (inbound) {
287             // Ignore disconnect: Device establishes the socket, sends NotifyxFullStatus and disconnects
288             return;
289         }
290         disconnect();
291         if (websocketHandler != null) {
292             websocketHandler.onClose(statusCode, reason);
293         }
294     }
295
296     /**
297      * WebSocket error handler
298      *
299      * @param cause WebSocket error/Exception
300      */
301     @OnWebSocketError
302     public void onError(Throwable cause) {
303         if (inbound) {
304             // Ignore disconnect: Device establishes the socket, sends NotifyxFullStatus and disconnects
305             return;
306         }
307         if (websocketHandler != null) {
308             websocketHandler.onError(cause);
309         }
310     }
311 }