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.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<?> initJob;
72 private @Nullable Future<?> webSocketPollingJob;
73 private @Nullable Future<?> webSocketReconnectionPollingJob;
74 public String receivedMessage = "";
75 private JsonObject recievedJson = new JsonObject();
76 private boolean heartBeat = false;
77 private boolean authenticationOk = false;
78 private String receivedStatus = "";
79 private IRCode irCodeReceivedHandler = new IRCode();
80 private IRCode irCodeSendHandler = new IRCode();
81 private IRCodeSendMessage irCodeSendMessageHandler = new IRCodeSendMessage(irCodeSendHandler);
82 private AuthenticationMessage authenticationMessageHandler = new AuthenticationMessage();
83 private IRReceiverMessage irReceiverMessageHandler = new IRReceiverMessage();
84 private PingMessage pingMessageHandler = new PingMessage();
85 private int reconnectionCounter = 0;
87 public YIOremoteDockHandler(Thing thing) {
92 public void initialize() {
93 updateStatus(ThingStatus.UNKNOWN);
94 initJob = scheduler.submit(() -> {
96 websocketAddress = new URI("ws://" + localConfig.host + ":946");
97 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.AUTHENTICATION_PROCESS;
98 } catch (URISyntaxException e) {
99 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
100 "Initialize web socket failed: " + e.getMessage());
103 yioremoteDockwebSocketClient.addMessageHandler(new WebsocketInterface() {
106 public void onConnect(boolean connected) {
108 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.CONNECTION_ESTABLISHED;
110 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.CONNECTION_FAILED;
115 public void onMessage(String message) {
116 receivedMessage = message;
117 logger.debug("Message recieved {}", message);
118 recievedJson = convertStringToJsonObject(receivedMessage);
119 if (recievedJson.size() > 0) {
120 if (decodeReceivedMessage(recievedJson)) {
121 switch (yioRemoteDockActualStatus) {
122 case CONNECTION_ESTABLISHED:
123 case AUTHENTICATION_PROCESS:
124 authenticateWebsocket();
126 case COMMUNICATION_ERROR:
127 disposeWebsocketPollingJob();
128 reconnectWebsocket();
130 case AUTHENTICATION_COMPLETE:
133 updateChannelString(GROUP_OUTPUT, STATUS_STRING_CHANNEL, receivedStatus);
134 triggerChannel(getChannelUuid(GROUP_OUTPUT, STATUS_STRING_CHANNEL));
139 logger.debug("Message {} decoded", receivedMessage);
141 logger.debug("Error during message {} decoding", receivedMessage);
147 public void onClose() {
148 logger.debug("onClose");
149 disposeWebsocketPollingJob();
150 reconnectWebsocket();
154 public void onError(Throwable cause) {
155 logger.debug("onError");
156 disposeWebsocketPollingJob();
157 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.COMMUNICATION_ERROR;
158 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
159 "Communication lost no ping from YIO DOCK");
160 reconnectWebsocket();
165 webSocketClient.start();
167 webSocketClient.connect(yioremoteDockwebSocketClient, websocketAddress,
168 yioremoteDockwebSocketClientrequest);
169 } catch (Exception e) {
170 logger.debug("Connection error {}", e.getMessage());
176 private boolean decodeReceivedMessage(JsonObject message) {
177 boolean success = false;
179 if (message.has("type")) {
180 if (message.get("type").toString().equalsIgnoreCase("\"auth_required\"")) {
182 receivedStatus = "Authentication required";
183 } else if (message.get("type").toString().equalsIgnoreCase("\"auth_ok\"")) {
184 authenticationOk = true;
186 receivedStatus = "Authentication ok";
187 } else if (message.get("type").toString().equalsIgnoreCase("\"dock\"") && message.has("message")) {
188 if (message.get("message").toString().equalsIgnoreCase("\"pong\"")) {
191 receivedStatus = "Heart beat received";
192 } else if (message.get("message").toString().equalsIgnoreCase("\"ir_send\"")) {
193 if (message.get("success").toString().equalsIgnoreCase("true")) {
194 receivedStatus = "Send IR Code successfully";
197 receivedStatus = "Send IR Code failure";
202 logger.warn("No known message {}", receivedMessage);
206 } else if (message.get("command").toString().equalsIgnoreCase("\"ir_receive\"")) {
207 receivedStatus = message.get("code").toString().replace("\"", "");
208 if (receivedStatus.matches("[0-9]?[0-9][;]0[xX][0-9a-fA-F]+[;][0-9]+[;][0-9]")) {
209 irCodeReceivedHandler.setCode(message.get("code").toString().replace("\"", ""));
211 irCodeReceivedHandler.setCode("");
214 logger.debug("ir_receive message {}", irCodeReceivedHandler.getCode());
218 logger.warn("No known message {}", irCodeReceivedHandler.getCode());
223 logger.warn("No known message {}", irCodeReceivedHandler.getCode());
230 private JsonObject convertStringToJsonObject(String jsonString) {
232 JsonElement jsonElement = JsonParser.parseString(jsonString);
235 if (jsonElement instanceof JsonObject) {
236 result = jsonElement.getAsJsonObject();
238 logger.debug("{} is not valid JSON stirng", jsonString);
239 result = new JsonObject();
240 throw new IllegalArgumentException(jsonString + "{} is not valid JSON stirng");
243 } catch (IllegalArgumentException e) {
244 JsonObject result = new JsonObject();
249 public void updateState(String group, String channelId, State value) {
250 ChannelUID id = new ChannelUID(getThing().getUID(), group, channelId);
251 updateState(id, value);
255 public Collection<Class<? extends ThingHandlerService>> getServices() {
256 return Collections.singleton(YIOremoteDockActions.class);
260 public void dispose() {
261 Future<?> job = initJob;
266 disposeWebsocketPollingJob();
267 disposeWebSocketReconnectionPollingJob();
269 webSocketClient.stop();
270 } catch (Exception e) {
271 logger.debug("Could not stop webSocketClient, message {}", e.getMessage());
276 public void handleCommand(ChannelUID channelUID, Command command) {
277 if (RECEIVER_SWITCH_CHANNEL.equals(channelUID.getIdWithoutGroup())) {
278 switch (yioRemoteDockActualStatus) {
279 case AUTHENTICATION_COMPLETE:
282 if (command == OnOffType.ON) {
283 logger.debug("YIODOCKRECEIVERSWITCH ON procedure: Switching IR Receiver on");
284 sendMessage(YioRemoteMessages.IR_RECEIVER_ON, "");
285 } else if (command == OnOffType.OFF) {
286 logger.debug("YIODOCKRECEIVERSWITCH OFF procedure: Switching IR Receiver off");
287 sendMessage(YioRemoteMessages.IR_RECEIVER_OFF, "");
289 logger.debug("YIODOCKRECEIVERSWITCH no procedure");
298 public void sendIRCode(@Nullable String irCode) {
299 if (irCode != null) {
300 if (yioRemoteDockActualStatus.equals(YioRemoteDockHandleStatus.AUTHENTICATION_COMPLETE)
301 || yioRemoteDockActualStatus.equals(YioRemoteDockHandleStatus.SEND_PING)
302 || yioRemoteDockActualStatus.equals(YioRemoteDockHandleStatus.CHECK_PONG)) {
303 if (irCode.matches("[0-9]?[0-9][;]0[xX][0-9a-fA-F]+[;][0-9]+[;][0-9]")) {
304 sendMessage(YioRemoteMessages.IR_SEND, irCode);
306 logger.warn("Wrong ir code format {}", irCode);
309 logger.debug("Wrong Dock Statusfor sending {}", irCode);
312 logger.warn("No ir code {}", irCode);
316 private ChannelUID getChannelUuid(String group, String typeId) {
317 return new ChannelUID(getThing().getUID(), group, typeId);
320 private void updateChannelString(String group, String channelId, String value) {
321 ChannelUID id = new ChannelUID(getThing().getUID(), group, channelId);
322 updateState(id, new StringType(value));
325 private void authenticateWebsocket() {
326 switch (yioRemoteDockActualStatus) {
327 case CONNECTION_ESTABLISHED:
328 authenticationMessageHandler.setToken(localConfig.accessToken);
329 sendMessage(YioRemoteMessages.AUTHENTICATE_MESSAGE, localConfig.accessToken);
330 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.AUTHENTICATION_PROCESS;
332 case AUTHENTICATION_PROCESS:
333 if (authenticationOk) {
334 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.AUTHENTICATION_COMPLETE;
335 disposeWebSocketReconnectionPollingJob();
336 reconnectionCounter = 0;
337 updateStatus(ThingStatus.ONLINE);
338 updateState(STATUS_STRING_CHANNEL, StringType.EMPTY);
339 updateState(RECEIVER_SWITCH_CHANNEL, OnOffType.OFF);
340 webSocketPollingJob = scheduler.scheduleWithFixedDelay(this::pollingWebsocketJob, 0, 40,
343 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.AUTHENTICATION_FAILED;
347 disposeWebsocketPollingJob();
348 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.COMMUNICATION_ERROR;
349 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
350 "Connection lost no ping from YIO DOCK");
355 private void disposeWebsocketPollingJob() {
356 Future<?> job = webSocketPollingJob;
359 webSocketPollingJob = null;
363 private void disposeWebSocketReconnectionPollingJob() {
364 Future<?> job = webSocketReconnectionPollingJob;
367 webSocketReconnectionPollingJob = null;
369 logger.debug("disposereconnection");
370 reconnectionCounter = 0;
373 private void pollingWebsocketJob() {
374 switch (yioRemoteDockActualStatus) {
375 case AUTHENTICATION_COMPLETE:
377 sendMessage(YioRemoteMessages.HEARTBEAT_MESSAGE, "");
378 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.CHECK_PONG;
382 sendMessage(YioRemoteMessages.HEARTBEAT_MESSAGE, "");
383 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.CHECK_PONG;
386 if (getHeartbeat()) {
387 updateChannelString(GROUP_OUTPUT, STATUS_STRING_CHANNEL, receivedStatus);
388 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.SEND_PING;
389 logger.debug("heartBeat ok");
391 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.COMMUNICATION_ERROR;
392 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
393 "Connection lost no ping from YIO DOCK");
394 disposeWebsocketPollingJob();
395 reconnectWebsocket();
399 disposeWebsocketPollingJob();
400 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.COMMUNICATION_ERROR;
401 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
402 "Connection lost no ping from YIO DOCK");
407 public boolean resetHeartbeat() {
412 public boolean getHeartbeat() {
416 public void reconnectWebsocket() {
417 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.COMMUNICATION_ERROR;
418 if (webSocketReconnectionPollingJob == null) {
419 webSocketReconnectionPollingJob = scheduler.scheduleWithFixedDelay(this::reconnectWebsocketJob, 0, 30,
421 } else if (reconnectionCounter == 5) {
422 reconnectionCounter = 0;
423 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
424 "Connection lost no ping from YIO DOCK");
425 if (webSocketReconnectionPollingJob == null) {
426 webSocketReconnectionPollingJob = scheduler.scheduleWithFixedDelay(this::reconnectWebsocketJob, 0, 1,
429 disposeWebSocketReconnectionPollingJob();
430 if (webSocketReconnectionPollingJob == null) {
431 webSocketReconnectionPollingJob = scheduler.scheduleWithFixedDelay(this::reconnectWebsocketJob, 0,
432 5, TimeUnit.MINUTES);
439 public void reconnectWebsocketJob() {
440 reconnectionCounter++;
441 switch (yioRemoteDockActualStatus) {
442 case COMMUNICATION_ERROR:
443 logger.debug("Reconnecting YIORemoteHandler");
445 disposeWebsocketPollingJob();
446 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
447 "Connection lost no ping from YIO DOCK");
448 yioremoteDockwebSocketClient.closeWebsocketSession();
449 webSocketClient.stop();
450 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.RECONNECTION_PROCESS;
451 } catch (Exception e) {
452 logger.debug("Connection error {}", e.getMessage());
455 websocketAddress = new URI("ws://" + localConfig.host + ":946");
456 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.AUTHENTICATION_PROCESS;
457 } catch (URISyntaxException e) {
458 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
459 "Initialize web socket failed: " + e.getMessage());
462 webSocketClient.start();
463 webSocketClient.connect(yioremoteDockwebSocketClient, websocketAddress,
464 yioremoteDockwebSocketClientrequest);
465 } catch (Exception e) {
466 logger.debug("Connection error {}", e.getMessage());
469 case AUTHENTICATION_COMPLETE:
470 disposeWebSocketReconnectionPollingJob();
471 reconnectionCounter = 0;
478 public void sendMessage(YioRemoteMessages messageType, String messagePayload) {
479 switch (messageType) {
480 case AUTHENTICATE_MESSAGE:
481 yioremoteDockwebSocketClient.sendMessage(authenticationMessageHandler.getAuthenticationMessageString());
482 logger.debug("sending authenticating {}",
483 authenticationMessageHandler.getAuthenticationMessageString());
485 case HEARTBEAT_MESSAGE:
486 yioremoteDockwebSocketClient.sendMessage(pingMessageHandler.getPingMessageString());
487 logger.debug("sending ping {}", pingMessageHandler.getPingMessageString());
490 irReceiverMessageHandler.setOn();
491 yioremoteDockwebSocketClient.sendMessage(irReceiverMessageHandler.getIRreceiverMessageString());
492 logger.debug("sending IR receiver on message: {}",
493 irReceiverMessageHandler.getIRreceiverMessageString());
495 case IR_RECEIVER_OFF:
496 irReceiverMessageHandler.setOff();
497 yioremoteDockwebSocketClient.sendMessage(irReceiverMessageHandler.getIRreceiverMessageString());
498 logger.debug("sending IR receiver on message: {}",
499 irReceiverMessageHandler.getIRreceiverMessageString());
502 irCodeSendHandler.setCode(messagePayload);
503 yioremoteDockwebSocketClient.sendMessage(irCodeSendMessageHandler.getIRcodeSendMessageString());
504 logger.debug("sending IR code: {}", irCodeSendMessageHandler.getIRcodeSendMessageString());