]> git.basschouten.com Git - openhab-addons.git/blob
308ab06ed67cef03c75e65a109cc6b1637376aa4
[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.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;
46
47 import com.google.gson.Gson;
48
49 /**
50  * {@link Shelly1HttpApi} wraps the Shelly REST API and provides various low level function to access the device api
51  * (not
52  * cloud api).
53  *
54  * @author Markus Michels - Initial contribution
55  */
56 @NonNullByDefault
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();
61
62     private String thingName = "";
63     private String deviceIp = "";
64     private boolean inbound = false;
65     private CountDownLatch connectLatch = new CountDownLatch(1);
66
67     private @Nullable Session session;
68     private @Nullable Shelly2RpctInterface websocketHandler;
69     private WebSocketClient client = new WebSocketClient();
70     private @Nullable ShellyThingTable thingTable;
71
72     public Shelly2RpcSocket() {
73     }
74
75     /**
76      * Regular constructor for Thing and Discover handler
77      *
78      * @param thingName Thing/Service name
79      * @param thingTable
80      * @param deviceIp IP address for the device
81      */
82     public Shelly2RpcSocket(String thingName, @Nullable ShellyThingTable thingTable, String deviceIp) {
83         this.thingName = thingName;
84         this.deviceIp = deviceIp;
85         this.thingTable = thingTable;
86     }
87
88     /**
89      * Constructor called from Servlet handler
90      *
91      * @param thingTable
92      * @param inbound
93      */
94     public Shelly2RpcSocket(ShellyThingTable thingTable, boolean inbound) {
95         this.thingTable = thingTable;
96         this.inbound = inbound;
97     }
98
99     /**
100      * Add listener for inbound messages implementing Shelly2RpctInterface
101      *
102      * @param interfacehandler
103      */
104     public void addMessageHandler(Shelly2RpctInterface interfacehandler) {
105         this.websocketHandler = interfacehandler;
106     }
107
108     /**
109      * Connect outbound Web Socket
110      *
111      * @throws ShellyApiException
112      */
113     public void connect() throws ShellyApiException {
114         try {
115             disconnect(); // for safety
116
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");
123
124             logger.debug("{}: Connect WebSocket, URI={}", thingName, uri);
125             client = new WebSocketClient();
126             connectLatch = new CountDownLatch(1);
127             client.start();
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);
133         }
134     }
135
136     /**
137      * Web Socket is connected, lookup thing and create connectLatch to synchronize first sendMessage()
138      *
139      * @param session Newly created WebSocket connection
140      */
141     @OnWebSocketConnect
142     public void onConnect(Session session) {
143         try {
144             if (session.getRemoteAddress() == null) {
145                 logger.debug("{}: Invalid inbound WebSocket connect", thingName);
146                 session.close(StatusCode.ABNORMAL, "Invalid remote IP");
147                 return;
148             }
149             this.session = session;
150             if (deviceIp.isEmpty()) {
151                 // This is the inbound event web socket
152                 deviceIp = session.getRemoteAddress().getAddress().getHostAddress();
153             }
154             if (websocketHandler == null) {
155                 if (thingTable != null) {
156                     ShellyThingInterface thing = thingTable.getThing(deviceIp);
157                     Shelly2ApiRpc api = (Shelly2ApiRpc) thing.getApi();
158                     websocketHandler = api.getRpcHandler();
159                 }
160             }
161             connectLatch.countDown();
162
163             logger.debug("{}: WebSocket connected {}<-{}, Idle Timeout={}", thingName, session.getLocalAddress(),
164                     session.getRemoteAddress(), session.getIdleTimeout());
165             if (websocketHandler != null) {
166                 websocketHandler.onConnect(deviceIp, true);
167                 return;
168             }
169         } catch (IllegalArgumentException e) { // unknown thing
170             // debug is below
171         }
172
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");
176         }
177     }
178
179     /**
180      * Send request over WebSocket
181      *
182      * @param str API request message
183      * @throws ShellyApiException
184      */
185     @SuppressWarnings("null")
186     public void sendMessage(String str) throws ShellyApiException {
187         if (session != null) {
188             try {
189                 connectLatch.await();
190                 session.getRemote().sendString(str);
191                 return;
192             } catch (IOException | InterruptedException e) {
193                 throw new ShellyApiException("Error RpcSend failed", e);
194             }
195         }
196         throw new ShellyApiException("Unable to send API request (No Rpc session)");
197     }
198
199     /**
200      * Close WebSocket session
201      */
202     public void disconnect() {
203         try {
204             if (session != null) {
205                 Session s = session;
206                 if (s.isOpen()) {
207                     logger.debug("{}: Disconnecting WebSocket ({} -> {})", thingName, s.getLocalAddress(),
208                             s.getRemoteAddress());
209                 }
210                 s.disconnect();
211                 s.close(StatusCode.NORMAL, "Socket closed");
212                 session = null;
213             }
214         } catch (Exception e) {
215             if (e.getCause() instanceof InterruptedException) {
216                 logger.debug("{}: Unable to close socket - interrupted", thingName); // e.g. device was rebooted
217             } else {
218                 logger.debug("{}: Unable to close socket", thingName, e);
219             }
220         } finally {
221             // make sure client is stopped / thread terminates / socket resource is free up
222             try {
223                 client.stop();
224             } catch (Exception e) {
225                 logger.debug("{}: Unable to close Web Socket", thingName, e);
226             }
227         }
228     }
229
230     /**
231      * Inbound WebSocket message
232      *
233      * @param session WebSpcket session
234      * @param receivedMessage Textial API message
235      */
236     @OnWebSocketMessage
237     public void onText(Session session, String receivedMessage) {
238         try {
239             Shelly2RpctInterface handler = websocketHandler;
240             Shelly2RpcBaseMessage message = fromJson(gson, receivedMessage, Shelly2RpcBaseMessage.class);
241             logger.trace("{}: Inbound Rpc message: {}", thingName, receivedMessage);
242             if (handler != null) {
243                 if (thingName.isEmpty()) {
244                     thingName = getString(message.src);
245                 }
246                 if (message.method == null) {
247                     message.method = SHELLYRPC_METHOD_NOTIFYFULLSTATUS;
248                 }
249                 switch (getString(message.method)) {
250                     case SHELLYRPC_METHOD_NOTIFYSTATUS:
251                     case SHELLYRPC_METHOD_NOTIFYFULLSTATUS:
252                         Shelly2RpcNotifyStatus status = fromJson(gson, receivedMessage, Shelly2RpcNotifyStatus.class);
253                         if (status.params == null) {
254                             status.params = status.result;
255                         }
256                         handler.onNotifyStatus(status);
257                         return;
258                     case SHELLYRPC_METHOD_NOTIFYEVENT:
259                         Shelly2RpcNotifyEvent events = fromJson(gson, receivedMessage, Shelly2RpcNotifyEvent.class);
260                         events.src = message.src;
261                         if (events.params == null || events.params.events == null) {
262                             logger.debug("{}: Malformed event data: {}", thingName, receivedMessage);
263                         } else {
264                             for (Shelly2NotifyEvent e : events.params.events) {
265                                 if (getString(e.event).startsWith(SHELLY2_EVENT_BLUPREFIX)) {
266                                     String address = getString(e.data.addr).replace(":", "");
267                                     if (thingTable != null && thingTable.findThing(address) != null) {
268                                         if (thingTable != null) { // known device
269                                             ShellyThingInterface thing = thingTable.getThing(address);
270                                             Shelly2ApiRpc api = (Shelly2ApiRpc) thing.getApi();
271                                             handler = api.getRpcHandler();
272                                             handler.onNotifyEvent(
273                                                     fromJson(gson, receivedMessage, Shelly2RpcNotifyEvent.class));
274                                         }
275                                     } else { // new device
276                                         if (e.event.equals(SHELLY2_EVENT_BLUSCAN)) {
277                                             ShellyBluSensorHandler.addBluThing(message.src, e, thingTable);
278                                         } else {
279                                             logger.debug("{}: NotifyEvent {} for unknown device {}", message.src,
280                                                     e.event, e.data.name);
281                                         }
282                                     }
283                                 } else {
284                                     handler.onNotifyEvent(fromJson(gson, receivedMessage, Shelly2RpcNotifyEvent.class));
285                                 }
286                             }
287                         }
288                         break;
289                     default:
290                         handler.onMessage(receivedMessage);
291                 }
292             } else {
293                 logger.debug("{}: No Rpc listener registered for device {}, skip message: {}", thingName,
294                         getString(message.src), receivedMessage);
295             }
296         } catch (ShellyApiException | IllegalArgumentException e) {
297             logger.debug("{}: Unable to process Rpc message ({}): {}", thingName, e.getMessage(), receivedMessage);
298         } catch (NullPointerException e) {
299             logger.debug("{}: Unable to process Rpc message: {}", thingName, receivedMessage, e);
300         }
301     }
302
303     public boolean isConnected() {
304         return session != null && session.isOpen();
305     }
306
307     public boolean isInbound() {
308         return inbound;
309     }
310
311     /**
312      * Web Socket closed, notify thing handler
313      *
314      * @param statusCode StatusCode
315      * @param reason Textual reason
316      */
317     @OnWebSocketClose
318     public void onClose(int statusCode, String reason) {
319         if (statusCode != StatusCode.NORMAL) {
320             logger.trace("{}: Rpc connection closed: {} - {}", thingName, statusCode, getString(reason));
321         }
322         if (inbound) {
323             // Ignore disconnect: Device establishes the socket, sends NotifyxFullStatus and disconnects
324             return;
325         }
326         disconnect();
327         if (websocketHandler != null) {
328             websocketHandler.onClose(statusCode, reason);
329         }
330     }
331
332     /**
333      * WebSocket error handler
334      *
335      * @param cause WebSocket error/Exception
336      */
337     @OnWebSocketError
338     public void onError(Throwable cause) {
339         if (inbound) {
340             // Ignore disconnect: Device establishes the socket, sends NotifyxFullStatus and disconnects
341             return;
342         }
343         if (websocketHandler != null) {
344             websocketHandler.onError(cause);
345         }
346     }
347 }