added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.dlinksmarthome-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-dlinksmarthome" description="D-Link Smart Home Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<feature>openhab-transport-mdns</feature>
<feature dependency="true">openhab.tp-jaxws</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.dlinksmarthome/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,466 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.dlinksmarthome.internal;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.StringWriter;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Iterator;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.soap.MessageFactory;
import javax.xml.soap.MimeHeader;
import javax.xml.soap.MimeHeaders;
import javax.xml.soap.SOAPBody;
import javax.xml.soap.SOAPElement;
import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPMessage;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.BytesContentProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;
/**
* The {@link DLinkHNAPCommunication} is responsible for communicating with D-Link
* Smart Home devices using the HNAP interface.
*
* This abstract class handles login and authentication which is common between devices.
*
* Reverse engineered from Login.html and soapclient.js retrieved from the device.
*
* @author Mike Major - Initial contribution
*/
public abstract class DLinkHNAPCommunication {
// SOAP actions
private static final String LOGIN_ACTION = "\"http://purenetworks.com/HNAP1/LOGIN\"";
// Strings used more than once
private static final String LOGIN = "LOGIN";
private static final String ACTION = "Action";
private static final String USERNAME = "Username";
private static final String LOGINPASSWORD = "LoginPassword";
private static final String CAPTCHA = "Captcha";
private static final String ADMIN = "Admin";
private static final String LOGINRESULT = "LOGINResult";
private static final String COOKIE = "Cookie";
/**
* HNAP XMLNS
*/
protected static final String HNAP_XMLNS = "http://purenetworks.com/HNAP1";
/**
* The SOAP action HTML header
*/
protected static final String SOAPACTION = "SOAPAction";
/**
* OK represents a successful action
*/
protected static final String OK = "OK";
/**
* Use to log connection issues
*/
private final Logger logger = LoggerFactory.getLogger(DLinkHNAPCommunication.class);
private URI uri;
private final HttpClient httpClient;
private final String pin;
private String privateKey;
private DocumentBuilder parser;
private SOAPMessage requestAction;
private SOAPMessage loginAction;
private HNAPStatus status = HNAPStatus.INITIALISED;
/**
* Indicates the status of the HNAP interface
*
*/
protected enum HNAPStatus {
/**
* Ready to start communication with device
*/
INITIALISED,
/**
* Successfully logged in to device
*/
LOGGED_IN,
/**
* Problem communicating with device
*/
COMMUNICATION_ERROR,
/**
* Internal error
*/
INTERNAL_ERROR,
/**
* Error due to unsupported firmware
*/
UNSUPPORTED_FIRMWARE,
/**
* Error due to invalid pin code
*/
INVALID_PIN
}
/**
* Use {@link #getHNAPStatus()} to determine the status of the HNAP connection
* after construction.
*
* @param ipAddress
* @param pin
*/
public DLinkHNAPCommunication(final String ipAddress, final String pin) {
this.pin = pin;
httpClient = new HttpClient();
try {
uri = new URI("http://" + ipAddress + "/HNAP1");
httpClient.start();
parser = DocumentBuilderFactory.newInstance().newDocumentBuilder();
final MessageFactory messageFactory = MessageFactory.newInstance();
requestAction = messageFactory.createMessage();
loginAction = messageFactory.createMessage();
buildRequestAction();
buildLoginAction();
} catch (final SOAPException e) {
logger.debug("DLinkHNAPCommunication - Internal error", e);
status = HNAPStatus.INTERNAL_ERROR;
} catch (final URISyntaxException e) {
logger.debug("DLinkHNAPCommunication - Internal error", e);
status = HNAPStatus.INTERNAL_ERROR;
} catch (final ParserConfigurationException e) {
logger.debug("DLinkHNAPCommunication - Internal error", e);
status = HNAPStatus.INTERNAL_ERROR;
} catch (final Exception e) {
// Thrown by httpClient.start()
logger.debug("DLinkHNAPCommunication - Internal error", e);
status = HNAPStatus.INTERNAL_ERROR;
}
}
/**
* Stop communicating with the device
*/
public void dispose() {
try {
httpClient.stop();
} catch (final Exception e) {
// Ignored
}
}
/**
* This is the first SOAP message used in the login process and is used to retrieve
* the cookie, challenge and public key used for authentication.
*
* @throws SOAPException
*/
private void buildRequestAction() throws SOAPException {
requestAction.getSOAPHeader().detachNode();
final SOAPBody soapBody = requestAction.getSOAPBody();
final SOAPElement soapBodyElem = soapBody.addChildElement(LOGIN, "", HNAP_XMLNS);
soapBodyElem.addChildElement(ACTION).addTextNode("request");
soapBodyElem.addChildElement(USERNAME).addTextNode(ADMIN);
soapBodyElem.addChildElement(LOGINPASSWORD);
soapBodyElem.addChildElement(CAPTCHA);
final MimeHeaders headers = requestAction.getMimeHeaders();
headers.addHeader(SOAPACTION, LOGIN_ACTION);
requestAction.saveChanges();
}
/**
* This is the second SOAP message used in the login process and uses a password derived
* from the challenge, public key and the device's pin code.
*
* @throws SOAPException
*/
private void buildLoginAction() throws SOAPException {
loginAction.getSOAPHeader().detachNode();
final SOAPBody soapBody = loginAction.getSOAPBody();
final SOAPElement soapBodyElem = soapBody.addChildElement(LOGIN, "", HNAP_XMLNS);
soapBodyElem.addChildElement(ACTION).addTextNode("login");
soapBodyElem.addChildElement(USERNAME).addTextNode(ADMIN);
soapBodyElem.addChildElement(LOGINPASSWORD);
soapBodyElem.addChildElement(CAPTCHA);
final MimeHeaders headers = loginAction.getMimeHeaders();
headers.addHeader(SOAPACTION, LOGIN_ACTION);
}
/**
* Sets the password for the second login message based on the data received from the
* first login message. Also sets the private key used to generate the authentication header.
*
* @param challenge
* @param cookie
* @param publicKey
* @throws SOAPException
* @throws InvalidKeyException
* @throws NoSuchAlgorithmException
*/
private void setAuthenticationData(final String challenge, final String cookie, final String publicKey)
throws SOAPException, InvalidKeyException, NoSuchAlgorithmException {
final MimeHeaders loginHeaders = loginAction.getMimeHeaders();
loginHeaders.setHeader(COOKIE, "uid=" + cookie);
privateKey = hash(challenge, publicKey + pin);
final String password = hash(challenge, privateKey);
loginAction.getSOAPBody().getElementsByTagName(LOGINPASSWORD).item(0).setTextContent(password);
loginAction.saveChanges();
}
/**
* Used to hash the authentication data such as the login password and the authentication header
* for the detection message.
*
* @param data
* @param key
* @return The hashed data
* @throws NoSuchAlgorithmException
* @throws InvalidKeyException
*/
private String hash(final String data, final String key) throws NoSuchAlgorithmException, InvalidKeyException {
final Mac mac = Mac.getInstance("HMACMD5");
final SecretKeySpec sKey = new SecretKeySpec(key.getBytes(), "ASCII");
mac.init(sKey);
final byte[] bytes = mac.doFinal(data.getBytes());
final StringBuilder hashBuf = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
final String hex = Integer.toHexString(0xFF & bytes[i]).toUpperCase();
if (hex.length() == 1) {
hashBuf.append('0');
}
hashBuf.append(hex);
}
return hashBuf.toString();
}
/**
* Output unexpected responses to the debug log and sets the FIRMWARE error.
*
* @param message
* @param soapResponse
*/
private void unexpectedResult(final String message, final Document soapResponse) {
logUnexpectedResult(message, soapResponse);
// Best guess when receiving unexpected responses
status = HNAPStatus.UNSUPPORTED_FIRMWARE;
}
/**
* Get the status of the HNAP interface
*
* @return the HNAP status
*/
protected HNAPStatus getHNAPStatus() {
return status;
}
/**
* Sends the two login messages and stores the private key used to generate the
* authentication header required for actions.
*
* Use {@link #getHNAPStatus()} to determine the status of the HNAP connection
* after calling this method.
*
* @param timeout - Connection timeout in milliseconds
*/
protected void login(final int timeout) {
if (status != HNAPStatus.INTERNAL_ERROR) {
try {
Document soapResponse = sendReceive(requestAction, timeout);
Node result = soapResponse.getElementsByTagName(LOGINRESULT).item(0);
if (result != null && OK.equals(result.getTextContent())) {
final Node challengeNode = soapResponse.getElementsByTagName("Challenge").item(0);
final Node cookieNode = soapResponse.getElementsByTagName(COOKIE).item(0);
final Node publicKeyNode = soapResponse.getElementsByTagName("PublicKey").item(0);
if (challengeNode != null && cookieNode != null && publicKeyNode != null) {
setAuthenticationData(challengeNode.getTextContent(), cookieNode.getTextContent(),
publicKeyNode.getTextContent());
soapResponse = sendReceive(loginAction, timeout);
result = soapResponse.getElementsByTagName(LOGINRESULT).item(0);
if (result != null) {
if ("success".equals(result.getTextContent())) {
status = HNAPStatus.LOGGED_IN;
} else {
logger.debug("login - Check pin is correct");
// Assume pin code problem rather than a firmware change
status = HNAPStatus.INVALID_PIN;
}
} else {
unexpectedResult("login - Unexpected login response", soapResponse);
}
} else {
unexpectedResult("login - Unexpected request response", soapResponse);
}
} else {
unexpectedResult("login - Unexpected request response", soapResponse);
}
} catch (final InvalidKeyException e) {
logger.debug("login - Internal error", e);
status = HNAPStatus.INTERNAL_ERROR;
} catch (final NoSuchAlgorithmException e) {
logger.debug("login - Internal error", e);
status = HNAPStatus.INTERNAL_ERROR;
} catch (final Exception e) {
// Assume there has been some problem trying to send one of the messages
if (status != HNAPStatus.COMMUNICATION_ERROR) {
logger.debug("login - Communication error", e);
status = HNAPStatus.COMMUNICATION_ERROR;
}
}
}
}
/**
* Sets the authentication headers for the action message. This should only be called
* after a successful login.
*
* Use {@link #getHNAPStatus()} to determine the status of the HNAP connection
* after calling this method.
*
* @param action - SOAP Action to add headers
*/
protected void setAuthenticationHeaders(final SOAPMessage action) {
if (status == HNAPStatus.LOGGED_IN) {
try {
final MimeHeaders loginHeaders = loginAction.getMimeHeaders();
final MimeHeaders actionHeaders = action.getMimeHeaders();
actionHeaders.setHeader(COOKIE, loginHeaders.getHeader(COOKIE)[0]);
final String timeStamp = String.valueOf(System.currentTimeMillis() / 1000);
final String auth = hash(timeStamp + actionHeaders.getHeader(SOAPACTION)[0], privateKey) + " "
+ timeStamp;
actionHeaders.setHeader("HNAP_AUTH", auth);
action.saveChanges();
} catch (final InvalidKeyException e) {
logger.debug("setAuthenticationHeaders - Internal error", e);
status = HNAPStatus.INTERNAL_ERROR;
} catch (final NoSuchAlgorithmException e) {
logger.debug("setAuthenticationHeaders - Internal error", e);
status = HNAPStatus.INTERNAL_ERROR;
} catch (final SOAPException e) {
// No communication happening so assume system error
logger.debug("setAuthenticationHeaders - Internal error", e);
status = HNAPStatus.INTERNAL_ERROR;
}
}
}
/**
* Send the SOAP message using Jetty HTTP client. Jetty is used in preference to
* HttpURLConnection which can result in the HNAP interface becoming unresponsive.
*
* @param action - SOAP Action to send
* @param timeout - Connection timeout in milliseconds
* @return The result
* @throws IOException
* @throws SOAPException
* @throws SAXException
* @throws ExecutionException
* @throws TimeoutException
* @throws InterruptedException
*/
protected Document sendReceive(final SOAPMessage action, final int timeout) throws IOException, SOAPException,
SAXException, InterruptedException, TimeoutException, ExecutionException {
Document result;
final Request request = httpClient.POST(uri);
request.timeout(timeout, TimeUnit.MILLISECONDS);
final Iterator<?> it = action.getMimeHeaders().getAllHeaders();
while (it.hasNext()) {
final MimeHeader header = (MimeHeader) it.next();
request.header(header.getName(), header.getValue());
}
try (final ByteArrayOutputStream os = new ByteArrayOutputStream()) {
action.writeTo(os);
request.content(new BytesContentProvider(os.toByteArray()));
final ContentResponse response = request.send();
try (final ByteArrayInputStream is = new ByteArrayInputStream(response.getContent())) {
result = parser.parse(is);
}
}
return result;
}
/**
* Output unexpected responses to the debug log.
*
* @param message
* @param soapResponse
*/
protected void logUnexpectedResult(final String message, final Document soapResponse) {
// No point formatting for output if debug logging is not enabled
if (logger.isDebugEnabled()) {
try {
final TransformerFactory transFactory = TransformerFactory.newInstance();
final Transformer transformer = transFactory.newTransformer();
final StringWriter buffer = new StringWriter();
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
transformer.transform(new DOMSource(soapResponse), new StreamResult(buffer));
logger.debug("{} : {}", message, buffer);
} catch (final TransformerException e) {
logger.debug("{}", message);
}
}
}
}

View File

@@ -0,0 +1,39 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.dlinksmarthome.internal;
import java.util.Collections;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link DLinkSmartHomeBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Mike Major - Initial contribution
*/
@NonNullByDefault
public class DLinkSmartHomeBindingConstants {
public static final String BINDING_ID = "dlinksmarthome";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_DCHS150 = new ThingTypeUID(BINDING_ID, "DCH-S150");
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_DCHS150);
// Motion trigger channel
public static final String MOTION = "motion";
}

View File

@@ -0,0 +1,110 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.dlinksmarthome.internal;
import static org.openhab.binding.dlinksmarthome.internal.DLinkSmartHomeBindingConstants.*;
import static org.openhab.binding.dlinksmarthome.internal.motionsensor.DLinkMotionSensorConfig.IP_ADDRESS;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import javax.jmdns.ServiceInfo;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.mdns.MDNSDiscoveryParticipant;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link DLinkSmartHomeDiscoveryParticipant} is responsible for discovering devices through UPnP.
*
* @author Mike Major - Initial contribution
*
*/
@Component(immediate = true)
public class DLinkSmartHomeDiscoveryParticipant implements MDNSDiscoveryParticipant {
private static final String SERVICE_TYPE = "_dhnap._tcp.local.";
private final Logger logger = LoggerFactory.getLogger(getClass());
@Override
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
return SUPPORTED_THING_TYPES_UIDS;
}
@Override
public String getServiceType() {
return SERVICE_TYPE;
}
@Override
public DiscoveryResult createResult(final ServiceInfo serviceInfo) {
final ThingUID thingUID = getThingUID(serviceInfo);
if (thingUID == null) {
return null;
}
final ThingTypeUID thingTypeUID = getThingType(serviceInfo);
if (THING_TYPE_DCHS150.equals(thingTypeUID)) {
return createMotionSensor(thingUID, thingTypeUID, serviceInfo);
} else {
return null;
}
}
@Override
public ThingUID getThingUID(final ServiceInfo serviceInfo) {
final ThingTypeUID thingTypeUID = getThingType(serviceInfo);
if (thingTypeUID != null) {
final String mac = serviceInfo.getPropertyString("mac").replace(":", "").toLowerCase();
return new ThingUID(thingTypeUID, mac);
} else {
return null;
}
}
private ThingTypeUID getThingType(final ServiceInfo serviceInfo) {
final String model = serviceInfo.getPropertyString("model_number");
if (model == null) {
return null;
} else if (model.equals("DCH-S150")) {
return THING_TYPE_DCHS150;
} else {
logger.debug("D-Link HNAP Type: {}", model);
return null;
}
}
private DiscoveryResult createMotionSensor(final ThingUID thingUID, final ThingTypeUID thingType,
final ServiceInfo serviceInfo) {
final String host = serviceInfo.getHostAddresses()[0];
final String mac = serviceInfo.getPropertyString("mac");
final Map<String, Object> properties = new HashMap<>();
properties.put(IP_ADDRESS, host);
logger.debug("DCH-S150 found: {}", host);
return DiscoveryResultBuilder.create(thingUID).withThingType(thingType).withProperties(properties)
.withLabel("Motion Sensor (" + mac + ")").build();
}
}

View File

@@ -0,0 +1,49 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.dlinksmarthome.internal;
import static org.openhab.binding.dlinksmarthome.internal.DLinkSmartHomeBindingConstants.*;
import org.openhab.binding.dlinksmarthome.internal.handler.DLinkMotionSensorHandler;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Component;
/**
* The {@link DLinkSmartHomeHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Mike Major - Initial contribution
*/
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.dlinksmarthome")
public class DLinkSmartHomeHandlerFactory extends BaseThingHandlerFactory {
@Override
public boolean supportsThingType(final ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected ThingHandler createHandler(final Thing thing) {
final ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (thingTypeUID.equals(THING_TYPE_DCHS150)) {
return new DLinkMotionSensorHandler(thing);
}
return null;
}
}

View File

@@ -0,0 +1,91 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.dlinksmarthome.internal.handler;
import static org.openhab.binding.dlinksmarthome.internal.DLinkSmartHomeBindingConstants.MOTION;
import org.openhab.binding.dlinksmarthome.internal.motionsensor.DLinkMotionSensorCommunication;
import org.openhab.binding.dlinksmarthome.internal.motionsensor.DLinkMotionSensorCommunication.DeviceStatus;
import org.openhab.binding.dlinksmarthome.internal.motionsensor.DLinkMotionSensorConfig;
import org.openhab.binding.dlinksmarthome.internal.motionsensor.DLinkMotionSensorListener;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.types.Command;
/**
* The {@link DLinkMotionSensorHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Mike Major - Initial contribution
*/
public class DLinkMotionSensorHandler extends BaseThingHandler implements DLinkMotionSensorListener {
private DLinkMotionSensorCommunication motionSensor;
private final ChannelUID motionChannel;
public DLinkMotionSensorHandler(final Thing thing) {
super(thing);
motionChannel = new ChannelUID(getThing().getUID(), MOTION);
}
@Override
public void handleCommand(final ChannelUID channelUID, final Command command) {
// Does not support commands
}
@Override
public void initialize() {
final DLinkMotionSensorConfig config = getConfigAs(DLinkMotionSensorConfig.class);
motionSensor = new DLinkMotionSensorCommunication(config, this, scheduler);
}
@Override
public void motionDetected() {
triggerChannel(motionChannel);
}
@Override
public void sensorStatus(final DeviceStatus status) {
switch (status) {
case ONLINE:
updateStatus(ThingStatus.ONLINE);
break;
case COMMUNICATION_ERROR:
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
break;
case INVALID_PIN:
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Invalid pin code");
break;
case INTERNAL_ERROR:
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "System error");
break;
case UNSUPPORTED_FIRMWARE:
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Unsupported firmware");
break;
default:
break;
}
}
@Override
public void dispose() {
if (motionSensor != null) {
motionSensor.dispose();
}
super.dispose();
}
}

View File

@@ -0,0 +1,292 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.dlinksmarthome.internal.motionsensor;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import javax.xml.soap.MessageFactory;
import javax.xml.soap.MimeHeaders;
import javax.xml.soap.SOAPBody;
import javax.xml.soap.SOAPElement;
import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPMessage;
import org.openhab.binding.dlinksmarthome.internal.DLinkHNAPCommunication;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
/**
* The {@link DLinkMotionSensorCommunication} is responsible for communicating with a DCH-S150
* motion sensor.
*
* Motion is detected by polling the last detection time via the HNAP interface.
*
* Reverse engineered from Login.html and soapclient.js retrieved from the device.
*
* @author Mike Major - Initial contribution
*/
public class DLinkMotionSensorCommunication extends DLinkHNAPCommunication {
// SOAP actions
private static final String DETECTION_ACTION = "\"http://purenetworks.com/HNAP1/GetLatestDetection\"";
private static final int DETECT_TIMEOUT_MS = 5000;
private static final int DETECT_POLL_S = 1;
/**
* Indicates the device status
*
*/
public enum DeviceStatus {
/**
* Starting communication with device
*/
INITIALISING,
/**
* Successfully communicated with device
*/
ONLINE,
/**
* Problem communicating with device
*/
COMMUNICATION_ERROR,
/**
* Internal error
*/
INTERNAL_ERROR,
/**
* Error due to unsupported firmware
*/
UNSUPPORTED_FIRMWARE,
/**
* Error due to invalid pin code
*/
INVALID_PIN
}
/**
* Use to log connection issues
*/
private final Logger logger = LoggerFactory.getLogger(DLinkMotionSensorCommunication.class);
private final DLinkMotionSensorListener listener;
private SOAPMessage detectionAction;
private boolean loginSuccess;
private boolean detectSuccess;
private long prevDetection;
private long lastDetection;
private final ScheduledFuture<?> detectFuture;
private boolean online = true;
private DeviceStatus status = DeviceStatus.INITIALISING;
/**
* Inform the listener if motion is detected
*/
private final Runnable detect = new Runnable() {
@Override
public void run() {
boolean updateStatus = false;
switch (status) {
case INITIALISING:
online = false;
updateStatus = true;
// FALL-THROUGH
case COMMUNICATION_ERROR:
case ONLINE:
if (!loginSuccess) {
login(detectionAction, DETECT_TIMEOUT_MS);
}
if (!getLastDetection(false)) {
// Try login again in case the session has timed out
login(detectionAction, DETECT_TIMEOUT_MS);
getLastDetection(true);
}
break;
default:
break;
}
if (loginSuccess && detectSuccess) {
status = DeviceStatus.ONLINE;
if (!online) {
online = true;
listener.sensorStatus(status);
// Ignore old detections
prevDetection = lastDetection;
}
if (lastDetection != prevDetection) {
listener.motionDetected();
}
} else {
if (online || updateStatus) {
online = false;
listener.sensorStatus(status);
}
}
}
};
public DLinkMotionSensorCommunication(final DLinkMotionSensorConfig config,
final DLinkMotionSensorListener listener, final ScheduledExecutorService scheduler) {
super(config.ipAddress, config.pin);
this.listener = listener;
if (getHNAPStatus() == HNAPStatus.INTERNAL_ERROR) {
status = DeviceStatus.INTERNAL_ERROR;
}
try {
final MessageFactory messageFactory = MessageFactory.newInstance();
detectionAction = messageFactory.createMessage();
buildDetectionAction();
} catch (final SOAPException e) {
logger.debug("DLinkMotionSensorCommunication - Internal error", e);
status = DeviceStatus.INTERNAL_ERROR;
}
detectFuture = scheduler.scheduleWithFixedDelay(detect, 0, DETECT_POLL_S, TimeUnit.SECONDS);
}
/**
* Stop communicating with the device
*/
@Override
public void dispose() {
detectFuture.cancel(true);
super.dispose();
}
/**
* This is the SOAP message used to retrieve the last detection time. This message will
* only receive a successful response after the login process has been completed and the
* authentication data has been set.
*
* @throws SOAPException
*/
private void buildDetectionAction() throws SOAPException {
detectionAction.getSOAPHeader().detachNode();
final SOAPBody soapBody = detectionAction.getSOAPBody();
final SOAPElement soapBodyElem = soapBody.addChildElement("GetLatestDetection", "", HNAP_XMLNS);
soapBodyElem.addChildElement("ModuleID").addTextNode("1");
final MimeHeaders headers = detectionAction.getMimeHeaders();
headers.addHeader(SOAPACTION, DETECTION_ACTION);
}
/**
* Output unexpected responses to the debug log and sets the FIRMWARE error.
*
* @param message
* @param soapResponse
*/
private void unexpectedResult(final String message, final Document soapResponse) {
logUnexpectedResult(message, soapResponse);
// Best guess when receiving unexpected responses
status = DeviceStatus.UNSUPPORTED_FIRMWARE;
}
/**
* Sends the two login messages and sets the authentication header for the action
* message.
*
* @param action
* @param timeout
*/
private void login(final SOAPMessage action, final int timeout) {
loginSuccess = false;
login(timeout);
setAuthenticationHeaders(action);
switch (getHNAPStatus()) {
case LOGGED_IN:
loginSuccess = true;
break;
case COMMUNICATION_ERROR:
status = DeviceStatus.COMMUNICATION_ERROR;
break;
case INVALID_PIN:
status = DeviceStatus.INVALID_PIN;
break;
case INTERNAL_ERROR:
status = DeviceStatus.INTERNAL_ERROR;
break;
case UNSUPPORTED_FIRMWARE:
status = DeviceStatus.UNSUPPORTED_FIRMWARE;
break;
case INITIALISED:
default:
break;
}
}
/**
* Sends the detection message
*
* @param isRetry - Has this been called as a result of a login retry
* @return true, if the last detection time was successfully retrieved, otherwise false
*/
private boolean getLastDetection(final boolean isRetry) {
detectSuccess = false;
if (loginSuccess) {
try {
final Document soapResponse = sendReceive(detectionAction, DETECT_TIMEOUT_MS);
final Node result = soapResponse.getElementsByTagName("GetLatestDetectionResult").item(0);
if (result != null) {
if (OK.equals(result.getTextContent())) {
final Node timeNode = soapResponse.getElementsByTagName("LatestDetectTime").item(0);
if (timeNode != null) {
prevDetection = lastDetection;
lastDetection = Long.valueOf(timeNode.getTextContent());
detectSuccess = true;
} else {
unexpectedResult("getLastDetection - Unexpected response", soapResponse);
}
} else if (isRetry) {
unexpectedResult("getLastDetection - Unexpected response", soapResponse);
}
} else {
unexpectedResult("getLastDetection - Unexpected response", soapResponse);
}
} catch (final Exception e) {
// Assume there has been some problem trying to send one of the messages
if (status != DeviceStatus.COMMUNICATION_ERROR) {
logger.debug("getLastDetection - Communication error", e);
status = DeviceStatus.COMMUNICATION_ERROR;
}
}
}
return detectSuccess;
}
}

View File

@@ -0,0 +1,37 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.dlinksmarthome.internal.motionsensor;
/**
* The {@link DLinkMotionSensorConfig} provides configuration data
*
* @author Mike Major - Initial contribution
*/
public class DLinkMotionSensorConfig {
/**
* Constants representing the configuration strings
*/
public static final String IP_ADDRESS = "ipAddress";
public static final String PIN = "pin";
/**
* The IP address of the device
*/
public String ipAddress;
/**
* The pin code of the device
*/
public String pin;
}

View File

@@ -0,0 +1,34 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.dlinksmarthome.internal.motionsensor;
import org.openhab.binding.dlinksmarthome.internal.motionsensor.DLinkMotionSensorCommunication.DeviceStatus;
/**
* The {@link DLinkMotionSensorListener} provides callbacks for motion detection
* and device status changes.
*
* @author Mike Major - Initial contribution
*/
public interface DLinkMotionSensorListener {
/**
* Callback to indicate motion has been detected
*/
void motionDetected();
/**
* Callback to indicate a change in the device status
*/
void sensorStatus(final DeviceStatus status);
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="dlinksmarthome" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>D-Link Smart Home Binding</name>
<description>This is the binding for D-Link Smart Home devices</description>
<author>Mike Major</author>
</binding:binding>

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="dlinksmarthome"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<!-- Motion Sensor DCH-S150 Thing Type -->
<thing-type id="DCH-S150">
<label>Motion Sensor</label>
<description>D-Link DCH-S150 WiFi motion sensor</description>
<channels>
<channel id="motion" typeId="system.trigger">
<label>Motion Detected</label>
<description>Triggered when the sensor detects motion</description>
</channel>
</channels>
<config-description>
<parameter name="ipAddress" type="text" required="true">
<context>network-address</context>
<label>Hostname or IP</label>
<description>Hostname or IP of the device.</description>
</parameter>
<parameter name="pin" type="text" required="true">
<context>password</context>
<label>PIN Code</label>
<description>PIN code from the back of the device.</description>
</parameter>
</config-description>
</thing-type>
</thing:thing-descriptions>