]> git.basschouten.com Git - openhab-addons.git/blob
5d8deeaab3dbbbaf48754f98fb0d47fbcf030585
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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.gardena.internal;
14
15 import java.io.IOException;
16 import java.net.URI;
17 import java.nio.ByteBuffer;
18 import java.nio.charset.StandardCharsets;
19 import java.time.Instant;
20 import java.util.concurrent.ScheduledExecutorService;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
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.annotations.OnWebSocketClose;
28 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
29 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
30 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketFrame;
31 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
32 import org.eclipse.jetty.websocket.api.annotations.WebSocket;
33 import org.eclipse.jetty.websocket.api.extensions.Frame;
34 import org.eclipse.jetty.websocket.client.WebSocketClient;
35 import org.eclipse.jetty.websocket.common.WebSocketSession;
36 import org.eclipse.jetty.websocket.common.frames.PongFrame;
37 import org.openhab.binding.gardena.internal.config.GardenaConfig;
38 import org.openhab.binding.gardena.internal.model.dto.api.PostOAuth2Response;
39 import org.openhab.binding.gardena.internal.model.dto.api.WebSocketCreatedResponse;
40 import org.openhab.core.io.net.http.WebSocketFactory;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
43
44 /**
45  * The {@link GardenaSmartWebSocket} implements the websocket for receiving constant updates from the Gardena smart
46  * system.
47  *
48  * @author Gerhard Riegler - Initial contribution
49  */
50 @NonNullByDefault
51 @WebSocket
52 public class GardenaSmartWebSocket {
53     private final Logger logger = LoggerFactory.getLogger(GardenaSmartWebSocket.class);
54     private final GardenaSmartWebSocketListener socketEventListener;
55     private final long WEBSOCKET_IDLE_TIMEOUT = 300;
56
57     private WebSocketSession session;
58     private WebSocketClient webSocketClient;
59     private boolean closing;
60     private Instant lastPong = Instant.now();
61     private ScheduledExecutorService scheduler;
62     private @Nullable ScheduledFuture<?> connectionTracker;
63     private ByteBuffer pingPayload = ByteBuffer.wrap("ping".getBytes(StandardCharsets.UTF_8));
64     private @Nullable PostOAuth2Response token;
65     private String socketId;
66
67     /**
68      * Starts the websocket session.
69      */
70     public GardenaSmartWebSocket(GardenaSmartWebSocketListener socketEventListener,
71             WebSocketCreatedResponse webSocketCreatedResponse, GardenaConfig config, ScheduledExecutorService scheduler,
72             WebSocketFactory webSocketFactory, @Nullable PostOAuth2Response token, String socketId) throws Exception {
73         this.socketEventListener = socketEventListener;
74         this.scheduler = scheduler;
75         this.token = token;
76         this.socketId = socketId;
77
78         String webSocketId = String.valueOf(hashCode());
79         webSocketClient = webSocketFactory.createWebSocketClient(webSocketId);
80         webSocketClient.setConnectTimeout(config.getConnectionTimeout() * 1000L);
81         webSocketClient.setStopTimeout(3000);
82         webSocketClient.setMaxIdleTimeout(150000);
83         webSocketClient.start();
84
85         logger.debug("Connecting to Gardena Webservice ({})", socketId);
86         session = (WebSocketSession) webSocketClient
87                 .connect(this, new URI(webSocketCreatedResponse.data.attributes.url)).get();
88         session.setStopTimeout(3000);
89     }
90
91     /**
92      * Stops the websocket session.
93      */
94     public synchronized void stop() {
95         closing = true;
96         final ScheduledFuture<?> connectionTracker = this.connectionTracker;
97         if (connectionTracker != null) {
98             connectionTracker.cancel(true);
99         }
100         if (isRunning()) {
101             logger.debug("Closing Gardena Webservice client ({})", socketId);
102             try {
103                 session.close();
104             } catch (Exception ex) {
105                 // ignore
106             } finally {
107                 try {
108                     webSocketClient.stop();
109                 } catch (Exception e) {
110                     // ignore
111                 }
112             }
113         }
114     }
115
116     /**
117      * Returns true, if the websocket is running.
118      */
119     public synchronized boolean isRunning() {
120         return session.isOpen();
121     }
122
123     @OnWebSocketConnect
124     public void onConnect(Session session) {
125         closing = false;
126         logger.debug("Connected to Gardena Webservice ({})", socketId);
127
128         connectionTracker = scheduler.scheduleWithFixedDelay(this::sendKeepAlivePing, 2, 2, TimeUnit.MINUTES);
129     }
130
131     @OnWebSocketFrame
132     public void onFrame(Frame pong) {
133         if (pong instanceof PongFrame) {
134             lastPong = Instant.now();
135             logger.trace("Pong received ({})", socketId);
136         }
137     }
138
139     @OnWebSocketClose
140     public void onClose(int statusCode, String reason) {
141         if (!closing) {
142             logger.debug("Connection to Gardena Webservice was closed ({}): code: {}, reason: {}", socketId, statusCode,
143                     reason);
144             socketEventListener.onWebSocketClose();
145         }
146     }
147
148     @OnWebSocketError
149     public void onError(Throwable cause) {
150         if (!closing) {
151             logger.warn("Gardena Webservice error ({}): {}, restarting", socketId, cause.getMessage());
152             logger.debug("{}", cause.getMessage(), cause);
153             socketEventListener.onWebSocketError();
154         }
155     }
156
157     @OnWebSocketMessage
158     public void onMessage(String msg) {
159         if (!closing) {
160             logger.trace("<<< event ({}): {}", socketId, msg);
161             socketEventListener.onWebSocketMessage(msg);
162         }
163     }
164
165     /**
166      * Sends a ping to tell the Gardena smart system that the client is alive.
167      */
168     private void sendKeepAlivePing() {
169         try {
170             logger.trace("Sending ping ({})", socketId);
171             session.getRemote().sendPing(pingPayload);
172             final PostOAuth2Response accessToken = token;
173             if ((Instant.now().getEpochSecond() - lastPong.getEpochSecond() > WEBSOCKET_IDLE_TIMEOUT)
174                     || accessToken == null || accessToken.isAccessTokenExpired()) {
175                 session.close(1000, "Timeout manually closing dead connection (" + socketId + ")");
176             }
177         } catch (IOException ex) {
178             logger.debug("{}", ex.getMessage());
179         }
180     }
181 }