2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.gardena.internal;
15 import java.io.IOException;
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;
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;
42 * The {@link GardenaSmartWebSocket} implements the websocket for receiving constant updates from the Gardena smart
45 * @author Gerhard Riegler - Initial contribution
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;
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;
66 * Starts the websocket session.
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;
75 this.socketId = socketId;
76 this.locationID = locationID;
78 session = (WebSocketSession) webSocketClient.connect(this, new URI(url)).get();
79 logger.debug("Connecting to Gardena Webservice ({})", socketId);
83 * Stops the websocket session.
85 public synchronized void stop() {
87 final ScheduledFuture<?> connectionTracker = this.connectionTracker;
88 if (connectionTracker != null) {
89 connectionTracker.cancel(true);
92 logger.debug("Closing Gardena Webservice ({})", socketId);
95 } catch (Exception ex) {
100 public boolean isClosing() {
104 public String getSocketID() {
105 return this.socketId;
108 public String getLocationID() {
109 return this.locationID;
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();
118 public void onConnect(Session session) {
120 logger.debug("Connected to Gardena Webservice ({})", socketId);
122 ScheduledFuture<?> connectionTracker = this.connectionTracker;
123 if (connectionTracker != null && !connectionTracker.isCancelled()) {
124 connectionTracker.cancel(true);
127 // start sending PING every two minutes
128 this.connectionTracker = scheduler.scheduleWithFixedDelay(this::sendKeepAlivePing, 1, 2, TimeUnit.MINUTES);
132 public void onFrame(Frame pong) {
133 if (pong instanceof PongFrame) {
134 lastPong = Instant.now();
135 logger.trace("Pong received ({})", socketId);
140 public void onClose(int statusCode, String reason) {
141 logger.debug("Connection to Gardena Webservice was closed ({}): code: {}, reason: {}", socketId, statusCode,
145 // let listener handle restart of socket
146 socketEventListener.onWebSocketClose(locationID);
151 public void onError(Throwable cause) {
152 logger.debug("Gardena Webservice error ({})", socketId, cause); // log whole stack trace
155 // let listener handle restart of socket
156 socketEventListener.onWebSocketError(locationID);
161 public void onMessage(String msg) {
163 logger.trace("<<< event ({}): {}", socketId, msg);
164 socketEventListener.onWebSocketMessage(msg);
169 * Sends a ping to tell the Gardena smart system that the client is alive.
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 + ")");
177 if (session.isOpen()) {
179 logger.trace("Sending ping ({})", socketId);
180 session.getRemote().sendPing(pingPayload);
181 } catch (IOException ex) {
182 logger.debug("Error while sending ping: {}", ex.getMessage());