2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.dlinksmarthome.internal.motionsensor;
15 import java.util.concurrent.ScheduledExecutorService;
16 import java.util.concurrent.ScheduledFuture;
17 import java.util.concurrent.TimeUnit;
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;
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;
33 * The {@link DLinkMotionSensorCommunication} is responsible for communicating with a DCH-S150
36 * Motion is detected by polling the last detection time via the HNAP interface.
38 * Reverse engineered from Login.html and soapclient.js retrieved from the device.
40 * @author Mike Major - Initial contribution
42 public class DLinkMotionSensorCommunication extends DLinkHNAPCommunication {
45 private static final String DETECTION_ACTION = "\"http://purenetworks.com/HNAP1/GetLatestDetection\"";
47 private static final int DETECT_TIMEOUT_MS = 5000;
48 private static final int DETECT_POLL_S = 1;
51 * Indicates the device status
54 public enum DeviceStatus {
56 * Starting communication with device
60 * Successfully communicated with device
64 * Problem communicating with device
72 * Error due to unsupported firmware
76 * Error due to invalid pin code
82 * Use to log connection issues
84 private final Logger logger = LoggerFactory.getLogger(DLinkMotionSensorCommunication.class);
86 private final DLinkMotionSensorListener listener;
88 private SOAPMessage detectionAction;
90 private boolean loginSuccess;
91 private boolean detectSuccess;
93 private long prevDetection;
94 private long lastDetection;
96 private final ScheduledFuture<?> detectFuture;
98 private boolean online = true;
99 private DeviceStatus status = DeviceStatus.INITIALISING;
102 * Inform the listener if motion is detected
104 private final Runnable detect = new Runnable() {
107 boolean updateStatus = false;
114 case COMMUNICATION_ERROR:
117 login(detectionAction, DETECT_TIMEOUT_MS);
120 if (!getLastDetection(false)) {
121 // Try login again in case the session has timed out
122 login(detectionAction, DETECT_TIMEOUT_MS);
123 getLastDetection(true);
130 if (loginSuccess && detectSuccess) {
131 status = DeviceStatus.ONLINE;
134 listener.sensorStatus(status);
136 // Ignore old detections
137 prevDetection = lastDetection;
140 if (lastDetection != prevDetection) {
141 listener.motionDetected();
144 if (online || updateStatus) {
146 listener.sensorStatus(status);
152 public DLinkMotionSensorCommunication(final DLinkMotionSensorConfig config,
153 final DLinkMotionSensorListener listener, final ScheduledExecutorService scheduler) {
154 super(config.ipAddress, config.pin);
155 this.listener = listener;
157 if (getHNAPStatus() == HNAPStatus.INTERNAL_ERROR) {
158 status = DeviceStatus.INTERNAL_ERROR;
162 final MessageFactory messageFactory = MessageFactory.newInstance();
163 detectionAction = messageFactory.createMessage();
165 buildDetectionAction();
167 } catch (final SOAPException e) {
168 logger.debug("DLinkMotionSensorCommunication - Internal error", e);
169 status = DeviceStatus.INTERNAL_ERROR;
172 detectFuture = scheduler.scheduleWithFixedDelay(detect, 0, DETECT_POLL_S, TimeUnit.SECONDS);
176 * Stop communicating with the device
179 public void dispose() {
180 detectFuture.cancel(true);
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.
189 * @throws SOAPException
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");
197 final MimeHeaders headers = detectionAction.getMimeHeaders();
198 headers.addHeader(SOAPACTION, DETECTION_ACTION);
202 * Output unexpected responses to the debug log and sets the FIRMWARE error.
205 * @param soapResponse
207 private void unexpectedResult(final String message, final Document soapResponse) {
208 logUnexpectedResult(message, soapResponse);
210 // Best guess when receiving unexpected responses
211 status = DeviceStatus.UNSUPPORTED_FIRMWARE;
215 * Sends the two login messages and sets the authentication header for the action
221 private void login(final SOAPMessage action, final int timeout) {
222 loginSuccess = false;
225 setAuthenticationHeaders(action);
227 switch (getHNAPStatus()) {
231 case COMMUNICATION_ERROR:
232 status = DeviceStatus.COMMUNICATION_ERROR;
235 status = DeviceStatus.INVALID_PIN;
238 status = DeviceStatus.INTERNAL_ERROR;
240 case UNSUPPORTED_FIRMWARE:
241 status = DeviceStatus.UNSUPPORTED_FIRMWARE;
250 * Sends the detection message
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
255 private boolean getLastDetection(final boolean isRetry) {
256 detectSuccess = false;
260 final Document soapResponse = sendReceive(detectionAction, DETECT_TIMEOUT_MS);
262 final Node result = soapResponse.getElementsByTagName("GetLatestDetectionResult").item(0);
264 if (result != null) {
265 if (OK.equals(result.getTextContent())) {
266 final Node timeNode = soapResponse.getElementsByTagName("LatestDetectTime").item(0);
268 if (timeNode != null) {
269 prevDetection = lastDetection;
270 lastDetection = Long.valueOf(timeNode.getTextContent());
271 detectSuccess = true;
273 unexpectedResult("getLastDetection - Unexpected response", soapResponse);
275 } else if (isRetry) {
276 unexpectedResult("getLastDetection - Unexpected response", soapResponse);
279 unexpectedResult("getLastDetection - Unexpected response", soapResponse);
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;
293 return detectSuccess;