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