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