]> git.basschouten.com Git - openhab-addons.git/blob
f003a8bf952379746d6c2d9f07ed90feffce9031
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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         String webSocketId = String.valueOf(hashCode());
74         webSocketClient = webSocketFactory.createWebSocketClient(webSocketId);
75         webSocketClient.setConnectTimeout(config.getConnectionTimeout() * 1000L);
76         webSocketClient.setStopTimeout(3000);
77         webSocketClient.setMaxIdleTimeout(150000);
78         webSocketClient.start();
79
80         logger.debug("Connecting to Gardena Webservice ({})", socketId);
81         session = (WebSocketSession) webSocketClient
82                 .connect(this, new URI(webSocketCreatedResponse.data.attributes.url)).get();
83         session.setStopTimeout(3000);
84     }
85
86     /**
87      * Stops the websocket session.
88      */
89     public synchronized void stop() {
90         closing = true;
91         final ScheduledFuture<?> connectionTracker = this.connectionTracker;
92         if (connectionTracker != null) {
93             connectionTracker.cancel(true);
94         }
95         if (isRunning()) {
96             logger.debug("Closing Gardena Webservice client ({})", socketId);
97             try {
98                 session.close();
99             } catch (Exception ex) {
100                 // ignore
101             } finally {
102                 try {
103                     webSocketClient.stop();
104                 } catch (Exception e) {
105                     // ignore
106                 }
107             }
108         }
109     }
110
111     /**
112      * Returns true, if the websocket is running.
113      */
114     public synchronized boolean isRunning() {
115         return session.isOpen();
116     }
117
118     @OnWebSocketConnect
119     public void onConnect(Session session) {
120         closing = false;
121         logger.debug("Connected to Gardena Webservice ({})", socketId);
122
123         connectionTracker = scheduler.scheduleWithFixedDelay(this::sendKeepAlivePing, 2, 2, TimeUnit.MINUTES);
124     }
125
126     @OnWebSocketFrame
127     public void onFrame(Frame pong) {
128         if (pong instanceof PongFrame) {
129             lastPong = Instant.now();
130             logger.trace("Pong received ({})", socketId);
131         }
132     }
133
134     @OnWebSocketClose
135     public void onClose(int statusCode, String reason) {
136         if (!closing) {
137             logger.debug("Connection to Gardena Webservice was closed ({}): code: {}, reason: {}", socketId, statusCode,
138                     reason);
139             socketEventListener.onWebSocketClose();
140         }
141     }
142
143     @OnWebSocketError
144     public void onError(Throwable cause) {
145         if (!closing) {
146             logger.warn("Gardena Webservice error ({}): {}, restarting", socketId, cause.getMessage());
147             logger.debug("{}", cause.getMessage(), cause);
148             socketEventListener.onWebSocketError();
149         }
150     }
151
152     @OnWebSocketMessage
153     public void onMessage(String msg) {
154         if (!closing) {
155             logger.trace("<<< event ({}): {}", socketId, msg);
156             socketEventListener.onWebSocketMessage(msg);
157         }
158     }
159
160     /**
161      * Sends a ping to tell the Gardena smart system that the client is alive.
162      */
163     private void sendKeepAlivePing() {
164         try {
165             logger.trace("Sending ping ({})", socketId);
166             session.getRemote().sendPing(pingPayload);
167             final PostOAuth2Response accessToken = token;
168             if ((Instant.now().getEpochSecond() - lastPong.getEpochSecond() > WEBSOCKET_IDLE_TIMEOUT)
169                     || accessToken == null || accessToken.isAccessTokenExpired()) {
170                 session.close(1000, "Timeout manually closing dead connection (" + socketId + ")");
171             }
172         } catch (IOException ex) {
173             logger.debug("{}", ex.getMessage());
174         }
175     }
176 }