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.vektiva.internal.handler;
15 import static org.openhab.binding.vektiva.internal.VektivaBindingConstants.*;
17 import java.io.IOException;
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;
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;
52 * The {@link VektivaSmarwiHandler} is responsible for handling commands, which are
53 * sent to one of the channels.
55 * @author Ondrej Pecta - Initial contribution
58 public class VektivaSmarwiHandler extends BaseThingHandler {
60 private final Logger logger = LoggerFactory.getLogger(VektivaSmarwiHandler.class);
62 private VektivaSmarwiConfiguration config = new VektivaSmarwiConfiguration();
64 private final HttpClient httpClient;
66 private final WebSocketClient webSocketClient;
68 private @Nullable Session session;
70 private @Nullable ScheduledFuture<?> future = null;
72 private int lastPosition = -1;
74 public VektivaSmarwiHandler(Thing thing, HttpClient httpClient, WebSocketClient webSocketClient) {
76 this.httpClient = httpClient;
77 this.webSocketClient = webSocketClient;
81 public void handleCommand(ChannelUID channelUID, Command command) {
82 if (CHANNEL_STATUS.equals(channelUID.getId()) && command instanceof RefreshType) {
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;
97 if (command instanceof PercentType) {
98 if (RESPONSE_OK.equals(sendCommand(COMMAND_OPEN + "/" + cmd))) {
99 lastPosition = Integer.parseInt(cmd);
106 public void dispose() {
108 if (future != null && !(future.isCancelled() || future.isDone())) {
114 private void closeSession() {
115 if (session != null && session.isOpen()) {
120 private String getSmarwiCommand(Command command) {
121 if (UpDownType.UP.equals(command)) {
124 if (UpDownType.DOWN.equals(command)) {
125 return COMMAND_CLOSE;
127 if (StopMoveType.STOP.equals(command)) {
130 return command.toString();
133 private @Nullable String sendCommand(String cmd) {
134 String url = "http://" + config.ip + "/cmd/" + cmd;
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);
145 updateStatus(ThingStatus.OFFLINE);
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());
165 public void initialize() {
166 config = getConfigAs(VektivaSmarwiConfiguration.class);
167 logger.debug("IP address: {}", config.ip);
169 future = scheduler.scheduleWithFixedDelay(this::checkStatus, 0, config.refreshInterval, TimeUnit.SECONDS);
172 private synchronized void initializeWebSocketSession() {
173 if (config.useWebSockets) {
175 session = createSession();
176 if (session != null) {
177 logger.debug("WebSocket connected!");
182 private void checkStatus() {
183 String url = "http://" + config.ip + "/statusn";
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);
192 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
193 "got response code: " + resp.getStatus());
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();
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());
218 public synchronized void processStatusResponse(String content) {
219 if (ThingStatus.ONLINE != getThing().getStatus()) {
220 updateStatus(ThingStatus.ONLINE);
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]));
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));
231 String statusMessage = "Stopped";
232 if (!"250".equals(values.getOrDefault("s", NA))) {
233 statusMessage = "Moving";
236 if ("1".equals(values.getOrDefault("ro", NA))) {
237 statusMessage = "Not ready";
240 if ("10".equals(values.getOrDefault("e", NA))) {
241 statusMessage = "Blocked";
244 int position = "o".equals(values.getOrDefault("pos", NA)) ? 0 : 100;
245 if (position == 0 && lastPosition != -1) {
246 position = lastPosition;
248 updateState(CHANNEL_CONTROL, new PercentType(position));
249 updateState(CHANNEL_STATUS, new StringType(statusMessage));
252 private @Nullable Session createSession() {
253 String url = "ws://" + config.ip + "/ws";
254 URI uri = URI.create(url);
257 // The socket that receives events
258 VektivaSmarwiSocket socket = new VektivaSmarwiSocket(this);
260 Future<Session> fut = webSocketClient.connect(socket, uri);
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");