2 * Copyright (c) 2010-2020 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.openhab.core.types.UnDefType;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
51 import com.google.gson.JsonElement;
52 import com.google.gson.JsonObject;
53 import com.google.gson.JsonParser;
56 * The {@link YIOremoteDockHandler} is responsible for handling commands, which are
57 * sent to one of the channels.
59 * @author Michael Loercher - Initial contribution
62 public class YIOremoteDockHandler extends BaseThingHandler {
64 private final Logger logger = LoggerFactory.getLogger(YIOremoteDockHandler.class);
66 YIOremoteConfiguration localConfig = getConfigAs(YIOremoteConfiguration.class);
67 private WebSocketClient webSocketClient = new WebSocketClient();
68 private Websocket yioremoteDockwebSocketClient = new Websocket();
69 private ClientUpgradeRequest yioremoteDockwebSocketClientrequest = new ClientUpgradeRequest();
70 private @Nullable URI websocketAddress;
71 private YioRemoteDockHandleStatus yioRemoteDockActualStatus = YioRemoteDockHandleStatus.UNINITIALIZED_STATE;
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();
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 triggerChannel(getChannelUuid(GROUP_OUTPUT, STATUS_STRING_CHANNEL));
121 updateChannelString(GROUP_OUTPUT, STATUS_STRING_CHANNEL, receivedStatus);
122 switch (yioRemoteDockActualStatus) {
123 case CONNECTION_ESTABLISHED:
124 case AUTHENTICATION_PROCESS:
125 authenticateWebsocket();
127 case COMMUNICATION_ERROR:
128 reconnectWebsocket();
133 logger.debug("Message {} decoded", receivedMessage);
135 logger.debug("Error during message {} decoding", receivedMessage);
141 public void onClose() {
142 disposeWebsocketPollingJob();
143 reconnectWebsocket();
147 public void onError(Throwable cause) {
148 disposeWebsocketPollingJob();
149 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.COMMUNICATION_ERROR;
150 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
151 "Communication lost no ping from YIO DOCK");
152 reconnectWebsocket();
157 webSocketClient.start();
159 webSocketClient.connect(yioremoteDockwebSocketClient, websocketAddress,
160 yioremoteDockwebSocketClientrequest);
161 } catch (Exception e) {
162 logger.debug("Connection error {}", e.getMessage());
168 private boolean decodeReceivedMessage(JsonObject message) {
169 boolean success = false;
171 if (message.has("type")) {
172 if (message.get("type").toString().equalsIgnoreCase("\"auth_required\"")) {
174 receivedStatus = "Authentication required";
175 } else if (message.get("type").toString().equalsIgnoreCase("\"auth_ok\"")) {
176 authenticationOk = true;
178 receivedStatus = "Authentication ok";
179 } else if (message.get("type").toString().equalsIgnoreCase("\"dock\"") && message.has("message")) {
180 if (message.get("message").toString().equalsIgnoreCase("\"pong\"")) {
183 receivedStatus = "Heart beat received";
184 } else if (message.get("message").toString().equalsIgnoreCase("\"ir_send\"")) {
185 if (message.get("success").toString().equalsIgnoreCase("true")) {
186 receivedStatus = "Send IR Code successfully";
189 receivedStatus = "Send IR Code failure";
194 logger.warn("No known message {}", receivedMessage);
198 } else if (message.get("command").toString().equalsIgnoreCase("\"ir_receive\"")) {
199 receivedStatus = message.get("code").toString().replace("\"", "");
200 if (receivedStatus.matches("[0-9]?[0-9][;]0[xX][0-9a-fA-F]+[;][0-9]+[;][0-9]")) {
201 irCodeReceivedHandler.setCode(message.get("code").toString().replace("\"", ""));
203 irCodeReceivedHandler.setCode("");
205 logger.debug("ir_receive message {}", irCodeReceivedHandler.getCode());
209 logger.warn("No known message {}", irCodeReceivedHandler.getCode());
214 logger.warn("No known message {}", irCodeReceivedHandler.getCode());
221 private JsonObject convertStringToJsonObject(String jsonString) {
223 JsonParser parser = new JsonParser();
224 JsonElement jsonElement = parser.parse(jsonString);
227 if (jsonElement instanceof JsonObject) {
228 result = jsonElement.getAsJsonObject();
230 logger.debug("{} is not valid JSON stirng", jsonString);
231 result = new JsonObject();
232 throw new IllegalArgumentException(jsonString + "{} is not valid JSON stirng");
235 } catch (IllegalArgumentException e) {
236 JsonObject result = new JsonObject();
241 public void updateState(String group, String channelId, State value) {
242 ChannelUID id = new ChannelUID(getThing().getUID(), group, channelId);
243 updateState(id, value);
247 public Collection<Class<? extends ThingHandlerService>> getServices() {
248 return Collections.singleton(YIOremoteDockActions.class);
252 public void dispose() {
253 disposeWebsocketPollingJob();
254 if (webSocketReconnectionPollingJob != null) {
255 if (!webSocketReconnectionPollingJob.isCancelled() && webSocketReconnectionPollingJob != null) {
256 webSocketReconnectionPollingJob.cancel(true);
257 authenticationOk = false;
260 webSocketReconnectionPollingJob = null;
265 public void handleCommand(ChannelUID channelUID, Command command) {
266 if (RECEIVER_SWITCH_CHANNEL.equals(channelUID.getIdWithoutGroup())) {
267 switch (yioRemoteDockActualStatus) {
268 case AUTHENTICATION_COMPLETE:
269 if (command == OnOffType.ON) {
270 logger.debug("YIODOCKRECEIVERSWITCH ON procedure: Switching IR Receiver on");
271 sendMessage(YioRemoteMessages.IR_RECEIVER_ON, "");
272 } else if (command == OnOffType.OFF) {
273 logger.debug("YIODOCKRECEIVERSWITCH OFF procedure: Switching IR Receiver off");
274 sendMessage(YioRemoteMessages.IR_RECEIVER_OFF, "");
276 logger.debug("YIODOCKRECEIVERSWITCH no procedure");
285 public void sendIRCode(@Nullable String irCode) {
286 if (irCode != null && yioRemoteDockActualStatus.equals(YioRemoteDockHandleStatus.AUTHENTICATION_COMPLETE)) {
287 if (irCode.matches("[0-9]?[0-9][;]0[xX][0-9a-fA-F]+[;][0-9]+[;][0-9]")) {
288 sendMessage(YioRemoteMessages.IR_SEND, irCode);
290 logger.warn("Wrong ir code format {}", irCode);
295 private ChannelUID getChannelUuid(String group, String typeId) {
296 return new ChannelUID(getThing().getUID(), group, typeId);
299 private void updateChannelString(String group, String channelId, String value) {
300 ChannelUID id = new ChannelUID(getThing().getUID(), group, channelId);
301 updateState(id, new StringType(value));
304 private void authenticateWebsocket() {
305 switch (yioRemoteDockActualStatus) {
306 case CONNECTION_ESTABLISHED:
307 authenticationMessageHandler.setToken(localConfig.accessToken);
308 sendMessage(YioRemoteMessages.AUTHENTICATE_MESSAGE, localConfig.accessToken);
309 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.AUTHENTICATION_PROCESS;
311 case AUTHENTICATION_PROCESS:
312 if (authenticationOk) {
313 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.AUTHENTICATION_COMPLETE;
314 updateStatus(ThingStatus.ONLINE);
315 webSocketPollingJob = scheduler.scheduleWithFixedDelay(this::pollingWebsocketJob, 0, 60,
318 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.AUTHENTICATION_FAILED;
322 disposeWebsocketPollingJob();
323 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.COMMUNICATION_ERROR;
324 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
325 "Connection lost no ping from YIO DOCK");
326 updateState(GROUP_OUTPUT, STATUS_STRING_CHANNEL, UnDefType.UNDEF);
331 private void disposeWebsocketPollingJob() {
332 if (webSocketPollingJob != null) {
333 if (!webSocketPollingJob.isCancelled() && webSocketPollingJob != null) {
334 webSocketPollingJob.cancel(true);
336 webSocketPollingJob = null;
340 private void pollingWebsocketJob() {
341 switch (yioRemoteDockActualStatus) {
342 case AUTHENTICATION_COMPLETE:
344 sendMessage(YioRemoteMessages.HEARTBEAT_MESSAGE, "");
345 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.CHECK_PONG;
349 sendMessage(YioRemoteMessages.HEARTBEAT_MESSAGE, "");
350 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.CHECK_PONG;
353 if (getHeartbeat()) {
354 updateChannelString(GROUP_OUTPUT, STATUS_STRING_CHANNEL, receivedStatus);
355 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.SEND_PING;
356 logger.debug("heartBeat ok");
358 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.COMMUNICATION_ERROR;
359 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
360 "Connection lost no ping from YIO DOCK");
361 disposeWebsocketPollingJob();
362 reconnectWebsocket();
366 disposeWebsocketPollingJob();
367 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.COMMUNICATION_ERROR;
368 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
369 "Connection lost no ping from YIO DOCK");
370 updateState(GROUP_OUTPUT, STATUS_STRING_CHANNEL, UnDefType.UNDEF);
375 public boolean resetHeartbeat() {
380 public boolean getHeartbeat() {
384 public void reconnectWebsocket() {
385 if (webSocketReconnectionPollingJob == null) {
386 webSocketReconnectionPollingJob = scheduler.scheduleWithFixedDelay(this::reconnectWebsocketJob, 0, 30,
391 public void reconnectWebsocketJob() {
392 switch (yioRemoteDockActualStatus) {
393 case COMMUNICATION_ERROR:
394 logger.debug("Reconnecting YIORemoteHandler");
396 disposeWebsocketPollingJob();
397 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
398 "Connection lost no ping from YIO DOCK");
399 yioremoteDockwebSocketClient.closeWebsocketSession();
400 webSocketClient.stop();
401 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.RECONNECTION_PROCESS;
402 } catch (Exception e) {
403 logger.debug("Connection error {}", e.getMessage());
406 websocketAddress = new URI("ws://" + localConfig.host + ":946");
407 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.AUTHENTICATION_PROCESS;
408 } catch (URISyntaxException e) {
409 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
410 "Initialize web socket failed: " + e.getMessage());
413 webSocketClient.start();
414 webSocketClient.connect(yioremoteDockwebSocketClient, websocketAddress,
415 yioremoteDockwebSocketClientrequest);
416 } catch (Exception e) {
417 logger.debug("Connection error {}", e.getMessage());
420 case AUTHENTICATION_COMPLETE:
421 if (webSocketReconnectionPollingJob != null) {
422 if (!webSocketReconnectionPollingJob.isCancelled() && webSocketReconnectionPollingJob != null) {
423 webSocketReconnectionPollingJob.cancel(true);
425 webSocketReconnectionPollingJob = null;
433 public void sendMessage(YioRemoteMessages messageType, String messagePayload) {
434 switch (messageType) {
435 case AUTHENTICATE_MESSAGE:
436 yioremoteDockwebSocketClient.sendMessage(authenticationMessageHandler.getAuthenticationMessageString());
437 logger.debug("sending authenticating {}",
438 authenticationMessageHandler.getAuthenticationMessageString());
440 case HEARTBEAT_MESSAGE:
441 yioremoteDockwebSocketClient.sendMessage(pingMessageHandler.getPingMessageString());
442 logger.debug("sending ping {}", pingMessageHandler.getPingMessageString());
445 irReceiverMessageHandler.setOn();
446 yioremoteDockwebSocketClient.sendMessage(irReceiverMessageHandler.getIRreceiverMessageString());
447 logger.debug("sending IR receiver on message: {}",
448 irReceiverMessageHandler.getIRreceiverMessageString());
450 case IR_RECEIVER_OFF:
451 irReceiverMessageHandler.setOff();
452 yioremoteDockwebSocketClient.sendMessage(irReceiverMessageHandler.getIRreceiverMessageString());
453 logger.debug("sending IR receiver on message: {}",
454 irReceiverMessageHandler.getIRreceiverMessageString());
457 irCodeSendHandler.setCode(messagePayload);
458 yioremoteDockwebSocketClient.sendMessage(irCodeSendMessageHandler.getIRcodeSendMessageString());
459 logger.debug("sending heartBeat message: {}", irCodeSendMessageHandler.getIRcodeSendMessageString());