]> git.basschouten.com Git - openhab-addons.git/blob
6c4e06910aee8d82541513e2877210efd1c1bf28
[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.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.util.concurrent.ScheduledExecutorService;
20 import java.util.concurrent.ScheduledFuture;
21 import java.util.concurrent.TimeUnit;
22
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.eclipse.jetty.websocket.api.Session;
26 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
27 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
28 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
29 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketFrame;
30 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
31 import org.eclipse.jetty.websocket.api.annotations.WebSocket;
32 import org.eclipse.jetty.websocket.api.extensions.Frame;
33 import org.eclipse.jetty.websocket.client.WebSocketClient;
34 import org.eclipse.jetty.websocket.common.WebSocketSession;
35 import org.eclipse.jetty.websocket.common.frames.PongFrame;
36 import org.openhab.binding.gardena.internal.model.dto.api.PostOAuth2Response;
37 import org.slf4j.Logger;
38 import org.slf4j.LoggerFactory;
39
40 /**
41  * The {@link GardenaSmartWebSocket} implements the websocket for receiving constant updates from the Gardena smart
42  * system.
43  *
44  * @author Gerhard Riegler - Initial contribution
45  */
46 @NonNullByDefault
47 @WebSocket
48 public class GardenaSmartWebSocket {
49     private final Logger logger = LoggerFactory.getLogger(GardenaSmartWebSocket.class);
50     private final GardenaSmartWebSocketListener socketEventListener;
51     private static final int MAX_UNANSWERED_PINGS = 5;
52
53     private WebSocketSession session;
54     private WebSocketClient webSocketClient;
55     private boolean closing;
56     private int unansweredPings = 0;
57     private ScheduledExecutorService scheduler;
58     private @Nullable ScheduledFuture<?> connectionTracker;
59     private ByteBuffer pingPayload = ByteBuffer.wrap("ping".getBytes(StandardCharsets.UTF_8));
60     private @Nullable PostOAuth2Response token;
61     private String socketId;
62     private String locationID;
63
64     /**
65      * Starts the websocket session.
66      */
67     public GardenaSmartWebSocket(GardenaSmartWebSocketListener socketEventListener, WebSocketClient webSocketClient,
68             ScheduledExecutorService scheduler, String url, @Nullable PostOAuth2Response token, String socketId,
69             String locationID) throws Exception {
70         this.socketEventListener = socketEventListener;
71         this.webSocketClient = webSocketClient;
72         this.scheduler = scheduler;
73         this.token = token;
74         this.socketId = socketId;
75         this.locationID = locationID;
76
77         session = (WebSocketSession) webSocketClient.connect(this, new URI(url)).get();
78         logger.debug("Connecting to Gardena Webservice ({})", socketId);
79     }
80
81     /**
82      * Stops the websocket session.
83      */
84     public synchronized void stop() {
85         closing = true;
86         final ScheduledFuture<?> connectionTracker = this.connectionTracker;
87         if (connectionTracker != null) {
88             connectionTracker.cancel(true);
89         }
90
91         logger.debug("Closing Gardena Webservice ({})", socketId);
92         try {
93             session.close();
94         } catch (Exception ex) {
95             // ignore
96         }
97     }
98
99     public boolean isClosing() {
100         return this.closing;
101     }
102
103     public String getSocketID() {
104         return this.socketId;
105     }
106
107     public String getLocationID() {
108         return this.locationID;
109     }
110
111     public void restart(String newUrl) throws Exception {
112         logger.debug("Reconnecting to Gardena Webservice ({})", socketId);
113         session = (WebSocketSession) webSocketClient.connect(this, new URI(newUrl)).get();
114     }
115
116     @OnWebSocketConnect
117     public synchronized void onConnect(Session session) {
118         closing = false;
119         unansweredPings = 0;
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 synchronized void onFrame(Frame pong) {
133         if (pong instanceof PongFrame) {
134             unansweredPings = 0;
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 (unansweredPings > MAX_UNANSWERED_PINGS || accessToken == null || accessToken.isAccessTokenExpired()) {
174             session.close(1000, "Timeout manually closing dead connection (" + socketId + ")");
175         } else {
176             if (session.isOpen()) {
177                 try {
178                     logger.trace("Sending ping ({})", socketId);
179                     session.getRemote().sendPing(pingPayload);
180                     ++unansweredPings;
181                 } catch (IOException ex) {
182                     logger.debug("Error while sending ping: {}", ex.getMessage());
183                 }
184             }
185         }
186     }
187 }