]> git.basschouten.com Git - openhab-addons.git/blob
c90a2cbaae251fd439d4ff4e84ed2fd319faeb90
[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.utils.Websocket;
35 import org.openhab.binding.yioremote.internal.utils.WebsocketInterface;
36 import org.openhab.core.library.types.OnOffType;
37 import org.openhab.core.library.types.StringType;
38 import org.openhab.core.thing.ChannelUID;
39 import org.openhab.core.thing.Thing;
40 import org.openhab.core.thing.ThingStatus;
41 import org.openhab.core.thing.ThingStatusDetail;
42 import org.openhab.core.thing.binding.BaseThingHandler;
43 import org.openhab.core.thing.binding.ThingHandlerService;
44 import org.openhab.core.types.Command;
45 import org.openhab.core.types.State;
46 import org.openhab.core.types.UnDefType;
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
84     public YIOremoteDockHandler(Thing thing) {
85         super(thing);
86     }
87
88     @Override
89     public void initialize() {
90         updateStatus(ThingStatus.UNKNOWN);
91         scheduler.execute(() -> {
92             try {
93                 websocketAddress = new URI("ws://" + localConfig.host + ":946");
94                 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.AUTHENTICATION_PROCESS;
95             } catch (URISyntaxException e) {
96                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
97                         "Initialize web socket failed: " + e.getMessage());
98             }
99
100             yioremoteDockwebSocketClient.addMessageHandler(new WebsocketInterface() {
101
102                 @Override
103                 public void onConnect(boolean connected) {
104                     if (connected) {
105                         yioRemoteDockActualStatus = YioRemoteDockHandleStatus.CONNECTION_ESTABLISHED;
106                     } else {
107                         yioRemoteDockActualStatus = YioRemoteDockHandleStatus.CONNECTION_FAILED;
108                     }
109                 }
110
111                 @Override
112                 public void onMessage(String message) {
113                     receivedMessage = message;
114                     logger.debug("Message recieved {}", message);
115                     recievedJson = convertStringToJsonObject(receivedMessage);
116                     if (recievedJson.size() > 0) {
117                         if (decodeReceivedMessage(recievedJson)) {
118                             triggerChannel(getChannelUuid(GROUP_OUTPUT, STATUS_STRING_CHANNEL));
119                             updateChannelString(GROUP_OUTPUT, STATUS_STRING_CHANNEL, receivedStatus);
120                             switch (yioRemoteDockActualStatus) {
121                                 case CONNECTION_ESTABLISHED:
122                                 case AUTHENTICATION_PROCESS:
123                                     authenticateWebsocket();
124                                     break;
125                                 case COMMUNICATION_ERROR:
126                                     reconnectWebsocket();
127                                     break;
128                                 default:
129                                     break;
130                             }
131                             logger.debug("Message {} decoded", receivedMessage);
132                         } else {
133                             logger.debug("Error during message {} decoding", receivedMessage);
134                         }
135                     }
136                 }
137
138                 @Override
139                 public void onClose() {
140                     reconnectWebsocket();
141                 }
142
143                 @Override
144                 public void onError(Throwable cause) {
145                     yioRemoteDockActualStatus = YioRemoteDockHandleStatus.COMMUNICATION_ERROR;
146                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
147                             "Communication lost no ping from YIO DOCK");
148                 }
149             });
150
151             try {
152                 webSocketClient.start();
153
154                 webSocketClient.connect(yioremoteDockwebSocketClient, websocketAddress,
155                         yioremoteDockwebSocketClientrequest);
156             } catch (Exception e) {
157                 logger.debug("Connection error {}", e.getMessage());
158             }
159
160         });
161     }
162
163     private boolean decodeReceivedMessage(JsonObject message) {
164         boolean success = false;
165
166         if (message.has("type")) {
167             if (message.get("type").toString().equalsIgnoreCase("\"auth_required\"")) {
168                 heartBeat = true;
169                 success = true;
170                 receivedStatus = "Authentication required";
171             } else if (message.get("type").toString().equalsIgnoreCase("\"auth_ok\"")) {
172                 authenticationOk = true;
173                 heartBeat = 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("\"ir_send\"")) {
178                     if (message.get("success").toString().equalsIgnoreCase("true")) {
179                         receivedStatus = "Send IR Code successfully";
180                         heartBeat = true;
181                         success = true;
182                     } else {
183                         if (irCodeSendHandler.getCode().equalsIgnoreCase("0;0x0;0;0")) {
184                             logger.debug("Send heartBeat Code success");
185                             receivedStatus = "Send heartBeat Code success";
186                         } else {
187                             receivedStatus = "Send IR Code failure";
188                         }
189                         heartBeat = true;
190                         success = true;
191                     }
192                 } else {
193                     logger.warn("No known message {}", receivedMessage);
194                     heartBeat = false;
195                     success = false;
196                 }
197             } else if (message.get("command").toString().equalsIgnoreCase("\"ir_receive\"")) {
198                 receivedStatus = message.get("code").toString().replace("\"", "");
199                 if (receivedStatus.matches("[0-9][;]0[xX][0-9a-fA-F]+[;][0-9]+[;][0-9]")) {
200                     irCodeReceivedHandler.setCode(message.get("code").toString().replace("\"", ""));
201                 } else {
202                     irCodeReceivedHandler.setCode("");
203                 }
204                 logger.debug("ir_receive message {}", irCodeReceivedHandler.getCode());
205                 heartBeat = true;
206                 success = true;
207             } else {
208                 logger.warn("No known message {}", irCodeReceivedHandler.getCode());
209                 heartBeat = false;
210                 success = false;
211             }
212         } else {
213             logger.warn("No known message {}", irCodeReceivedHandler.getCode());
214             heartBeat = false;
215             success = false;
216         }
217         return success;
218     }
219
220     private JsonObject convertStringToJsonObject(String jsonString) {
221         try {
222             JsonParser parser = new JsonParser();
223             JsonElement jsonElement = parser.parse(jsonString);
224             JsonObject result;
225
226             if (jsonElement instanceof JsonObject) {
227                 result = jsonElement.getAsJsonObject();
228             } else {
229                 logger.debug("{} is not valid JSON stirng", jsonString);
230                 result = new JsonObject();
231                 throw new IllegalArgumentException(jsonString + "{} is not valid JSON stirng");
232             }
233             return result;
234         } catch (IllegalArgumentException e) {
235             JsonObject result = new JsonObject();
236             return result;
237         }
238     }
239
240     public void updateState(String group, String channelId, State value) {
241         ChannelUID id = new ChannelUID(getThing().getUID(), group, channelId);
242         updateState(id, value);
243     }
244
245     @Override
246     public Collection<Class<? extends ThingHandlerService>> getServices() {
247         return Collections.singleton(YIOremoteDockActions.class);
248     }
249
250     @Override
251     public void dispose() {
252         disposeWebsocketPollingJob();
253         if (webSocketReconnectionPollingJob != null) {
254             if (!webSocketReconnectionPollingJob.isCancelled() && webSocketReconnectionPollingJob != null) {
255                 webSocketReconnectionPollingJob.cancel(true);
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, 150,
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                 if (getAndResetHeartbeat()) {
341                     updateChannelString(GROUP_OUTPUT, STATUS_STRING_CHANNEL,
342                             irCodeReceivedHandler.getCode() + irCodeReceivedHandler.getFormat());
343                     logger.debug("heartBeat ok");
344                     sendMessage(YioRemoteMessages.HEARTBEAT_MESSAGE, "");
345                 } else {
346                     yioRemoteDockActualStatus = YioRemoteDockHandleStatus.COMMUNICATION_ERROR;
347                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
348                             "Connection lost no ping from YIO DOCK");
349                     disposeWebsocketPollingJob();
350                     reconnectWebsocket();
351                 }
352                 break;
353             default:
354                 disposeWebsocketPollingJob();
355                 yioRemoteDockActualStatus = YioRemoteDockHandleStatus.COMMUNICATION_ERROR;
356                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
357                         "Connection lost no ping from YIO DOCK");
358                 updateState(GROUP_OUTPUT, STATUS_STRING_CHANNEL, UnDefType.UNDEF);
359                 break;
360         }
361     }
362
363     public boolean getAndResetHeartbeat() {
364         boolean result = heartBeat;
365         heartBeat = false;
366         return result;
367     }
368
369     public void reconnectWebsocket() {
370         if (webSocketReconnectionPollingJob == null) {
371             webSocketReconnectionPollingJob = scheduler.scheduleWithFixedDelay(this::reconnectWebsocketJob, 0, 30,
372                     TimeUnit.SECONDS);
373         }
374     }
375
376     public void reconnectWebsocketJob() {
377         switch (yioRemoteDockActualStatus) {
378             case COMMUNICATION_ERROR:
379                 logger.debug("Reconnecting YIORemoteHandler");
380                 try {
381                     disposeWebsocketPollingJob();
382                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
383                             "Connection lost no ping from YIO DOCK");
384                     yioremoteDockwebSocketClient.closeWebsocketSession();
385                     webSocketClient.stop();
386                     yioRemoteDockActualStatus = YioRemoteDockHandleStatus.RECONNECTION_PROCESS;
387                 } catch (Exception e) {
388                     logger.debug("Connection error {}", e.getMessage());
389                 }
390                 try {
391                     websocketAddress = new URI("ws://" + localConfig.host + ":946");
392                     yioRemoteDockActualStatus = YioRemoteDockHandleStatus.AUTHENTICATION_PROCESS;
393                 } catch (URISyntaxException e) {
394                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
395                             "Initialize web socket failed: " + e.getMessage());
396                 }
397                 try {
398                     webSocketClient.start();
399                     webSocketClient.connect(yioremoteDockwebSocketClient, websocketAddress,
400                             yioremoteDockwebSocketClientrequest);
401                 } catch (Exception e) {
402                     logger.debug("Connection error {}", e.getMessage());
403                 }
404                 break;
405             case AUTHENTICATION_COMPLETE:
406                 if (webSocketReconnectionPollingJob != null) {
407                     if (!webSocketReconnectionPollingJob.isCancelled() && webSocketReconnectionPollingJob != null) {
408                         webSocketReconnectionPollingJob.cancel(true);
409                     }
410                     webSocketReconnectionPollingJob = null;
411                 }
412                 break;
413             default:
414                 break;
415         }
416     }
417
418     public void sendMessage(YioRemoteMessages messageType, String messagePayload) {
419         switch (messageType) {
420             case AUTHENTICATE_MESSAGE:
421                 yioremoteDockwebSocketClient.sendMessage(authenticationMessageHandler.getAuthenticationMessageString());
422                 logger.debug("sending authenticating {}",
423                         authenticationMessageHandler.getAuthenticationMessageString());
424                 break;
425             case HEARTBEAT_MESSAGE:
426                 irCodeSendHandler.setCode("0;0x0;0;0");
427                 yioremoteDockwebSocketClient.sendMessage(irCodeSendMessageHandler.getIRcodeSendMessageString());
428                 logger.debug("sending heartBeat message: {}", irCodeSendMessageHandler.getIRcodeSendMessageString());
429                 break;
430             case IR_RECEIVER_ON:
431                 irReceiverMessageHandler.setOn();
432                 yioremoteDockwebSocketClient.sendMessage(irReceiverMessageHandler.getIRreceiverMessageString());
433                 logger.debug("sending IR receiver on message: {}",
434                         irReceiverMessageHandler.getIRreceiverMessageString());
435                 break;
436             case IR_RECEIVER_OFF:
437                 irReceiverMessageHandler.setOff();
438                 yioremoteDockwebSocketClient.sendMessage(irReceiverMessageHandler.getIRreceiverMessageString());
439                 logger.debug("sending IR receiver on message: {}",
440                         irReceiverMessageHandler.getIRreceiverMessageString());
441                 break;
442             case IR_SEND:
443                 irCodeSendHandler.setCode(messagePayload);
444                 yioremoteDockwebSocketClient.sendMessage(irCodeSendMessageHandler.getIRcodeSendMessageString());
445                 logger.debug("sending heartBeat message: {}", irCodeSendMessageHandler.getIRcodeSendMessageString());
446                 break;
447         }
448     }
449 }