]> git.basschouten.com Git - openhab-addons.git/blob
ecf724ff0801e2ecd23cc8871b0c840cb25390c6
[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 + "/rpc");
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                     s.disconnect();
210                 }
211                 s.close(StatusCode.NORMAL, "Socket closed");
212                 session = null;
213             }
214             if (client.isStarted()) {
215                 client.stop();
216             }
217         } catch (Exception e) {
218             if (e.getCause() instanceof InterruptedException) {
219                 logger.debug("{}: Unable to close socket - interrupted", thingName); // e.g. device was rebooted
220             } else {
221                 logger.debug("{}: Unable to close socket", thingName, e);
222             }
223         }
224     }
225
226     /**
227      * Inbound WebSocket message
228      *
229      * @param session WebSpcket session
230      * @param receivedMessage Textial API message
231      */
232     @OnWebSocketMessage
233     public void onText(Session session, String receivedMessage) {
234         try {
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);
241                 }
242                 if (message.method == null) {
243                     message.method = SHELLYRPC_METHOD_NOTIFYFULLSTATUS;
244                 }
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;
251                         }
252                         handler.onNotifyStatus(status);
253                         return;
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);
259                         } else {
260                             for (Shelly2NotifyEvent e : events.params.events) {
261                                 if (getString(e.event).startsWith(SHELLY2_EVENT_BLUPREFIX)) {
262                                     String address = getString(e.data.addr).replaceAll(":", "");
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));
270                                         }
271                                     } else { // new device
272                                         if (e.event.equals(SHELLY2_EVENT_BLUSCAN)) {
273                                             ShellyBluSensorHandler.addBluThing(message.src, e, thingTable);
274                                         } else {
275                                             logger.debug("{}: NotifyEvent {} for unknown device {}", message.src,
276                                                     e.event, e.data.name);
277                                         }
278                                     }
279                                 }
280                             }
281                         }
282                         break;
283                     default:
284                         handler.onMessage(receivedMessage);
285                 }
286             } else {
287                 logger.debug("{}: No Rpc listener registered for device {}, skip message: {}", thingName,
288                         getString(message.src), receivedMessage);
289             }
290         } catch (ShellyApiException | IllegalArgumentException e) {
291             logger.debug("{}: Unable to process Rpc message ({}): {}", thingName, e.getMessage(), receivedMessage);
292         } catch (NullPointerException e) {
293             logger.debug("{}: Unable to process Rpc message: {}", thingName, receivedMessage, e);
294         }
295     }
296
297     public boolean isConnected() {
298         return session != null && session.isOpen();
299     }
300
301     public boolean isInbound() {
302         return inbound;
303     }
304
305     /**
306      * Web Socket closed, notify thing handler
307      *
308      * @param statusCode StatusCode
309      * @param reason Textual reason
310      */
311     @OnWebSocketClose
312     public void onClose(int statusCode, String reason) {
313         if (statusCode != StatusCode.NORMAL) {
314             logger.trace("{}: Rpc connection closed: {} - {}", thingName, statusCode, getString(reason));
315         }
316         if (inbound) {
317             // Ignore disconnect: Device establishes the socket, sends NotifyxFullStatus and disconnects
318             return;
319         }
320         disconnect();
321         if (websocketHandler != null) {
322             websocketHandler.onClose(statusCode, reason);
323         }
324     }
325
326     /**
327      * WebSocket error handler
328      *
329      * @param cause WebSocket error/Exception
330      */
331     @OnWebSocketError
332     public void onError(Throwable cause) {
333         if (inbound) {
334             // Ignore disconnect: Device establishes the socket, sends NotifyxFullStatus and disconnects
335             return;
336         }
337         if (websocketHandler != null) {
338             websocketHandler.onError(cause);
339         }
340     }
341 }