]> git.basschouten.com Git - openhab-addons.git/blob
731375eb0cb801889a41b62fec7ce043db98133d
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.yioremote.internal;
14
15 import static org.openhab.binding.yioremote.internal.YIOremoteBindingConstants.*;
16
17 import java.net.URI;
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;
23
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;
49
50 import com.google.gson.JsonElement;
51 import com.google.gson.JsonObject;
52 import com.google.gson.JsonParser;
53
54 /**
55  * The {@link YIOremoteDockHandler} is responsible for handling commands, which are
56  * sent to one of the channels.
57  *
58  * @author Michael Loercher - Initial contribution
59  */
60 @NonNullByDefault
61 public class YIOremoteDockHandler extends BaseThingHandler {
62
63     private final Logger logger = LoggerFactory.getLogger(YIOremoteDockHandler.class);
64
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
85     public YIOremoteDockHandler(Thing thing) {
86         super(thing);
87     }
88
89     @Override
90     public void initialize() {
91         updateStatus(ThingStatus.UNKNOWN);
92         scheduler.execute(() -> {
93             try {
94                 websocketAddress = new URI("ws://" + localConfig.host + ":946");
95                 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.AUTHENTICATION_PROCESS;
96             } catch (URISyntaxException e) {
97                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
98                         "Initialize web socket failed: " + e.getMessage());
99             }
100
101             yioremoteDockwebSocketClient.addMessageHandler(new WebsocketInterface() {
102
103                 @Override
104                 public void onConnect(boolean connected) {
105                     if (connected) {
106                         yioRemoteDockActualStatus = YioRemoteDockHandleStatus.CONNECTION_ESTABLISHED;
107                     } else {
108                         yioRemoteDockActualStatus = YioRemoteDockHandleStatus.CONNECTION_FAILED;
109                     }
110                 }
111
112                 @Override
113                 public void onMessage(String message) {
114                     receivedMessage = message;
115                     logger.debug("Message recieved {}", message);
116                     recievedJson = convertStringToJsonObject(receivedMessage);
117                     if (recievedJson.size() > 0) {
118                         if (decodeReceivedMessage(recievedJson)) {
119                             switch (yioRemoteDockActualStatus) {
120                                 case CONNECTION_ESTABLISHED:
121                                 case AUTHENTICATION_PROCESS:
122                                     authenticateWebsocket();
123                                     break;
124                                 case COMMUNICATION_ERROR:
125                                     reconnectWebsocket();
126                                     break;
127                                 case AUTHENTICATION_COMPLETE:
128                                 case CHECK_PONG:
129                                 case SEND_PING:
130                                     updateChannelString(GROUP_OUTPUT, STATUS_STRING_CHANNEL, receivedStatus);
131                                     triggerChannel(getChannelUuid(GROUP_OUTPUT, STATUS_STRING_CHANNEL));
132                                     break;
133                                 default:
134                                     break;
135                             }
136                             logger.debug("Message {} decoded", receivedMessage);
137                         } else {
138                             logger.debug("Error during message {} decoding", receivedMessage);
139                         }
140                     }
141                 }
142
143                 @Override
144                 public void onClose() {
145                     disposeWebsocketPollingJob();
146                     reconnectWebsocket();
147                 }
148
149                 @Override
150                 public void onError(Throwable cause) {
151                     disposeWebsocketPollingJob();
152                     yioRemoteDockActualStatus = YioRemoteDockHandleStatus.COMMUNICATION_ERROR;
153                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
154                             "Communication lost no ping from YIO DOCK");
155                     reconnectWebsocket();
156                 }
157             });
158
159             try {
160                 webSocketClient.start();
161
162                 webSocketClient.connect(yioremoteDockwebSocketClient, websocketAddress,
163                         yioremoteDockwebSocketClientrequest);
164             } catch (Exception e) {
165                 logger.debug("Connection error {}", e.getMessage());
166             }
167
168         });
169     }
170
171     private boolean decodeReceivedMessage(JsonObject message) {
172         boolean success = false;
173
174         if (message.has("type")) {
175             if (message.get("type").toString().equalsIgnoreCase("\"auth_required\"")) {
176                 success = true;
177                 receivedStatus = "Authentication required";
178             } else if (message.get("type").toString().equalsIgnoreCase("\"auth_ok\"")) {
179                 authenticationOk = true;
180                 success = true;
181                 receivedStatus = "Authentication ok";
182             } else if (message.get("type").toString().equalsIgnoreCase("\"dock\"") && message.has("message")) {
183                 if (message.get("message").toString().equalsIgnoreCase("\"pong\"")) {
184                     heartBeat = true;
185                     success = true;
186                     receivedStatus = "Heart beat received";
187                 } else if (message.get("message").toString().equalsIgnoreCase("\"ir_send\"")) {
188                     if (message.get("success").toString().equalsIgnoreCase("true")) {
189                         receivedStatus = "Send IR Code successfully";
190                         success = true;
191                     } else {
192                         receivedStatus = "Send IR Code failure";
193                         heartBeat = true;
194                         success = true;
195                     }
196                 } else {
197                     logger.warn("No known message {}", receivedMessage);
198                     heartBeat = false;
199                     success = false;
200                 }
201             } else if (message.get("command").toString().equalsIgnoreCase("\"ir_receive\"")) {
202                 receivedStatus = message.get("code").toString().replace("\"", "");
203                 if (receivedStatus.matches("[0-9]?[0-9][;]0[xX][0-9a-fA-F]+[;][0-9]+[;][0-9]")) {
204                     irCodeReceivedHandler.setCode(message.get("code").toString().replace("\"", ""));
205                 } else {
206                     irCodeReceivedHandler.setCode("");
207                 }
208
209                 logger.debug("ir_receive message {}", irCodeReceivedHandler.getCode());
210                 heartBeat = true;
211                 success = true;
212             } else {
213                 logger.warn("No known message {}", irCodeReceivedHandler.getCode());
214                 heartBeat = false;
215                 success = false;
216             }
217         } else {
218             logger.warn("No known message {}", irCodeReceivedHandler.getCode());
219             heartBeat = false;
220             success = false;
221         }
222         return success;
223     }
224
225     private JsonObject convertStringToJsonObject(String jsonString) {
226         try {
227             JsonParser parser = new JsonParser();
228             JsonElement jsonElement = parser.parse(jsonString);
229             JsonObject result;
230
231             if (jsonElement instanceof JsonObject) {
232                 result = jsonElement.getAsJsonObject();
233             } else {
234                 logger.debug("{} is not valid JSON stirng", jsonString);
235                 result = new JsonObject();
236                 throw new IllegalArgumentException(jsonString + "{} is not valid JSON stirng");
237             }
238             return result;
239         } catch (IllegalArgumentException e) {
240             JsonObject result = new JsonObject();
241             return result;
242         }
243     }
244
245     public void updateState(String group, String channelId, State value) {
246         ChannelUID id = new ChannelUID(getThing().getUID(), group, channelId);
247         updateState(id, value);
248     }
249
250     @Override
251     public Collection<Class<? extends ThingHandlerService>> getServices() {
252         return Collections.singleton(YIOremoteDockActions.class);
253     }
254
255     @Override
256     public void dispose() {
257         disposeWebsocketPollingJob();
258         if (webSocketReconnectionPollingJob != null) {
259             if (!webSocketReconnectionPollingJob.isCancelled() && webSocketReconnectionPollingJob != null) {
260                 webSocketReconnectionPollingJob.cancel(true);
261                 authenticationOk = false;
262                 heartBeat = false;
263             }
264             webSocketReconnectionPollingJob = null;
265         }
266     }
267
268     @Override
269     public void handleCommand(ChannelUID channelUID, Command command) {
270         if (RECEIVER_SWITCH_CHANNEL.equals(channelUID.getIdWithoutGroup())) {
271             switch (yioRemoteDockActualStatus) {
272                 case AUTHENTICATION_COMPLETE:
273                 case SEND_PING:
274                 case CHECK_PONG:
275                     if (command == OnOffType.ON) {
276                         logger.debug("YIODOCKRECEIVERSWITCH ON procedure: Switching IR Receiver on");
277                         sendMessage(YioRemoteMessages.IR_RECEIVER_ON, "");
278                     } else if (command == OnOffType.OFF) {
279                         logger.debug("YIODOCKRECEIVERSWITCH OFF procedure: Switching IR Receiver off");
280                         sendMessage(YioRemoteMessages.IR_RECEIVER_OFF, "");
281                     } else {
282                         logger.debug("YIODOCKRECEIVERSWITCH no procedure");
283                     }
284                     break;
285                 default:
286                     break;
287             }
288         }
289     }
290
291     public void sendIRCode(@Nullable String irCode) {
292         if (irCode != null) {
293             if (yioRemoteDockActualStatus.equals(YioRemoteDockHandleStatus.AUTHENTICATION_COMPLETE)
294                     || yioRemoteDockActualStatus.equals(YioRemoteDockHandleStatus.SEND_PING)
295                     || yioRemoteDockActualStatus.equals(YioRemoteDockHandleStatus.CHECK_PONG)) {
296                 if (irCode.matches("[0-9]?[0-9][;]0[xX][0-9a-fA-F]+[;][0-9]+[;][0-9]")) {
297                     sendMessage(YioRemoteMessages.IR_SEND, irCode);
298                 } else {
299                     logger.warn("Wrong ir code format {}", irCode);
300                 }
301             } else {
302                 logger.debug("Wrong Dock Statusfor sending  {}", irCode);
303             }
304         } else {
305             logger.warn("No ir code {}", irCode);
306         }
307     }
308
309     private ChannelUID getChannelUuid(String group, String typeId) {
310         return new ChannelUID(getThing().getUID(), group, typeId);
311     }
312
313     private void updateChannelString(String group, String channelId, String value) {
314         ChannelUID id = new ChannelUID(getThing().getUID(), group, channelId);
315         updateState(id, new StringType(value));
316     }
317
318     private void authenticateWebsocket() {
319         switch (yioRemoteDockActualStatus) {
320             case CONNECTION_ESTABLISHED:
321                 authenticationMessageHandler.setToken(localConfig.accessToken);
322                 sendMessage(YioRemoteMessages.AUTHENTICATE_MESSAGE, localConfig.accessToken);
323                 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.AUTHENTICATION_PROCESS;
324                 break;
325             case AUTHENTICATION_PROCESS:
326                 if (authenticationOk) {
327                     yioRemoteDockActualStatus = YioRemoteDockHandleStatus.AUTHENTICATION_COMPLETE;
328                     updateStatus(ThingStatus.ONLINE);
329                     updateState(STATUS_STRING_CHANNEL, StringType.EMPTY);
330                     updateState(RECEIVER_SWITCH_CHANNEL, OnOffType.OFF);
331                     webSocketPollingJob = scheduler.scheduleWithFixedDelay(this::pollingWebsocketJob, 0, 60,
332                             TimeUnit.SECONDS);
333                 } else {
334                     yioRemoteDockActualStatus = YioRemoteDockHandleStatus.AUTHENTICATION_FAILED;
335                 }
336                 break;
337             default:
338                 disposeWebsocketPollingJob();
339                 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.COMMUNICATION_ERROR;
340                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
341                         "Connection lost no ping from YIO DOCK");
342                 break;
343         }
344     }
345
346     private void disposeWebsocketPollingJob() {
347         if (webSocketPollingJob != null) {
348             if (!webSocketPollingJob.isCancelled() && webSocketPollingJob != null) {
349                 webSocketPollingJob.cancel(true);
350             }
351             webSocketPollingJob = null;
352         }
353     }
354
355     private void pollingWebsocketJob() {
356         switch (yioRemoteDockActualStatus) {
357             case AUTHENTICATION_COMPLETE:
358                 resetHeartbeat();
359                 sendMessage(YioRemoteMessages.HEARTBEAT_MESSAGE, "");
360                 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.CHECK_PONG;
361                 break;
362             case SEND_PING:
363                 resetHeartbeat();
364                 sendMessage(YioRemoteMessages.HEARTBEAT_MESSAGE, "");
365                 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.CHECK_PONG;
366                 break;
367             case CHECK_PONG:
368                 if (getHeartbeat()) {
369                     updateChannelString(GROUP_OUTPUT, STATUS_STRING_CHANNEL, receivedStatus);
370                     yioRemoteDockActualStatus = YioRemoteDockHandleStatus.SEND_PING;
371                     logger.debug("heartBeat ok");
372                 } else {
373                     yioRemoteDockActualStatus = YioRemoteDockHandleStatus.COMMUNICATION_ERROR;
374                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
375                             "Connection lost no ping from YIO DOCK");
376                     disposeWebsocketPollingJob();
377                     reconnectWebsocket();
378                 }
379                 break;
380             default:
381                 disposeWebsocketPollingJob();
382                 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.COMMUNICATION_ERROR;
383                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
384                         "Connection lost no ping from YIO DOCK");
385                 break;
386         }
387     }
388
389     public boolean resetHeartbeat() {
390         heartBeat = false;
391         return true;
392     }
393
394     public boolean getHeartbeat() {
395         return heartBeat;
396     }
397
398     public void reconnectWebsocket() {
399         if (webSocketReconnectionPollingJob == null) {
400             webSocketReconnectionPollingJob = scheduler.scheduleWithFixedDelay(this::reconnectWebsocketJob, 0, 30,
401                     TimeUnit.SECONDS);
402         }
403     }
404
405     public void reconnectWebsocketJob() {
406         switch (yioRemoteDockActualStatus) {
407             case COMMUNICATION_ERROR:
408                 logger.debug("Reconnecting YIORemoteHandler");
409                 try {
410                     disposeWebsocketPollingJob();
411                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
412                             "Connection lost no ping from YIO DOCK");
413                     yioremoteDockwebSocketClient.closeWebsocketSession();
414                     webSocketClient.stop();
415                     yioRemoteDockActualStatus = YioRemoteDockHandleStatus.RECONNECTION_PROCESS;
416                 } catch (Exception e) {
417                     logger.debug("Connection error {}", e.getMessage());
418                 }
419                 try {
420                     websocketAddress = new URI("ws://" + localConfig.host + ":946");
421                     yioRemoteDockActualStatus = YioRemoteDockHandleStatus.AUTHENTICATION_PROCESS;
422                 } catch (URISyntaxException e) {
423                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
424                             "Initialize web socket failed: " + e.getMessage());
425                 }
426                 try {
427                     webSocketClient.start();
428                     webSocketClient.connect(yioremoteDockwebSocketClient, websocketAddress,
429                             yioremoteDockwebSocketClientrequest);
430                 } catch (Exception e) {
431                     logger.debug("Connection error {}", e.getMessage());
432                 }
433                 break;
434             case AUTHENTICATION_COMPLETE:
435                 if (webSocketReconnectionPollingJob != null) {
436                     if (!webSocketReconnectionPollingJob.isCancelled() && webSocketReconnectionPollingJob != null) {
437                         webSocketReconnectionPollingJob.cancel(true);
438                     }
439                     webSocketReconnectionPollingJob = null;
440                 }
441                 break;
442             default:
443                 break;
444         }
445     }
446
447     public void sendMessage(YioRemoteMessages messageType, String messagePayload) {
448         switch (messageType) {
449             case AUTHENTICATE_MESSAGE:
450                 yioremoteDockwebSocketClient.sendMessage(authenticationMessageHandler.getAuthenticationMessageString());
451                 logger.debug("sending authenticating {}",
452                         authenticationMessageHandler.getAuthenticationMessageString());
453                 break;
454             case HEARTBEAT_MESSAGE:
455                 yioremoteDockwebSocketClient.sendMessage(pingMessageHandler.getPingMessageString());
456                 logger.debug("sending ping {}", pingMessageHandler.getPingMessageString());
457                 break;
458             case IR_RECEIVER_ON:
459                 irReceiverMessageHandler.setOn();
460                 yioremoteDockwebSocketClient.sendMessage(irReceiverMessageHandler.getIRreceiverMessageString());
461                 logger.debug("sending IR receiver on message: {}",
462                         irReceiverMessageHandler.getIRreceiverMessageString());
463                 break;
464             case IR_RECEIVER_OFF:
465                 irReceiverMessageHandler.setOff();
466                 yioremoteDockwebSocketClient.sendMessage(irReceiverMessageHandler.getIRreceiverMessageString());
467                 logger.debug("sending IR receiver on message: {}",
468                         irReceiverMessageHandler.getIRreceiverMessageString());
469                 break;
470             case IR_SEND:
471                 irCodeSendHandler.setCode(messagePayload);
472                 yioremoteDockwebSocketClient.sendMessage(irCodeSendMessageHandler.getIRcodeSendMessageString());
473                 logger.debug("sending IR code: {}", irCodeSendMessageHandler.getIRcodeSendMessageString());
474                 break;
475         }
476     }
477 }