added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user