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.utils.Websocket;
35 import org.openhab.binding.yioremote.internal.utils.WebsocketInterface;
36 import org.openhab.core.library.types.OnOffType;
37 import org.openhab.core.library.types.StringType;
38 import org.openhab.core.thing.ChannelUID;
39 import org.openhab.core.thing.Thing;
40 import org.openhab.core.thing.ThingStatus;
41 import org.openhab.core.thing.ThingStatusDetail;
42 import org.openhab.core.thing.binding.BaseThingHandler;
43 import org.openhab.core.thing.binding.ThingHandlerService;
44 import org.openhab.core.types.Command;
45 import org.openhab.core.types.State;
46 import org.openhab.core.types.UnDefType;
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();
84 public YIOremoteDockHandler(Thing thing) {
89 public void initialize() {
90 updateStatus(ThingStatus.UNKNOWN);
91 scheduler.execute(() -> {
93 websocketAddress = new URI("ws://" + localConfig.host + ":946");
94 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.AUTHENTICATION_PROCESS;
95 } catch (URISyntaxException e) {
96 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
97 "Initialize web socket failed: " + e.getMessage());
100 yioremoteDockwebSocketClient.addMessageHandler(new WebsocketInterface() {
103 public void onConnect(boolean connected) {
105 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.CONNECTION_ESTABLISHED;
107 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.CONNECTION_FAILED;
112 public void onMessage(String message) {
113 receivedMessage = message;
114 logger.debug("Message recieved {}", message);
115 recievedJson = convertStringToJsonObject(receivedMessage);
116 if (recievedJson.size() > 0) {
117 if (decodeReceivedMessage(recievedJson)) {
118 triggerChannel(getChannelUuid(GROUP_OUTPUT, STATUS_STRING_CHANNEL));
119 updateChannelString(GROUP_OUTPUT, STATUS_STRING_CHANNEL, receivedStatus);
120 switch (yioRemoteDockActualStatus) {
121 case CONNECTION_ESTABLISHED:
122 case AUTHENTICATION_PROCESS:
123 authenticateWebsocket();
125 case COMMUNICATION_ERROR:
126 reconnectWebsocket();
131 logger.debug("Message {} decoded", receivedMessage);
133 logger.debug("Error during message {} decoding", receivedMessage);
139 public void onClose() {
140 reconnectWebsocket();
144 public void onError(Throwable cause) {
145 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.COMMUNICATION_ERROR;
146 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
147 "Communication lost no ping from YIO DOCK");
152 webSocketClient.start();
154 webSocketClient.connect(yioremoteDockwebSocketClient, websocketAddress,
155 yioremoteDockwebSocketClientrequest);
156 } catch (Exception e) {
157 logger.debug("Connection error {}", e.getMessage());
163 private boolean decodeReceivedMessage(JsonObject message) {
164 boolean success = false;
166 if (message.has("type")) {
167 if (message.get("type").toString().equalsIgnoreCase("\"auth_required\"")) {
170 receivedStatus = "Authentication required";
171 } else if (message.get("type").toString().equalsIgnoreCase("\"auth_ok\"")) {
172 authenticationOk = true;
175 receivedStatus = "Authentication ok";
176 } else if (message.get("type").toString().equalsIgnoreCase("\"dock\"") && message.has("message")) {
177 if (message.get("message").toString().equalsIgnoreCase("\"ir_send\"")) {
178 if (message.get("success").toString().equalsIgnoreCase("true")) {
179 receivedStatus = "Send IR Code successfully";
183 if (irCodeSendHandler.getCode().equalsIgnoreCase("0;0x0;0;0")) {
184 logger.debug("Send heartBeat Code success");
185 receivedStatus = "Send heartBeat Code success";
187 receivedStatus = "Send IR Code failure";
193 logger.warn("No known message {}", receivedMessage);
197 } else if (message.get("command").toString().equalsIgnoreCase("\"ir_receive\"")) {
198 receivedStatus = message.get("code").toString().replace("\"", "");
199 if (receivedStatus.matches("[0-9][;]0[xX][0-9a-fA-F]+[;][0-9]+[;][0-9]")) {
200 irCodeReceivedHandler.setCode(message.get("code").toString().replace("\"", ""));
202 irCodeReceivedHandler.setCode("");
204 logger.debug("ir_receive message {}", irCodeReceivedHandler.getCode());
208 logger.warn("No known message {}", irCodeReceivedHandler.getCode());
213 logger.warn("No known message {}", irCodeReceivedHandler.getCode());
220 private JsonObject convertStringToJsonObject(String jsonString) {
222 JsonParser parser = new JsonParser();
223 JsonElement jsonElement = parser.parse(jsonString);
226 if (jsonElement instanceof JsonObject) {
227 result = jsonElement.getAsJsonObject();
229 logger.debug("{} is not valid JSON stirng", jsonString);
230 result = new JsonObject();
231 throw new IllegalArgumentException(jsonString + "{} is not valid JSON stirng");
234 } catch (IllegalArgumentException e) {
235 JsonObject result = new JsonObject();
240 public void updateState(String group, String channelId, State value) {
241 ChannelUID id = new ChannelUID(getThing().getUID(), group, channelId);
242 updateState(id, value);
246 public Collection<Class<? extends ThingHandlerService>> getServices() {
247 return Collections.singleton(YIOremoteDockActions.class);
251 public void dispose() {
252 disposeWebsocketPollingJob();
253 if (webSocketReconnectionPollingJob != null) {
254 if (!webSocketReconnectionPollingJob.isCancelled() && webSocketReconnectionPollingJob != null) {
255 webSocketReconnectionPollingJob.cancel(true);
257 webSocketReconnectionPollingJob = null;
262 public void handleCommand(ChannelUID channelUID, Command command) {
263 if (RECEIVER_SWITCH_CHANNEL.equals(channelUID.getIdWithoutGroup())) {
264 switch (yioRemoteDockActualStatus) {
265 case AUTHENTICATION_COMPLETE:
266 if (command == OnOffType.ON) {
267 logger.debug("YIODOCKRECEIVERSWITCH ON procedure: Switching IR Receiver on");
268 sendMessage(YioRemoteMessages.IR_RECEIVER_ON, "");
269 } else if (command == OnOffType.OFF) {
270 logger.debug("YIODOCKRECEIVERSWITCH OFF procedure: Switching IR Receiver off");
271 sendMessage(YioRemoteMessages.IR_RECEIVER_OFF, "");
273 logger.debug("YIODOCKRECEIVERSWITCH no procedure");
282 public void sendIRCode(@Nullable String irCode) {
283 if (irCode != null && yioRemoteDockActualStatus.equals(YioRemoteDockHandleStatus.AUTHENTICATION_COMPLETE)) {
284 if (irCode.matches("[0-9][;]0[xX][0-9a-fA-F]+[;][0-9]+[;][0-9]")) {
285 sendMessage(YioRemoteMessages.IR_SEND, irCode);
287 logger.warn("Wrong ir code format {}", irCode);
292 private ChannelUID getChannelUuid(String group, String typeId) {
293 return new ChannelUID(getThing().getUID(), group, typeId);
296 private void updateChannelString(String group, String channelId, String value) {
297 ChannelUID id = new ChannelUID(getThing().getUID(), group, channelId);
298 updateState(id, new StringType(value));
301 private void authenticateWebsocket() {
302 switch (yioRemoteDockActualStatus) {
303 case CONNECTION_ESTABLISHED:
304 authenticationMessageHandler.setToken(localConfig.accessToken);
305 sendMessage(YioRemoteMessages.AUTHENTICATE_MESSAGE, localConfig.accessToken);
306 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.AUTHENTICATION_PROCESS;
308 case AUTHENTICATION_PROCESS:
309 if (authenticationOk) {
310 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.AUTHENTICATION_COMPLETE;
311 updateStatus(ThingStatus.ONLINE);
312 webSocketPollingJob = scheduler.scheduleWithFixedDelay(this::pollingWebsocketJob, 0, 150,
315 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.AUTHENTICATION_FAILED;
319 disposeWebsocketPollingJob();
320 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.COMMUNICATION_ERROR;
321 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
322 "Connection lost no ping from YIO DOCK");
323 updateState(GROUP_OUTPUT, STATUS_STRING_CHANNEL, UnDefType.UNDEF);
328 private void disposeWebsocketPollingJob() {
329 if (webSocketPollingJob != null) {
330 if (!webSocketPollingJob.isCancelled() && webSocketPollingJob != null) {
331 webSocketPollingJob.cancel(true);
333 webSocketPollingJob = null;
337 private void pollingWebsocketJob() {
338 switch (yioRemoteDockActualStatus) {
339 case AUTHENTICATION_COMPLETE:
340 if (getAndResetHeartbeat()) {
341 updateChannelString(GROUP_OUTPUT, STATUS_STRING_CHANNEL,
342 irCodeReceivedHandler.getCode() + irCodeReceivedHandler.getFormat());
343 logger.debug("heartBeat ok");
344 sendMessage(YioRemoteMessages.HEARTBEAT_MESSAGE, "");
346 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.COMMUNICATION_ERROR;
347 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
348 "Connection lost no ping from YIO DOCK");
349 disposeWebsocketPollingJob();
350 reconnectWebsocket();
354 disposeWebsocketPollingJob();
355 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.COMMUNICATION_ERROR;
356 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
357 "Connection lost no ping from YIO DOCK");
358 updateState(GROUP_OUTPUT, STATUS_STRING_CHANNEL, UnDefType.UNDEF);
363 public boolean getAndResetHeartbeat() {
364 boolean result = heartBeat;
369 public void reconnectWebsocket() {
370 if (webSocketReconnectionPollingJob == null) {
371 webSocketReconnectionPollingJob = scheduler.scheduleWithFixedDelay(this::reconnectWebsocketJob, 0, 30,
376 public void reconnectWebsocketJob() {
377 switch (yioRemoteDockActualStatus) {
378 case COMMUNICATION_ERROR:
379 logger.debug("Reconnecting YIORemoteHandler");
381 disposeWebsocketPollingJob();
382 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
383 "Connection lost no ping from YIO DOCK");
384 yioremoteDockwebSocketClient.closeWebsocketSession();
385 webSocketClient.stop();
386 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.RECONNECTION_PROCESS;
387 } catch (Exception e) {
388 logger.debug("Connection error {}", e.getMessage());
391 websocketAddress = new URI("ws://" + localConfig.host + ":946");
392 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.AUTHENTICATION_PROCESS;
393 } catch (URISyntaxException e) {
394 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
395 "Initialize web socket failed: " + e.getMessage());
398 webSocketClient.start();
399 webSocketClient.connect(yioremoteDockwebSocketClient, websocketAddress,
400 yioremoteDockwebSocketClientrequest);
401 } catch (Exception e) {
402 logger.debug("Connection error {}", e.getMessage());
405 case AUTHENTICATION_COMPLETE:
406 if (webSocketReconnectionPollingJob != null) {
407 if (!webSocketReconnectionPollingJob.isCancelled() && webSocketReconnectionPollingJob != null) {
408 webSocketReconnectionPollingJob.cancel(true);
410 webSocketReconnectionPollingJob = null;
418 public void sendMessage(YioRemoteMessages messageType, String messagePayload) {
419 switch (messageType) {
420 case AUTHENTICATE_MESSAGE:
421 yioremoteDockwebSocketClient.sendMessage(authenticationMessageHandler.getAuthenticationMessageString());
422 logger.debug("sending authenticating {}",
423 authenticationMessageHandler.getAuthenticationMessageString());
425 case HEARTBEAT_MESSAGE:
426 irCodeSendHandler.setCode("0;0x0;0;0");
427 yioremoteDockwebSocketClient.sendMessage(irCodeSendMessageHandler.getIRcodeSendMessageString());
428 logger.debug("sending heartBeat message: {}", irCodeSendMessageHandler.getIRcodeSendMessageString());
431 irReceiverMessageHandler.setOn();
432 yioremoteDockwebSocketClient.sendMessage(irReceiverMessageHandler.getIRreceiverMessageString());
433 logger.debug("sending IR receiver on message: {}",
434 irReceiverMessageHandler.getIRreceiverMessageString());
436 case IR_RECEIVER_OFF:
437 irReceiverMessageHandler.setOff();
438 yioremoteDockwebSocketClient.sendMessage(irReceiverMessageHandler.getIRreceiverMessageString());
439 logger.debug("sending IR receiver on message: {}",
440 irReceiverMessageHandler.getIRreceiverMessageString());
443 irCodeSendHandler.setCode(messagePayload);
444 yioremoteDockwebSocketClient.sendMessage(irCodeSendMessageHandler.getIRcodeSendMessageString());
445 logger.debug("sending heartBeat message: {}", irCodeSendMessageHandler.getIRcodeSendMessageString());