]> git.basschouten.com Git - openhab-addons.git/blob
5e5c215775fcb44697b074f2d0962acd6f95f9d8
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.freeboxos.internal.api.rest;
14
15 import static org.openhab.binding.freeboxos.internal.FreeboxOsBindingConstants.*;
16
17 import java.io.IOException;
18 import java.net.URI;
19 import java.util.HashMap;
20 import java.util.List;
21 import java.util.Locale;
22 import java.util.Map;
23 import java.util.Optional;
24 import java.util.concurrent.ScheduledExecutorService;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
27
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;
47
48 import com.google.gson.JsonElement;
49
50 import inet.ipaddr.mac.MACAddress;
51
52 /**
53  * The {@link WebSocketManager} is the Java class register to the websocket server and handle notifications
54  *
55  * @author GaĆ«l L'hopital - Initial contribution
56  */
57 @NonNullByDefault
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";
64
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;
72
73     private record Register(String action, List<String> events) {
74         Register(String... events) {
75             this("register", List.of(events));
76         }
77     }
78
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());
83     }
84
85     private enum Action {
86         REGISTER,
87         NOTIFICATION,
88         UNKNOWN
89     }
90
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;
95         }
96     }
97
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);
103             try {
104                 client.start();
105                 stopReconnect();
106                 reconnectJob = Optional.of(scheduler.scheduleWithFixedDelay(() -> {
107                     try {
108                         closeSession();
109                         client.connect(this, uri, request);
110                         // Update listeners in case we would have lost data while disconnecting / reconnecting
111                         listeners.values()
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());
117                     }
118                 }, 0, reconnectInterval, TimeUnit.MINUTES));
119             } catch (Exception e) {
120                 logger.warn("Error starting websocket client: {}", e.getMessage());
121             }
122         }
123     }
124
125     private void stopReconnect() {
126         reconnectJob.ifPresent(job -> job.cancel(true));
127         reconnectJob = Optional.empty();
128     }
129
130     public void dispose() {
131         stopReconnect();
132         closeSession();
133         try {
134             client.stop();
135         } catch (Exception e) {
136             logger.warn("Error stopping websocket client: {}", e.getMessage());
137         }
138     }
139
140     private void closeSession() {
141         logger.debug("Awaiting closure from remote");
142         Session localSession = wsSession;
143         if (localSession != null) {
144             localSession.close();
145             wsSession = null;
146         }
147     }
148
149     @Override
150     public void onWebSocketConnect(@NonNullByDefault({}) Session wsSession) {
151         this.wsSession = wsSession;
152         logger.debug("Websocket connection establisehd");
153         try {
154             wsSession.getRemote().sendString(apiHandler.serialize(REGISTRATION));
155         } catch (IOException e) {
156             logger.warn("Error registering to websocket: {}", e.getMessage());
157         }
158     }
159
160     @Override
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");
165             return;
166         }
167
168         WebSocketResponse result = apiHandler.deserialize(WebSocketResponse.class, message);
169         if (result.success) {
170             switch (result.action) {
171                 case REGISTER:
172                     logger.debug("Event registration successfull");
173                     break;
174                 case NOTIFICATION:
175                     handleNotification(result);
176                     break;
177                 default:
178                     logger.warn("Unhandled notification received: {}", result.action);
179             }
180         }
181     }
182
183     private void handleNotification(WebSocketResponse response) {
184         JsonElement json = response.result;
185         if (json != null) {
186             switch (response.getEvent()) {
187                 case VM_CHANGED:
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);
193                     }
194                     break;
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);
200                     }
201                     break;
202                 default:
203                     logger.warn("Unhandled event received: {}", response.getEvent());
204             }
205         } else {
206             logger.warn("Empty json element in notification");
207         }
208     }
209
210     @Override
211     public void onWebSocketClose(int statusCode, @NonNullByDefault({}) String reason) {
212         logger.debug("Socket Closed: [{}] - reason {}", statusCode, reason);
213         this.wsSession = null;
214     }
215
216     @Override
217     public void onWebSocketError(@NonNullByDefault({}) Throwable cause) {
218         logger.warn("Error on websocket: {}", cause.getMessage());
219     }
220
221     @Override
222     public void onWebSocketBinary(byte @Nullable [] payload, int offset, int len) {
223         /* do nothing */
224     }
225
226     public boolean registerListener(MACAddress mac, ApiConsumerHandler hostHandler) {
227         if (wsSession != null) {
228             listeners.put(mac, hostHandler);
229             return true;
230         }
231         return false;
232     }
233
234     public void unregisterListener(MACAddress mac) {
235         listeners.remove(mac);
236     }
237 }