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.freeboxos.internal.api.rest;
15 import static org.openhab.binding.freeboxos.internal.FreeboxOsBindingConstants.*;
17 import java.io.IOException;
19 import java.util.HashMap;
20 import java.util.List;
21 import java.util.Locale;
23 import java.util.Optional;
24 import java.util.concurrent.ScheduledExecutorService;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.eclipse.jetty.websocket.api.Session;
31 import org.eclipse.jetty.websocket.api.StatusCode;
32 import org.eclipse.jetty.websocket.api.WebSocketListener;
33 import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
34 import org.eclipse.jetty.websocket.client.WebSocketClient;
35 import org.openhab.binding.freeboxos.internal.api.ApiHandler;
36 import org.openhab.binding.freeboxos.internal.api.FreeboxException;
37 import org.openhab.binding.freeboxos.internal.api.rest.LanBrowserManager.LanHost;
38 import org.openhab.binding.freeboxos.internal.api.rest.VmManager.VirtualMachine;
39 import org.openhab.binding.freeboxos.internal.handler.ApiConsumerHandler;
40 import org.openhab.binding.freeboxos.internal.handler.HostHandler;
41 import org.openhab.binding.freeboxos.internal.handler.VmHandler;
42 import org.openhab.core.common.ThreadPoolManager;
43 import org.openhab.core.thing.ChannelUID;
44 import org.openhab.core.types.RefreshType;
45 import org.slf4j.Logger;
46 import org.slf4j.LoggerFactory;
48 import com.google.gson.JsonElement;
50 import inet.ipaddr.mac.MACAddress;
53 * The {@link WebSocketManager} is the Java class register to the websocket server and handle notifications
55 * @author Gaƫl L'hopital - Initial contribution
58 public class WebSocketManager extends RestManager implements WebSocketListener {
59 private static final String HOST_UNREACHABLE = "lan_host_l3addr_unreachable";
60 private static final String HOST_REACHABLE = "lan_host_l3addr_reachable";
61 private static final String VM_CHANGED = "vm_state_changed";
62 private static final Register REGISTRATION = new Register(VM_CHANGED, HOST_REACHABLE, HOST_UNREACHABLE);
63 private static final String WS_PATH = "ws/event";
65 private final Logger logger = LoggerFactory.getLogger(WebSocketManager.class);
66 private final Map<MACAddress, ApiConsumerHandler> listeners = new HashMap<>();
67 private final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool(BINDING_ID);
68 private final ApiHandler apiHandler;
69 private final WebSocketClient client;
70 private Optional<ScheduledFuture<?>> reconnectJob = Optional.empty();
71 private volatile @Nullable Session wsSession;
73 private record Register(String action, List<String> events) {
74 Register(String... events) {
75 this("register", List.of(events));
79 public WebSocketManager(FreeboxOsSession session) throws FreeboxException {
80 super(session, LoginManager.Permission.NONE, session.getUriBuilder().path(WS_PATH));
81 this.apiHandler = session.getApiHandler();
82 this.client = new WebSocketClient(apiHandler.getHttpClient());
91 private static record WebSocketResponse(boolean success, Action action, String event, String source,
92 @Nullable JsonElement result) {
93 public String getEvent() {
94 return source + "_" + event;
98 public void openSession(@Nullable String sessionToken, int reconnectInterval) {
99 if (reconnectInterval > 0) {
100 URI uri = getUriBuilder().scheme(getUriBuilder().build().getScheme().contains("s") ? "wss" : "ws").build();
101 ClientUpgradeRequest request = new ClientUpgradeRequest();
102 request.setHeader(ApiHandler.AUTH_HEADER, sessionToken);
106 reconnectJob = Optional.of(scheduler.scheduleWithFixedDelay(() -> {
109 client.connect(this, uri, request);
110 // Update listeners in case we would have lost data while disconnecting / reconnecting
112 .forEach(host -> host.handleCommand(new ChannelUID(host.getThing().getUID(), REACHABLE),
113 RefreshType.REFRESH));
114 logger.debug("Websocket manager connected to {}", uri);
115 } catch (IOException e) {
116 logger.warn("Error connecting websocket client: {}", e.getMessage());
118 }, 0, reconnectInterval, TimeUnit.MINUTES));
119 } catch (Exception e) {
120 logger.warn("Error starting websocket client: {}", e.getMessage());
125 private void stopReconnect() {
126 reconnectJob.ifPresent(job -> job.cancel(true));
127 reconnectJob = Optional.empty();
130 public void dispose() {
135 } catch (Exception e) {
136 logger.warn("Error stopping websocket client: {}", e.getMessage());
140 private void closeSession() {
141 logger.debug("Awaiting closure from remote");
142 Session localSession = wsSession;
143 if (localSession != null) {
144 localSession.close();
150 public void onWebSocketConnect(@NonNullByDefault({}) Session wsSession) {
151 this.wsSession = wsSession;
152 logger.debug("Websocket connection establisehd");
154 wsSession.getRemote().sendString(apiHandler.serialize(REGISTRATION));
155 } catch (IOException e) {
156 logger.warn("Error registering to websocket: {}", e.getMessage());
161 public void onWebSocketText(@NonNullByDefault({}) String message) {
162 Session localSession = wsSession;
163 if (message.toLowerCase(Locale.US).contains("bye") && localSession != null) {
164 localSession.close(StatusCode.NORMAL, "Thanks");
168 WebSocketResponse result = apiHandler.deserialize(WebSocketResponse.class, message);
169 if (result.success) {
170 switch (result.action) {
172 logger.debug("Event registration successfull");
175 handleNotification(result);
178 logger.warn("Unhandled notification received: {}", result.action);
183 private void handleNotification(WebSocketResponse response) {
184 JsonElement json = response.result;
186 switch (response.getEvent()) {
188 VirtualMachine vm = apiHandler.deserialize(VirtualMachine.class, json.toString());
189 logger.debug("Received notification for VM {}", vm.id());
190 ApiConsumerHandler handler = listeners.get(vm.mac());
191 if (handler instanceof VmHandler vmHandler) {
192 vmHandler.updateVmChannels(vm);
195 case HOST_UNREACHABLE, HOST_REACHABLE:
196 LanHost host = apiHandler.deserialize(LanHost.class, json.toString());
197 ApiConsumerHandler handler2 = listeners.get(host.getMac());
198 if (handler2 instanceof HostHandler hostHandler) {
199 hostHandler.updateConnectivityChannels(host);
203 logger.warn("Unhandled event received: {}", response.getEvent());
206 logger.warn("Empty json element in notification");
211 public void onWebSocketClose(int statusCode, @NonNullByDefault({}) String reason) {
212 logger.debug("Socket Closed: [{}] - reason {}", statusCode, reason);
213 this.wsSession = null;
217 public void onWebSocketError(@NonNullByDefault({}) Throwable cause) {
218 logger.warn("Error on websocket: {}", cause.getMessage());
222 public void onWebSocketBinary(byte @Nullable [] payload, int offset, int len) {
226 public boolean registerListener(MACAddress mac, ApiConsumerHandler hostHandler) {
227 if (wsSession != null) {
228 listeners.put(mac, hostHandler);
234 public void unregisterListener(MACAddress mac) {
235 listeners.remove(mac);