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.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;
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;
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;
35 * The {@link DLinkMotionSensorCommunication} is responsible for communicating with a DCH-S150
38 * Motion is detected by polling the last detection time via the HNAP interface.
40 * Reverse engineered from Login.html and soapclient.js retrieved from the device.
42 * @author Mike Major - Initial contribution
44 public class DLinkMotionSensorCommunication extends DLinkHNAPCommunication {
47 private static final String DETECTION_ACTION = "\"http://purenetworks.com/HNAP1/GetLatestDetection\"";
48 private static final String REBOOT_ACTION = "\"http://purenetworks.com/HNAP1/Reboot\"";
50 private static final int DETECT_TIMEOUT_MS = 5000;
51 private static final int DETECT_POLL_S = 1;
53 private static final int REBOOT_TIMEOUT_MS = 60000;
54 private static final int REBOOT_WAIT_S = 35;
57 * Indicates the device status
60 public enum DeviceStatus {
62 * Starting communication with device
66 * Successfully communicated with device
70 * Problem communicating with device
74 * Device is being rebooted
82 * Error due to unsupported firmware
86 * Error due to invalid pin code
92 * Use to log connection issues
94 private final Logger logger = LoggerFactory.getLogger(DLinkMotionSensorCommunication.class);
96 private final DLinkMotionSensorListener listener;
97 private final ScheduledExecutorService scheduler;
99 private int rebootHour;
101 private SOAPMessage detectionAction;
102 private SOAPMessage rebootAction;
104 private boolean loginSuccess;
105 private boolean detectSuccess;
106 private boolean rebootSuccess;
108 private long prevDetection;
109 private long lastDetection;
111 private ScheduledFuture<?> detectFuture;
112 private ScheduledFuture<?> rebootFuture;
114 private boolean rebootRequired = false;
115 private DeviceStatus status = DeviceStatus.INITIALISING;
118 * Inform the listener if motion is detected
120 private final Runnable detect = new Runnable() {
123 final DeviceStatus currentStatus = status;
124 final boolean tryReboot = rebootRequired;
129 loginSuccess = false;
131 case COMMUNICATION_ERROR:
135 login(detectionAction, DETECT_TIMEOUT_MS);
138 if (!getLastDetection(false)) {
139 // Try login again in case the session has timed out
140 login(detectionAction, DETECT_TIMEOUT_MS);
141 getLastDetection(true);
144 login(rebootAction, REBOOT_TIMEOUT_MS);
154 rebootRequired = false;
155 status = DeviceStatus.REBOOTING;
156 detectFuture.cancel(false);
157 detectFuture = scheduler.scheduleWithFixedDelay(detect, REBOOT_WAIT_S, DETECT_POLL_S,
160 } else if (loginSuccess && detectSuccess) {
161 status = DeviceStatus.ONLINE;
162 if (currentStatus != DeviceStatus.ONLINE) {
163 // Ignore old detections
164 prevDetection = lastDetection;
167 if (lastDetection != prevDetection) {
168 listener.motionDetected();
172 if (currentStatus != status) {
173 listener.sensorStatus(status);
181 private final Runnable reboot = new Runnable() {
184 rebootRequired = true;
185 rebootFuture = scheduler.schedule(reboot, getNextRebootTime(), TimeUnit.MILLISECONDS);
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;
196 if (getHNAPStatus() == HNAPStatus.INTERNAL_ERROR) {
197 status = DeviceStatus.INTERNAL_ERROR;
201 final MessageFactory messageFactory = MessageFactory.newInstance();
202 detectionAction = messageFactory.createMessage();
203 rebootAction = messageFactory.createMessage();
205 buildDetectionAction();
208 } catch (final SOAPException e) {
209 logger.debug("DLinkMotionSensorCommunication - Internal error", e);
210 status = DeviceStatus.INTERNAL_ERROR;
213 detectFuture = scheduler.scheduleWithFixedDelay(detect, 0, DETECT_POLL_S, TimeUnit.SECONDS);
214 rebootFuture = scheduler.schedule(reboot, getNextRebootTime(), TimeUnit.MILLISECONDS);
218 * Stop communicating with the device
221 public void dispose() {
222 detectFuture.cancel(true);
223 rebootFuture.cancel(true);
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.
232 * @throws SOAPException
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");
240 final MimeHeaders headers = detectionAction.getMimeHeaders();
241 headers.addHeader(SOAPACTION, DETECTION_ACTION);
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.
250 * @throws SOAPException
252 private void buildRebootAction() throws SOAPException {
253 rebootAction.getSOAPHeader().detachNode();
254 final SOAPBody soapBody = rebootAction.getSOAPBody();
255 soapBody.addChildElement("Reboot", "", HNAP_XMLNS);
257 final MimeHeaders headers = rebootAction.getMimeHeaders();
258 headers.addHeader(SOAPACTION, REBOOT_ACTION);
262 * Get the number of milliseconds to the next reboot time
264 * @return Time in ms to next reboot
266 private long getNextRebootTime() {
267 final LocalDateTime now = LocalDateTime.now();
268 LocalDateTime nextReboot = LocalDateTime.of(now.getYear(), now.getMonth(), now.getDayOfMonth(), rebootHour, 0,
271 if (!nextReboot.isAfter(now)) {
272 nextReboot = nextReboot.plusDays(1);
275 return now.until(nextReboot, ChronoUnit.MILLIS);
279 * Output unexpected responses to the debug log and sets the FIRMWARE error.
282 * @param soapResponse
284 private void unexpectedResult(final String message, final Document soapResponse) {
285 logUnexpectedResult(message, soapResponse);
287 // Best guess when receiving unexpected responses
288 status = DeviceStatus.UNSUPPORTED_FIRMWARE;
292 * Sends the two login messages and sets the authentication header for the action
298 private void login(final SOAPMessage action, final int timeout) {
299 loginSuccess = false;
302 setAuthenticationHeaders(action);
304 switch (getHNAPStatus()) {
308 case COMMUNICATION_ERROR:
309 status = DeviceStatus.COMMUNICATION_ERROR;
312 status = DeviceStatus.INVALID_PIN;
315 status = DeviceStatus.INTERNAL_ERROR;
317 case UNSUPPORTED_FIRMWARE:
318 status = DeviceStatus.UNSUPPORTED_FIRMWARE;
327 * Sends the detection message
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
332 private boolean getLastDetection(final boolean isRetry) {
333 detectSuccess = false;
337 final Document soapResponse = sendReceive(detectionAction, DETECT_TIMEOUT_MS);
339 final Node result = soapResponse.getElementsByTagName("GetLatestDetectionResult").item(0);
341 if (result != null) {
342 if (OK.equals(result.getTextContent())) {
343 final Node timeNode = soapResponse.getElementsByTagName("LatestDetectTime").item(0);
345 if (timeNode != null) {
346 prevDetection = lastDetection;
347 lastDetection = Long.valueOf(timeNode.getTextContent());
348 detectSuccess = true;
350 unexpectedResult("getLastDetection - Unexpected response", soapResponse);
352 } else if (isRetry) {
353 unexpectedResult("getLastDetection - Unexpected response", soapResponse);
356 unexpectedResult("getLastDetection - Unexpected response", soapResponse);
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;
370 return detectSuccess;
374 * Sends the reboot message
377 private void reboot() {
378 rebootSuccess = false;
382 final Document soapResponse = sendReceive(rebootAction, REBOOT_TIMEOUT_MS);
384 final Node result = soapResponse.getElementsByTagName("RebootResult").item(0);
386 if (result != null && OK.equals(result.getTextContent())) {
387 rebootSuccess = true;
389 unexpectedResult("reboot - Unexpected response", soapResponse);
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;