]> git.basschouten.com Git - openhab-addons.git/blob
ca90a66523fb3052382bea448417dbf554b58f5a
[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.util.concurrent.ScheduledExecutorService;
16 import java.util.concurrent.ScheduledFuture;
17 import java.util.concurrent.TimeUnit;
18
19 import javax.xml.soap.MessageFactory;
20 import javax.xml.soap.MimeHeaders;
21 import javax.xml.soap.SOAPBody;
22 import javax.xml.soap.SOAPElement;
23 import javax.xml.soap.SOAPException;
24 import javax.xml.soap.SOAPMessage;
25
26 import org.openhab.binding.dlinksmarthome.internal.DLinkHNAPCommunication;
27 import org.slf4j.Logger;
28 import org.slf4j.LoggerFactory;
29 import org.w3c.dom.Document;
30 import org.w3c.dom.Node;
31
32 /**
33  * The {@link DLinkMotionSensorCommunication} is responsible for communicating with a DCH-S150
34  * motion sensor.
35  *
36  * Motion is detected by polling the last detection time via the HNAP interface.
37  *
38  * Reverse engineered from Login.html and soapclient.js retrieved from the device.
39  *
40  * @author Mike Major - Initial contribution
41  */
42 public class DLinkMotionSensorCommunication extends DLinkHNAPCommunication {
43
44     // SOAP actions
45     private static final String DETECTION_ACTION = "\"http://purenetworks.com/HNAP1/GetLatestDetection\"";
46
47     private static final int DETECT_TIMEOUT_MS = 5000;
48     private static final int DETECT_POLL_S = 1;
49
50     /**
51      * Indicates the device status
52      *
53      */
54     public enum DeviceStatus {
55         /**
56          * Starting communication with device
57          */
58         INITIALISING,
59         /**
60          * Successfully communicated with device
61          */
62         ONLINE,
63         /**
64          * Problem communicating with device
65          */
66         COMMUNICATION_ERROR,
67         /**
68          * Internal error
69          */
70         INTERNAL_ERROR,
71         /**
72          * Error due to unsupported firmware
73          */
74         UNSUPPORTED_FIRMWARE,
75         /**
76          * Error due to invalid pin code
77          */
78         INVALID_PIN
79     }
80
81     /**
82      * Use to log connection issues
83      */
84     private final Logger logger = LoggerFactory.getLogger(DLinkMotionSensorCommunication.class);
85
86     private final DLinkMotionSensorListener listener;
87
88     private SOAPMessage detectionAction;
89
90     private boolean loginSuccess;
91     private boolean detectSuccess;
92
93     private long prevDetection;
94     private long lastDetection;
95
96     private final ScheduledFuture<?> detectFuture;
97
98     private boolean online = true;
99     private DeviceStatus status = DeviceStatus.INITIALISING;
100
101     /**
102      * Inform the listener if motion is detected
103      */
104     private final Runnable detect = new Runnable() {
105         @Override
106         public void run() {
107             boolean updateStatus = false;
108
109             switch (status) {
110                 case INITIALISING:
111                     online = false;
112                     updateStatus = true;
113                     // FALL-THROUGH
114                 case COMMUNICATION_ERROR:
115                 case ONLINE:
116                     if (!loginSuccess) {
117                         login(detectionAction, DETECT_TIMEOUT_MS);
118                     }
119
120                     if (!getLastDetection(false)) {
121                         // Try login again in case the session has timed out
122                         login(detectionAction, DETECT_TIMEOUT_MS);
123                         getLastDetection(true);
124                     }
125                     break;
126                 default:
127                     break;
128             }
129
130             if (loginSuccess && detectSuccess) {
131                 status = DeviceStatus.ONLINE;
132                 if (!online) {
133                     online = true;
134                     listener.sensorStatus(status);
135
136                     // Ignore old detections
137                     prevDetection = lastDetection;
138                 }
139
140                 if (lastDetection != prevDetection) {
141                     listener.motionDetected();
142                 }
143             } else {
144                 if (online || updateStatus) {
145                     online = false;
146                     listener.sensorStatus(status);
147                 }
148             }
149         }
150     };
151
152     public DLinkMotionSensorCommunication(final DLinkMotionSensorConfig config,
153             final DLinkMotionSensorListener listener, final ScheduledExecutorService scheduler) {
154         super(config.ipAddress, config.pin);
155         this.listener = listener;
156
157         if (getHNAPStatus() == HNAPStatus.INTERNAL_ERROR) {
158             status = DeviceStatus.INTERNAL_ERROR;
159         }
160
161         try {
162             final MessageFactory messageFactory = MessageFactory.newInstance();
163             detectionAction = messageFactory.createMessage();
164
165             buildDetectionAction();
166
167         } catch (final SOAPException e) {
168             logger.debug("DLinkMotionSensorCommunication - Internal error", e);
169             status = DeviceStatus.INTERNAL_ERROR;
170         }
171
172         detectFuture = scheduler.scheduleWithFixedDelay(detect, 0, DETECT_POLL_S, TimeUnit.SECONDS);
173     }
174
175     /**
176      * Stop communicating with the device
177      */
178     @Override
179     public void dispose() {
180         detectFuture.cancel(true);
181         super.dispose();
182     }
183
184     /**
185      * This is the SOAP message used to retrieve the last detection time. This message will
186      * only receive a successful response after the login process has been completed and the
187      * authentication data has been set.
188      *
189      * @throws SOAPException
190      */
191     private void buildDetectionAction() throws SOAPException {
192         detectionAction.getSOAPHeader().detachNode();
193         final SOAPBody soapBody = detectionAction.getSOAPBody();
194         final SOAPElement soapBodyElem = soapBody.addChildElement("GetLatestDetection", "", HNAP_XMLNS);
195         soapBodyElem.addChildElement("ModuleID").addTextNode("1");
196
197         final MimeHeaders headers = detectionAction.getMimeHeaders();
198         headers.addHeader(SOAPACTION, DETECTION_ACTION);
199     }
200
201     /**
202      * Output unexpected responses to the debug log and sets the FIRMWARE error.
203      *
204      * @param message
205      * @param soapResponse
206      */
207     private void unexpectedResult(final String message, final Document soapResponse) {
208         logUnexpectedResult(message, soapResponse);
209
210         // Best guess when receiving unexpected responses
211         status = DeviceStatus.UNSUPPORTED_FIRMWARE;
212     }
213
214     /**
215      * Sends the two login messages and sets the authentication header for the action
216      * message.
217      *
218      * @param action
219      * @param timeout
220      */
221     private void login(final SOAPMessage action, final int timeout) {
222         loginSuccess = false;
223
224         login(timeout);
225         setAuthenticationHeaders(action);
226
227         switch (getHNAPStatus()) {
228             case LOGGED_IN:
229                 loginSuccess = true;
230                 break;
231             case COMMUNICATION_ERROR:
232                 status = DeviceStatus.COMMUNICATION_ERROR;
233                 break;
234             case INVALID_PIN:
235                 status = DeviceStatus.INVALID_PIN;
236                 break;
237             case INTERNAL_ERROR:
238                 status = DeviceStatus.INTERNAL_ERROR;
239                 break;
240             case UNSUPPORTED_FIRMWARE:
241                 status = DeviceStatus.UNSUPPORTED_FIRMWARE;
242                 break;
243             case INITIALISED:
244             default:
245                 break;
246         }
247     }
248
249     /**
250      * Sends the detection message
251      *
252      * @param isRetry - Has this been called as a result of a login retry
253      * @return true, if the last detection time was successfully retrieved, otherwise false
254      */
255     private boolean getLastDetection(final boolean isRetry) {
256         detectSuccess = false;
257
258         if (loginSuccess) {
259             try {
260                 final Document soapResponse = sendReceive(detectionAction, DETECT_TIMEOUT_MS);
261
262                 final Node result = soapResponse.getElementsByTagName("GetLatestDetectionResult").item(0);
263
264                 if (result != null) {
265                     if (OK.equals(result.getTextContent())) {
266                         final Node timeNode = soapResponse.getElementsByTagName("LatestDetectTime").item(0);
267
268                         if (timeNode != null) {
269                             prevDetection = lastDetection;
270                             lastDetection = Long.valueOf(timeNode.getTextContent());
271                             detectSuccess = true;
272                         } else {
273                             unexpectedResult("getLastDetection - Unexpected response", soapResponse);
274                         }
275                     } else if (isRetry) {
276                         unexpectedResult("getLastDetection - Unexpected response", soapResponse);
277                     }
278                 } else {
279                     unexpectedResult("getLastDetection - Unexpected response", soapResponse);
280                 }
281             } catch (final InterruptedException e) {
282                 status = DeviceStatus.COMMUNICATION_ERROR;
283                 Thread.currentThread().interrupt();
284             } catch (final Exception e) {
285                 // Assume there has been some problem trying to send one of the messages
286                 if (status != DeviceStatus.COMMUNICATION_ERROR) {
287                     logger.debug("getLastDetection - Communication error", e);
288                     status = DeviceStatus.COMMUNICATION_ERROR;
289                 }
290             }
291         }
292
293         return detectSuccess;
294     }
295 }