2 * Copyright (c) 2010-2022 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.config.GardenaConfig;
38 import org.openhab.binding.gardena.internal.model.dto.api.PostOAuth2Response;
39 import org.openhab.binding.gardena.internal.model.dto.api.WebSocketCreatedResponse;
40 import org.openhab.core.io.net.http.WebSocketFactory;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
45 * The {@link GardenaSmartWebSocket} implements the websocket for receiving constant updates from the Gardena smart
48 * @author Gerhard Riegler - Initial contribution
52 public class GardenaSmartWebSocket {
53 private final Logger logger = LoggerFactory.getLogger(GardenaSmartWebSocket.class);
54 private final GardenaSmartWebSocketListener socketEventListener;
55 private final long WEBSOCKET_IDLE_TIMEOUT = 300;
57 private WebSocketSession session;
58 private WebSocketClient webSocketClient;
59 private boolean closing;
60 private Instant lastPong = Instant.now();
61 private ScheduledExecutorService scheduler;
62 private @Nullable ScheduledFuture<?> connectionTracker;
63 private ByteBuffer pingPayload = ByteBuffer.wrap("ping".getBytes(StandardCharsets.UTF_8));
64 private @Nullable PostOAuth2Response token;
65 private String socketId;
68 * Starts the websocket session.
70 public GardenaSmartWebSocket(GardenaSmartWebSocketListener socketEventListener,
71 WebSocketCreatedResponse webSocketCreatedResponse, GardenaConfig config, ScheduledExecutorService scheduler,
72 WebSocketFactory webSocketFactory, @Nullable PostOAuth2Response token, String socketId) throws Exception {
73 this.socketEventListener = socketEventListener;
74 this.scheduler = scheduler;
76 this.socketId = socketId;
78 String webSocketId = String.valueOf(hashCode());
79 webSocketClient = webSocketFactory.createWebSocketClient(webSocketId);
80 webSocketClient.setConnectTimeout(config.getConnectionTimeout() * 1000L);
81 webSocketClient.setStopTimeout(3000);
82 webSocketClient.setMaxIdleTimeout(150000);
83 webSocketClient.start();
85 logger.debug("Connecting to Gardena Webservice ({})", socketId);
86 session = (WebSocketSession) webSocketClient
87 .connect(this, new URI(webSocketCreatedResponse.data.attributes.url)).get();
88 session.setStopTimeout(3000);
92 * Stops the websocket session.
94 public synchronized void stop() {
96 final ScheduledFuture<?> connectionTracker = this.connectionTracker;
97 if (connectionTracker != null) {
98 connectionTracker.cancel(true);
101 logger.debug("Closing Gardena Webservice client ({})", socketId);
104 } catch (Exception ex) {
108 webSocketClient.stop();
109 } catch (Exception e) {
117 * Returns true, if the websocket is running.
119 public synchronized boolean isRunning() {
120 return session.isOpen();
124 public void onConnect(Session session) {
126 logger.debug("Connected to Gardena Webservice ({})", socketId);
128 connectionTracker = scheduler.scheduleWithFixedDelay(this::sendKeepAlivePing, 2, 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) {
142 logger.debug("Connection to Gardena Webservice was closed ({}): code: {}, reason: {}", socketId, statusCode,
144 socketEventListener.onWebSocketClose();
149 public void onError(Throwable cause) {
151 logger.warn("Gardena Webservice error ({}): {}, restarting", socketId, cause.getMessage());
152 logger.debug("{}", cause.getMessage(), cause);
153 socketEventListener.onWebSocketError();
158 public void onMessage(String msg) {
160 logger.trace("<<< event ({}): {}", socketId, msg);
161 socketEventListener.onWebSocketMessage(msg);
166 * Sends a ping to tell the Gardena smart system that the client is alive.
168 private void sendKeepAlivePing() {
170 logger.trace("Sending ping ({})", socketId);
171 session.getRemote().sendPing(pingPayload);
172 final PostOAuth2Response accessToken = token;
173 if ((Instant.now().getEpochSecond() - lastPong.getEpochSecond() > WEBSOCKET_IDLE_TIMEOUT)
174 || accessToken == null || accessToken.isAccessTokenExpired()) {
175 session.close(1000, "Timeout manually closing dead connection (" + socketId + ")");
177 } catch (IOException ex) {
178 logger.debug("{}", ex.getMessage());