]> git.basschouten.com Git - openhab-addons.git/blob
e446b86751315991cc6b5e8392740236beed1382
[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.dlinksmarthome.internal.motionsensor;
14
15 import java.time.LocalDateTime;
16 import java.time.temporal.ChronoUnit;
17 import java.util.concurrent.ScheduledExecutorService;
18 import java.util.concurrent.ScheduledFuture;
19 import java.util.concurrent.TimeUnit;
20
21 import javax.xml.soap.MessageFactory;
22 import javax.xml.soap.MimeHeaders;
23 import javax.xml.soap.SOAPBody;
24 import javax.xml.soap.SOAPElement;
25 import javax.xml.soap.SOAPException;
26 import javax.xml.soap.SOAPMessage;
27
28 import org.openhab.binding.dlinksmarthome.internal.DLinkHNAPCommunication;
29 import org.slf4j.Logger;
30 import org.slf4j.LoggerFactory;
31 import org.w3c.dom.Document;
32 import org.w3c.dom.Node;
33
34 /**
35  * The {@link DLinkMotionSensorCommunication} is responsible for communicating with a DCH-S150
36  * motion sensor.
37  *
38  * Motion is detected by polling the last detection time via the HNAP interface.
39  *
40  * Reverse engineered from Login.html and soapclient.js retrieved from the device.
41  *
42  * @author Mike Major - Initial contribution
43  */
44 public class DLinkMotionSensorCommunication extends DLinkHNAPCommunication {
45
46     // SOAP actions
47     private static final String DETECTION_ACTION = "\"http://purenetworks.com/HNAP1/GetLatestDetection\"";
48     private static final String REBOOT_ACTION = "\"http://purenetworks.com/HNAP1/Reboot\"";
49
50     private static final int DETECT_TIMEOUT_MS = 5000;
51     private static final int DETECT_POLL_S = 1;
52
53     private static final int REBOOT_TIMEOUT_MS = 60000;
54     private static final int REBOOT_WAIT_S = 35;
55
56     /**
57      * Indicates the device status
58      *
59      */
60     public enum DeviceStatus {
61         /**
62          * Starting communication with device
63          */
64         INITIALISING,
65         /**
66          * Successfully communicated with device
67          */
68         ONLINE,
69         /**
70          * Problem communicating with device
71          */
72         COMMUNICATION_ERROR,
73         /**
74          * Device is being rebooted
75          */
76         REBOOTING,
77         /**
78          * Internal error
79          */
80         INTERNAL_ERROR,
81         /**
82          * Error due to unsupported firmware
83          */
84         UNSUPPORTED_FIRMWARE,
85         /**
86          * Error due to invalid pin code
87          */
88         INVALID_PIN
89     }
90
91     /**
92      * Use to log connection issues
93      */
94     private final Logger logger = LoggerFactory.getLogger(DLinkMotionSensorCommunication.class);
95
96     private final DLinkMotionSensorListener listener;
97     private final ScheduledExecutorService scheduler;
98
99     private int rebootHour;
100
101     private SOAPMessage detectionAction;
102     private SOAPMessage rebootAction;
103
104     private boolean loginSuccess;
105     private boolean detectSuccess;
106     private boolean rebootSuccess;
107
108     private long prevDetection;
109     private long lastDetection;
110
111     private ScheduledFuture<?> detectFuture;
112     private ScheduledFuture<?> rebootFuture;
113
114     private boolean rebootRequired = false;
115     private DeviceStatus status = DeviceStatus.INITIALISING;
116
117     /**
118      * Inform the listener if motion is detected
119      */
120     private final Runnable detect = new Runnable() {
121         @Override
122         public void run() {
123             final DeviceStatus currentStatus = status;
124             final boolean tryReboot = rebootRequired;
125
126             switch (status) {
127                 case INITIALISING:
128                 case REBOOTING:
129                     loginSuccess = false;
130                     // FALL-THROUGH
131                 case COMMUNICATION_ERROR:
132                 case ONLINE:
133                     if (!tryReboot) {
134                         if (!loginSuccess) {
135                             login(detectionAction, DETECT_TIMEOUT_MS);
136                         }
137
138                         if (!getLastDetection(false)) {
139                             // Try login again in case the session has timed out
140                             login(detectionAction, DETECT_TIMEOUT_MS);
141                             getLastDetection(true);
142                         }
143                     } else {
144                         login(rebootAction, REBOOT_TIMEOUT_MS);
145                         reboot();
146                     }
147                     break;
148                 default:
149                     break;
150             }
151
152             if (tryReboot) {
153                 if (rebootSuccess) {
154                     rebootRequired = false;
155                     status = DeviceStatus.REBOOTING;
156                     detectFuture.cancel(false);
157                     detectFuture = scheduler.scheduleWithFixedDelay(detect, REBOOT_WAIT_S, DETECT_POLL_S,
158                             TimeUnit.SECONDS);
159                 }
160             } else if (loginSuccess && detectSuccess) {
161                 status = DeviceStatus.ONLINE;
162                 if (currentStatus != DeviceStatus.ONLINE) {
163                     // Ignore old detections
164                     prevDetection = lastDetection;
165                 }
166
167                 if (lastDetection != prevDetection) {
168                     listener.motionDetected();
169                 }
170             }
171
172             if (currentStatus != status) {
173                 listener.sensorStatus(status);
174             }
175         }
176     };
177
178     /**
179      * Reboot the device
180      */
181     private final Runnable reboot = new Runnable() {
182         @Override
183         public void run() {
184             rebootRequired = true;
185             rebootFuture = scheduler.schedule(reboot, getNextRebootTime(), TimeUnit.MILLISECONDS);
186         }
187     };
188
189     public DLinkMotionSensorCommunication(final DLinkMotionSensorConfig config,
190             final DLinkMotionSensorListener listener, final ScheduledExecutorService scheduler) {
191         super(config.ipAddress, config.pin);
192         this.listener = listener;
193         this.scheduler = scheduler;
194         this.rebootHour = config.rebootHour;
195
196         if (getHNAPStatus() == HNAPStatus.INTERNAL_ERROR) {
197             status = DeviceStatus.INTERNAL_ERROR;
198         }
199
200         try {
201             final MessageFactory messageFactory = MessageFactory.newInstance();
202             detectionAction = messageFactory.createMessage();
203             rebootAction = messageFactory.createMessage();
204
205             buildDetectionAction();
206             buildRebootAction();
207
208         } catch (final SOAPException e) {
209             logger.debug("DLinkMotionSensorCommunication - Internal error", e);
210             status = DeviceStatus.INTERNAL_ERROR;
211         }
212
213         detectFuture = scheduler.scheduleWithFixedDelay(detect, 0, DETECT_POLL_S, TimeUnit.SECONDS);
214         rebootFuture = scheduler.schedule(reboot, getNextRebootTime(), TimeUnit.MILLISECONDS);
215     }
216
217     /**
218      * Stop communicating with the device
219      */
220     @Override
221     public void dispose() {
222         detectFuture.cancel(true);
223         rebootFuture.cancel(true);
224         super.dispose();
225     }
226
227     /**
228      * This is the SOAP message used to retrieve the last detection time. This message will
229      * only receive a successful response after the login process has been completed and the
230      * authentication data has been set.
231      *
232      * @throws SOAPException
233      */
234     private void buildDetectionAction() throws SOAPException {
235         detectionAction.getSOAPHeader().detachNode();
236         final SOAPBody soapBody = detectionAction.getSOAPBody();
237         final SOAPElement soapBodyElem = soapBody.addChildElement("GetLatestDetection", "", HNAP_XMLNS);
238         soapBodyElem.addChildElement("ModuleID").addTextNode("1");
239
240         final MimeHeaders headers = detectionAction.getMimeHeaders();
241         headers.addHeader(SOAPACTION, DETECTION_ACTION);
242     }
243
244     /**
245      * This is the SOAP message used to reboot the device. This message will
246      * only receive a successful response after the login process has been completed and the
247      * authentication data has been set. Device needs rebooting as it eventually becomes
248      * unresponsive due to cloud services being shutdown.
249      *
250      * @throws SOAPException
251      */
252     private void buildRebootAction() throws SOAPException {
253         rebootAction.getSOAPHeader().detachNode();
254         final SOAPBody soapBody = rebootAction.getSOAPBody();
255         soapBody.addChildElement("Reboot", "", HNAP_XMLNS);
256
257         final MimeHeaders headers = rebootAction.getMimeHeaders();
258         headers.addHeader(SOAPACTION, REBOOT_ACTION);
259     }
260
261     /**
262      * Get the number of milliseconds to the next reboot time
263      *
264      * @return Time in ms to next reboot
265      */
266     private long getNextRebootTime() {
267         final LocalDateTime now = LocalDateTime.now();
268         LocalDateTime nextReboot = LocalDateTime.of(now.getYear(), now.getMonth(), now.getDayOfMonth(), rebootHour, 0,
269                 0);
270
271         if (!nextReboot.isAfter(now)) {
272             nextReboot = nextReboot.plusDays(1);
273         }
274
275         return now.until(nextReboot, ChronoUnit.MILLIS);
276     }
277
278     /**
279      * Output unexpected responses to the debug log and sets the FIRMWARE error.
280      *
281      * @param message
282      * @param soapResponse
283      */
284     private void unexpectedResult(final String message, final Document soapResponse) {
285         logUnexpectedResult(message, soapResponse);
286
287         // Best guess when receiving unexpected responses
288         status = DeviceStatus.UNSUPPORTED_FIRMWARE;
289     }
290
291     /**
292      * Sends the two login messages and sets the authentication header for the action
293      * message.
294      *
295      * @param action
296      * @param timeout
297      */
298     private void login(final SOAPMessage action, final int timeout) {
299         loginSuccess = false;
300
301         login(timeout);
302         setAuthenticationHeaders(action);
303
304         switch (getHNAPStatus()) {
305             case LOGGED_IN:
306                 loginSuccess = true;
307                 break;
308             case COMMUNICATION_ERROR:
309                 status = DeviceStatus.COMMUNICATION_ERROR;
310                 break;
311             case INVALID_PIN:
312                 status = DeviceStatus.INVALID_PIN;
313                 break;
314             case INTERNAL_ERROR:
315                 status = DeviceStatus.INTERNAL_ERROR;
316                 break;
317             case UNSUPPORTED_FIRMWARE:
318                 status = DeviceStatus.UNSUPPORTED_FIRMWARE;
319                 break;
320             case INITIALISED:
321             default:
322                 break;
323         }
324     }
325
326     /**
327      * Sends the detection message
328      *
329      * @param isRetry - Has this been called as a result of a login retry
330      * @return true, if the last detection time was successfully retrieved, otherwise false
331      */
332     private boolean getLastDetection(final boolean isRetry) {
333         detectSuccess = false;
334
335         if (loginSuccess) {
336             try {
337                 final Document soapResponse = sendReceive(detectionAction, DETECT_TIMEOUT_MS);
338
339                 final Node result = soapResponse.getElementsByTagName("GetLatestDetectionResult").item(0);
340
341                 if (result != null) {
342                     if (OK.equals(result.getTextContent())) {
343                         final Node timeNode = soapResponse.getElementsByTagName("LatestDetectTime").item(0);
344
345                         if (timeNode != null) {
346                             prevDetection = lastDetection;
347                             lastDetection = Long.valueOf(timeNode.getTextContent());
348                             detectSuccess = true;
349                         } else {
350                             unexpectedResult("getLastDetection - Unexpected response", soapResponse);
351                         }
352                     } else if (isRetry) {
353                         unexpectedResult("getLastDetection - Unexpected response", soapResponse);
354                     }
355                 } else {
356                     unexpectedResult("getLastDetection - Unexpected response", soapResponse);
357                 }
358             } catch (final InterruptedException e) {
359                 status = DeviceStatus.COMMUNICATION_ERROR;
360                 Thread.currentThread().interrupt();
361             } catch (final Exception e) {
362                 // Assume there has been some problem trying to send one of the messages
363                 if (status != DeviceStatus.COMMUNICATION_ERROR) {
364                     logger.debug("getLastDetection - Communication error", e);
365                     status = DeviceStatus.COMMUNICATION_ERROR;
366                 }
367             }
368         }
369
370         return detectSuccess;
371     }
372
373     /**
374      * Sends the reboot message
375      *
376      */
377     private void reboot() {
378         rebootSuccess = false;
379
380         if (loginSuccess) {
381             try {
382                 final Document soapResponse = sendReceive(rebootAction, REBOOT_TIMEOUT_MS);
383
384                 final Node result = soapResponse.getElementsByTagName("RebootResult").item(0);
385
386                 if (result != null && OK.equals(result.getTextContent())) {
387                     rebootSuccess = true;
388                 } else {
389                     unexpectedResult("reboot - Unexpected response", soapResponse);
390                 }
391             } catch (final Exception e) {
392                 // Assume there has been some problem trying to send one of the messages
393                 if (status != DeviceStatus.COMMUNICATION_ERROR) {
394                     logger.debug("getLastDetection - Communication error", e);
395                     status = DeviceStatus.COMMUNICATION_ERROR;
396                 }
397             }
398         }
399     }
400 }