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.util.concurrent.ScheduledExecutorService;
20 import java.util.concurrent.ScheduledFuture;
21 import java.util.concurrent.TimeUnit;
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;
41 * The {@link GardenaSmartWebSocket} implements the websocket for receiving constant updates from the Gardena smart
44 * @author Gerhard Riegler - Initial contribution
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;
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;
65 * Starts the websocket session.
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;
74 this.socketId = socketId;
75 this.locationID = locationID;
77 session = (WebSocketSession) webSocketClient.connect(this, new URI(url)).get();
78 logger.debug("Connecting to Gardena Webservice ({})", socketId);
82 * Stops the websocket session.
84 public synchronized void stop() {
86 final ScheduledFuture<?> connectionTracker = this.connectionTracker;
87 if (connectionTracker != null) {
88 connectionTracker.cancel(true);
91 logger.debug("Closing Gardena Webservice ({})", socketId);
94 } catch (Exception ex) {
99 public boolean isClosing() {
103 public String getSocketID() {
104 return this.socketId;
107 public String getLocationID() {
108 return this.locationID;
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();
117 public synchronized 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 synchronized void onFrame(Frame pong) {
133 if (pong instanceof PongFrame) {
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 (unansweredPings > MAX_UNANSWERED_PINGS || accessToken == null || accessToken.isAccessTokenExpired()) {
174 session.close(1000, "Timeout manually closing dead connection (" + socketId + ")");
176 if (session.isOpen()) {
178 logger.trace("Sending ping ({})", socketId);
179 session.getRemote().sendPing(pingPayload);
181 } catch (IOException ex) {
182 logger.debug("Error while sending ping: {}", ex.getMessage());