]> git.basschouten.com Git - openhab-addons.git/blob
cd6c112bb2a85dc9c13bd67e3ee9aa618d8b0ed6
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.Set;
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<?> 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;
86
87     public YIOremoteDockHandler(Thing thing) {
88         super(thing);
89     }
90
91     @Override
92     public void initialize() {
93         updateStatus(ThingStatus.UNKNOWN);
94         initJob = scheduler.submit(() -> {
95             try {
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());
101             }
102
103             yioremoteDockwebSocketClient.addMessageHandler(new WebsocketInterface() {
104
105                 @Override
106                 public void onConnect(boolean connected) {
107                     if (connected) {
108                         yioRemoteDockActualStatus = YioRemoteDockHandleStatus.CONNECTION_ESTABLISHED;
109                     } else {
110                         yioRemoteDockActualStatus = YioRemoteDockHandleStatus.CONNECTION_FAILED;
111                     }
112                 }
113
114                 @Override
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();
125                                     break;
126                                 case COMMUNICATION_ERROR:
127                                     disposeWebsocketPollingJob();
128                                     reconnectWebsocket();
129                                     break;
130                                 case AUTHENTICATION_COMPLETE:
131                                 case CHECK_PONG:
132                                 case SEND_PING:
133                                     updateChannelString(GROUP_OUTPUT, STATUS_STRING_CHANNEL, receivedStatus);
134                                     triggerChannel(getChannelUuid(GROUP_OUTPUT, STATUS_STRING_CHANNEL));
135                                     break;
136                                 default:
137                                     break;
138                             }
139                             logger.debug("Message {} decoded", receivedMessage);
140                         } else {
141                             logger.debug("Error during message {} decoding", receivedMessage);
142                         }
143                     }
144                 }
145
146                 @Override
147                 public void onClose() {
148                     logger.debug("onClose");
149                     disposeWebsocketPollingJob();
150                     reconnectWebsocket();
151                 }
152
153                 @Override
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();
161                 }
162             });
163
164             try {
165                 webSocketClient.start();
166
167                 webSocketClient.connect(yioremoteDockwebSocketClient, websocketAddress,
168                         yioremoteDockwebSocketClientrequest);
169             } catch (Exception e) {
170                 logger.debug("Connection error {}", e.getMessage());
171             }
172
173         });
174     }
175
176     private boolean decodeReceivedMessage(JsonObject message) {
177         boolean success = false;
178
179         if (message.has("type")) {
180             if ("\"auth_required\"".equalsIgnoreCase(message.get("type").toString())) {
181                 success = true;
182                 receivedStatus = "Authentication required";
183             } else if ("\"auth_ok\"".equalsIgnoreCase(message.get("type").toString())) {
184                 authenticationOk = true;
185                 success = true;
186                 receivedStatus = "Authentication ok";
187             } else if ("\"dock\"".equalsIgnoreCase(message.get("type").toString()) && message.has("message")) {
188                 if ("\"pong\"".equalsIgnoreCase(message.get("message").toString())) {
189                     heartBeat = true;
190                     success = true;
191                     receivedStatus = "Heart beat received";
192                 } else if ("\"ir_send\"".equalsIgnoreCase(message.get("message").toString())) {
193                     if ("true".equalsIgnoreCase(message.get("success").toString())) {
194                         receivedStatus = "Send IR Code successfully";
195                         success = true;
196                     } else {
197                         receivedStatus = "Send IR Code failure";
198                         heartBeat = true;
199                         success = true;
200                     }
201                 } else {
202                     logger.warn("No known message {}", receivedMessage);
203                     heartBeat = false;
204                     success = false;
205                 }
206             } else if ("\"ir_receive\"".equalsIgnoreCase(message.get("command").toString())) {
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("\"", ""));
210                 } else {
211                     irCodeReceivedHandler.setCode("");
212                 }
213
214                 logger.debug("ir_receive message {}", irCodeReceivedHandler.getCode());
215                 heartBeat = true;
216                 success = true;
217             } else {
218                 logger.warn("No known message {}", irCodeReceivedHandler.getCode());
219                 heartBeat = false;
220                 success = false;
221             }
222         } else {
223             logger.warn("No known message {}", irCodeReceivedHandler.getCode());
224             heartBeat = false;
225             success = false;
226         }
227         return success;
228     }
229
230     private JsonObject convertStringToJsonObject(String jsonString) {
231         try {
232             JsonElement jsonElement = JsonParser.parseString(jsonString);
233             JsonObject result;
234
235             if (jsonElement instanceof JsonObject) {
236                 result = jsonElement.getAsJsonObject();
237             } else {
238                 logger.debug("{} is not valid JSON stirng", jsonString);
239                 result = new JsonObject();
240                 throw new IllegalArgumentException(jsonString + "{} is not valid JSON stirng");
241             }
242             return result;
243         } catch (IllegalArgumentException e) {
244             return new JsonObject();
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 Set.of(YIOremoteDockActions.class);
256     }
257
258     @Override
259     public void dispose() {
260         Future<?> job = initJob;
261         if (job != null) {
262             job.cancel(true);
263             initJob = null;
264         }
265         disposeWebsocketPollingJob();
266         disposeWebSocketReconnectionPollingJob();
267         try {
268             webSocketClient.stop();
269         } catch (Exception e) {
270             logger.debug("Could not stop webSocketClient,  message {}", e.getMessage());
271         }
272     }
273
274     @Override
275     public void handleCommand(ChannelUID channelUID, Command command) {
276         if (RECEIVER_SWITCH_CHANNEL.equals(channelUID.getIdWithoutGroup())) {
277             switch (yioRemoteDockActualStatus) {
278                 case AUTHENTICATION_COMPLETE:
279                 case SEND_PING:
280                 case CHECK_PONG:
281                     if (command == OnOffType.ON) {
282                         logger.debug("YIODOCKRECEIVERSWITCH ON procedure: Switching IR Receiver on");
283                         sendMessage(YioRemoteMessages.IR_RECEIVER_ON, "");
284                     } else if (command == OnOffType.OFF) {
285                         logger.debug("YIODOCKRECEIVERSWITCH OFF procedure: Switching IR Receiver off");
286                         sendMessage(YioRemoteMessages.IR_RECEIVER_OFF, "");
287                     } else {
288                         logger.debug("YIODOCKRECEIVERSWITCH no procedure");
289                     }
290                     break;
291                 default:
292                     break;
293             }
294         }
295     }
296
297     public void sendIRCode(@Nullable String irCode) {
298         if (irCode != null) {
299             if (yioRemoteDockActualStatus.equals(YioRemoteDockHandleStatus.AUTHENTICATION_COMPLETE)
300                     || yioRemoteDockActualStatus.equals(YioRemoteDockHandleStatus.SEND_PING)
301                     || yioRemoteDockActualStatus.equals(YioRemoteDockHandleStatus.CHECK_PONG)) {
302                 if (irCode.matches("[0-9]?[0-9][;]0[xX][0-9a-fA-F]+[;][0-9]+[;][0-9]")) {
303                     sendMessage(YioRemoteMessages.IR_SEND, irCode);
304                 } else {
305                     logger.warn("Wrong ir code format {}", irCode);
306                 }
307             } else {
308                 logger.debug("Wrong Dock Statusfor sending  {}", irCode);
309             }
310         } else {
311             logger.warn("No ir code {}", irCode);
312         }
313     }
314
315     private ChannelUID getChannelUuid(String group, String typeId) {
316         return new ChannelUID(getThing().getUID(), group, typeId);
317     }
318
319     private void updateChannelString(String group, String channelId, String value) {
320         ChannelUID id = new ChannelUID(getThing().getUID(), group, channelId);
321         updateState(id, new StringType(value));
322     }
323
324     private void authenticateWebsocket() {
325         switch (yioRemoteDockActualStatus) {
326             case CONNECTION_ESTABLISHED:
327                 authenticationMessageHandler.setToken(localConfig.accessToken);
328                 sendMessage(YioRemoteMessages.AUTHENTICATE_MESSAGE, localConfig.accessToken);
329                 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.AUTHENTICATION_PROCESS;
330                 break;
331             case AUTHENTICATION_PROCESS:
332                 if (authenticationOk) {
333                     yioRemoteDockActualStatus = YioRemoteDockHandleStatus.AUTHENTICATION_COMPLETE;
334                     disposeWebSocketReconnectionPollingJob();
335                     reconnectionCounter = 0;
336                     updateStatus(ThingStatus.ONLINE);
337                     updateState(STATUS_STRING_CHANNEL, StringType.EMPTY);
338                     updateState(RECEIVER_SWITCH_CHANNEL, OnOffType.OFF);
339                     webSocketPollingJob = scheduler.scheduleWithFixedDelay(this::pollingWebsocketJob, 0, 40,
340                             TimeUnit.SECONDS);
341                 } else {
342                     yioRemoteDockActualStatus = YioRemoteDockHandleStatus.AUTHENTICATION_FAILED;
343                 }
344                 break;
345             default:
346                 disposeWebsocketPollingJob();
347                 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.COMMUNICATION_ERROR;
348                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
349                         "Connection lost no ping from YIO DOCK");
350                 break;
351         }
352     }
353
354     private void disposeWebsocketPollingJob() {
355         Future<?> job = webSocketPollingJob;
356         if (job != null) {
357             job.cancel(true);
358             webSocketPollingJob = null;
359         }
360     }
361
362     private void disposeWebSocketReconnectionPollingJob() {
363         Future<?> job = webSocketReconnectionPollingJob;
364         if (job != null) {
365             job.cancel(true);
366             webSocketReconnectionPollingJob = null;
367         }
368         logger.debug("disposereconnection");
369         reconnectionCounter = 0;
370     }
371
372     private void pollingWebsocketJob() {
373         switch (yioRemoteDockActualStatus) {
374             case AUTHENTICATION_COMPLETE:
375                 resetHeartbeat();
376                 sendMessage(YioRemoteMessages.HEARTBEAT_MESSAGE, "");
377                 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.CHECK_PONG;
378                 break;
379             case SEND_PING:
380                 resetHeartbeat();
381                 sendMessage(YioRemoteMessages.HEARTBEAT_MESSAGE, "");
382                 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.CHECK_PONG;
383                 break;
384             case CHECK_PONG:
385                 if (getHeartbeat()) {
386                     updateChannelString(GROUP_OUTPUT, STATUS_STRING_CHANNEL, receivedStatus);
387                     yioRemoteDockActualStatus = YioRemoteDockHandleStatus.SEND_PING;
388                     logger.debug("heartBeat ok");
389                 } else {
390                     yioRemoteDockActualStatus = YioRemoteDockHandleStatus.COMMUNICATION_ERROR;
391                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
392                             "Connection lost no ping from YIO DOCK");
393                     disposeWebsocketPollingJob();
394                     reconnectWebsocket();
395                 }
396                 break;
397             default:
398                 disposeWebsocketPollingJob();
399                 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.COMMUNICATION_ERROR;
400                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
401                         "Connection lost no ping from YIO DOCK");
402                 break;
403         }
404     }
405
406     public boolean resetHeartbeat() {
407         heartBeat = false;
408         return true;
409     }
410
411     public boolean getHeartbeat() {
412         return heartBeat;
413     }
414
415     public void reconnectWebsocket() {
416         yioRemoteDockActualStatus = YioRemoteDockHandleStatus.COMMUNICATION_ERROR;
417         if (webSocketReconnectionPollingJob == null) {
418             webSocketReconnectionPollingJob = scheduler.scheduleWithFixedDelay(this::reconnectWebsocketJob, 0, 30,
419                     TimeUnit.SECONDS);
420         } else if (reconnectionCounter == 5) {
421             reconnectionCounter = 0;
422             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
423                     "Connection lost no ping from YIO DOCK");
424             if (webSocketReconnectionPollingJob == null) {
425                 webSocketReconnectionPollingJob = scheduler.scheduleWithFixedDelay(this::reconnectWebsocketJob, 0, 1,
426                         TimeUnit.MINUTES);
427             } else {
428                 disposeWebSocketReconnectionPollingJob();
429                 if (webSocketReconnectionPollingJob == null) {
430                     webSocketReconnectionPollingJob = scheduler.scheduleWithFixedDelay(this::reconnectWebsocketJob, 0,
431                             5, TimeUnit.MINUTES);
432                 }
433             }
434         } else {
435         }
436     }
437
438     public void reconnectWebsocketJob() {
439         reconnectionCounter++;
440         switch (yioRemoteDockActualStatus) {
441             case COMMUNICATION_ERROR:
442                 logger.debug("Reconnecting YIORemoteHandler");
443                 try {
444                     disposeWebsocketPollingJob();
445                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
446                             "Connection lost no ping from YIO DOCK");
447                     yioremoteDockwebSocketClient.closeWebsocketSession();
448                     webSocketClient.stop();
449                     yioRemoteDockActualStatus = YioRemoteDockHandleStatus.RECONNECTION_PROCESS;
450                 } catch (Exception e) {
451                     logger.debug("Connection error {}", e.getMessage());
452                 }
453                 try {
454                     websocketAddress = new URI("ws://" + localConfig.host + ":946");
455                     yioRemoteDockActualStatus = YioRemoteDockHandleStatus.AUTHENTICATION_PROCESS;
456                 } catch (URISyntaxException e) {
457                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
458                             "Initialize web socket failed: " + e.getMessage());
459                 }
460                 try {
461                     webSocketClient.start();
462                     webSocketClient.connect(yioremoteDockwebSocketClient, websocketAddress,
463                             yioremoteDockwebSocketClientrequest);
464                 } catch (Exception e) {
465                     logger.debug("Connection error {}", e.getMessage());
466                 }
467                 break;
468             case AUTHENTICATION_COMPLETE:
469                 disposeWebSocketReconnectionPollingJob();
470                 reconnectionCounter = 0;
471                 break;
472             default:
473                 break;
474         }
475     }
476
477     public void sendMessage(YioRemoteMessages messageType, String messagePayload) {
478         switch (messageType) {
479             case AUTHENTICATE_MESSAGE:
480                 yioremoteDockwebSocketClient.sendMessage(authenticationMessageHandler.getAuthenticationMessageString());
481                 logger.debug("sending authenticating {}",
482                         authenticationMessageHandler.getAuthenticationMessageString());
483                 break;
484             case HEARTBEAT_MESSAGE:
485                 yioremoteDockwebSocketClient.sendMessage(pingMessageHandler.getPingMessageString());
486                 logger.debug("sending ping {}", pingMessageHandler.getPingMessageString());
487                 break;
488             case IR_RECEIVER_ON:
489                 irReceiverMessageHandler.setOn();
490                 yioremoteDockwebSocketClient.sendMessage(irReceiverMessageHandler.getIRreceiverMessageString());
491                 logger.debug("sending IR receiver on message: {}",
492                         irReceiverMessageHandler.getIRreceiverMessageString());
493                 break;
494             case IR_RECEIVER_OFF:
495                 irReceiverMessageHandler.setOff();
496                 yioremoteDockwebSocketClient.sendMessage(irReceiverMessageHandler.getIRreceiverMessageString());
497                 logger.debug("sending IR receiver on message: {}",
498                         irReceiverMessageHandler.getIRreceiverMessageString());
499                 break;
500             case IR_SEND:
501                 irCodeSendHandler.setCode(messagePayload);
502                 yioremoteDockwebSocketClient.sendMessage(irCodeSendMessageHandler.getIRcodeSendMessageString());
503                 logger.debug("sending IR code: {}", irCodeSendMessageHandler.getIRcodeSendMessageString());
504                 break;
505         }
506     }
507 }