2 * Copyright (c) 2010-2022 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.yioremote.internal;
15 import static org.openhab.binding.yioremote.internal.YIOremoteBindingConstants.*;
18 import java.net.URISyntaxException;
19 import java.util.Collection;
20 import java.util.Collections;
21 import java.util.concurrent.Future;
22 import java.util.concurrent.TimeUnit;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
27 import org.eclipse.jetty.websocket.client.WebSocketClient;
28 import org.openhab.binding.yioremote.internal.YIOremoteBindingConstants.YioRemoteDockHandleStatus;
29 import org.openhab.binding.yioremote.internal.YIOremoteBindingConstants.YioRemoteMessages;
30 import org.openhab.binding.yioremote.internal.dto.AuthenticationMessage;
31 import org.openhab.binding.yioremote.internal.dto.IRCode;
32 import org.openhab.binding.yioremote.internal.dto.IRCodeSendMessage;
33 import org.openhab.binding.yioremote.internal.dto.IRReceiverMessage;
34 import org.openhab.binding.yioremote.internal.dto.PingMessage;
35 import org.openhab.binding.yioremote.internal.utils.Websocket;
36 import org.openhab.binding.yioremote.internal.utils.WebsocketInterface;
37 import org.openhab.core.library.types.OnOffType;
38 import org.openhab.core.library.types.StringType;
39 import org.openhab.core.thing.ChannelUID;
40 import org.openhab.core.thing.Thing;
41 import org.openhab.core.thing.ThingStatus;
42 import org.openhab.core.thing.ThingStatusDetail;
43 import org.openhab.core.thing.binding.BaseThingHandler;
44 import org.openhab.core.thing.binding.ThingHandlerService;
45 import org.openhab.core.types.Command;
46 import org.openhab.core.types.State;
47 import org.slf4j.Logger;
48 import org.slf4j.LoggerFactory;
50 import com.google.gson.JsonElement;
51 import com.google.gson.JsonObject;
52 import com.google.gson.JsonParser;
55 * The {@link YIOremoteDockHandler} is responsible for handling commands, which are
56 * sent to one of the channels.
58 * @author Michael Loercher - Initial contribution
61 public class YIOremoteDockHandler extends BaseThingHandler {
63 private final Logger logger = LoggerFactory.getLogger(YIOremoteDockHandler.class);
65 YIOremoteConfiguration localConfig = getConfigAs(YIOremoteConfiguration.class);
66 private WebSocketClient webSocketClient = new WebSocketClient();
67 private Websocket yioremoteDockwebSocketClient = new Websocket();
68 private ClientUpgradeRequest yioremoteDockwebSocketClientrequest = new ClientUpgradeRequest();
69 private @Nullable URI websocketAddress;
70 private YioRemoteDockHandleStatus yioRemoteDockActualStatus = YioRemoteDockHandleStatus.UNINITIALIZED_STATE;
71 private @Nullable Future<?> webSocketPollingJob;
72 private @Nullable Future<?> webSocketReconnectionPollingJob;
73 public String receivedMessage = "";
74 private JsonObject recievedJson = new JsonObject();
75 private boolean heartBeat = false;
76 private boolean authenticationOk = false;
77 private String receivedStatus = "";
78 private IRCode irCodeReceivedHandler = new IRCode();
79 private IRCode irCodeSendHandler = new IRCode();
80 private IRCodeSendMessage irCodeSendMessageHandler = new IRCodeSendMessage(irCodeSendHandler);
81 private AuthenticationMessage authenticationMessageHandler = new AuthenticationMessage();
82 private IRReceiverMessage irReceiverMessageHandler = new IRReceiverMessage();
83 private PingMessage pingMessageHandler = new PingMessage();
84 private int reconnectionCounter = 0;
86 public YIOremoteDockHandler(Thing thing) {
91 public void initialize() {
92 updateStatus(ThingStatus.UNKNOWN);
93 scheduler.execute(() -> {
95 websocketAddress = new URI("ws://" + localConfig.host + ":946");
96 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.AUTHENTICATION_PROCESS;
97 } catch (URISyntaxException e) {
98 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
99 "Initialize web socket failed: " + e.getMessage());
102 yioremoteDockwebSocketClient.addMessageHandler(new WebsocketInterface() {
105 public void onConnect(boolean connected) {
107 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.CONNECTION_ESTABLISHED;
109 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.CONNECTION_FAILED;
114 public void onMessage(String message) {
115 receivedMessage = message;
116 logger.debug("Message recieved {}", message);
117 recievedJson = convertStringToJsonObject(receivedMessage);
118 if (recievedJson.size() > 0) {
119 if (decodeReceivedMessage(recievedJson)) {
120 switch (yioRemoteDockActualStatus) {
121 case CONNECTION_ESTABLISHED:
122 case AUTHENTICATION_PROCESS:
123 authenticateWebsocket();
125 case COMMUNICATION_ERROR:
126 disposeWebsocketPollingJob();
127 reconnectWebsocket();
129 case AUTHENTICATION_COMPLETE:
132 updateChannelString(GROUP_OUTPUT, STATUS_STRING_CHANNEL, receivedStatus);
133 triggerChannel(getChannelUuid(GROUP_OUTPUT, STATUS_STRING_CHANNEL));
138 logger.debug("Message {} decoded", receivedMessage);
140 logger.debug("Error during message {} decoding", receivedMessage);
146 public void onClose() {
147 logger.debug("onClose");
148 disposeWebsocketPollingJob();
149 reconnectWebsocket();
153 public void onError(Throwable cause) {
154 logger.debug("onError");
155 disposeWebsocketPollingJob();
156 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.COMMUNICATION_ERROR;
157 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
158 "Communication lost no ping from YIO DOCK");
159 reconnectWebsocket();
164 webSocketClient.start();
166 webSocketClient.connect(yioremoteDockwebSocketClient, websocketAddress,
167 yioremoteDockwebSocketClientrequest);
168 } catch (Exception e) {
169 logger.debug("Connection error {}", e.getMessage());
175 private boolean decodeReceivedMessage(JsonObject message) {
176 boolean success = false;
178 if (message.has("type")) {
179 if (message.get("type").toString().equalsIgnoreCase("\"auth_required\"")) {
181 receivedStatus = "Authentication required";
182 } else if (message.get("type").toString().equalsIgnoreCase("\"auth_ok\"")) {
183 authenticationOk = true;
185 receivedStatus = "Authentication ok";
186 } else if (message.get("type").toString().equalsIgnoreCase("\"dock\"") && message.has("message")) {
187 if (message.get("message").toString().equalsIgnoreCase("\"pong\"")) {
190 receivedStatus = "Heart beat received";
191 } else if (message.get("message").toString().equalsIgnoreCase("\"ir_send\"")) {
192 if (message.get("success").toString().equalsIgnoreCase("true")) {
193 receivedStatus = "Send IR Code successfully";
196 receivedStatus = "Send IR Code failure";
201 logger.warn("No known message {}", receivedMessage);
205 } else if (message.get("command").toString().equalsIgnoreCase("\"ir_receive\"")) {
206 receivedStatus = message.get("code").toString().replace("\"", "");
207 if (receivedStatus.matches("[0-9]?[0-9][;]0[xX][0-9a-fA-F]+[;][0-9]+[;][0-9]")) {
208 irCodeReceivedHandler.setCode(message.get("code").toString().replace("\"", ""));
210 irCodeReceivedHandler.setCode("");
213 logger.debug("ir_receive message {}", irCodeReceivedHandler.getCode());
217 logger.warn("No known message {}", irCodeReceivedHandler.getCode());
222 logger.warn("No known message {}", irCodeReceivedHandler.getCode());
229 private JsonObject convertStringToJsonObject(String jsonString) {
231 JsonElement jsonElement = JsonParser.parseString(jsonString);
234 if (jsonElement instanceof JsonObject) {
235 result = jsonElement.getAsJsonObject();
237 logger.debug("{} is not valid JSON stirng", jsonString);
238 result = new JsonObject();
239 throw new IllegalArgumentException(jsonString + "{} is not valid JSON stirng");
242 } catch (IllegalArgumentException e) {
243 JsonObject result = new JsonObject();
248 public void updateState(String group, String channelId, State value) {
249 ChannelUID id = new ChannelUID(getThing().getUID(), group, channelId);
250 updateState(id, value);
254 public Collection<Class<? extends ThingHandlerService>> getServices() {
255 return Collections.singleton(YIOremoteDockActions.class);
259 public void dispose() {
260 disposeWebsocketPollingJob();
261 disposeWebSocketReconnectionPollingJob();
265 public void handleCommand(ChannelUID channelUID, Command command) {
266 if (RECEIVER_SWITCH_CHANNEL.equals(channelUID.getIdWithoutGroup())) {
267 switch (yioRemoteDockActualStatus) {
268 case AUTHENTICATION_COMPLETE:
271 if (command == OnOffType.ON) {
272 logger.debug("YIODOCKRECEIVERSWITCH ON procedure: Switching IR Receiver on");
273 sendMessage(YioRemoteMessages.IR_RECEIVER_ON, "");
274 } else if (command == OnOffType.OFF) {
275 logger.debug("YIODOCKRECEIVERSWITCH OFF procedure: Switching IR Receiver off");
276 sendMessage(YioRemoteMessages.IR_RECEIVER_OFF, "");
278 logger.debug("YIODOCKRECEIVERSWITCH no procedure");
287 public void sendIRCode(@Nullable String irCode) {
288 if (irCode != null) {
289 if (yioRemoteDockActualStatus.equals(YioRemoteDockHandleStatus.AUTHENTICATION_COMPLETE)
290 || yioRemoteDockActualStatus.equals(YioRemoteDockHandleStatus.SEND_PING)
291 || yioRemoteDockActualStatus.equals(YioRemoteDockHandleStatus.CHECK_PONG)) {
292 if (irCode.matches("[0-9]?[0-9][;]0[xX][0-9a-fA-F]+[;][0-9]+[;][0-9]")) {
293 sendMessage(YioRemoteMessages.IR_SEND, irCode);
295 logger.warn("Wrong ir code format {}", irCode);
298 logger.debug("Wrong Dock Statusfor sending {}", irCode);
301 logger.warn("No ir code {}", irCode);
305 private ChannelUID getChannelUuid(String group, String typeId) {
306 return new ChannelUID(getThing().getUID(), group, typeId);
309 private void updateChannelString(String group, String channelId, String value) {
310 ChannelUID id = new ChannelUID(getThing().getUID(), group, channelId);
311 updateState(id, new StringType(value));
314 private void authenticateWebsocket() {
315 switch (yioRemoteDockActualStatus) {
316 case CONNECTION_ESTABLISHED:
317 authenticationMessageHandler.setToken(localConfig.accessToken);
318 sendMessage(YioRemoteMessages.AUTHENTICATE_MESSAGE, localConfig.accessToken);
319 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.AUTHENTICATION_PROCESS;
321 case AUTHENTICATION_PROCESS:
322 if (authenticationOk) {
323 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.AUTHENTICATION_COMPLETE;
324 disposeWebSocketReconnectionPollingJob();
325 reconnectionCounter = 0;
326 updateStatus(ThingStatus.ONLINE);
327 updateState(STATUS_STRING_CHANNEL, StringType.EMPTY);
328 updateState(RECEIVER_SWITCH_CHANNEL, OnOffType.OFF);
329 webSocketPollingJob = scheduler.scheduleWithFixedDelay(this::pollingWebsocketJob, 0, 40,
332 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.AUTHENTICATION_FAILED;
336 disposeWebsocketPollingJob();
337 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.COMMUNICATION_ERROR;
338 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
339 "Connection lost no ping from YIO DOCK");
344 private void disposeWebsocketPollingJob() {
345 if (webSocketPollingJob != null) {
346 if (!webSocketPollingJob.isCancelled() && webSocketPollingJob != null) {
347 webSocketPollingJob.cancel(true);
349 webSocketPollingJob = null;
353 private void disposeWebSocketReconnectionPollingJob() {
354 if (webSocketReconnectionPollingJob != null) {
355 if (!webSocketReconnectionPollingJob.isCancelled() && webSocketReconnectionPollingJob != null) {
356 webSocketReconnectionPollingJob.cancel(true);
359 webSocketReconnectionPollingJob = null;
360 logger.debug("disposereconnection");
361 reconnectionCounter = 0;
364 private void pollingWebsocketJob() {
365 switch (yioRemoteDockActualStatus) {
366 case AUTHENTICATION_COMPLETE:
368 sendMessage(YioRemoteMessages.HEARTBEAT_MESSAGE, "");
369 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.CHECK_PONG;
373 sendMessage(YioRemoteMessages.HEARTBEAT_MESSAGE, "");
374 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.CHECK_PONG;
377 if (getHeartbeat()) {
378 updateChannelString(GROUP_OUTPUT, STATUS_STRING_CHANNEL, receivedStatus);
379 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.SEND_PING;
380 logger.debug("heartBeat ok");
382 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.COMMUNICATION_ERROR;
383 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
384 "Connection lost no ping from YIO DOCK");
385 disposeWebsocketPollingJob();
386 reconnectWebsocket();
390 disposeWebsocketPollingJob();
391 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.COMMUNICATION_ERROR;
392 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
393 "Connection lost no ping from YIO DOCK");
398 public boolean resetHeartbeat() {
403 public boolean getHeartbeat() {
407 public void reconnectWebsocket() {
408 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.COMMUNICATION_ERROR;
409 if (webSocketReconnectionPollingJob == null) {
410 webSocketReconnectionPollingJob = scheduler.scheduleWithFixedDelay(this::reconnectWebsocketJob, 0, 30,
412 } else if (reconnectionCounter == 5) {
413 reconnectionCounter = 0;
414 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
415 "Connection lost no ping from YIO DOCK");
416 if (webSocketReconnectionPollingJob == null) {
417 webSocketReconnectionPollingJob = scheduler.scheduleWithFixedDelay(this::reconnectWebsocketJob, 0, 1,
420 disposeWebSocketReconnectionPollingJob();
421 if (webSocketReconnectionPollingJob == null) {
422 webSocketReconnectionPollingJob = scheduler.scheduleWithFixedDelay(this::reconnectWebsocketJob, 0,
423 5, TimeUnit.MINUTES);
430 public void reconnectWebsocketJob() {
431 reconnectionCounter++;
432 switch (yioRemoteDockActualStatus) {
433 case COMMUNICATION_ERROR:
434 logger.debug("Reconnecting YIORemoteHandler");
436 disposeWebsocketPollingJob();
437 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
438 "Connection lost no ping from YIO DOCK");
439 yioremoteDockwebSocketClient.closeWebsocketSession();
440 webSocketClient.stop();
441 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.RECONNECTION_PROCESS;
442 } catch (Exception e) {
443 logger.debug("Connection error {}", e.getMessage());
446 websocketAddress = new URI("ws://" + localConfig.host + ":946");
447 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.AUTHENTICATION_PROCESS;
448 } catch (URISyntaxException e) {
449 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
450 "Initialize web socket failed: " + e.getMessage());
453 webSocketClient.start();
454 webSocketClient.connect(yioremoteDockwebSocketClient, websocketAddress,
455 yioremoteDockwebSocketClientrequest);
456 } catch (Exception e) {
457 logger.debug("Connection error {}", e.getMessage());
460 case AUTHENTICATION_COMPLETE:
461 disposeWebSocketReconnectionPollingJob();
462 reconnectionCounter = 0;
469 public void sendMessage(YioRemoteMessages messageType, String messagePayload) {
470 switch (messageType) {
471 case AUTHENTICATE_MESSAGE:
472 yioremoteDockwebSocketClient.sendMessage(authenticationMessageHandler.getAuthenticationMessageString());
473 logger.debug("sending authenticating {}",
474 authenticationMessageHandler.getAuthenticationMessageString());
476 case HEARTBEAT_MESSAGE:
477 yioremoteDockwebSocketClient.sendMessage(pingMessageHandler.getPingMessageString());
478 logger.debug("sending ping {}", pingMessageHandler.getPingMessageString());
481 irReceiverMessageHandler.setOn();
482 yioremoteDockwebSocketClient.sendMessage(irReceiverMessageHandler.getIRreceiverMessageString());
483 logger.debug("sending IR receiver on message: {}",
484 irReceiverMessageHandler.getIRreceiverMessageString());
486 case IR_RECEIVER_OFF:
487 irReceiverMessageHandler.setOff();
488 yioremoteDockwebSocketClient.sendMessage(irReceiverMessageHandler.getIRreceiverMessageString());
489 logger.debug("sending IR receiver on message: {}",
490 irReceiverMessageHandler.getIRreceiverMessageString());
493 irCodeSendHandler.setCode(messagePayload);
494 yioremoteDockwebSocketClient.sendMessage(irCodeSendMessageHandler.getIRcodeSendMessageString());
495 logger.debug("sending IR code: {}", irCodeSendMessageHandler.getIRcodeSendMessageString());