diff --git a/CODEOWNERS b/CODEOWNERS
index 7222bf6c3..1a4e6e29f 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -245,6 +245,7 @@
/bundles/org.openhab.binding.tibber/ @kjoglum
/bundles/org.openhab.binding.touchwand /@roieg
/bundles/org.openhab.binding.tplinksmarthome/ @Hilbrand
+/bundles/org.openhab.binding.tr064/ @J-N-K
/bundles/org.openhab.binding.tradfri/ @cweitkamp @kaikreuzer
/bundles/org.openhab.binding.unifi/ @mgbowman
/bundles/org.openhab.binding.unifiedremote/ @GiviMAD
diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index 169663f18..c6579d85a 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -1211,6 +1211,11 @@
org.openhab.binding.tplinksmarthome
${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.tr064
+ ${project.version}
+
org.openhab.addons.bundles
org.openhab.binding.tradfri
diff --git a/bundles/org.openhab.binding.tr064/NOTICE b/bundles/org.openhab.binding.tr064/NOTICE
new file mode 100644
index 000000000..4c20ef446
--- /dev/null
+++ b/bundles/org.openhab.binding.tr064/NOTICE
@@ -0,0 +1,13 @@
+This content is produced and maintained by the openHAB project.
+
+* Project home: https://www.openhab.org
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/openhab/openhab2-addons
diff --git a/bundles/org.openhab.binding.tr064/README.md b/bundles/org.openhab.binding.tr064/README.md
new file mode 100644
index 000000000..429699552
--- /dev/null
+++ b/bundles/org.openhab.binding.tr064/README.md
@@ -0,0 +1,119 @@
+# TR-064 Binding
+
+This binding brings support for internet gateway devices that support the TR-064 protocol.
+It can be used to gather information from the device and/or re-configure it.
+
+## Supported Things
+
+Four thing types are supported:
+
+- `generic`: the internet gateway device itself (generic device)
+- `fritzbox`: similar to `generic` with extensions for AVM FritzBox devices
+- `subDevice`: a sub-device of a `rootDevice` (e.g. a WAN interface)
+- `subDeviceLan`: a special type of sub-device that supports MAC-detection
+
+## Discovery
+
+The gateway device needs to be added manually.
+After that, sub-devices are detected automatically.
+
+## Thing Configuration
+
+All thing types have a `refresh` parameter.
+It sets the refresh-interval in seconds for each device channel.
+The default value is 60.
+
+### `generic`, `fritzbox`
+
+The `host` parameter is required to communicate with the device.
+It can be a hostname or an IP address.
+
+For accessing the device you need to supply credentials.
+If you only configured password authentication for your device, the `user` parameter must be skipped and it will default to `dslf-config`.
+The second credential parameter is `password`, which is mandatory.
+For security reasons it is highly recommended to set both, username and password.
+
+### `fritzbox`
+
+All additional parameters for `fritzbox` devices (i.e. except those that are shared with `generic`) are advanced parameters.
+
+One or more TAM (telephone answering machines) are supported by most devices.
+By setting the `tamIndices` parameter you can instruct the binding to add channels for these devices to the thing.
+Values start with `0`.
+This is an optional parameter and multiple values are allowed.
+
+Most devices allow to configure call deflections.
+If the `callDeflectionIndices` parameter is set, channels for the status of the pre-configured call deflections are added.
+Values start with `0`, including the number of "Call Blocks" (two configured call-blocks -> first deflection is `2`).
+This is an optional parameter and multiple values are allowed.
+
+Most devices support call lists.
+The binding can analyze these call lists and provide channels for the number of missed calls, inbound calls, outbound calls and rejected (blocked) calls.
+The days for which this analysis takes place can be controlled with the `missedCallDays`, `rejectedCallDays`, `inboundCallDays` and `outboundCallDays`
+This is an optional parameter and multiple values are allowed.
+
+Since FritzOS! 7.20 WAN access of local devices can be controlled by their IPs.
+If the `wanBlockIPs` parameter is set, a channel for each IP is created to block/unblock WAN access for this IP.
+Values need to be IPv4 addresses in the format `a.b.c.d`.
+This is an optional parameter and multiple values are allowed.
+
+If the `PHONEBOOK` profile shall be used, it is necessary to retrieve the phonebooks from the FritzBox.
+The `phonebookInterval` is uses to set the refresh cycle for phonebooks.
+
+### `subdevice`, `subdeviceLan`
+
+Besides the bridge that the thing is attached to, sub-devices have a `uuid` parameter.
+This is the UUID/UDN of the device and a mandatory parameter.
+Since the value can only be determined by examining the SCPD of the root device, the simplest way to get hold of them is through auto-discovery.
+
+For `subdeviceLan` devices (type is detected automatically during discovery) the parameter `macOnline` can be defined.
+It adds a channel for each MAC (format 11:11:11:11:11:11) that shows the online status of the respective device.
+This is an optional parameter and multiple values are allowed.
+
+## Channels
+
+| channel | item-type | advanced | description |
+|----------------------------|---------------------------|:--------:|----------------------------------------------------------------|
+| `callDeflectionEnable` | `Switch` | | Enable/Disable the call deflection setup with the given index. |
+| `deviceLog` | `String` | x | A string containing the last log messages. |
+| `dslCRCErrors` | `Number:Dimensionless` | x | DSL CRC Errors |
+| `dslDownstreamNoiseMargin` | `Number:Dimensionless` | x | DSL Downstream Noise Margin |
+| `dslDownstreamNoiseMargin` | `Number:Dimensionless` | x | DSL Downstream Attenuation |
+| `dslEnable` | `Switch` | | DSL Enable |
+| `dslFECErrors` | `Number:Dimensionless` | x | DSL FEC Errors |
+| `dslHECErrors` | `Number:Dimensionless` | x | DSL HEC Errors |
+| `dslStatus` | `Switch` | | DSL Status |
+| `dslUpstreamNoiseMargin` | `Number:Dimensionless` | x | DSL Upstream Noise Margin |
+| `dslUpstreamNoiseMargin` | `Number:Dimensionless` | x | DSL Upstream Attenuation |
+| `inboundCalls` | `Number` | x | Number of inbound calls within the given number of days. |
+| `macOnline` | `Switch` | x | Online status of the device with the given MAC |
+| `missedCalls` | `Number` | | Number of missed calls within the given number of days. |
+| `outboundCalls` | `Number` | x | Number of outbound calls within the given number of days. |
+| `reboot` | `Switch` | | Reboot |
+| `rejectedCalls` | `Number` | x | Number of rejected calls within the given number of days. |
+| `securityPort` | `Number` | x | The port for connecting via HTTPS to the TR-064 service. |
+| `tamEnable` | `Switch` | | Enable/Disable the answering machine with the given index. |
+| `tamNewMessages` | `Number` | | The number of new messages of the given answering machine. |
+| `uptime` | `Number:Time` | | Uptime |
+| `wanAccessType` | `String` | x | Access Type |
+| `wanConnectionStatus` | `String` | | Connection Status |
+| `wanIpAddress` | `String` | x | WAN IP Address |
+| `wanMaxDownstreamRate` | `Number:DataTransferRate` | x | Max. Downstream Rate |
+| `wanMaxUpstreamRate` | `Number:DataTransferRate` | x | Max. Upstream Rate |
+| `wanPhysicalLinkStatus` | `String` | x | Link Status |
+| `wanTotalBytesReceived` | `Number:DataAmount` | x | Total Bytes Received |
+| `wanTotalBytesSent` | `Number:DataAmount` | x | Total Bytes Send |
+| `wifi24GHzEnable` | `Switch` | | Enable/Disable the 2.4 GHz WiFi device. |
+| `wifi5GHzEnable` | `Switch` | | Enable/Disable the 5.0 GHz WiFi device. |
+| `wifiGuestEnable` | `Switch` | | Enable/Disable the guest WiFi. |
+
+## `PHONEBOOK` Profile
+
+The binding provides a profile for using the FritzBox phonebooks for resolving numbers to names.
+The `PHONEBOOK` profile takes strings containing the number as input and provides strings with the caller's name, if found.
+
+The parameter `thingUid` with the UID of the phonebook providing thing is a mandatory parameter.
+If only a specific phonebook from the device should be used, this can be specified with the `phonebookName` parameter.
+The default is to use all available phonebooks from the specified thing.
+In case the format of the number in the phonebook and the format of the number from the channel are different (e.g. regarding country prefixes), the `matchCount` parameter can be used.
+The configured `matchCount` is counted from the right end and denotes the number of matching characters needed to consider this number as matching.
diff --git a/bundles/org.openhab.binding.tr064/pom.xml b/bundles/org.openhab.binding.tr064/pom.xml
new file mode 100644
index 000000000..8c71c147f
--- /dev/null
+++ b/bundles/org.openhab.binding.tr064/pom.xml
@@ -0,0 +1,52 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 3.0.0-SNAPSHOT
+
+
+ org.openhab.binding.tr064
+
+ openHAB Add-ons :: Bundles :: TR-064 Binding
+
+
+
+
+ org.jvnet.jaxb2.maven2
+ maven-jaxb2-plugin
+ 0.14.0
+
+
+ generate-jaxb-sources
+
+ generate
+
+
+
+
+ src/main/resources/xsd
+ true
+ en
+ false
+ true
+
+ -Xxew
+ -Xxew:instantiate early
+
+
+
+ com.github.jaxb-xew-plugin
+ jaxb-xew-plugin
+ 1.10
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.tr064/src/main/feature/feature.xml b/bundles/org.openhab.binding.tr064/src/main/feature/feature.xml
new file mode 100644
index 000000000..9f9ca83fb
--- /dev/null
+++ b/bundles/org.openhab.binding.tr064/src/main/feature/feature.xml
@@ -0,0 +1,13 @@
+
+
+ mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
+
+
+ openhab-runtime-base
+ openhab.tp;filter:="(feature=jaxb)"
+ openhab.tp-jaxb
+ openhab.tp;filter:="(feature=jax-ws)"
+ openhab.tp-jaxws
+ mvn:org.openhab.addons.bundles/org.openhab.binding.tr064/${project.version}
+
+
diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/AvmFritzTlsTrustManagerProvider.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/AvmFritzTlsTrustManagerProvider.java
new file mode 100644
index 000000000..1f8f24f0c
--- /dev/null
+++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/AvmFritzTlsTrustManagerProvider.java
@@ -0,0 +1,40 @@
+/**
+ * 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.tr064.internal;
+
+import javax.net.ssl.X509ExtendedTrustManager;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.io.net.http.TlsTrustManagerProvider;
+import org.openhab.core.io.net.http.TrustAllTrustManager;
+import org.osgi.service.component.annotations.Component;
+
+/**
+ * Provides a TrustManager to allow secure connections to any FRITZ!Box
+ *
+ * @author Christoph Weitkamp - Initial Contribution
+ */
+@Component
+@NonNullByDefault
+public class AvmFritzTlsTrustManagerProvider implements TlsTrustManagerProvider {
+
+ @Override
+ public String getHostName() {
+ return "fritz.box";
+ }
+
+ @Override
+ public X509ExtendedTrustManager getTrustManager() {
+ return TrustAllTrustManager.getInstance();
+ }
+}
diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/ChannelConfigException.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/ChannelConfigException.java
new file mode 100644
index 000000000..362927966
--- /dev/null
+++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/ChannelConfigException.java
@@ -0,0 +1,30 @@
+/**
+ * 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.tr064.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ *
+ * The{@link ChannelConfigException} is a catched Exception that is thrown during channel configuration
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class ChannelConfigException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public ChannelConfigException(String message) {
+ super(message);
+ }
+}
diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/PostProcessingException.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/PostProcessingException.java
new file mode 100644
index 000000000..9b2c0dd9a
--- /dev/null
+++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/PostProcessingException.java
@@ -0,0 +1,31 @@
+/**
+ * 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.tr064.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ *
+ * The{@link PostProcessingException} is a catched Exception that is thrown in case of conversion errors during post
+ * processing
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class PostProcessingException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public PostProcessingException(String message, Throwable t) {
+ super(message, t);
+ }
+}
diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/SCPDException.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/SCPDException.java
new file mode 100644
index 000000000..f9af40cef
--- /dev/null
+++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/SCPDException.java
@@ -0,0 +1,30 @@
+/**
+ * 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.tr064.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ *
+ * The{@link SCPDException} is a catched Exception that is thrown in case of errors during SCPD processing
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class SCPDException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public SCPDException(String message) {
+ super(message);
+ }
+}
diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/SOAPConnector.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/SOAPConnector.java
new file mode 100644
index 000000000..9abf59e80
--- /dev/null
+++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/SOAPConnector.java
@@ -0,0 +1,254 @@
+/**
+ * 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.tr064.internal;
+
+import static org.openhab.binding.tr064.internal.util.Util.getSOAPElement;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.stream.Collectors;
+
+import javax.xml.soap.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+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.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.HttpStatus;
+import org.openhab.binding.tr064.internal.config.Tr064ChannelConfig;
+import org.openhab.binding.tr064.internal.dto.config.ActionType;
+import org.openhab.binding.tr064.internal.dto.config.ChannelTypeDescription;
+import org.openhab.binding.tr064.internal.dto.scpd.root.SCPDServiceType;
+import org.openhab.binding.tr064.internal.dto.scpd.service.SCPDActionType;
+import org.openhab.core.cache.ExpiringCacheMap;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link SOAPConnector} provides communication with a remote SOAP device
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class SOAPConnector {
+ private static final int SOAP_TIMEOUT = 2000; // in ms
+ private final Logger logger = LoggerFactory.getLogger(SOAPConnector.class);
+ private final HttpClient httpClient;
+ private final String endpointBaseURL;
+ private final SOAPValueConverter soapValueConverter;
+
+ public SOAPConnector(HttpClient httpClient, String endpointBaseURL) {
+ this.httpClient = httpClient;
+ this.endpointBaseURL = endpointBaseURL;
+ this.soapValueConverter = new SOAPValueConverter(httpClient);
+ }
+
+ /**
+ * prepare a SOAP request for an action request to a service
+ *
+ * @param service the service
+ * @param soapAction the action to send
+ * @param arguments arguments to send along with the request
+ * @return a jetty Request containing the full SOAP message
+ * @throws IOException if a problem while writing the SOAP message to the Request occurs
+ * @throws SOAPException if a problem with creating the SOAP message occurs
+ */
+ private Request prepareSOAPRequest(SCPDServiceType service, String soapAction, Map arguments)
+ throws IOException, SOAPException {
+ MessageFactory messageFactory = MessageFactory.newInstance();
+ SOAPMessage soapMessage = messageFactory.createMessage();
+ SOAPPart soapPart = soapMessage.getSOAPPart();
+ SOAPEnvelope envelope = soapPart.getEnvelope();
+ envelope.setEncodingStyle("http://schemas.xmlsoap.org/soap/encoding/");
+
+ // SOAP body
+ SOAPBody soapBody = envelope.getBody();
+ SOAPElement soapBodyElem = soapBody.addChildElement(soapAction, "u", service.getServiceType());
+ arguments.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach(argument -> {
+ try {
+ soapBodyElem.addChildElement(argument.getKey()).setTextContent(argument.getValue());
+ } catch (SOAPException e) {
+ logger.warn("Could not add {}:{} to SOAP Request: {}", argument.getKey(), argument.getValue(),
+ e.getMessage());
+ }
+ });
+
+ // SOAP headers
+ MimeHeaders headers = soapMessage.getMimeHeaders();
+ headers.addHeader("SOAPAction", service.getServiceType() + "#" + soapAction);
+ soapMessage.saveChanges();
+
+ // create Request and add headers and content
+ Request request = httpClient.newRequest(endpointBaseURL + service.getControlURL()).method(HttpMethod.POST);
+ ((Iterator) soapMessage.getMimeHeaders().getAllHeaders())
+ .forEachRemaining(header -> request.header(header.getName(), header.getValue()));
+ try (final ByteArrayOutputStream os = new ByteArrayOutputStream()) {
+ soapMessage.writeTo(os);
+ byte[] content = os.toByteArray();
+ request.content(new BytesContentProvider(content));
+ }
+
+ return request;
+ }
+
+ /**
+ * execute a SOAP request
+ *
+ * @param service the service to send the action to
+ * @param soapAction the action itself
+ * @param arguments arguments to send along with the request
+ * @return the SOAPMessage answer from the remote host
+ * @throws Tr064CommunicationException if an error occurs during the request
+ */
+ public synchronized SOAPMessage doSOAPRequest(SCPDServiceType service, String soapAction,
+ Map arguments) throws Tr064CommunicationException {
+ try {
+ Request request = prepareSOAPRequest(service, soapAction, arguments).timeout(SOAP_TIMEOUT,
+ TimeUnit.MILLISECONDS);
+ if (logger.isTraceEnabled()) {
+ request.getContent().forEach(buffer -> logger.trace("Request: {}", new String(buffer.array())));
+ }
+
+ ContentResponse response = request.send();
+ if (response.getStatus() == HttpStatus.UNAUTHORIZED_401) {
+ // retry once if authentication expired
+ logger.trace("Re-Auth needed.");
+ httpClient.getAuthenticationStore().clearAuthenticationResults();
+ request = prepareSOAPRequest(service, soapAction, arguments).timeout(SOAP_TIMEOUT,
+ TimeUnit.MILLISECONDS);
+ response = request.send();
+ }
+ try (final ByteArrayInputStream is = new ByteArrayInputStream(response.getContent())) {
+ logger.trace("Received response: {}", response.getContentAsString());
+
+ SOAPMessage soapMessage = MessageFactory.newInstance().createMessage(null, is);
+ if (soapMessage.getSOAPBody().hasFault()) {
+ String soapError = getSOAPElement(soapMessage, "errorCode").orElse("unknown");
+ String soapReason = getSOAPElement(soapMessage, "errorDescription").orElse("unknown");
+ String error = String.format("HTTP-Response-Code %d (%s), SOAP-Fault: %s (%s)",
+ response.getStatus(), response.getReason(), soapError, soapReason);
+ throw new Tr064CommunicationException(error);
+ }
+ return soapMessage;
+ }
+ } catch (IOException | SOAPException | InterruptedException | TimeoutException | ExecutionException e) {
+ throw new Tr064CommunicationException(e);
+ }
+ }
+
+ /**
+ * send a command to the remote device
+ *
+ * @param channelConfig the channel config containing all information
+ * @param command the command to send
+ */
+ public void sendChannelCommandToDevice(Tr064ChannelConfig channelConfig, Command command) {
+ soapValueConverter.getSOAPValueFromCommand(command, channelConfig.getDataType(),
+ channelConfig.getChannelTypeDescription().getItem().getUnit()).ifPresentOrElse(value -> {
+ final ChannelTypeDescription channelTypeDescription = channelConfig.getChannelTypeDescription();
+ final SCPDServiceType service = channelConfig.getService();
+ logger.debug("Sending {} as {} to {}/{}", command, value, service.getServiceId(),
+ channelTypeDescription.getSetAction().getName());
+ try {
+ Map arguments = new HashMap<>();
+ if (channelTypeDescription.getSetAction().getArgument() != null) {
+ arguments.put(channelTypeDescription.getSetAction().getArgument(), value);
+ }
+ String parameter = channelConfig.getParameter();
+ if (parameter != null) {
+ arguments.put(
+ channelConfig.getChannelTypeDescription().getGetAction().getParameter().getName(),
+ parameter);
+ }
+ doSOAPRequest(service, channelTypeDescription.getSetAction().getName(), arguments);
+ } catch (Tr064CommunicationException e) {
+ logger.warn("Could not send command {}: {}", command, e.getMessage());
+ }
+ }, () -> logger.warn("Could not convert {} to SOAP value", command));
+ }
+
+ /**
+ * get a value from the remote device - updates state cache for all possible channels
+ *
+ * @param channelConfig the channel config containing all information
+ * @param channelConfigMap map of all channels in the device
+ * @param stateCache the ExpiringCacheMap for states of the device
+ * @return the value for the requested channel
+ */
+ public State getChannelStateFromDevice(final Tr064ChannelConfig channelConfig,
+ Map channelConfigMap, ExpiringCacheMap stateCache) {
+ try {
+ final SCPDActionType getAction = channelConfig.getGetAction();
+ if (getAction == null) {
+ // channel has no get action, return a default
+ switch (channelConfig.getDataType()) {
+ case "boolean":
+ return OnOffType.OFF;
+ case "string":
+ return StringType.EMPTY;
+ default:
+ return UnDefType.UNDEF;
+ }
+ }
+
+ // get value(s) from remote device
+ Map arguments = new HashMap<>();
+ String parameter = channelConfig.getParameter();
+ ActionType action = channelConfig.getChannelTypeDescription().getGetAction();
+ if (parameter != null && !action.getParameter().isInternalOnly()) {
+ arguments.put(action.getParameter().getName(), parameter);
+ }
+ SOAPMessage soapResponse = doSOAPRequest(channelConfig.getService(), getAction.getName(), arguments);
+
+ String argumentName = channelConfig.getChannelTypeDescription().getGetAction().getArgument();
+ // find all other channels with the same action that are already in cache, so we can update them
+ Map channelsInRequest = channelConfigMap.entrySet().stream()
+ .filter(map -> getAction.equals(map.getValue().getGetAction())
+ && stateCache.containsKey(map.getKey())
+ && !argumentName
+ .equals(map.getValue().getChannelTypeDescription().getGetAction().getArgument()))
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+ channelsInRequest
+ .forEach(
+ (channelUID,
+ channelConfig1) -> soapValueConverter
+ .getStateFromSOAPValue(soapResponse,
+ channelConfig1.getChannelTypeDescription().getGetAction()
+ .getArgument(),
+ channelConfig1)
+ .ifPresent(state -> stateCache.putValue(channelUID, state)));
+
+ return soapValueConverter.getStateFromSOAPValue(soapResponse, argumentName, channelConfig)
+ .orElseThrow(() -> new Tr064CommunicationException("failed to transform '"
+ + channelConfig.getChannelTypeDescription().getGetAction().getArgument() + "'"));
+ } catch (Tr064CommunicationException e) {
+ logger.info("Failed to get {}: {}", channelConfig, e.getMessage());
+ return UnDefType.UNDEF;
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/SOAPValueConverter.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/SOAPValueConverter.java
new file mode 100644
index 000000000..214a49b4e
--- /dev/null
+++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/SOAPValueConverter.java
@@ -0,0 +1,255 @@
+/**
+ * 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.tr064.internal;
+
+import static org.openhab.binding.tr064.internal.util.Util.getSOAPElement;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.math.BigDecimal;
+import java.util.Optional;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import javax.xml.soap.SOAPMessage;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.openhab.binding.tr064.internal.config.Tr064ChannelConfig;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link SOAPValueConverter} converts SOAP values and openHAB states
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class SOAPValueConverter {
+ private static final int REQUEST_TIMEOUT = 5000; // in ms
+ private final Logger logger = LoggerFactory.getLogger(SOAPValueConverter.class);
+ private final HttpClient httpClient;
+
+ public SOAPValueConverter(HttpClient httpClient) {
+ this.httpClient = httpClient;
+ }
+
+ /**
+ * convert an openHAB command to a SOAP value
+ *
+ * @param command the command to be converted
+ * @param dataType the datatype to send
+ * @param unit if available, the unit of the converted value
+ * @return a string optional containing the converted value
+ */
+ public Optional getSOAPValueFromCommand(Command command, String dataType, String unit) {
+ if (dataType.isEmpty()) {
+ // we don't have data to send
+ return Optional.of("");
+ }
+ if (command instanceof QuantityType) {
+ QuantityType> value = (unit.isEmpty()) ? ((QuantityType>) command)
+ : ((QuantityType>) command).toUnit(unit);
+ if (value == null) {
+ logger.warn("Could not convert {} to unit {}", command, unit);
+ return Optional.empty();
+ }
+ switch (dataType) {
+ case "ui2":
+ return Optional.of(String.valueOf(value.shortValue()));
+ case "ui4":
+ return Optional.of(String.valueOf(value.intValue()));
+ default:
+ }
+ } else if (command instanceof DecimalType) {
+ BigDecimal value = ((DecimalType) command).toBigDecimal();
+ switch (dataType) {
+ case "ui2":
+ return Optional.of(String.valueOf(value.shortValue()));
+ case "ui4":
+ return Optional.of(String.valueOf(value.intValue()));
+ default:
+ }
+ } else if (command instanceof StringType) {
+ if (dataType.equals("string")) {
+ return Optional.of(command.toString());
+ }
+ } else if (command instanceof OnOffType) {
+ if (dataType.equals("boolean")) {
+ return Optional.of(OnOffType.ON.equals(command) ? "1" : "0");
+ }
+ }
+ return Optional.empty();
+ }
+
+ /**
+ * convert the value from a SOAP message to an openHAB value
+ *
+ * @param soapMessage the inbound SOAP message
+ * @param element the element that needs to be extracted
+ * @param channelConfig the channel config containing additional information (if null a data-type "string" and
+ * missing unit is assumed)
+ * @return an Optional of State containing the converted value
+ */
+ public Optional getStateFromSOAPValue(SOAPMessage soapMessage, String element,
+ @Nullable Tr064ChannelConfig channelConfig) {
+ String dataType = channelConfig != null ? channelConfig.getDataType() : "string";
+ String unit = channelConfig != null ? channelConfig.getChannelTypeDescription().getItem().getUnit() : "";
+
+ return getSOAPElement(soapMessage, element).map(rawValue -> {
+ // map rawValue to State
+ switch (dataType) {
+ case "boolean":
+ return rawValue.equals("0") ? OnOffType.OFF : OnOffType.ON;
+ case "string":
+ return new StringType(rawValue);
+ case "ui2":
+ case "ui4":
+ if (!unit.isEmpty()) {
+ return new QuantityType<>(rawValue + " " + unit);
+ } else {
+ return new DecimalType(rawValue);
+ }
+ default:
+ return null;
+ }
+ }).map(state -> {
+ // check if we need post processing
+ if (channelConfig == null
+ || channelConfig.getChannelTypeDescription().getGetAction().getPostProcessor() == null) {
+ return state;
+ }
+ String postProcessor = channelConfig.getChannelTypeDescription().getGetAction().getPostProcessor();
+ try {
+ Method method = SOAPValueConverter.class.getDeclaredMethod(postProcessor, State.class,
+ Tr064ChannelConfig.class);
+ Object o = method.invoke(this, state, channelConfig);
+ if (o instanceof State) {
+ return (State) o;
+ }
+ } catch (NoSuchMethodException | IllegalAccessException e) {
+ logger.warn("Postprocessor {} not found, this most likely is a programming error", postProcessor, e);
+ } catch (InvocationTargetException e) {
+ Throwable cause = e.getCause();
+ logger.info("Postprocessor {} failed: {}", postProcessor,
+ cause != null ? cause.getMessage() : e.getMessage());
+ }
+ return null;
+ }).or(Optional::empty);
+ }
+
+ /**
+ * post processor for answering machine new messages channel
+ *
+ * @param state the message list URL
+ * @param channelConfig channel config of the TAM new message channel
+ * @return the number of new messages
+ * @throws PostProcessingException if the message list could not be retrieved
+ */
+ @SuppressWarnings("unused")
+ private State processTamListURL(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException {
+ try {
+ ContentResponse response = httpClient.newRequest(state.toString()).timeout(1000, TimeUnit.MILLISECONDS)
+ .send();
+ String responseContent = response.getContentAsString();
+ int messageCount = responseContent.split("1").length - 1;
+
+ return new DecimalType(messageCount);
+ } catch (InterruptedException | TimeoutException | ExecutionException e) {
+ throw new PostProcessingException("Failed to get TAM list from URL " + state.toString(), e);
+ }
+ }
+
+ /**
+ * post processor for missed calls
+ *
+ * @param state the call list URL
+ * @param channelConfig channel config of the missed call channel (contains day number)
+ * @return the number of missed calls
+ * @throws PostProcessingException if call list could not be retrieved
+ */
+ @SuppressWarnings("unused")
+ private State processMissedCalls(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException {
+ return processCallList(state, channelConfig.getParameter(), "2");
+ }
+
+ /**
+ * post processor for inbound calls
+ *
+ * @param state the call list URL
+ * @param channelConfig channel config of the inbound call channel (contains day number)
+ * @return the number of inbound calls
+ * @throws PostProcessingException if call list could not be retrieved
+ */
+ @SuppressWarnings("unused")
+ private State processInboundCalls(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException {
+ return processCallList(state, channelConfig.getParameter(), "1");
+ }
+
+ /**
+ * post processor for rejected calls
+ *
+ * @param state the call list URL
+ * @param channelConfig channel config of the rejected call channel (contains day number)
+ * @return the number of rejected calls
+ * @throws PostProcessingException if call list could not be retrieved
+ */
+ @SuppressWarnings("unused")
+ private State processRejectedCalls(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException {
+ return processCallList(state, channelConfig.getParameter(), "3");
+ }
+
+ /**
+ * post processor for outbound calls
+ *
+ * @param state the call list URL
+ * @param channelConfig channel config of the outbound call channel (contains day number)
+ * @return the number of outbound calls
+ * @throws PostProcessingException if call list could not be retrieved
+ */
+ @SuppressWarnings("unused")
+ private State processOutboundCalls(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException {
+ return processCallList(state, channelConfig.getParameter(), "4");
+ }
+
+ /**
+ * internal helper for call list post processors
+ *
+ * @param state the call list URL
+ * @param days number of days to get
+ * @param type type of call (1=missed 2=inbound 3=rejected 4=outbund)
+ * @return the quantity of calls of the given type within the given number of days
+ * @throws PostProcessingException if the call list could not be retrieved
+ */
+ private State processCallList(State state, @Nullable String days, String type) throws PostProcessingException {
+ try {
+ ContentResponse response = httpClient.newRequest(state.toString() + "&days=" + days)
+ .timeout(REQUEST_TIMEOUT, TimeUnit.MILLISECONDS).send();
+ String responseContent = response.getContentAsString();
+ int callCount = responseContent.split("" + type + "").length - 1;
+
+ return new DecimalType(callCount);
+ } catch (InterruptedException | TimeoutException | ExecutionException e) {
+ throw new PostProcessingException("Failed to get call list from URL " + state.toString(), e);
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064BindingConstants.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064BindingConstants.java
new file mode 100644
index 000000000..c8cbaaab7
--- /dev/null
+++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064BindingConstants.java
@@ -0,0 +1,38 @@
+/**
+ * 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.tr064.internal;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.tr064.internal.dto.config.ChannelTypeDescription;
+import org.openhab.binding.tr064.internal.util.Util;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link Tr064BindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class Tr064BindingConstants {
+ public static final String BINDING_ID = "tr064";
+
+ public static final ThingTypeUID THING_TYPE_GENERIC = new ThingTypeUID(BINDING_ID, "generic");
+ public static final ThingTypeUID THING_TYPE_FRITZBOX = new ThingTypeUID(BINDING_ID, "fritzbox");
+ public static final ThingTypeUID THING_TYPE_SUBDEVICE = new ThingTypeUID(BINDING_ID, "subdevice");
+ public static final ThingTypeUID THING_TYPE_SUBDEVICE_LAN = new ThingTypeUID(BINDING_ID, "subdeviceLan");
+
+ public static final List CHANNEL_TYPES = Util.readXMLChannelConfig();
+}
diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064ChannelTypeProvider.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064ChannelTypeProvider.java
new file mode 100644
index 000000000..d9c6e38af
--- /dev/null
+++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064ChannelTypeProvider.java
@@ -0,0 +1,72 @@
+/**
+ * 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.tr064.internal;
+
+import static org.openhab.binding.tr064.internal.Tr064BindingConstants.CHANNEL_TYPES;
+
+import java.util.Collection;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.thing.type.*;
+import org.openhab.core.types.StateDescriptionFragmentBuilder;
+import org.osgi.service.component.annotations.Component;
+
+/**
+ * The {@link Tr064ChannelTypeProvider} is used for providing dynamic channel types
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+@Component(service = { ChannelTypeProvider.class, Tr064ChannelTypeProvider.class })
+public class Tr064ChannelTypeProvider implements ChannelTypeProvider {
+ private final Map channelTypeMap = new ConcurrentHashMap<>();
+
+ public Tr064ChannelTypeProvider() {
+ CHANNEL_TYPES.forEach(channelTypeDescription -> {
+ ChannelTypeUID channelTypeUID = new ChannelTypeUID(Tr064BindingConstants.BINDING_ID,
+ channelTypeDescription.getName());
+ // create state description
+ StateDescriptionFragmentBuilder stateDescriptionFragmentBuilder = StateDescriptionFragmentBuilder.create()
+ .withReadOnly(channelTypeDescription.getSetAction() == null);
+ if (channelTypeDescription.getItem().getStatePattern() != null) {
+ stateDescriptionFragmentBuilder.withPattern(channelTypeDescription.getItem().getStatePattern());
+ }
+
+ // create channel type
+ ChannelTypeBuilder channelTypeBuilder = ChannelTypeBuilder
+ .state(channelTypeUID, channelTypeDescription.getLabel(),
+ channelTypeDescription.getItem().getType())
+ .withStateDescriptionFragment(stateDescriptionFragmentBuilder.build())
+ .isAdvanced(channelTypeDescription.isAdvanced());
+ if (channelTypeDescription.getDescription() != null) {
+ channelTypeBuilder.withDescription(channelTypeDescription.getDescription());
+ }
+
+ channelTypeMap.put(channelTypeUID, channelTypeBuilder.build());
+ });
+ }
+
+ @Override
+ public Collection getChannelTypes(@Nullable Locale locale) {
+ return channelTypeMap.values();
+ }
+
+ @Override
+ public @Nullable ChannelType getChannelType(ChannelTypeUID channelTypeUID, @Nullable Locale locale) {
+ return channelTypeMap.get(channelTypeUID);
+ }
+}
diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064CommunicationException.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064CommunicationException.java
new file mode 100644
index 000000000..8e1bee6c6
--- /dev/null
+++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064CommunicationException.java
@@ -0,0 +1,33 @@
+/**
+ * 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.tr064.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link Tr064CommunicationException} is thrown for communication errors
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class Tr064CommunicationException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public Tr064CommunicationException(Exception e) {
+ super(e);
+ }
+
+ public Tr064CommunicationException(String s) {
+ super(s);
+ }
+}
diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064DiscoveryService.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064DiscoveryService.java
new file mode 100644
index 000000000..f48c3f6a3
--- /dev/null
+++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064DiscoveryService.java
@@ -0,0 +1,117 @@
+/**
+ * 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.tr064.internal;
+
+import static org.openhab.binding.tr064.internal.Tr064BindingConstants.THING_TYPE_SUBDEVICE;
+import static org.openhab.binding.tr064.internal.Tr064BindingConstants.THING_TYPE_SUBDEVICE_LAN;
+
+import java.util.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.tr064.internal.dto.scpd.root.SCPDDeviceType;
+import org.openhab.core.config.discovery.AbstractDiscoveryService;
+import org.openhab.core.config.discovery.DiscoveryResult;
+import org.openhab.core.config.discovery.DiscoveryResultBuilder;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.BridgeHandler;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.openhab.core.util.UIDUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link Tr064DiscoveryService} discovers sub devices of a root device.
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class Tr064DiscoveryService extends AbstractDiscoveryService implements ThingHandlerService {
+ private static final int SEARCH_TIME = 5;
+ public static final Set SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_SUBDEVICE);
+
+ private final Logger logger = LoggerFactory.getLogger(Tr064DiscoveryService.class);
+ private @Nullable Tr064RootHandler bridgeHandler;
+
+ public Tr064DiscoveryService() {
+ super(SEARCH_TIME);
+ }
+
+ @Override
+ public void setThingHandler(ThingHandler thingHandler) {
+ if (thingHandler instanceof Tr064RootHandler) {
+ this.bridgeHandler = (Tr064RootHandler) thingHandler;
+ }
+ }
+
+ @Override
+ public @Nullable ThingHandler getThingHandler() {
+ return bridgeHandler;
+ }
+
+ @Override
+ public void deactivate() {
+ BridgeHandler bridgeHandler = this.bridgeHandler;
+ if (bridgeHandler == null) {
+ logger.warn("Bridgehandler not found, could not cleanup discovery results.");
+ return;
+ }
+ removeOlderResults(new Date().getTime(), bridgeHandler.getThing().getUID());
+ }
+
+ @Override
+ public Set getSupportedThingTypes() {
+ return SUPPORTED_THING_TYPES;
+ }
+
+ @Override
+ public void startScan() {
+ Tr064RootHandler bridgeHandler = this.bridgeHandler;
+ if (bridgeHandler == null) {
+ logger.warn("Could not start discovery, bridge handler not set");
+ return;
+ }
+ List devices = bridgeHandler.getAllSubDevices();
+ ThingUID bridgeUID = bridgeHandler.getThing().getUID();
+ devices.forEach(device -> {
+ logger.trace("Trying to add {} to discovery results on {}", device, bridgeUID);
+ String udn = device.getUDN();
+ if (udn != null) {
+ ThingTypeUID thingTypeUID;
+ if ("urn:dslforum-org:device:LANDevice:1".equals(device.getDeviceType())) {
+ thingTypeUID = THING_TYPE_SUBDEVICE_LAN;
+ } else {
+ thingTypeUID = THING_TYPE_SUBDEVICE;
+ }
+ ThingUID thingUID = new ThingUID(thingTypeUID, bridgeUID, UIDUtils.encode(udn));
+
+ Map properties = new HashMap<>(2);
+ properties.put("uuid", udn);
+ properties.put("deviceType", device.getDeviceType());
+
+ DiscoveryResult result = DiscoveryResultBuilder.create(thingUID).withLabel(device.getFriendlyName())
+ .withBridge(bridgeHandler.getThing().getUID()).withProperties(properties)
+ .withRepresentationProperty("uuid").build();
+ thingDiscovered(result);
+ }
+ });
+ }
+
+ @Override
+ protected synchronized void stopScan() {
+ super.stopScan();
+ removeOlderResults(getTimestampOfLastScan());
+ }
+}
diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064DynamicStateDescriptionProvider.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064DynamicStateDescriptionProvider.java
new file mode 100644
index 000000000..4cd4fa6cd
--- /dev/null
+++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064DynamicStateDescriptionProvider.java
@@ -0,0 +1,73 @@
+/**
+ * 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.tr064.internal;
+
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.thing.Channel;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
+import org.openhab.core.types.StateDescription;
+import org.osgi.service.component.annotations.Component;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Dynamic channel state description provider.
+ * Overrides the state description for the controls, which receive its configuration in the runtime.
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+@Component(service = { DynamicStateDescriptionProvider.class, Tr064DynamicStateDescriptionProvider.class })
+public class Tr064DynamicStateDescriptionProvider implements DynamicStateDescriptionProvider {
+ private final Logger logger = LoggerFactory.getLogger(Tr064DynamicStateDescriptionProvider.class);
+ private final Map descriptions = new ConcurrentHashMap<>();
+
+ /**
+ * Set a state description for a channel. This description will be used when preparing the channel state by
+ * the framework for presentation. A previous description, if existed, will be replaced.
+ *
+ * @param channelUID channel UID
+ * @param description state description for the channel
+ */
+ public void setDescription(ChannelUID channelUID, StateDescription description) {
+ logger.trace("adding state description for channel {}", channelUID);
+ descriptions.put(channelUID, description);
+ }
+
+ /**
+ * remove all descriptions for a given thing
+ *
+ * @param thingUID the thing's UID
+ */
+ public void removeDescriptionsForThing(ThingUID thingUID) {
+ logger.trace("removing state description for thing {}", thingUID);
+ descriptions.entrySet().removeIf(entry -> entry.getKey().getThingUID().equals(thingUID));
+ }
+
+ @Override
+ public @Nullable StateDescription getStateDescription(Channel channel,
+ @Nullable StateDescription originalStateDescription, @Nullable Locale locale) {
+ if (descriptions.containsKey(channel.getUID())) {
+ return descriptions.get(channel.getUID());
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064HandlerFactory.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064HandlerFactory.java
new file mode 100644
index 000000000..c40dd503d
--- /dev/null
+++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064HandlerFactory.java
@@ -0,0 +1,95 @@
+/**
+ * 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.tr064.internal;
+
+import static org.openhab.binding.tr064.internal.Tr064BindingConstants.THING_TYPE_FRITZBOX;
+
+import java.util.*;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.openhab.binding.tr064.internal.phonebook.PhonebookProfileFactory;
+import org.openhab.core.io.net.http.HttpClientFactory;
+import org.openhab.core.thing.Bridge;
+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.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * The {@link Tr064HandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+@Component(service = { ThingHandlerFactory.class }, configurationPid = "binding.tr064")
+public class Tr064HandlerFactory extends BaseThingHandlerFactory {
+ private static final Set SUPPORTED_THING_TYPES_UIDS = Stream
+ .of(Tr064RootHandler.SUPPORTED_THING_TYPES, Tr064SubHandler.SUPPORTED_THING_TYPES).flatMap(Set::stream)
+ .collect(Collectors.toSet());
+
+ private final HttpClient httpClient;
+ private final PhonebookProfileFactory phonebookProfileFactory;
+
+ // the Tr064ChannelTypeProvider is needed for creating the channels and
+ // referenced here to make sure it is available before things are
+ // initialized
+ @SuppressWarnings("unused")
+ private final Tr064ChannelTypeProvider channelTypeProvider;
+
+ @Activate
+ public Tr064HandlerFactory(@Reference HttpClientFactory httpClientFactory,
+ @Reference Tr064ChannelTypeProvider channelTypeProvider,
+ @Reference PhonebookProfileFactory phonebookProfileFactory) {
+ httpClient = httpClientFactory.getCommonHttpClient();
+ this.channelTypeProvider = channelTypeProvider;
+ this.phonebookProfileFactory = phonebookProfileFactory;
+ }
+
+ @Override
+ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+ return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
+ }
+
+ @Override
+ protected @Nullable ThingHandler createHandler(Thing thing) {
+ ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+
+ if (Tr064RootHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) {
+ Tr064RootHandler handler = new Tr064RootHandler((Bridge) thing, httpClient);
+ if (thingTypeUID.equals(THING_TYPE_FRITZBOX)) {
+ phonebookProfileFactory.registerPhonebookProvider(handler);
+ }
+ return handler;
+ } else if (Tr064SubHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) {
+ return new Tr064SubHandler(thing);
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void removeHandler(ThingHandler thingHandler) {
+ if (thingHandler instanceof Tr064RootHandler) {
+ phonebookProfileFactory.unregisterPhonebookProvider((Tr064RootHandler) thingHandler);
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064RootHandler.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064RootHandler.java
new file mode 100644
index 000000000..af03c1dcf
--- /dev/null
+++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064RootHandler.java
@@ -0,0 +1,386 @@
+/**
+ * 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.tr064.internal;
+
+import static org.openhab.binding.tr064.internal.Tr064BindingConstants.THING_TYPE_FRITZBOX;
+import static org.openhab.binding.tr064.internal.Tr064BindingConstants.THING_TYPE_GENERIC;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.*;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import javax.xml.soap.SOAPException;
+import javax.xml.soap.SOAPMessage;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.Authentication;
+import org.eclipse.jetty.client.api.AuthenticationStore;
+import org.eclipse.jetty.client.util.DigestAuthentication;
+import org.openhab.binding.tr064.internal.config.Tr064ChannelConfig;
+import org.openhab.binding.tr064.internal.config.Tr064RootConfiguration;
+import org.openhab.binding.tr064.internal.dto.scpd.root.SCPDDeviceType;
+import org.openhab.binding.tr064.internal.dto.scpd.root.SCPDServiceType;
+import org.openhab.binding.tr064.internal.dto.scpd.service.SCPDActionType;
+import org.openhab.binding.tr064.internal.phonebook.Phonebook;
+import org.openhab.binding.tr064.internal.phonebook.PhonebookProvider;
+import org.openhab.binding.tr064.internal.phonebook.Tr064PhonebookImpl;
+import org.openhab.binding.tr064.internal.util.SCPDUtil;
+import org.openhab.binding.tr064.internal.util.Util;
+import org.openhab.core.cache.ExpiringCacheMap;
+import org.openhab.core.thing.*;
+import org.openhab.core.thing.binding.BaseBridgeHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.openhab.core.thing.binding.builder.ThingBuilder;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.openhab.core.types.State;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link Tr064RootHandler} is responsible for handling commands, which are
+ * sent to one of the channels and update channel values
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProvider {
+ public static final Set SUPPORTED_THING_TYPES = Set.of(THING_TYPE_GENERIC, THING_TYPE_FRITZBOX);
+ private static final int RETRY_INTERVAL = 60;
+ private static final Set PROPERTY_ARGUMENTS = Set.of("NewSerialNumber", "NewSoftwareVersion",
+ "NewModelName");
+
+ private final Logger logger = LoggerFactory.getLogger(Tr064RootHandler.class);
+ private final HttpClient httpClient;
+
+ private Tr064RootConfiguration config = new Tr064RootConfiguration();
+ private String deviceType = "";
+
+ private @Nullable SCPDUtil scpdUtil;
+ private SOAPConnector soapConnector;
+ private String endpointBaseURL = "http://fritz.box:49000";
+
+ private final Map channels = new HashMap<>();
+ // caching is used to prevent excessive calls to the same action
+ private final ExpiringCacheMap stateCache = new ExpiringCacheMap<>(2000);
+ private Collection phonebooks = Collections.emptyList();
+
+ private @Nullable ScheduledFuture> connectFuture;
+ private @Nullable ScheduledFuture> pollFuture;
+ private @Nullable ScheduledFuture> phonebookFuture;
+
+ Tr064RootHandler(Bridge bridge, HttpClient httpClient) {
+ super(bridge);
+ this.httpClient = httpClient;
+ soapConnector = new SOAPConnector(httpClient, endpointBaseURL);
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ Tr064ChannelConfig channelConfig = channels.get(channelUID);
+ if (channelConfig == null) {
+ logger.trace("Channel {} not supported.", channelUID);
+ return;
+ }
+
+ if (command instanceof RefreshType) {
+ SOAPConnector soapConnector = this.soapConnector;
+ State state = stateCache.putIfAbsentAndGet(channelUID,
+ () -> soapConnector.getChannelStateFromDevice(channelConfig, channels, stateCache));
+ if (state != null) {
+ updateState(channelUID, state);
+ }
+ return;
+ }
+
+ if (channelConfig.getChannelTypeDescription().getSetAction() == null) {
+ logger.debug("Discarding command {} to {}, read-only channel", command, channelUID);
+ return;
+ }
+ scheduler.execute(() -> soapConnector.sendChannelCommandToDevice(channelConfig, command));
+ }
+
+ @Override
+ public void initialize() {
+ config = getConfigAs(Tr064RootConfiguration.class);
+ if (!config.isValid()) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ "At least one mandatory configuration field is empty");
+ return;
+ }
+
+ endpointBaseURL = "http://" + config.host + ":49000";
+ updateStatus(ThingStatus.UNKNOWN);
+
+ connectFuture = scheduler.scheduleWithFixedDelay(this::internalInitialize, 0, RETRY_INTERVAL, TimeUnit.SECONDS);
+ }
+
+ /**
+ * internal thing initializer (sets SCPDUtil and connects to remote device)
+ */
+ private void internalInitialize() {
+ try {
+ scpdUtil = new SCPDUtil(httpClient, endpointBaseURL);
+ } catch (SCPDException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "could not get device definitions from " + config.host);
+ return;
+ }
+
+ if (establishSecureConnectionAndUpdateProperties()) {
+ removeConnectScheduler();
+
+ // connection successful, check channels
+ ThingBuilder thingBuilder = editThing();
+ thingBuilder.withoutChannels(thing.getChannels());
+ final SCPDUtil scpdUtil = this.scpdUtil;
+ if (scpdUtil != null) {
+ Util.checkAvailableChannels(thing, thingBuilder, scpdUtil, "", deviceType, channels);
+ updateThing(thingBuilder.build());
+ }
+
+ installPolling();
+ updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
+ }
+ }
+
+ private void removeConnectScheduler() {
+ final ScheduledFuture> connectFuture = this.connectFuture;
+ if (connectFuture != null) {
+ connectFuture.cancel(true);
+ this.connectFuture = null;
+ }
+ }
+
+ @Override
+ public void dispose() {
+ removeConnectScheduler();
+ uninstallPolling();
+ stateCache.clear();
+
+ super.dispose();
+ }
+
+ /**
+ * poll remote device for channel values
+ */
+ private void poll() {
+ channels.forEach((channelUID, channelConfig) -> {
+ if (isLinked(channelUID)) {
+ State state = stateCache.putIfAbsentAndGet(channelUID,
+ () -> soapConnector.getChannelStateFromDevice(channelConfig, channels, stateCache));
+ if (state != null) {
+ updateState(channelUID, state);
+ }
+ }
+ });
+ }
+
+ /**
+ * establish the connection - get secure port (if avallable), install authentication, get device properties
+ *
+ * @return true if successful
+ */
+ private boolean establishSecureConnectionAndUpdateProperties() {
+ final SCPDUtil scpdUtil = this.scpdUtil;
+ if (scpdUtil != null) {
+ try {
+ SCPDDeviceType device = scpdUtil.getDevice("")
+ .orElseThrow(() -> new SCPDException("Root device not found"));
+ SCPDServiceType deviceService = device.getServiceList().stream()
+ .filter(service -> service.getServiceId().equals("urn:DeviceInfo-com:serviceId:DeviceInfo1"))
+ .findFirst().orElseThrow(() -> new SCPDException(
+ "service 'urn:DeviceInfo-com:serviceId:DeviceInfo1' not found"));
+
+ this.deviceType = device.getDeviceType();
+
+ // try to get security (https) port
+ SOAPMessage soapResponse = soapConnector.doSOAPRequest(deviceService, "GetSecurityPort",
+ Collections.emptyMap());
+ if (!soapResponse.getSOAPBody().hasFault()) {
+ SOAPValueConverter soapValueConverter = new SOAPValueConverter(httpClient);
+ soapValueConverter.getStateFromSOAPValue(soapResponse, "NewSecurityPort", null)
+ .ifPresentOrElse(port -> {
+ endpointBaseURL = "https://" + config.host + ":" + port.toString();
+ soapConnector = new SOAPConnector(httpClient, endpointBaseURL);
+ logger.debug("endpointBaseURL is now '{}'", endpointBaseURL);
+ }, () -> logger.warn("Could not determine secure port, disabling https"));
+ } else {
+ logger.warn("Could not determine secure port, disabling https");
+ }
+
+ // clear auth cache and force re-auth
+ httpClient.getAuthenticationStore().clearAuthenticationResults();
+ AuthenticationStore auth = httpClient.getAuthenticationStore();
+ auth.addAuthentication(new DigestAuthentication(new URI(endpointBaseURL), Authentication.ANY_REALM,
+ config.user, config.password));
+
+ // check & update properties
+ SCPDActionType getInfoAction = scpdUtil.getService(deviceService.getServiceId())
+ .orElseThrow(() -> new SCPDException(
+ "Could not get service definition for 'urn:DeviceInfo-com:serviceId:DeviceInfo1'"))
+ .getActionList().stream().filter(action -> action.getName().equals("GetInfo")).findFirst()
+ .orElseThrow(() -> new SCPDException("Action 'GetInfo' not found"));
+ SOAPMessage soapResponse1 = soapConnector.doSOAPRequest(deviceService, getInfoAction.getName(),
+ Collections.emptyMap());
+ SOAPValueConverter soapValueConverter = new SOAPValueConverter(httpClient);
+ Map properties = editProperties();
+ PROPERTY_ARGUMENTS.forEach(argumentName -> getInfoAction.getArgumentList().stream()
+ .filter(argument -> argument.getName().equals(argumentName)).findFirst()
+ .ifPresent(argument -> soapValueConverter
+ .getStateFromSOAPValue(soapResponse1, argumentName, null).ifPresent(value -> properties
+ .put(argument.getRelatedStateVariable(), value.toString()))));
+ properties.put("deviceType", device.getDeviceType());
+ updateProperties(properties);
+
+ return true;
+ } catch (SCPDException | SOAPException | Tr064CommunicationException | URISyntaxException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+ return false;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * get all sub devices of this root device (used for discovery)
+ *
+ * @return the list
+ */
+ public List getAllSubDevices() {
+ final SCPDUtil scpdUtil = this.scpdUtil;
+ return (scpdUtil == null) ? Collections.emptyList() : scpdUtil.getAllSubDevices();
+ }
+
+ /**
+ * get the SOAP connector (used by sub devices for communication with the remote device)
+ *
+ * @return the SOAP connector
+ */
+ public SOAPConnector getSOAPConnector() {
+ return soapConnector;
+ }
+
+ /**
+ * get the SCPD processing utility
+ *
+ * @return the SCPD utility (or null if not available)
+ */
+ public @Nullable SCPDUtil getSCPDUtil() {
+ return scpdUtil;
+ }
+
+ /**
+ * uninstall the polling
+ */
+ private void uninstallPolling() {
+ final ScheduledFuture> pollFuture = this.pollFuture;
+ if (pollFuture != null) {
+ pollFuture.cancel(true);
+ this.pollFuture = null;
+ }
+ final ScheduledFuture> phonebookFuture = this.phonebookFuture;
+ if (phonebookFuture != null) {
+ phonebookFuture.cancel(true);
+ this.phonebookFuture = null;
+ }
+ }
+
+ /**
+ * install the polling
+ */
+ private void installPolling() {
+ uninstallPolling();
+ pollFuture = scheduler.scheduleWithFixedDelay(this::poll, 0, config.refresh, TimeUnit.SECONDS);
+ if (config.phonebookInterval > 0) {
+ phonebookFuture = scheduler.scheduleWithFixedDelay(this::retrievePhonebooks, 0, config.phonebookInterval,
+ TimeUnit.SECONDS);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private Collection processPhonebookList(SOAPMessage soapMessagePhonebookList,
+ SCPDServiceType scpdService) {
+ SOAPValueConverter soapValueConverter = new SOAPValueConverter(httpClient);
+ return (Collection) soapValueConverter
+ .getStateFromSOAPValue(soapMessagePhonebookList, "NewPhonebookList", null)
+ .map(phonebookList -> Arrays.stream(phonebookList.toString().split(","))).orElse(Stream.empty())
+ .map(index -> {
+ try {
+ SOAPMessage soapMessageURL = soapConnector.doSOAPRequest(scpdService, "GetPhonebook",
+ Map.of("NewPhonebookID", index));
+ return soapValueConverter.getStateFromSOAPValue(soapMessageURL, "NewPhonebookURL", null)
+ .map(url -> (Phonebook) new Tr064PhonebookImpl(httpClient, url.toString()));
+ } catch (Tr064CommunicationException e) {
+ logger.warn("Failed to get phonebook with index {}:", index, e);
+ }
+ return Optional.empty();
+ }).filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList());
+ }
+
+ private void retrievePhonebooks() {
+ String serviceId = "urn:X_AVM-DE_OnTel-com:serviceId:X_AVM-DE_OnTel1";
+ SCPDUtil scpdUtil = this.scpdUtil;
+ if (scpdUtil == null) {
+ logger.warn("Cannot find SCPDUtil. This is most likely a programming error.");
+ return;
+ }
+ Optional scpdService = scpdUtil.getDevice("").flatMap(deviceType -> deviceType.getServiceList()
+ .stream().filter(service -> service.getServiceId().equals(serviceId)).findFirst());
+
+ phonebooks = scpdService.map(service -> {
+ try {
+ return processPhonebookList(
+ soapConnector.doSOAPRequest(service, "GetPhonebookList", Collections.emptyMap()), service);
+ } catch (Tr064CommunicationException e) {
+ return Collections. emptyList();
+ }
+ }).orElse(Collections.emptyList());
+
+ if (phonebooks.isEmpty()) {
+ logger.warn("Could not get phonebooks for thing {}", thing.getUID());
+ }
+ }
+
+ @Override
+ public Optional getPhonebookByName(String name) {
+ return phonebooks.stream().filter(p -> name.equals(p.getName())).findAny();
+ }
+
+ @Override
+ public Collection getPhonebooks() {
+ return phonebooks;
+ }
+
+ @Override
+ public ThingUID getUID() {
+ return thing.getUID();
+ }
+
+ @Override
+ public String getFriendlyName() {
+ String friendlyName = thing.getLabel();
+ return friendlyName != null ? friendlyName : getUID().getId();
+ }
+
+ @Override
+ public Collection> getServices() {
+ return Set.of(Tr064DiscoveryService.class);
+ }
+}
diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064SubHandler.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064SubHandler.java
new file mode 100644
index 000000000..ab70fdd45
--- /dev/null
+++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064SubHandler.java
@@ -0,0 +1,254 @@
+/**
+ * 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.tr064.internal;
+
+import static org.openhab.binding.tr064.internal.Tr064BindingConstants.THING_TYPE_SUBDEVICE;
+import static org.openhab.binding.tr064.internal.Tr064BindingConstants.THING_TYPE_SUBDEVICE_LAN;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.tr064.internal.config.Tr064ChannelConfig;
+import org.openhab.binding.tr064.internal.config.Tr064SubConfiguration;
+import org.openhab.binding.tr064.internal.dto.scpd.root.SCPDDeviceType;
+import org.openhab.binding.tr064.internal.util.SCPDUtil;
+import org.openhab.binding.tr064.internal.util.Util;
+import org.openhab.core.cache.ExpiringCacheMap;
+import org.openhab.core.thing.*;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.thing.binding.builder.ThingBuilder;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link Tr064SubHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class Tr064SubHandler extends BaseThingHandler {
+ public static final Set SUPPORTED_THING_TYPES = Set.of(THING_TYPE_SUBDEVICE,
+ THING_TYPE_SUBDEVICE_LAN);
+ private static final int RETRY_INTERVAL = 60;
+
+ private final Logger logger = LoggerFactory.getLogger(Tr064SubHandler.class);
+
+ private Tr064SubConfiguration config = new Tr064SubConfiguration();
+
+ private String deviceType = "";
+ private boolean isInitialized = false;
+
+ private final Map channels = new HashMap<>();
+ // caching is used to prevent excessive calls to the same action
+ private final ExpiringCacheMap stateCache = new ExpiringCacheMap<>(2000);
+
+ private @Nullable SOAPConnector soapConnector;
+ private @Nullable ScheduledFuture> connectFuture;
+ private @Nullable ScheduledFuture> pollFuture;
+
+ Tr064SubHandler(Thing thing) {
+ super(thing);
+ }
+
+ @Override
+ @SuppressWarnings("null")
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ Tr064ChannelConfig channelConfig = channels.get(channelUID);
+ if (channelConfig == null) {
+ logger.trace("Channel {} not supported.", channelUID);
+ return;
+ }
+
+ if (command instanceof RefreshType) {
+ State state = stateCache.putIfAbsentAndGet(channelUID, () -> soapConnector == null ? UnDefType.UNDEF
+ : soapConnector.getChannelStateFromDevice(channelConfig, channels, stateCache));
+ if (state != null) {
+ updateState(channelUID, state);
+ }
+ return;
+ }
+
+ if (channelConfig.getChannelTypeDescription().getSetAction() == null) {
+ logger.debug("Discarding command {} to {}, read-only channel", command, channelUID);
+ return;
+ }
+ scheduler.execute(() -> {
+ if (soapConnector == null) {
+ logger.warn("Could not send command because connector not available");
+ } else {
+ soapConnector.sendChannelCommandToDevice(channelConfig, command);
+ }
+ });
+ }
+
+ @Override
+ public void initialize() {
+ config = getConfigAs(Tr064SubConfiguration.class);
+ if (!config.isValid()) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ "One or more mandatory configuration fields are empty");
+ return;
+ }
+
+ final Bridge bridge = getBridge();
+ if (bridge != null && bridge.getStatus().equals(ThingStatus.ONLINE)) {
+ updateStatus(ThingStatus.UNKNOWN);
+ connectFuture = scheduler.scheduleWithFixedDelay(this::internalInitialize, 0, 30, TimeUnit.SECONDS);
+ } else {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
+ }
+ }
+
+ private void internalInitialize() {
+ final Bridge bridge = getBridge();
+ if (bridge == null) {
+ return;
+ }
+ final Tr064RootHandler bridgeHandler = (Tr064RootHandler) bridge.getHandler();
+ if (bridgeHandler == null) {
+ logger.warn("Bridge-handler is null in thing {}", thing.getUID());
+ return;
+ }
+ final SCPDUtil scpdUtil = bridgeHandler.getSCPDUtil();
+ if (scpdUtil == null) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "Could not get device definitions");
+ return;
+ }
+
+ if (checkProperties(scpdUtil)) {
+ // properties set, check channels
+ ThingBuilder thingBuilder = editThing();
+ thingBuilder.withoutChannels(thing.getChannels());
+ Util.checkAvailableChannels(thing, thingBuilder, scpdUtil, config.uuid, deviceType, channels);
+ updateThing(thingBuilder.build());
+
+ // remove connect scheduler
+ removeConnectScheduler();
+ soapConnector = bridgeHandler.getSOAPConnector();
+
+ isInitialized = true;
+ installPolling();
+ updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
+ }
+ }
+
+ private void removeConnectScheduler() {
+ final ScheduledFuture> connectFuture = this.connectFuture;
+ if (connectFuture != null) {
+ connectFuture.cancel(true);
+ this.connectFuture = null;
+ }
+ }
+
+ @Override
+ public void dispose() {
+ removeConnectScheduler();
+ uninstallPolling();
+
+ stateCache.clear();
+ isInitialized = false;
+
+ super.dispose();
+ }
+
+ /**
+ * poll remote device for channel values
+ */
+ private void poll() {
+ SOAPConnector soapConnector = this.soapConnector;
+ channels.forEach((channelUID, channelConfig) -> {
+ if (isLinked(channelUID)) {
+ State state = stateCache.putIfAbsentAndGet(channelUID, () -> soapConnector == null ? UnDefType.UNDEF
+ : soapConnector.getChannelStateFromDevice(channelConfig, channels, stateCache));
+ if (state != null) {
+ updateState(channelUID, state);
+ }
+ }
+ });
+ }
+
+ /**
+ * get device properties from remote device
+ *
+ * @param scpdUtil the SCPD util of this device
+ * @return true if successfull
+ */
+ private boolean checkProperties(SCPDUtil scpdUtil) {
+ try {
+ SCPDDeviceType device = scpdUtil.getDevice(config.uuid)
+ .orElseThrow(() -> new SCPDException("Could not find device " + config.uuid));
+ String deviceType = device.getDeviceType();
+ if (deviceType == null) {
+ throw new SCPDException("deviceType can't be null ");
+ }
+ this.deviceType = deviceType;
+
+ Map properties = editProperties();
+ properties.put("deviceType", deviceType);
+ updateProperties(properties);
+
+ return true;
+ } catch (SCPDException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "Failed to update device properties: " + e.getMessage());
+
+ return false;
+ }
+ }
+
+ @Override
+ public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
+ if (!bridgeStatusInfo.getStatus().equals(ThingStatus.ONLINE)) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
+ removeConnectScheduler();
+ } else {
+ if (isInitialized) {
+ updateStatus(ThingStatus.ONLINE);
+ } else {
+ updateStatus(ThingStatus.UNKNOWN);
+ connectFuture = scheduler.scheduleWithFixedDelay(this::internalInitialize, 0, RETRY_INTERVAL,
+ TimeUnit.SECONDS);
+ }
+ }
+ }
+
+ /**
+ * uninstall update polling
+ */
+ private void uninstallPolling() {
+ final ScheduledFuture> pollFuture = this.pollFuture;
+ if (pollFuture != null) {
+ pollFuture.cancel(true);
+ this.pollFuture = null;
+ }
+ }
+
+ /**
+ * install update polling
+ */
+ private void installPolling() {
+ uninstallPolling();
+ pollFuture = scheduler.scheduleWithFixedDelay(this::poll, 0, config.refresh, TimeUnit.SECONDS);
+ }
+}
diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/config/Tr064BaseThingConfiguration.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/config/Tr064BaseThingConfiguration.java
new file mode 100644
index 000000000..cc63ed767
--- /dev/null
+++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/config/Tr064BaseThingConfiguration.java
@@ -0,0 +1,25 @@
+/**
+ * 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.tr064.internal.config;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link Tr064BaseThingConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class Tr064BaseThingConfiguration {
+ public int refresh = 60;
+}
diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/config/Tr064ChannelConfig.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/config/Tr064ChannelConfig.java
new file mode 100644
index 000000000..d0f77c856
--- /dev/null
+++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/config/Tr064ChannelConfig.java
@@ -0,0 +1,86 @@
+/**
+ * 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.tr064.internal.config;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.tr064.internal.dto.config.ChannelTypeDescription;
+import org.openhab.binding.tr064.internal.dto.scpd.root.SCPDServiceType;
+import org.openhab.binding.tr064.internal.dto.scpd.service.SCPDActionType;
+
+/**
+ * The {@link Tr064ChannelConfig} class holds a channel configuration
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class Tr064ChannelConfig {
+ private ChannelTypeDescription channelTypeDescription;
+ private SCPDServiceType service;
+ private @Nullable SCPDActionType getAction;
+ private String dataType = "";
+ private @Nullable String parameter;
+
+ public Tr064ChannelConfig(ChannelTypeDescription channelTypeDescription, SCPDServiceType service) {
+ this.channelTypeDescription = channelTypeDescription;
+ this.service = service;
+ }
+
+ public Tr064ChannelConfig(Tr064ChannelConfig o) {
+ this.channelTypeDescription = o.channelTypeDescription;
+ this.service = o.service;
+ this.getAction = o.getAction;
+ this.dataType = o.dataType;
+ this.parameter = o.parameter;
+ }
+
+ public ChannelTypeDescription getChannelTypeDescription() {
+ return channelTypeDescription;
+ }
+
+ public SCPDServiceType getService() {
+ return service;
+ }
+
+ public String getDataType() {
+ return dataType;
+ }
+
+ public void setDataType(String dataType) {
+ this.dataType = dataType;
+ }
+
+ public @Nullable SCPDActionType getGetAction() {
+ return getAction;
+ }
+
+ public void setGetAction(SCPDActionType getAction) {
+ this.getAction = getAction;
+ }
+
+ public @Nullable String getParameter() {
+ return parameter;
+ }
+
+ public void setParameter(String parameter) {
+ this.parameter = parameter;
+ }
+
+ @Override
+ public String toString() {
+ final SCPDActionType getAction = this.getAction;
+ return "Tr064ChannelConfig{" + "channelType=" + channelTypeDescription.getName() + ", getAction="
+ + ((getAction == null) ? "(null)" : getAction.getName()) + ", dataType='" + dataType + ", parameter='"
+ + parameter + "'}";
+ }
+}
diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/config/Tr064RootConfiguration.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/config/Tr064RootConfiguration.java
new file mode 100644
index 000000000..51b391edd
--- /dev/null
+++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/config/Tr064RootConfiguration.java
@@ -0,0 +1,43 @@
+/**
+ * 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.tr064.internal.config;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link Tr064RootConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class Tr064RootConfiguration extends Tr064BaseThingConfiguration {
+ public String host = "";
+ public String user = "dslf-config";
+ public String password = "";
+
+ /* following parameters only available in fritzbox thing */
+ public List tamIndices = Collections.emptyList();
+ public List callDeflectionIndices = Collections.emptyList();
+ public List missedCallDays = Collections.emptyList();
+ public List rejectedCallDays = Collections.emptyList();
+ public List inboundCallDays = Collections.emptyList();
+ public List outboundCallDays = Collections.emptyList();
+ public int phonebookInterval = 0;
+
+ public boolean isValid() {
+ return !host.isEmpty() && !user.isEmpty() && !password.isEmpty();
+ }
+}
diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/config/Tr064SubConfiguration.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/config/Tr064SubConfiguration.java
new file mode 100644
index 000000000..41e40b728
--- /dev/null
+++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/config/Tr064SubConfiguration.java
@@ -0,0 +1,35 @@
+/**
+ * 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.tr064.internal.config;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link Tr064SubConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class Tr064SubConfiguration extends Tr064BaseThingConfiguration {
+ public String uuid = "";
+
+ // Lan Device
+ public List macOnline = Collections.emptyList();
+
+ public boolean isValid() {
+ return !uuid.isEmpty();
+ }
+}
diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/Phonebook.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/Phonebook.java
new file mode 100644
index 000000000..fb7e11708
--- /dev/null
+++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/Phonebook.java
@@ -0,0 +1,42 @@
+/**
+ * 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.tr064.internal.phonebook;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link Phonebook} interface is used by phonebook providers to implement phonebooks
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public interface Phonebook {
+
+ /**
+ * get the name of this phonebook
+ *
+ * @return
+ */
+ String getName();
+
+ /**
+ * lookup a number in this phonebook
+ *
+ * @param number the number
+ * @param matchCount the number of matching digits, counting from far right
+ * @return an Optional containing the name associated with this number (empty of not present)
+ */
+ Optional lookupNumber(String number, int matchCount);
+}
diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/PhonebookProfile.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/PhonebookProfile.java
new file mode 100644
index 000000000..33bc639f2
--- /dev/null
+++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/PhonebookProfile.java
@@ -0,0 +1,142 @@
+/**
+ * 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.tr064.internal.phonebook;
+
+import java.util.Map;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.config.core.Configuration;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.profiles.*;
+import org.openhab.core.transform.TransformationService;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.openhab.core.util.UIDUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link PhonebookProfile} class provides a profile for resolving phone number strings to names
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class PhonebookProfile implements StateProfile {
+ public static final ProfileTypeUID PHONEBOOK_PROFILE_TYPE_UID = new ProfileTypeUID(
+ TransformationService.TRANSFORM_PROFILE_SCOPE, "PHONEBOOK");
+ public static final ProfileType PHONEBOOK_PROFILE_TYPE = ProfileTypeBuilder
+ .newState(PhonebookProfile.PHONEBOOK_PROFILE_TYPE_UID, "Phonebook").build();
+
+ public static final String PHONEBOOK_PARAM = "phonebook";
+ private static final String MATCH_COUNT_PARAM = "matchCount";
+
+ private final Logger logger = LoggerFactory.getLogger(PhonebookProfile.class);
+
+ private final ProfileCallback callback;
+
+ private final @Nullable String phonebookName;
+ private final @Nullable ThingUID thingUID;
+ private final Map phonebookProviders;
+ private final int matchCount;
+
+ public PhonebookProfile(ProfileCallback callback, ProfileContext context,
+ Map phonebookProviders) {
+ this.callback = callback;
+ this.phonebookProviders = phonebookProviders;
+
+ Configuration configuration = context.getConfiguration();
+ Object phonebookParam = configuration.get(PHONEBOOK_PARAM);
+ Object matchCountParam = configuration.get(MATCH_COUNT_PARAM);
+
+ logger.debug("Profile configured with '{}'='{}', '{}'='{}'", PHONEBOOK_PARAM, phonebookParam, MATCH_COUNT_PARAM,
+ matchCountParam);
+
+ ThingUID thingUID;
+ String phonebookName = null;
+ int matchCount = 0;
+
+ try {
+ if (!(phonebookParam instanceof String)
+ || ((matchCountParam != null) && !(matchCountParam instanceof String))) {
+ throw new IllegalArgumentException("Parameters need to be Strings");
+ }
+ String[] phonebookParams = ((String) phonebookParam).split(":");
+ if (phonebookParams.length > 2) {
+ throw new IllegalArgumentException("Could not split 'phonebook' parameter");
+ }
+ thingUID = new ThingUID(UIDUtils.decode(phonebookParams[0]));
+ if (phonebookParams.length == 2) {
+ phonebookName = UIDUtils.decode(phonebookParams[1]);
+ }
+ if (matchCountParam != null) {
+ matchCount = Integer.parseInt((String) matchCountParam);
+ }
+ } catch (IllegalArgumentException e) {
+ logger.warn("Could not initialize PHONEBOOK transformation profile: {}. Profile will be inactive.",
+ e.getMessage());
+ thingUID = null;
+ }
+
+ this.thingUID = thingUID;
+ this.phonebookName = phonebookName;
+ this.matchCount = matchCount;
+ }
+
+ @Override
+ public void onCommandFromItem(Command command) {
+ }
+
+ @Override
+ public void onCommandFromHandler(Command command) {
+ }
+
+ @Override
+ public void onStateUpdateFromHandler(State state) {
+ if (state instanceof StringType) {
+ PhonebookProvider provider = phonebookProviders.get(thingUID);
+ if (provider == null) {
+ logger.warn("Could not get phonebook provider with thing UID '{}'.", thingUID);
+ return;
+ }
+ final String phonebookName = this.phonebookName;
+ Optional match;
+ if (phonebookName != null) {
+ match = provider.getPhonebookByName(phonebookName).or(() -> {
+ logger.warn("Could not get phonebook '{}' from provider '{}'", phonebookName, thingUID);
+ return Optional.empty();
+ }).flatMap(phonebook -> phonebook.lookupNumber(state.toString(), matchCount));
+ } else {
+ match = provider.getPhonebooks().stream().map(p -> p.lookupNumber(state.toString(), matchCount))
+ .filter(Optional::isPresent).map(Optional::get).findAny();
+ }
+ State newState = match.map(name -> (State) new StringType(name)).orElse(state);
+ if (newState == state) {
+ logger.debug("Number '{}' not found in phonebook '{}' from provider '{}'", state, phonebookName,
+ thingUID);
+ }
+ callback.sendUpdate(newState);
+ }
+ }
+
+ @Override
+ public ProfileTypeUID getProfileTypeUID() {
+ return PHONEBOOK_PROFILE_TYPE_UID;
+ }
+
+ @Override
+ public void onStateUpdateFromItem(State state) {
+ }
+}
diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/PhonebookProfileFactory.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/PhonebookProfileFactory.java
new file mode 100644
index 000000000..054e09c0c
--- /dev/null
+++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/PhonebookProfileFactory.java
@@ -0,0 +1,118 @@
+/**
+ * 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.tr064.internal.phonebook;
+
+import java.net.URI;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.config.core.ConfigOptionProvider;
+import org.openhab.core.config.core.ParameterOption;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.profiles.Profile;
+import org.openhab.core.thing.profiles.ProfileCallback;
+import org.openhab.core.thing.profiles.ProfileContext;
+import org.openhab.core.thing.profiles.ProfileFactory;
+import org.openhab.core.thing.profiles.ProfileType;
+import org.openhab.core.thing.profiles.ProfileTypeProvider;
+import org.openhab.core.thing.profiles.ProfileTypeUID;
+import org.openhab.core.util.UIDUtils;
+import org.osgi.service.component.annotations.Component;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link PhonebookProfileFactory} class is used to create phonebook profiles
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+@Component(service = { ProfileFactory.class, ProfileTypeProvider.class, PhonebookProfileFactory.class,
+ ConfigOptionProvider.class })
+public class PhonebookProfileFactory implements ProfileFactory, ProfileTypeProvider, ConfigOptionProvider {
+ private final Logger logger = LoggerFactory.getLogger(PhonebookProfileFactory.class);
+ private final Map phonebookProviders = new ConcurrentHashMap<>();
+
+ @Override
+ public @Nullable Profile createProfile(ProfileTypeUID profileTypeUID, ProfileCallback callback,
+ ProfileContext profileContext) {
+ return new PhonebookProfile(callback, profileContext, phonebookProviders);
+ }
+
+ @Override
+ public Collection getSupportedProfileTypeUIDs() {
+ return Collections.singleton(PhonebookProfile.PHONEBOOK_PROFILE_TYPE_UID);
+ }
+
+ @Override
+ public Collection getProfileTypes(@Nullable Locale locale) {
+ return Collections.singleton(PhonebookProfile.PHONEBOOK_PROFILE_TYPE);
+ }
+
+ /**
+ * register a phonebook provider
+ *
+ * @param phonebookProvider the provider that shall be added
+ */
+ public void registerPhonebookProvider(PhonebookProvider phonebookProvider) {
+ if (phonebookProviders.put(phonebookProvider.getUID(), phonebookProvider) != null) {
+ logger.warn("Tried to register a phonebook provider with UID '{}' for the second time.",
+ phonebookProvider.getUID());
+ }
+ }
+
+ /**
+ * unregister a phonebook provider
+ *
+ * @param phonebookProvider the provider that shall be removed
+ */
+ public void unregisterPhonebookProvider(PhonebookProvider phonebookProvider) {
+ if (phonebookProviders.remove(phonebookProvider.getUID()) == null) {
+ logger.warn("Tried to unregister a phonebook provider with UID '{}' but it was not found.",
+ phonebookProvider.getUID());
+ }
+ }
+
+ private Stream createPhonebookList(Map.Entry entry) {
+ String thingUid = UIDUtils.encode(entry.getKey().toString());
+ String thingName = entry.getValue().getFriendlyName();
+
+ Stream parameterOptions = entry.getValue().getPhonebooks().stream()
+ .map(phonebook -> new ParameterOption(thingUid + ":" + UIDUtils.encode(phonebook.getName()),
+ thingName + " " + phonebook.getName()));
+
+ if (parameterOptions.count() > 0) {
+ return Stream.concat(Stream.of(new ParameterOption(thingUid, thingName)), parameterOptions);
+ }
+
+ return parameterOptions;
+ }
+
+ @Override
+ public @Nullable Collection getParameterOptions(URI uri, String s, @Nullable String s1,
+ @Nullable Locale locale) {
+ if (uri.getSchemeSpecificPart().equals(PhonebookProfile.PHONEBOOK_PROFILE_TYPE_UID.toString())
+ && s.equals(PhonebookProfile.PHONEBOOK_PARAM)) {
+ return phonebookProviders.entrySet().stream().flatMap(this::createPhonebookList)
+ .collect(Collectors.toSet());
+ }
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/PhonebookProvider.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/PhonebookProvider.java
new file mode 100644
index 000000000..5a41c0139
--- /dev/null
+++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/PhonebookProvider.java
@@ -0,0 +1,36 @@
+/**
+ * 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.tr064.internal.phonebook;
+
+import java.util.Collection;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingUID;
+
+/**
+ * The {@link PhonebookProvider} interface provides methods to lookup a phone number from a phonebook
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public interface PhonebookProvider {
+
+ Optional getPhonebookByName(String name);
+
+ Collection getPhonebooks();
+
+ ThingUID getUID();
+
+ String getFriendlyName();
+}
diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/Tr064PhonebookImpl.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/Tr064PhonebookImpl.java
new file mode 100644
index 000000000..bf105c313
--- /dev/null
+++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/Tr064PhonebookImpl.java
@@ -0,0 +1,100 @@
+/**
+ * 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.tr064.internal.phonebook;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.stream.Collectors;
+
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.Unmarshaller;
+import javax.xml.transform.stream.StreamSource;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.http.HttpMethod;
+import org.openhab.binding.tr064.internal.dto.phonebook.NumberType;
+import org.openhab.binding.tr064.internal.dto.phonebook.PhonebooksType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link Tr064PhonebookImpl} class implements a phonebook
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class Tr064PhonebookImpl implements Phonebook {
+ private final Logger logger = LoggerFactory.getLogger(Tr064PhonebookImpl.class);
+
+ private Map phonebook = new HashMap<>();
+
+ private final HttpClient httpClient;
+ private final String phonebookUrl;
+
+ private String phonebookName = "";
+
+ public Tr064PhonebookImpl(HttpClient httpClient, String phonebookUrl) {
+ this.httpClient = httpClient;
+ this.phonebookUrl = phonebookUrl;
+ getPhonebook();
+ }
+
+ private void getPhonebook() {
+ try {
+ ContentResponse contentResponse = httpClient.newRequest(phonebookUrl).method(HttpMethod.GET)
+ .timeout(2, TimeUnit.SECONDS).send();
+ InputStream xml = new ByteArrayInputStream(contentResponse.getContent());
+
+ JAXBContext context = JAXBContext.newInstance(PhonebooksType.class);
+ Unmarshaller um = context.createUnmarshaller();
+ PhonebooksType phonebooksType = um.unmarshal(new StreamSource(xml), PhonebooksType.class).getValue();
+
+ phonebookName = phonebooksType.getPhonebook().getName();
+
+ phonebook = phonebooksType.getPhonebook().getContact().stream().map(contact -> {
+ String contactName = contact.getPerson().getRealName();
+ return contact.getTelephony().getNumber().stream()
+ .collect(Collectors.toMap(NumberType::getValue, number -> contactName));
+ }).collect(HashMap::new, HashMap::putAll, HashMap::putAll);
+ logger.debug("Downloaded phonebook {}: {}", phonebookName, phonebook);
+ } catch (JAXBException | InterruptedException | ExecutionException | TimeoutException e) {
+ logger.warn("Failed to get phonebook with URL {}:", phonebookUrl, e);
+ }
+ }
+
+ @Override
+ public String getName() {
+ return phonebookName;
+ }
+
+ @Override
+ public Optional lookupNumber(String number, int matchCount) {
+ String matchString = matchCount < number.length() ? number.substring(number.length() - matchCount) : number;
+ logger.trace("matchString for {} is {}", number, matchString);
+ return phonebook.keySet().stream().filter(n -> n.endsWith(matchString)).findAny().map(phonebook::get);
+ }
+
+ @Override
+ public String toString() {
+ return "Phonebook{" + "phonebookName='" + phonebookName + "', phonebook=" + phonebook + '}';
+ }
+}
diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/util/SCPDUtil.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/util/SCPDUtil.java
new file mode 100644
index 000000000..3de746978
--- /dev/null
+++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/util/SCPDUtil.java
@@ -0,0 +1,150 @@
+/**
+ * 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.tr064.internal.util;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.Unmarshaller;
+import javax.xml.transform.stream.StreamSource;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.http.HttpMethod;
+import org.openhab.binding.tr064.internal.SCPDException;
+import org.openhab.binding.tr064.internal.dto.scpd.root.SCPDDeviceType;
+import org.openhab.binding.tr064.internal.dto.scpd.root.SCPDRootType;
+import org.openhab.binding.tr064.internal.dto.scpd.root.SCPDServiceType;
+import org.openhab.binding.tr064.internal.dto.scpd.service.SCPDScpdType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link SCPDUtil} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class SCPDUtil {
+ private final Logger logger = LoggerFactory.getLogger(SCPDUtil.class);
+
+ private final HttpClient httpClient;
+
+ private SCPDRootType scpdRoot;
+ private final List scpdDevicesList = new ArrayList<>();
+ private final Map serviceMap = new HashMap<>();
+
+ public SCPDUtil(HttpClient httpClient, String endpoint) throws SCPDException {
+ this.httpClient = httpClient;
+
+ SCPDRootType scpdRoot = getAndUnmarshalSCPD(endpoint + "/tr64desc.xml", SCPDRootType.class);
+ if (scpdRoot == null) {
+ throw new SCPDException("could not get SCPD root");
+ }
+ this.scpdRoot = scpdRoot;
+
+ scpdDevicesList.addAll(flatDeviceList(scpdRoot.getDevice()).collect(Collectors.toList()));
+ for (SCPDDeviceType device : scpdDevicesList) {
+ for (SCPDServiceType service : device.getServiceList()) {
+ SCPDScpdType scpd = serviceMap.computeIfAbsent(service.getServiceId(),
+ serviceId -> getAndUnmarshalSCPD(endpoint + service.getSCPDURL(), SCPDScpdType.class));
+ if (scpd == null) {
+ throw new SCPDException("could not get SCPD service");
+ }
+ }
+ }
+ }
+
+ /**
+ * generic unmarshaller
+ *
+ * @param uri the uri of the XML file
+ * @param clazz the class describing the XML file
+ * @return unmarshalling result
+ */
+ private @Nullable T getAndUnmarshalSCPD(String uri, Class clazz) {
+ try {
+ ContentResponse contentResponse = httpClient.newRequest(uri).timeout(2, TimeUnit.SECONDS)
+ .method(HttpMethod.GET).send();
+ InputStream xml = new ByteArrayInputStream(contentResponse.getContent());
+
+ JAXBContext context = JAXBContext.newInstance(clazz);
+ Unmarshaller um = context.createUnmarshaller();
+ return um.unmarshal(new StreamSource(xml), clazz).getValue();
+ } catch (ExecutionException | InterruptedException | TimeoutException e) {
+ logger.debug("HTTP Failed to GET uri '{}': {}", uri, e.getMessage());
+ } catch (JAXBException e) {
+ logger.debug("Unmarshalling failed: {}", e.getMessage());
+ }
+ return null;
+ }
+
+ /**
+ * recursively flatten the device tree to a stream
+ *
+ * @param device a device
+ * @return stream of sub-devices
+ */
+ private Stream flatDeviceList(SCPDDeviceType device) {
+ return Stream.concat(Stream.of(device), device.getDeviceList().stream().flatMap(this::flatDeviceList));
+ }
+
+ /**
+ * get a list of all sub-devices (root device not included)
+ *
+ * @return the device list
+ */
+ public List getAllSubDevices() {
+ return scpdDevicesList.stream().filter(device -> !device.getUDN().equals(scpdRoot.getDevice().getUDN()))
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * get a single device by it's UDN
+ *
+ * @param udn the device UDN
+ * @return the device
+ */
+ public Optional getDevice(String udn) {
+ if (udn.isEmpty()) {
+ return Optional.of(scpdRoot.getDevice());
+ } else {
+ return getAllSubDevices().stream().filter(device -> udn.equals(device.getUDN())).findFirst();
+ }
+ }
+
+ /**
+ * get a single service by it's serviceId
+ *
+ * @param serviceId the service id
+ * @return the service
+ */
+ public Optional getService(String serviceId) {
+ return Optional.ofNullable(serviceMap.get(serviceId));
+ }
+}
diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/util/Util.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/util/Util.java
new file mode 100644
index 000000000..a3146bc81
--- /dev/null
+++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/util/Util.java
@@ -0,0 +1,293 @@
+/**
+ * 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.tr064.internal.util;
+
+import static org.openhab.binding.tr064.internal.Tr064BindingConstants.*;
+
+import java.io.InputStream;
+import java.lang.reflect.Field;
+import java.util.*;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBElement;
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.Unmarshaller;
+import javax.xml.soap.SOAPException;
+import javax.xml.soap.SOAPMessage;
+import javax.xml.transform.stream.StreamSource;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.tr064.internal.ChannelConfigException;
+import org.openhab.binding.tr064.internal.Tr064RootHandler;
+import org.openhab.binding.tr064.internal.config.Tr064BaseThingConfiguration;
+import org.openhab.binding.tr064.internal.config.Tr064ChannelConfig;
+import org.openhab.binding.tr064.internal.config.Tr064RootConfiguration;
+import org.openhab.binding.tr064.internal.config.Tr064SubConfiguration;
+import org.openhab.binding.tr064.internal.dto.config.ActionType;
+import org.openhab.binding.tr064.internal.dto.config.ChannelTypeDescription;
+import org.openhab.binding.tr064.internal.dto.config.ChannelTypeDescriptions;
+import org.openhab.binding.tr064.internal.dto.config.ParameterType;
+import org.openhab.binding.tr064.internal.dto.scpd.root.SCPDServiceType;
+import org.openhab.binding.tr064.internal.dto.scpd.service.*;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.binding.builder.ChannelBuilder;
+import org.openhab.core.thing.binding.builder.ThingBuilder;
+import org.openhab.core.thing.type.ChannelTypeUID;
+import org.openhab.core.util.UIDUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.w3c.dom.NodeList;
+
+/**
+ * The {@link Util} is a set of helper functions
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class Util {
+ private static final Logger LOGGER = LoggerFactory.getLogger(Util.class);
+
+ /**
+ * read the channel config from the resource file (static initialization)
+ *
+ * @return a list of all available channel configurations
+ */
+ public static List readXMLChannelConfig() {
+ try {
+ InputStream resource = Thread.currentThread().getContextClassLoader().getResourceAsStream("channels.xml");
+ JAXBContext context = JAXBContext.newInstance(ChannelTypeDescriptions.class);
+ Unmarshaller um = context.createUnmarshaller();
+ JAXBElement root = um.unmarshal(new StreamSource(resource),
+ ChannelTypeDescriptions.class);
+ return root.getValue().getChannel();
+ } catch (JAXBException e) {
+ LOGGER.warn("Failed to read channel definitions", e);
+ return Collections.emptyList();
+ }
+ }
+
+ /**
+ * Extract an argument from an SCPD action definition
+ *
+ * @param scpdAction the action object
+ * @param argumentName the argument's name
+ * @param direction the direction (in or out)
+ * @return the requested argument object
+ * @throws ChannelConfigException if not found
+ */
+ private static SCPDArgumentType getArgument(SCPDActionType scpdAction, String argumentName, SCPDDirection direction)
+ throws ChannelConfigException {
+ return scpdAction.getArgumentList().stream()
+ .filter(argument -> argument.getName().equals(argumentName) && argument.getDirection() == direction)
+ .findFirst()
+ .orElseThrow(() -> new ChannelConfigException(
+ (direction == SCPDDirection.IN ? "Set-Argument '" : "Get-Argument '") + argumentName
+ + "' not found"));
+ }
+
+ /**
+ * Extract the related state variable from the service root for a given argument
+ *
+ * @param serviceRoot the service root object
+ * @param scpdArgument the argument object
+ * @return the related state variable object for this argument
+ * @throws ChannelConfigException if not found
+ */
+ private static SCPDStateVariableType getStateVariable(SCPDScpdType serviceRoot, SCPDArgumentType scpdArgument)
+ throws ChannelConfigException {
+ return serviceRoot.getServiceStateTable().stream()
+ .filter(stateVariable -> stateVariable.getName().equals(scpdArgument.getRelatedStateVariable()))
+ .findFirst().orElseThrow(() -> new ChannelConfigException(
+ "StateVariable '" + scpdArgument.getRelatedStateVariable() + "' not found"));
+ }
+
+ /**
+ * Extract an action from the service root
+ *
+ * @param serviceRoot the service root object
+ * @param actionName the action name
+ * @param actionType "Get-Action" or "Set-Action" (for exception string only)
+ * @return the requested action object
+ * @throws ChannelConfigException if not found
+ */
+ private static SCPDActionType getAction(SCPDScpdType serviceRoot, String actionName, String actionType)
+ throws ChannelConfigException {
+ return serviceRoot.getActionList().stream().filter(action -> actionName.equals(action.getName())).findFirst()
+ .orElseThrow(() -> new ChannelConfigException(actionType + " '" + actionName + "' not found"));
+ }
+
+ /**
+ * check and add available channels on a thing
+ *
+ * @param thing the Thing
+ * @param thingBuilder the ThingBuilder (needs to be passed as editThing is only available in the handler)
+ * @param scpdUtil the SCPDUtil instance for this thing
+ * @param deviceId the device id for this thing
+ * @param deviceType the (SCPD) device-type for this thing
+ * @param channels a (mutable) channel list for storing all channels
+ */
+ public static void checkAvailableChannels(Thing thing, ThingBuilder thingBuilder, SCPDUtil scpdUtil,
+ String deviceId, String deviceType, Map channels) {
+ Tr064BaseThingConfiguration thingConfig = Tr064RootHandler.SUPPORTED_THING_TYPES
+ .contains(thing.getThingTypeUID()) ? thing.getConfiguration().as(Tr064RootConfiguration.class)
+ : thing.getConfiguration().as(Tr064SubConfiguration.class);
+ channels.clear();
+ CHANNEL_TYPES.stream().filter(channel -> deviceType.equals(channel.getService().getDeviceType()))
+ .forEach(channelTypeDescription -> {
+ String channelId = channelTypeDescription.getName();
+ String serviceId = channelTypeDescription.getService().getServiceId();
+ Set parameters = new HashSet<>();
+ try {
+ SCPDServiceType deviceService = scpdUtil.getDevice(deviceId)
+ .flatMap(device -> device.getServiceList().stream()
+ .filter(service -> service.getServiceId().equals(serviceId)).findFirst())
+ .orElseThrow(() -> new ChannelConfigException("Service '" + serviceId + "' not found"));
+ SCPDScpdType serviceRoot = scpdUtil.getService(deviceService.getServiceId())
+ .orElseThrow(() -> new ChannelConfigException(
+ "Service definition for '" + serviceId + "' not found"));
+ Tr064ChannelConfig channelConfig = new Tr064ChannelConfig(channelTypeDescription,
+ deviceService);
+
+ // get
+ ActionType getAction = channelTypeDescription.getGetAction();
+ if (getAction != null) {
+ String actionName = getAction.getName();
+ String argumentName = getAction.getArgument();
+ SCPDActionType scpdAction = getAction(serviceRoot, actionName, "Get-Action");
+ SCPDArgumentType scpdArgument = getArgument(scpdAction, argumentName, SCPDDirection.OUT);
+ SCPDStateVariableType relatedStateVariable = getStateVariable(serviceRoot, scpdArgument);
+ parameters.addAll(
+ getAndCheckParameters(channelId, getAction, scpdAction, serviceRoot, thingConfig));
+
+ channelConfig.setGetAction(scpdAction);
+ channelConfig.setDataType(relatedStateVariable.getDataType());
+ }
+
+ // check set action
+ ActionType setAction = channelTypeDescription.getSetAction();
+ if (setAction != null) {
+ String actionName = setAction.getName();
+ String argumentName = setAction.getArgument();
+
+ SCPDActionType scpdAction = getAction(serviceRoot, actionName, "Set-Action");
+ if (argumentName != null) {
+ SCPDArgumentType scpdArgument = getArgument(scpdAction, argumentName, SCPDDirection.IN);
+ SCPDStateVariableType relatedStateVariable = getStateVariable(serviceRoot,
+ scpdArgument);
+ if (channelConfig.getDataType().isEmpty()) {
+ channelConfig.setDataType(relatedStateVariable.getDataType());
+ } else if (!channelConfig.getDataType().equals(relatedStateVariable.getDataType())) {
+ throw new ChannelConfigException("dataType of set and get action are different");
+ }
+ }
+ }
+
+ // everything is available, create the channel
+ ChannelTypeUID channelTypeUID = new ChannelTypeUID(BINDING_ID,
+ channelTypeDescription.getName());
+ if (parameters.isEmpty()) {
+ // we have no parameters, so create a single channel
+ ChannelUID channelUID = new ChannelUID(thing.getUID(), channelId);
+ ChannelBuilder channelBuilder = ChannelBuilder
+ .create(channelUID, channelTypeDescription.getItem().getType())
+ .withType(channelTypeUID);
+ thingBuilder.withChannel(channelBuilder.build());
+ channels.put(channelUID, channelConfig);
+ } else {
+ // create a channel for each parameter
+ parameters.forEach(parameter -> {
+ String normalizedParameter = UIDUtils.encode(parameter);
+ ChannelUID channelUID = new ChannelUID(thing.getUID(),
+ channelId + "_" + normalizedParameter);
+ ChannelBuilder channelBuilder = ChannelBuilder
+ .create(channelUID, channelTypeDescription.getItem().getType())
+ .withType(channelTypeUID)
+ .withLabel(channelTypeDescription.getLabel() + " " + parameter);
+ thingBuilder.withChannel(channelBuilder.build());
+ Tr064ChannelConfig channelConfig1 = new Tr064ChannelConfig(channelConfig);
+ channelConfig1.setParameter(parameter);
+ channels.put(channelUID, channelConfig1);
+ });
+ }
+ } catch (ChannelConfigException e) {
+ LOGGER.debug("Channel {} not available: {}", channelId, e.getMessage());
+ }
+ });
+ }
+
+ private static Set getAndCheckParameters(String channelId, ActionType action, SCPDActionType scpdAction,
+ SCPDScpdType serviceRoot, Tr064BaseThingConfiguration thingConfig) throws ChannelConfigException {
+ ParameterType parameter = action.getParameter();
+ if (parameter == null) {
+ return Collections.emptySet();
+ }
+ try {
+ Set parameters = new HashSet<>();
+
+ // get parameters by reflection from thing config
+ Field paramField = thingConfig.getClass().getField(parameter.getThingParameter());
+ Object rawFieldValue = paramField.get(thingConfig);
+ if ((rawFieldValue instanceof List>)) {
+ ((List>) rawFieldValue).forEach(obj -> {
+ if (obj instanceof String) {
+ parameters.add((String) obj);
+ }
+ });
+ }
+
+ // validate parameter against pattern
+ String parameterPattern = parameter.getPattern();
+ if (parameterPattern != null) {
+ parameters.removeIf(param -> !param.matches(parameterPattern));
+ }
+
+ // validate parameter against SCPD (if not internal only)
+ if (!parameter.isInternalOnly()) {
+ SCPDArgumentType scpdArgument = getArgument(scpdAction, parameter.getName(), SCPDDirection.IN);
+ SCPDStateVariableType relatedStateVariable = getStateVariable(serviceRoot, scpdArgument);
+ if (relatedStateVariable.getAllowedValueRange() != null) {
+ int paramMin = relatedStateVariable.getAllowedValueRange().getMinimum();
+ int paramMax = relatedStateVariable.getAllowedValueRange().getMaximum();
+ int paramStep = relatedStateVariable.getAllowedValueRange().getStep();
+ Set allowedValues = Stream.iterate(paramMin, i -> i <= paramMax, i -> i + paramStep)
+ .map(String::valueOf).collect(Collectors.toSet());
+ parameters.retainAll(allowedValues);
+ }
+ }
+
+ // check we have at least one valid parameter left
+ if (parameters.isEmpty()) {
+ throw new IllegalArgumentException();
+ }
+ return parameters;
+ } catch (NoSuchFieldException | IllegalAccessException | IllegalArgumentException e) {
+ throw new ChannelConfigException("Could not get required parameter '" + channelId
+ + "' from thing config (missing, empty or invalid)");
+ }
+ }
+
+ public static Optional getSOAPElement(SOAPMessage soapMessage, String elementName) {
+ try {
+ NodeList nodeList = soapMessage.getSOAPBody().getElementsByTagName(elementName);
+ if (nodeList != null && nodeList.getLength() > 0) {
+ return Optional.of(nodeList.item(0).getTextContent());
+ }
+ } catch (SOAPException e) {
+ // if an error occurs, returning an empty Optional is fine
+ }
+ return Optional.empty();
+ }
+}
diff --git a/bundles/org.openhab.binding.tr064/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.tr064/src/main/resources/OH-INF/binding/binding.xml
new file mode 100644
index 000000000..c50a9f7fc
--- /dev/null
+++ b/bundles/org.openhab.binding.tr064/src/main/resources/OH-INF/binding/binding.xml
@@ -0,0 +1,10 @@
+
+
+
+ TR-064 Binding
+ This is the binding for TR-064 device support.
+ Jan N. Klug
+
+
diff --git a/bundles/org.openhab.binding.tr064/src/main/resources/OH-INF/config/phonebookProfile.xml b/bundles/org.openhab.binding.tr064/src/main/resources/OH-INF/config/phonebookProfile.xml
new file mode 100644
index 000000000..f603d7d21
--- /dev/null
+++ b/bundles/org.openhab.binding.tr064/src/main/resources/OH-INF/config/phonebookProfile.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+ The name of the the phonebook
+
+
+
+ The number of digits matching the incoming value, counted from far right (default is 0 = all matching)
+
+
+
diff --git a/bundles/org.openhab.binding.tr064/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.tr064/src/main/resources/OH-INF/thing/thing-types.xml
new file mode 100644
index 000000000..e419d7139
--- /dev/null
+++ b/bundles/org.openhab.binding.tr064/src/main/resources/OH-INF/thing/thing-types.xml
@@ -0,0 +1,145 @@
+
+
+
+
+
+
+
+
+
+ Host name or IP address.
+ network-address
+
+
+
+ dslf-config
+
+
+
+ password
+
+
+
+ 60
+ s
+
+
+
+
+
+
+ A physical FritzBox Device.
+
+
+
+
+ Host name or IP address.
+ network-address
+
+
+
+ dslf-config
+
+
+
+ password
+
+
+
+ 60
+ s
+
+
+
+ List of answering machines (starting with 0).
+ true
+
+
+
+ List of call deflection IDs (starting with 0).
+ true
+
+
+
+ List of days for which missed calls should be calculated.
+ true
+
+
+
+ List of days for which rejected calls should be calculated.
+ true
+
+
+
+ List of days for which inbound calls should be calculated.
+ true
+
+
+
+ List of days for which outbound calls should be calculated.
+ true
+
+
+
+ List of IPs that can be blocked for WAN access.
+ true
+
+
+
+ The interval for refreshing the phonebook (disabled = 0)
+ 600
+ true
+
+
+
+
+
+
+
+
+
+
+ A virtual sub-device.
+
+
+
+
+ UUID of the sub-device
+
+
+
+ 60
+ s
+
+
+
+
+
+
+
+
+
+
+ A virtual Sub-Device (LAN).
+
+
+
+
+ UUID of the sub-device
+
+
+
+ 60
+ s
+
+
+
+ List of MACs for "online" status detection (format: 11:11:11:11:11:11).
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.tr064/src/main/resources/channels.xml b/bundles/org.openhab.binding.tr064/src/main/resources/channels.xml
new file mode 100644
index 000000000..c723baf3c
--- /dev/null
+++ b/bundles/org.openhab.binding.tr064/src/main/resources/channels.xml
@@ -0,0 +1,253 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.tr064/src/main/resources/xsd/bindings.xjb b/bundles/org.openhab.binding.tr064/src/main/resources/xsd/bindings.xjb
new file mode 100644
index 000000000..7cd2e8db7
--- /dev/null
+++ b/bundles/org.openhab.binding.tr064/src/main/resources/xsd/bindings.xjb
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.tr064/src/main/resources/xsd/channeltypes.xsd b/bundles/org.openhab.binding.tr064/src/main/resources/xsd/channeltypes.xsd
new file mode 100644
index 000000000..87a769195
--- /dev/null
+++ b/bundles/org.openhab.binding.tr064/src/main/resources/xsd/channeltypes.xsd
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.tr064/src/main/resources/xsd/phonebook.xsd b/bundles/org.openhab.binding.tr064/src/main/resources/xsd/phonebook.xsd
new file mode 100644
index 000000000..f5eaf5a7c
--- /dev/null
+++ b/bundles/org.openhab.binding.tr064/src/main/resources/xsd/phonebook.xsd
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.tr064/src/main/resources/xsd/scpddevice.xsd b/bundles/org.openhab.binding.tr064/src/main/resources/xsd/scpddevice.xsd
new file mode 100644
index 000000000..894e7d4b0
--- /dev/null
+++ b/bundles/org.openhab.binding.tr064/src/main/resources/xsd/scpddevice.xsd
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.tr064/src/main/resources/xsd/scpdservice.xsd b/bundles/org.openhab.binding.tr064/src/main/resources/xsd/scpdservice.xsd
new file mode 100644
index 000000000..bcf37a2f3
--- /dev/null
+++ b/bundles/org.openhab.binding.tr064/src/main/resources/xsd/scpdservice.xsd
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.tr064/src/test/java/org/openhab/binding/tr064/ChannelListUtilTest.java b/bundles/org.openhab.binding.tr064/src/test/java/org/openhab/binding/tr064/ChannelListUtilTest.java
new file mode 100644
index 000000000..d3267cad1
--- /dev/null
+++ b/bundles/org.openhab.binding.tr064/src/test/java/org/openhab/binding/tr064/ChannelListUtilTest.java
@@ -0,0 +1,60 @@
+/**
+ * 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.tr064;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.util.Comparator;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.tr064.internal.Tr064BindingConstants;
+import org.openhab.binding.tr064.internal.dto.config.ChannelTypeDescription;
+
+/**
+ * The {@link ChannelListUtilTest} is a tool for documentation generation
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class ChannelListUtilTest {
+
+ @Test
+ public void createChannelListTest() {
+ try {
+ final Writer writer = new OutputStreamWriter(new FileOutputStream("target/channelList.asc"),
+ StandardCharsets.UTF_8);
+
+ Tr064BindingConstants.CHANNEL_TYPES.stream().sorted(Comparator.comparing(ChannelTypeDescription::getName))
+ .forEach(channel -> {
+ String description = channel.getDescription() == null ? channel.getLabel()
+ : channel.getDescription();
+ String channelString = String.format("| `%s` | `%s`| %s |%c", channel.getName(),
+ channel.getItem().getType(), description, 13);
+ try {
+ writer.write(channelString);
+ } catch (IOException e) {
+ Assertions.fail(e.getMessage());
+ }
+ });
+
+ writer.close();
+ } catch (IOException e) {
+ Assertions.fail(e.getMessage());
+ }
+ }
+}
diff --git a/bundles/pom.xml b/bundles/pom.xml
index afb37fa9f..73be0d30a 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -277,6 +277,7 @@
org.openhab.binding.tibber
org.openhab.binding.touchwand
org.openhab.binding.tplinksmarthome
+ org.openhab.binding.tr064
org.openhab.binding.tradfri
org.openhab.binding.unifi
org.openhab.binding.unifiedremote