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