]> git.basschouten.com Git - openhab-addons.git/blob
6a49c473c995338c6342362e7f883ff389265e05
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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     private int reconnectionCounter = 0;
85
86     public YIOremoteDockHandler(Thing thing) {
87         super(thing);
88     }
89
90     @Override
91     public void initialize() {
92         updateStatus(ThingStatus.UNKNOWN);
93         scheduler.execute(() -> {
94             try {
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());
100             }
101
102             yioremoteDockwebSocketClient.addMessageHandler(new WebsocketInterface() {
103
104                 @Override
105                 public void onConnect(boolean connected) {
106                     if (connected) {
107                         yioRemoteDockActualStatus = YioRemoteDockHandleStatus.CONNECTION_ESTABLISHED;
108                     } else {
109                         yioRemoteDockActualStatus = YioRemoteDockHandleStatus.CONNECTION_FAILED;
110                     }
111                 }
112
113                 @Override
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();
124                                     break;
125                                 case COMMUNICATION_ERROR:
126                                     disposeWebsocketPollingJob();
127                                     reconnectWebsocket();
128                                     break;
129                                 case AUTHENTICATION_COMPLETE:
130                                 case CHECK_PONG:
131                                 case SEND_PING:
132                                     updateChannelString(GROUP_OUTPUT, STATUS_STRING_CHANNEL, receivedStatus);
133                                     triggerChannel(getChannelUuid(GROUP_OUTPUT, STATUS_STRING_CHANNEL));
134                                     break;
135                                 default:
136                                     break;
137                             }
138                             logger.debug("Message {} decoded", receivedMessage);
139                         } else {
140                             logger.debug("Error during message {} decoding", receivedMessage);
141                         }
142                     }
143                 }
144
145                 @Override
146                 public void onClose() {
147                     logger.debug("onClose");
148                     disposeWebsocketPollingJob();
149                     reconnectWebsocket();
150                 }
151
152                 @Override
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();
160                 }
161             });
162
163             try {
164                 webSocketClient.start();
165
166                 webSocketClient.connect(yioremoteDockwebSocketClient, websocketAddress,
167                         yioremoteDockwebSocketClientrequest);
168             } catch (Exception e) {
169                 logger.debug("Connection error {}", e.getMessage());
170             }
171
172         });
173     }
174
175     private boolean decodeReceivedMessage(JsonObject message) {
176         boolean success = false;
177
178         if (message.has("type")) {
179             if (message.get("type").toString().equalsIgnoreCase("\"auth_required\"")) {
180                 success = true;
181                 receivedStatus = "Authentication required";
182             } else if (message.get("type").toString().equalsIgnoreCase("\"auth_ok\"")) {
183                 authenticationOk = true;
184                 success = 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\"")) {
188                     heartBeat = true;
189                     success = true;
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";
194                         success = true;
195                     } else {
196                         receivedStatus = "Send IR Code failure";
197                         heartBeat = true;
198                         success = true;
199                     }
200                 } else {
201                     logger.warn("No known message {}", receivedMessage);
202                     heartBeat = false;
203                     success = false;
204                 }
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("\"", ""));
209                 } else {
210                     irCodeReceivedHandler.setCode("");
211                 }
212
213                 logger.debug("ir_receive message {}", irCodeReceivedHandler.getCode());
214                 heartBeat = true;
215                 success = true;
216             } else {
217                 logger.warn("No known message {}", irCodeReceivedHandler.getCode());
218                 heartBeat = false;
219                 success = false;
220             }
221         } else {
222             logger.warn("No known message {}", irCodeReceivedHandler.getCode());
223             heartBeat = false;
224             success = false;
225         }
226         return success;
227     }
228
229     private JsonObject convertStringToJsonObject(String jsonString) {
230         try {
231             JsonElement jsonElement = JsonParser.parseString(jsonString);
232             JsonObject result;
233
234             if (jsonElement instanceof JsonObject) {
235                 result = jsonElement.getAsJsonObject();
236             } else {
237                 logger.debug("{} is not valid JSON stirng", jsonString);
238                 result = new JsonObject();
239                 throw new IllegalArgumentException(jsonString + "{} is not valid JSON stirng");
240             }
241             return result;
242         } catch (IllegalArgumentException e) {
243             JsonObject result = new JsonObject();
244             return result;
245         }
246     }
247
248     public void updateState(String group, String channelId, State value) {
249         ChannelUID id = new ChannelUID(getThing().getUID(), group, channelId);
250         updateState(id, value);
251     }
252
253     @Override
254     public Collection<Class<? extends ThingHandlerService>> getServices() {
255         return Collections.singleton(YIOremoteDockActions.class);
256     }
257
258     @Override
259     public void dispose() {
260         disposeWebsocketPollingJob();
261         disposeWebSocketReconnectionPollingJob();
262     }
263
264     @Override
265     public void handleCommand(ChannelUID channelUID, Command command) {
266         if (RECEIVER_SWITCH_CHANNEL.equals(channelUID.getIdWithoutGroup())) {
267             switch (yioRemoteDockActualStatus) {
268                 case AUTHENTICATION_COMPLETE:
269                 case SEND_PING:
270                 case CHECK_PONG:
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, "");
277                     } else {
278                         logger.debug("YIODOCKRECEIVERSWITCH no procedure");
279                     }
280                     break;
281                 default:
282                     break;
283             }
284         }
285     }
286
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);
294                 } else {
295                     logger.warn("Wrong ir code format {}", irCode);
296                 }
297             } else {
298                 logger.debug("Wrong Dock Statusfor sending  {}", irCode);
299             }
300         } else {
301             logger.warn("No ir code {}", irCode);
302         }
303     }
304
305     private ChannelUID getChannelUuid(String group, String typeId) {
306         return new ChannelUID(getThing().getUID(), group, typeId);
307     }
308
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));
312     }
313
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;
320                 break;
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,
330                             TimeUnit.SECONDS);
331                 } else {
332                     yioRemoteDockActualStatus = YioRemoteDockHandleStatus.AUTHENTICATION_FAILED;
333                 }
334                 break;
335             default:
336                 disposeWebsocketPollingJob();
337                 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.COMMUNICATION_ERROR;
338                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
339                         "Connection lost no ping from YIO DOCK");
340                 break;
341         }
342     }
343
344     private void disposeWebsocketPollingJob() {
345         if (webSocketPollingJob != null) {
346             if (!webSocketPollingJob.isCancelled() && webSocketPollingJob != null) {
347                 webSocketPollingJob.cancel(true);
348             }
349             webSocketPollingJob = null;
350         }
351     }
352
353     private void disposeWebSocketReconnectionPollingJob() {
354         if (webSocketReconnectionPollingJob != null) {
355             if (!webSocketReconnectionPollingJob.isCancelled() && webSocketReconnectionPollingJob != null) {
356                 webSocketReconnectionPollingJob.cancel(true);
357             }
358         }
359         webSocketReconnectionPollingJob = null;
360         logger.debug("disposereconnection");
361         reconnectionCounter = 0;
362     }
363
364     private void pollingWebsocketJob() {
365         switch (yioRemoteDockActualStatus) {
366             case AUTHENTICATION_COMPLETE:
367                 resetHeartbeat();
368                 sendMessage(YioRemoteMessages.HEARTBEAT_MESSAGE, "");
369                 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.CHECK_PONG;
370                 break;
371             case SEND_PING:
372                 resetHeartbeat();
373                 sendMessage(YioRemoteMessages.HEARTBEAT_MESSAGE, "");
374                 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.CHECK_PONG;
375                 break;
376             case CHECK_PONG:
377                 if (getHeartbeat()) {
378                     updateChannelString(GROUP_OUTPUT, STATUS_STRING_CHANNEL, receivedStatus);
379                     yioRemoteDockActualStatus = YioRemoteDockHandleStatus.SEND_PING;
380                     logger.debug("heartBeat ok");
381                 } else {
382                     yioRemoteDockActualStatus = YioRemoteDockHandleStatus.COMMUNICATION_ERROR;
383                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
384                             "Connection lost no ping from YIO DOCK");
385                     disposeWebsocketPollingJob();
386                     reconnectWebsocket();
387                 }
388                 break;
389             default:
390                 disposeWebsocketPollingJob();
391                 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.COMMUNICATION_ERROR;
392                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
393                         "Connection lost no ping from YIO DOCK");
394                 break;
395         }
396     }
397
398     public boolean resetHeartbeat() {
399         heartBeat = false;
400         return true;
401     }
402
403     public boolean getHeartbeat() {
404         return heartBeat;
405     }
406
407     public void reconnectWebsocket() {
408         yioRemoteDockActualStatus = YioRemoteDockHandleStatus.COMMUNICATION_ERROR;
409         if (webSocketReconnectionPollingJob == null) {
410             webSocketReconnectionPollingJob = scheduler.scheduleWithFixedDelay(this::reconnectWebsocketJob, 0, 30,
411                     TimeUnit.SECONDS);
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,
418                         TimeUnit.MINUTES);
419             } else {
420                 disposeWebSocketReconnectionPollingJob();
421                 if (webSocketReconnectionPollingJob == null) {
422                     webSocketReconnectionPollingJob = scheduler.scheduleWithFixedDelay(this::reconnectWebsocketJob, 0,
423                             5, TimeUnit.MINUTES);
424                 }
425             }
426         } else {
427         }
428     }
429
430     public void reconnectWebsocketJob() {
431         reconnectionCounter++;
432         switch (yioRemoteDockActualStatus) {
433             case COMMUNICATION_ERROR:
434                 logger.debug("Reconnecting YIORemoteHandler");
435                 try {
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());
444                 }
445                 try {
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());
451                 }
452                 try {
453                     webSocketClient.start();
454                     webSocketClient.connect(yioremoteDockwebSocketClient, websocketAddress,
455                             yioremoteDockwebSocketClientrequest);
456                 } catch (Exception e) {
457                     logger.debug("Connection error {}", e.getMessage());
458                 }
459                 break;
460             case AUTHENTICATION_COMPLETE:
461                 disposeWebSocketReconnectionPollingJob();
462                 reconnectionCounter = 0;
463                 break;
464             default:
465                 break;
466         }
467     }
468
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());
475                 break;
476             case HEARTBEAT_MESSAGE:
477                 yioremoteDockwebSocketClient.sendMessage(pingMessageHandler.getPingMessageString());
478                 logger.debug("sending ping {}", pingMessageHandler.getPingMessageString());
479                 break;
480             case IR_RECEIVER_ON:
481                 irReceiverMessageHandler.setOn();
482                 yioremoteDockwebSocketClient.sendMessage(irReceiverMessageHandler.getIRreceiverMessageString());
483                 logger.debug("sending IR receiver on message: {}",
484                         irReceiverMessageHandler.getIRreceiverMessageString());
485                 break;
486             case IR_RECEIVER_OFF:
487                 irReceiverMessageHandler.setOff();
488                 yioremoteDockwebSocketClient.sendMessage(irReceiverMessageHandler.getIRreceiverMessageString());
489                 logger.debug("sending IR receiver on message: {}",
490                         irReceiverMessageHandler.getIRreceiverMessageString());
491                 break;
492             case IR_SEND:
493                 irCodeSendHandler.setCode(messagePayload);
494                 yioremoteDockwebSocketClient.sendMessage(irCodeSendMessageHandler.getIRcodeSendMessageString());
495                 logger.debug("sending IR code: {}", irCodeSendMessageHandler.getIRcodeSendMessageString());
496                 break;
497         }
498     }
499 }