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 webSocketClient = webSocketFactory.createWebSocketClient(socketId);
74 webSocketClient.setConnectTimeout(config.getConnectionTimeout() * 1000L);
75 webSocketClient.setStopTimeout(3000);
76 webSocketClient.setMaxIdleTimeout(150000);
77 webSocketClient.start();
79 logger.debug("Connecting to Gardena Webservice ({})", socketId);
80 session = (WebSocketSession) webSocketClient
81 .connect(this, new URI(webSocketCreatedResponse.data.attributes.url)).get();
82 session.setStopTimeout(3000);
86 * Stops the websocket session.
88 public synchronized void stop() {
90 final ScheduledFuture<?> connectionTracker = this.connectionTracker;
91 if (connectionTracker != null) {
92 connectionTracker.cancel(true);
95 logger.debug("Closing Gardena Webservice client ({})", socketId);
98 } catch (Exception ex) {
102 webSocketClient.stop();
103 } catch (Exception e) {
111 * Returns true, if the websocket is running.
113 public synchronized boolean isRunning() {
114 return session.isOpen();
118 public void onConnect(Session session) {
120 logger.debug("Connected to Gardena Webservice ({})", socketId);
122 connectionTracker = scheduler.scheduleWithFixedDelay(this::sendKeepAlivePing, 2, 2, TimeUnit.MINUTES);
126 public void onFrame(Frame pong) {
127 if (pong instanceof PongFrame) {
128 lastPong = Instant.now();
129 logger.trace("Pong received ({})", socketId);
134 public void onClose(int statusCode, String reason) {
136 logger.debug("Connection to Gardena Webservice was closed ({}): code: {}, reason: {}", socketId, statusCode,
138 socketEventListener.onWebSocketClose();
143 public void onError(Throwable cause) {
145 logger.warn("Gardena Webservice error ({}): {}, restarting", socketId, cause.getMessage());
146 logger.debug("{}", cause.getMessage(), cause);
147 socketEventListener.onWebSocketError();
152 public void onMessage(String msg) {
154 logger.trace("<<< event ({}): {}", socketId, msg);
155 socketEventListener.onWebSocketMessage(msg);
160 * Sends a ping to tell the Gardena smart system that the client is alive.
162 private void sendKeepAlivePing() {
164 logger.trace("Sending ping ({})", socketId);
165 session.getRemote().sendPing(pingPayload);
166 final PostOAuth2Response accessToken = token;
167 if ((Instant.now().getEpochSecond() - lastPong.getEpochSecond() > WEBSOCKET_IDLE_TIMEOUT)
168 || accessToken == null || accessToken.isAccessTokenExpired()) {
169 session.close(1000, "Timeout manually closing dead connection (" + socketId + ")");
171 } catch (IOException ex) {
172 logger.debug("{}", ex.getMessage());