]> git.basschouten.com Git - openhab-addons.git/blob
ceee0ff2d4f17d378f2ea5d3e9e3cdf7e3aebfd6
[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.vektiva.internal.handler;
14
15 import static org.openhab.binding.vektiva.internal.VektivaBindingConstants.*;
16
17 import java.io.IOException;
18 import java.net.URI;
19 import java.util.Map;
20 import java.util.concurrent.ExecutionException;
21 import java.util.concurrent.Future;
22 import java.util.concurrent.ScheduledFuture;
23 import java.util.concurrent.TimeUnit;
24 import java.util.concurrent.TimeoutException;
25 import java.util.stream.Collectors;
26 import java.util.stream.Stream;
27
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.eclipse.jetty.client.HttpClient;
31 import org.eclipse.jetty.client.api.ContentResponse;
32 import org.eclipse.jetty.http.HttpMethod;
33 import org.eclipse.jetty.websocket.api.Session;
34 import org.eclipse.jetty.websocket.client.WebSocketClient;
35 import org.openhab.binding.vektiva.internal.config.VektivaSmarwiConfiguration;
36 import org.openhab.binding.vektiva.internal.net.VektivaSmarwiSocket;
37 import org.openhab.core.library.types.PercentType;
38 import org.openhab.core.library.types.StopMoveType;
39 import org.openhab.core.library.types.StringType;
40 import org.openhab.core.library.types.UpDownType;
41 import org.openhab.core.thing.ChannelUID;
42 import org.openhab.core.thing.Thing;
43 import org.openhab.core.thing.ThingStatus;
44 import org.openhab.core.thing.ThingStatusDetail;
45 import org.openhab.core.thing.binding.BaseThingHandler;
46 import org.openhab.core.types.Command;
47 import org.openhab.core.types.RefreshType;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
50
51 /**
52  * The {@link VektivaSmarwiHandler} is responsible for handling commands, which are
53  * sent to one of the channels.
54  *
55  * @author Ondrej Pecta - Initial contribution
56  */
57 @NonNullByDefault
58 public class VektivaSmarwiHandler extends BaseThingHandler {
59
60     private final Logger logger = LoggerFactory.getLogger(VektivaSmarwiHandler.class);
61
62     private VektivaSmarwiConfiguration config = new VektivaSmarwiConfiguration();
63
64     private final HttpClient httpClient;
65
66     private final WebSocketClient webSocketClient;
67
68     private @Nullable Session session;
69
70     private @Nullable ScheduledFuture<?> future = null;
71
72     private int lastPosition = -1;
73
74     public VektivaSmarwiHandler(Thing thing, HttpClient httpClient, WebSocketClient webSocketClient) {
75         super(thing);
76         this.httpClient = httpClient;
77         this.webSocketClient = webSocketClient;
78     }
79
80     @Override
81     public void handleCommand(ChannelUID channelUID, Command command) {
82         if (CHANNEL_STATUS.equals(channelUID.getId()) && command instanceof RefreshType) {
83             checkStatus();
84             return;
85         }
86
87         if (CHANNEL_CONTROL.equals(channelUID.getId())) {
88             logger.trace("Received command: {}", command);
89             String cmd = getSmarwiCommand(command);
90             if (COMMAND_OPEN.equals(cmd) || COMMAND_CLOSE.equals(cmd) || COMMAND_STOP.equals(cmd)) {
91                 if (RESPONSE_OK.equals(sendCommand(cmd)) && !COMMAND_STOP.equals(cmd)) {
92                     lastPosition = COMMAND_OPEN.equals(cmd) ? 0 : 100;
93                 } else {
94                     lastPosition = -1;
95                 }
96             }
97             if (command instanceof PercentType) {
98                 if (RESPONSE_OK.equals(sendCommand(COMMAND_OPEN + "/" + cmd))) {
99                     lastPosition = Integer.parseInt(cmd);
100                 }
101             }
102         }
103     }
104
105     @Override
106     public void dispose() {
107         super.dispose();
108         if (future != null && !(future.isCancelled() || future.isDone())) {
109             future.cancel(true);
110         }
111         closeSession();
112     }
113
114     private void closeSession() {
115         if (session != null && session.isOpen()) {
116             session.close();
117         }
118     }
119
120     private String getSmarwiCommand(Command command) {
121         if (UpDownType.UP.equals(command)) {
122             return COMMAND_OPEN;
123         }
124         if (UpDownType.DOWN.equals(command)) {
125             return COMMAND_CLOSE;
126         }
127         if (StopMoveType.STOP.equals(command)) {
128             return COMMAND_STOP;
129         }
130         return command.toString();
131     }
132
133     private @Nullable String sendCommand(String cmd) {
134         String url = "http://" + config.ip + "/cmd/" + cmd;
135
136         try {
137             ContentResponse resp = httpClient.newRequest(url).method(HttpMethod.GET).send();
138             final String response = resp.getContentAsString();
139             logger.trace("Response: {}", response);
140             if (resp.getStatus() == 200) {
141                 if (ThingStatus.ONLINE != getThing().getStatus()) {
142                     updateStatus(ThingStatus.ONLINE);
143                 }
144             } else {
145                 updateStatus(ThingStatus.OFFLINE);
146             }
147             return response;
148         } catch (InterruptedException e) {
149             logger.debug("API execution has been interrupted", e);
150             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
151                     "API execution has been interrupted");
152             Thread.currentThread().interrupt();
153         } catch (TimeoutException e) {
154             logger.debug("Timeout during API execution", e);
155             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Timeout during API execution");
156         } catch (ExecutionException e) {
157             logger.debug("Exception during API execution", e);
158             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
159                     "Exception during API execution: " + e.getMessage());
160         }
161         return null;
162     }
163
164     @Override
165     public void initialize() {
166         config = getConfigAs(VektivaSmarwiConfiguration.class);
167         logger.debug("IP address: {}", config.ip);
168
169         future = scheduler.scheduleWithFixedDelay(this::checkStatus, 0, config.refreshInterval, TimeUnit.SECONDS);
170     }
171
172     private synchronized void initializeWebSocketSession() {
173         if (config.useWebSockets) {
174             closeSession();
175             session = createSession();
176             if (session != null) {
177                 logger.debug("WebSocket connected!");
178             }
179         }
180     }
181
182     private void checkStatus() {
183         String url = "http://" + config.ip + "/statusn";
184
185         try {
186             ContentResponse resp = httpClient.newRequest(url).method(HttpMethod.GET).send();
187             final String response = resp.getContentAsString();
188             logger.debug("status values: {}", response);
189             if (resp.getStatus() == 200) {
190                 processStatusResponse(response);
191             } else {
192                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
193                         "got response code: " + resp.getStatus());
194             }
195             // reconnect web socket if not connected
196             if (config.useWebSockets && (session == null || !session.isOpen())
197                     && ThingStatus.ONLINE == getThing().getStatus()) {
198                 logger.debug("Initializing WebSocket session");
199                 initializeWebSocketSession();
200                 return;
201             }
202         } catch (InterruptedException e) {
203             logger.debug("API execution has been interrupted", e);
204             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
205                     "API execution has been interrupted");
206             Thread.currentThread().interrupt();
207         } catch (TimeoutException e) {
208             logger.debug("Timeout during status update", e);
209             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Timeout during status update");
210         } catch (ExecutionException e) {
211             logger.debug("Exception during status update", e);
212             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
213                     "Exception during status update: " + e.getMessage());
214         }
215         session = null;
216     }
217
218     public synchronized void processStatusResponse(String content) {
219         if (ThingStatus.ONLINE != getThing().getStatus()) {
220             updateStatus(ThingStatus.ONLINE);
221         }
222
223         Map<String, String> values = Stream.of(content.split("\n")).map(s -> s.split(":")).filter(s -> s.length == 2)
224                 .collect(Collectors.toMap(s -> s[0], s -> s[1]));
225
226         updateProperty("Product type", values.getOrDefault("t", NA));
227         updateProperty(Thing.PROPERTY_FIRMWARE_VERSION, values.getOrDefault("fw", NA));
228         updateProperty("Wifi signal", values.getOrDefault("rssi", NA));
229         updateProperty("Product name", values.getOrDefault("cid", NA));
230
231         String statusMessage = "Stopped";
232         if (!"250".equals(values.getOrDefault("s", NA))) {
233             statusMessage = "Moving";
234         }
235
236         if ("1".equals(values.getOrDefault("ro", NA))) {
237             statusMessage = "Not ready";
238         }
239
240         if ("10".equals(values.getOrDefault("e", NA))) {
241             statusMessage = "Blocked";
242         }
243
244         int position = "o".equals(values.getOrDefault("pos", NA)) ? 0 : 100;
245         if (position == 0 && lastPosition != -1) {
246             position = lastPosition;
247         }
248         updateState(CHANNEL_CONTROL, new PercentType(position));
249         updateState(CHANNEL_STATUS, new StringType(statusMessage));
250     }
251
252     private @Nullable Session createSession() {
253         String url = "ws://" + config.ip + "/ws";
254         URI uri = URI.create(url);
255
256         try {
257             // The socket that receives events
258             VektivaSmarwiSocket socket = new VektivaSmarwiSocket(this);
259             // Attempt Connect
260             Future<Session> fut = webSocketClient.connect(socket, uri);
261             // Wait for Connect
262             return fut.get();
263         } catch (IOException ex) {
264             logger.debug("Cannot connect websocket client", ex);
265             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Cannot connect websocket client");
266         } catch (InterruptedException ex) {
267             logger.debug("Cannot create websocket session", ex);
268             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Cannot create websocket session");
269             Thread.currentThread().interrupt();
270         } catch (ExecutionException ex) {
271             logger.debug("Cannot create websocket session", ex);
272             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Cannot create websocket session");
273         }
274         return null;
275     }
276 }