2 * Copyright (c) 2010-2020 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.*;
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;
40 * The {@link GardenaSmartWebSocket} implements the websocket for receiving constant updates from the Gardena smart
43 * @author Gerhard Riegler - Initial contribution
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;
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;
63 * Starts the websocket session.
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;
71 this.socketId = socketId;
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();
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);
87 * Stops the websocket session.
89 public synchronized void stop() {
91 final ScheduledFuture<?> connectionTracker = this.connectionTracker;
92 if (connectionTracker != null) {
93 connectionTracker.cancel(true);
96 logger.debug("Closing Gardena Webservice client ({})", socketId);
99 } catch (Exception ex) {
103 webSocketClient.stop();
104 } catch (Exception e) {
112 * Returns true, if the websocket is running.
114 public synchronized boolean isRunning() {
115 return session.isOpen();
119 public void onConnect(Session session) {
121 logger.debug("Connected to Gardena Webservice ({})", socketId);
123 connectionTracker = scheduler.scheduleWithFixedDelay(this::sendKeepAlivePing, 2, 2, TimeUnit.MINUTES);
127 public void onFrame(Frame pong) {
128 if (pong instanceof PongFrame) {
129 lastPong = Instant.now();
130 logger.trace("Pong received ({})", socketId);
135 public void onClose(int statusCode, String reason) {
137 logger.debug("Connection to Gardena Webservice was closed ({}): code: {}, reason: {}", socketId, statusCode,
139 socketEventListener.onWebSocketClose();
144 public void onError(Throwable cause) {
146 logger.warn("Gardena Webservice error ({}): {}, restarting", socketId, cause.getMessage());
147 logger.debug("{}", cause.getMessage(), cause);
148 socketEventListener.onWebSocketError();
153 public void onMessage(String msg) {
155 logger.trace("<<< event ({}): {}", socketId, msg);
156 socketEventListener.onWebSocketMessage(msg);
161 * Sends a ping to tell the Gardena smart system that the client is alive.
163 private void sendKeepAlivePing() {
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 + ")");
172 } catch (IOException ex) {
173 logger.debug("{}", ex.getMessage());