]> git.basschouten.com Git - openhab-addons.git/blob
5b339248e4f6eca136cbaef2fd7733f0c9adf2a3
[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.model.dto.api.PostOAuth2Response;
38 import org.slf4j.Logger;
39 import org.slf4j.LoggerFactory;
40
41 /**
42  * The {@link GardenaSmartWebSocket} implements the websocket for receiving constant updates from the Gardena smart
43  * system.
44  *
45  * @author Gerhard Riegler - Initial contribution
46  */
47 @NonNullByDefault
48 @WebSocket
49 public class GardenaSmartWebSocket {
50     private final Logger logger = LoggerFactory.getLogger(GardenaSmartWebSocket.class);
51     private final GardenaSmartWebSocketListener socketEventListener;
52     private final long WEBSOCKET_IDLE_TIMEOUT = 300;
53
54     private WebSocketSession session;
55     private WebSocketClient webSocketClient;
56     private boolean closing;
57     private Instant lastPong = Instant.now();
58     private ScheduledExecutorService scheduler;
59     private @Nullable ScheduledFuture<?> connectionTracker;
60     private ByteBuffer pingPayload = ByteBuffer.wrap("ping".getBytes(StandardCharsets.UTF_8));
61     private @Nullable PostOAuth2Response token;
62     private String socketId;
63     private String locationID;
64
65     /**
66      * Starts the websocket session.
67      */
68     public GardenaSmartWebSocket(GardenaSmartWebSocketListener socketEventListener, WebSocketClient webSocketClient,
69             ScheduledExecutorService scheduler, String url, @Nullable PostOAuth2Response token, String socketId,
70             String locationID) throws Exception {
71         this.socketEventListener = socketEventListener;
72         this.webSocketClient = webSocketClient;
73         this.scheduler = scheduler;
74         this.token = token;
75         this.socketId = socketId;
76         this.locationID = locationID;
77
78         session = (WebSocketSession) webSocketClient.connect(this, new URI(url)).get();
79         logger.debug("Connecting to Gardena Webservice ({})", socketId);
80     }
81
82     /**
83      * Stops the websocket session.
84      */
85     public synchronized void stop() {
86         closing = true;
87         final ScheduledFuture<?> connectionTracker = this.connectionTracker;
88         if (connectionTracker != null) {
89             connectionTracker.cancel(true);
90         }
91
92         logger.debug("Closing Gardena Webservice ({})", socketId);
93         try {
94             session.close();
95         } catch (Exception ex) {
96             // ignore
97         }
98     }
99
100     public boolean isClosing() {
101         return this.closing;
102     }
103
104     public String getSocketID() {
105         return this.socketId;
106     }
107
108     public String getLocationID() {
109         return this.locationID;
110     }
111
112     public void restart(String newUrl) throws Exception {
113         logger.debug("Reconnecting to Gardena Webservice ({})", socketId);
114         session = (WebSocketSession) webSocketClient.connect(this, new URI(newUrl)).get();
115     }
116
117     @OnWebSocketConnect
118     public void onConnect(Session session) {
119         closing = false;
120         logger.debug("Connected to Gardena Webservice ({})", socketId);
121
122         ScheduledFuture<?> connectionTracker = this.connectionTracker;
123         if (connectionTracker != null && !connectionTracker.isCancelled()) {
124             connectionTracker.cancel(true);
125         }
126
127         // start sending PING every two minutes
128         this.connectionTracker = scheduler.scheduleWithFixedDelay(this::sendKeepAlivePing, 1, 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         logger.debug("Connection to Gardena Webservice was closed ({}): code: {}, reason: {}", socketId, statusCode,
142                 reason);
143
144         if (!closing) {
145             // let listener handle restart of socket
146             socketEventListener.onWebSocketClose(locationID);
147         }
148     }
149
150     @OnWebSocketError
151     public void onError(Throwable cause) {
152         logger.debug("Gardena Webservice error ({})", socketId, cause); // log whole stack trace
153
154         if (!closing) {
155             // let listener handle restart of socket
156             socketEventListener.onWebSocketError(locationID);
157         }
158     }
159
160     @OnWebSocketMessage
161     public void onMessage(String msg) {
162         if (!closing) {
163             logger.trace("<<< event ({}): {}", socketId, msg);
164             socketEventListener.onWebSocketMessage(msg);
165         }
166     }
167
168     /**
169      * Sends a ping to tell the Gardena smart system that the client is alive.
170      */
171     private synchronized void sendKeepAlivePing() {
172         final PostOAuth2Response accessToken = token;
173         if ((Instant.now().getEpochSecond() - lastPong.getEpochSecond() > WEBSOCKET_IDLE_TIMEOUT) || accessToken == null
174                 || accessToken.isAccessTokenExpired()) {
175             session.close(1000, "Timeout manually closing dead connection (" + socketId + ")");
176         } else {
177             if (session.isOpen()) {
178                 try {
179                     logger.trace("Sending ping ({})", socketId);
180                     session.getRemote().sendPing(pingPayload);
181                 } catch (IOException ex) {
182                     logger.debug("Error while sending ping: {}", ex.getMessage());
183                 }
184             }
185         }
186     }
187 }