]> git.basschouten.com Git - openhab-addons.git/blob
c45051b0d5214253ea89d053b87b76d68d89dd50
[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.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<?> 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 (message.get("type").toString().equalsIgnoreCase("\"auth_required\"")) {
181                 success = true;
182                 receivedStatus = "Authentication required";
183             } else if (message.get("type").toString().equalsIgnoreCase("\"auth_ok\"")) {
184                 authenticationOk = true;
185                 success = true;
186                 receivedStatus = "Authentication ok";
187             } else if (message.get("type").toString().equalsIgnoreCase("\"dock\"") && message.has("message")) {
188                 if (message.get("message").toString().equalsIgnoreCase("\"pong\"")) {
189                     heartBeat = true;
190                     success = true;
191                     receivedStatus = "Heart beat received";
192                 } else if (message.get("message").toString().equalsIgnoreCase("\"ir_send\"")) {
193                     if (message.get("success").toString().equalsIgnoreCase("true")) {
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 (message.get("command").toString().equalsIgnoreCase("\"ir_receive\"")) {
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             JsonObject result = new JsonObject();
245             return result;
246         }
247     }
248
249     public void updateState(String group, String channelId, State value) {
250         ChannelUID id = new ChannelUID(getThing().getUID(), group, channelId);
251         updateState(id, value);
252     }
253
254     @Override
255     public Collection<Class<? extends ThingHandlerService>> getServices() {
256         return Collections.singleton(YIOremoteDockActions.class);
257     }
258
259     @Override
260     public void dispose() {
261         Future<?> job = initJob;
262         if (job != null) {
263             job.cancel(true);
264             initJob = null;
265         }
266         disposeWebsocketPollingJob();
267         disposeWebSocketReconnectionPollingJob();
268         try {
269             webSocketClient.stop();
270         } catch (Exception e) {
271             logger.debug("Could not stop webSocketClient,  message {}", e.getMessage());
272         }
273     }
274
275     @Override
276     public void handleCommand(ChannelUID channelUID, Command command) {
277         if (RECEIVER_SWITCH_CHANNEL.equals(channelUID.getIdWithoutGroup())) {
278             switch (yioRemoteDockActualStatus) {
279                 case AUTHENTICATION_COMPLETE:
280                 case SEND_PING:
281                 case CHECK_PONG:
282                     if (command == OnOffType.ON) {
283                         logger.debug("YIODOCKRECEIVERSWITCH ON procedure: Switching IR Receiver on");
284                         sendMessage(YioRemoteMessages.IR_RECEIVER_ON, "");
285                     } else if (command == OnOffType.OFF) {
286                         logger.debug("YIODOCKRECEIVERSWITCH OFF procedure: Switching IR Receiver off");
287                         sendMessage(YioRemoteMessages.IR_RECEIVER_OFF, "");
288                     } else {
289                         logger.debug("YIODOCKRECEIVERSWITCH no procedure");
290                     }
291                     break;
292                 default:
293                     break;
294             }
295         }
296     }
297
298     public void sendIRCode(@Nullable String irCode) {
299         if (irCode != null) {
300             if (yioRemoteDockActualStatus.equals(YioRemoteDockHandleStatus.AUTHENTICATION_COMPLETE)
301                     || yioRemoteDockActualStatus.equals(YioRemoteDockHandleStatus.SEND_PING)
302                     || yioRemoteDockActualStatus.equals(YioRemoteDockHandleStatus.CHECK_PONG)) {
303                 if (irCode.matches("[0-9]?[0-9][;]0[xX][0-9a-fA-F]+[;][0-9]+[;][0-9]")) {
304                     sendMessage(YioRemoteMessages.IR_SEND, irCode);
305                 } else {
306                     logger.warn("Wrong ir code format {}", irCode);
307                 }
308             } else {
309                 logger.debug("Wrong Dock Statusfor sending  {}", irCode);
310             }
311         } else {
312             logger.warn("No ir code {}", irCode);
313         }
314     }
315
316     private ChannelUID getChannelUuid(String group, String typeId) {
317         return new ChannelUID(getThing().getUID(), group, typeId);
318     }
319
320     private void updateChannelString(String group, String channelId, String value) {
321         ChannelUID id = new ChannelUID(getThing().getUID(), group, channelId);
322         updateState(id, new StringType(value));
323     }
324
325     private void authenticateWebsocket() {
326         switch (yioRemoteDockActualStatus) {
327             case CONNECTION_ESTABLISHED:
328                 authenticationMessageHandler.setToken(localConfig.accessToken);
329                 sendMessage(YioRemoteMessages.AUTHENTICATE_MESSAGE, localConfig.accessToken);
330                 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.AUTHENTICATION_PROCESS;
331                 break;
332             case AUTHENTICATION_PROCESS:
333                 if (authenticationOk) {
334                     yioRemoteDockActualStatus = YioRemoteDockHandleStatus.AUTHENTICATION_COMPLETE;
335                     disposeWebSocketReconnectionPollingJob();
336                     reconnectionCounter = 0;
337                     updateStatus(ThingStatus.ONLINE);
338                     updateState(STATUS_STRING_CHANNEL, StringType.EMPTY);
339                     updateState(RECEIVER_SWITCH_CHANNEL, OnOffType.OFF);
340                     webSocketPollingJob = scheduler.scheduleWithFixedDelay(this::pollingWebsocketJob, 0, 40,
341                             TimeUnit.SECONDS);
342                 } else {
343                     yioRemoteDockActualStatus = YioRemoteDockHandleStatus.AUTHENTICATION_FAILED;
344                 }
345                 break;
346             default:
347                 disposeWebsocketPollingJob();
348                 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.COMMUNICATION_ERROR;
349                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
350                         "Connection lost no ping from YIO DOCK");
351                 break;
352         }
353     }
354
355     private void disposeWebsocketPollingJob() {
356         Future<?> job = webSocketPollingJob;
357         if (job != null) {
358             job.cancel(true);
359             webSocketPollingJob = null;
360         }
361     }
362
363     private void disposeWebSocketReconnectionPollingJob() {
364         Future<?> job = webSocketReconnectionPollingJob;
365         if (job != null) {
366             job.cancel(true);
367             webSocketReconnectionPollingJob = null;
368         }
369         logger.debug("disposereconnection");
370         reconnectionCounter = 0;
371     }
372
373     private void pollingWebsocketJob() {
374         switch (yioRemoteDockActualStatus) {
375             case AUTHENTICATION_COMPLETE:
376                 resetHeartbeat();
377                 sendMessage(YioRemoteMessages.HEARTBEAT_MESSAGE, "");
378                 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.CHECK_PONG;
379                 break;
380             case SEND_PING:
381                 resetHeartbeat();
382                 sendMessage(YioRemoteMessages.HEARTBEAT_MESSAGE, "");
383                 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.CHECK_PONG;
384                 break;
385             case CHECK_PONG:
386                 if (getHeartbeat()) {
387                     updateChannelString(GROUP_OUTPUT, STATUS_STRING_CHANNEL, receivedStatus);
388                     yioRemoteDockActualStatus = YioRemoteDockHandleStatus.SEND_PING;
389                     logger.debug("heartBeat ok");
390                 } else {
391                     yioRemoteDockActualStatus = YioRemoteDockHandleStatus.COMMUNICATION_ERROR;
392                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
393                             "Connection lost no ping from YIO DOCK");
394                     disposeWebsocketPollingJob();
395                     reconnectWebsocket();
396                 }
397                 break;
398             default:
399                 disposeWebsocketPollingJob();
400                 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.COMMUNICATION_ERROR;
401                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
402                         "Connection lost no ping from YIO DOCK");
403                 break;
404         }
405     }
406
407     public boolean resetHeartbeat() {
408         heartBeat = false;
409         return true;
410     }
411
412     public boolean getHeartbeat() {
413         return heartBeat;
414     }
415
416     public void reconnectWebsocket() {
417         yioRemoteDockActualStatus = YioRemoteDockHandleStatus.COMMUNICATION_ERROR;
418         if (webSocketReconnectionPollingJob == null) {
419             webSocketReconnectionPollingJob = scheduler.scheduleWithFixedDelay(this::reconnectWebsocketJob, 0, 30,
420                     TimeUnit.SECONDS);
421         } else if (reconnectionCounter == 5) {
422             reconnectionCounter = 0;
423             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
424                     "Connection lost no ping from YIO DOCK");
425             if (webSocketReconnectionPollingJob == null) {
426                 webSocketReconnectionPollingJob = scheduler.scheduleWithFixedDelay(this::reconnectWebsocketJob, 0, 1,
427                         TimeUnit.MINUTES);
428             } else {
429                 disposeWebSocketReconnectionPollingJob();
430                 if (webSocketReconnectionPollingJob == null) {
431                     webSocketReconnectionPollingJob = scheduler.scheduleWithFixedDelay(this::reconnectWebsocketJob, 0,
432                             5, TimeUnit.MINUTES);
433                 }
434             }
435         } else {
436         }
437     }
438
439     public void reconnectWebsocketJob() {
440         reconnectionCounter++;
441         switch (yioRemoteDockActualStatus) {
442             case COMMUNICATION_ERROR:
443                 logger.debug("Reconnecting YIORemoteHandler");
444                 try {
445                     disposeWebsocketPollingJob();
446                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
447                             "Connection lost no ping from YIO DOCK");
448                     yioremoteDockwebSocketClient.closeWebsocketSession();
449                     webSocketClient.stop();
450                     yioRemoteDockActualStatus = YioRemoteDockHandleStatus.RECONNECTION_PROCESS;
451                 } catch (Exception e) {
452                     logger.debug("Connection error {}", e.getMessage());
453                 }
454                 try {
455                     websocketAddress = new URI("ws://" + localConfig.host + ":946");
456                     yioRemoteDockActualStatus = YioRemoteDockHandleStatus.AUTHENTICATION_PROCESS;
457                 } catch (URISyntaxException e) {
458                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
459                             "Initialize web socket failed: " + e.getMessage());
460                 }
461                 try {
462                     webSocketClient.start();
463                     webSocketClient.connect(yioremoteDockwebSocketClient, websocketAddress,
464                             yioremoteDockwebSocketClientrequest);
465                 } catch (Exception e) {
466                     logger.debug("Connection error {}", e.getMessage());
467                 }
468                 break;
469             case AUTHENTICATION_COMPLETE:
470                 disposeWebSocketReconnectionPollingJob();
471                 reconnectionCounter = 0;
472                 break;
473             default:
474                 break;
475         }
476     }
477
478     public void sendMessage(YioRemoteMessages messageType, String messagePayload) {
479         switch (messageType) {
480             case AUTHENTICATE_MESSAGE:
481                 yioremoteDockwebSocketClient.sendMessage(authenticationMessageHandler.getAuthenticationMessageString());
482                 logger.debug("sending authenticating {}",
483                         authenticationMessageHandler.getAuthenticationMessageString());
484                 break;
485             case HEARTBEAT_MESSAGE:
486                 yioremoteDockwebSocketClient.sendMessage(pingMessageHandler.getPingMessageString());
487                 logger.debug("sending ping {}", pingMessageHandler.getPingMessageString());
488                 break;
489             case IR_RECEIVER_ON:
490                 irReceiverMessageHandler.setOn();
491                 yioremoteDockwebSocketClient.sendMessage(irReceiverMessageHandler.getIRreceiverMessageString());
492                 logger.debug("sending IR receiver on message: {}",
493                         irReceiverMessageHandler.getIRreceiverMessageString());
494                 break;
495             case IR_RECEIVER_OFF:
496                 irReceiverMessageHandler.setOff();
497                 yioremoteDockwebSocketClient.sendMessage(irReceiverMessageHandler.getIRreceiverMessageString());
498                 logger.debug("sending IR receiver on message: {}",
499                         irReceiverMessageHandler.getIRreceiverMessageString());
500                 break;
501             case IR_SEND:
502                 irCodeSendHandler.setCode(messagePayload);
503                 yioremoteDockwebSocketClient.sendMessage(irCodeSendMessageHandler.getIRcodeSendMessageString());
504                 logger.debug("sending IR code: {}", irCodeSendMessageHandler.getIRcodeSendMessageString());
505                 break;
506         }
507     }
508 }