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