]> git.basschouten.com Git - openhab-addons.git/commitdiff
[tr064] Initial contribution (#8523)
authorJ-N-K <J-N-K@users.noreply.github.com>
Tue, 3 Nov 2020 05:36:19 +0000 (06:36 +0100)
committerGitHub <noreply@github.com>
Tue, 3 Nov 2020 05:36:19 +0000 (21:36 -0800)
* Initial contribution

Signed-off-by: Jan N. Klug <jan.n.klug@rub.de>
42 files changed:
CODEOWNERS
bom/openhab-addons/pom.xml
bundles/org.openhab.binding.tr064/NOTICE [new file with mode: 0644]
bundles/org.openhab.binding.tr064/README.md [new file with mode: 0644]
bundles/org.openhab.binding.tr064/pom.xml [new file with mode: 0644]
bundles/org.openhab.binding.tr064/src/main/feature/feature.xml [new file with mode: 0644]
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/AvmFritzTlsTrustManagerProvider.java [new file with mode: 0644]
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/ChannelConfigException.java [new file with mode: 0644]
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/PostProcessingException.java [new file with mode: 0644]
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/SCPDException.java [new file with mode: 0644]
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/SOAPConnector.java [new file with mode: 0644]
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/SOAPValueConverter.java [new file with mode: 0644]
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064BindingConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064ChannelTypeProvider.java [new file with mode: 0644]
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064CommunicationException.java [new file with mode: 0644]
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064DiscoveryService.java [new file with mode: 0644]
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064DynamicStateDescriptionProvider.java [new file with mode: 0644]
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064HandlerFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064RootHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064SubHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/config/Tr064BaseThingConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/config/Tr064ChannelConfig.java [new file with mode: 0644]
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/config/Tr064RootConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/config/Tr064SubConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/Phonebook.java [new file with mode: 0644]
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/PhonebookProfile.java [new file with mode: 0644]
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/PhonebookProfileFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/PhonebookProvider.java [new file with mode: 0644]
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/Tr064PhonebookImpl.java [new file with mode: 0644]
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/util/SCPDUtil.java [new file with mode: 0644]
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/util/Util.java [new file with mode: 0644]
bundles/org.openhab.binding.tr064/src/main/resources/OH-INF/binding/binding.xml [new file with mode: 0644]
bundles/org.openhab.binding.tr064/src/main/resources/OH-INF/config/phonebookProfile.xml [new file with mode: 0644]
bundles/org.openhab.binding.tr064/src/main/resources/OH-INF/thing/thing-types.xml [new file with mode: 0644]
bundles/org.openhab.binding.tr064/src/main/resources/channels.xml [new file with mode: 0644]
bundles/org.openhab.binding.tr064/src/main/resources/xsd/bindings.xjb [new file with mode: 0644]
bundles/org.openhab.binding.tr064/src/main/resources/xsd/channeltypes.xsd [new file with mode: 0644]
bundles/org.openhab.binding.tr064/src/main/resources/xsd/phonebook.xsd [new file with mode: 0644]
bundles/org.openhab.binding.tr064/src/main/resources/xsd/scpddevice.xsd [new file with mode: 0644]
bundles/org.openhab.binding.tr064/src/main/resources/xsd/scpdservice.xsd [new file with mode: 0644]
bundles/org.openhab.binding.tr064/src/test/java/org/openhab/binding/tr064/ChannelListUtilTest.java [new file with mode: 0644]
bundles/pom.xml

index 7222bf6c389ccdfe6d67e16afaeb31fbb46ab160..1a4e6e29fb6b61f083c314c78d190b9edfd297a3 100644 (file)
 /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
index 169663f1803cecfbfc3709be998c3a2c5357d300..c6579d85a33b13f59c1082c3e7c334b038c8cb10 100644 (file)
       <artifactId>org.openhab.binding.tplinksmarthome</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.openhab.addons.bundles</groupId>
+      <artifactId>org.openhab.binding.tr064</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>org.openhab.addons.bundles</groupId>
       <artifactId>org.openhab.binding.tradfri</artifactId>
diff --git a/bundles/org.openhab.binding.tr064/NOTICE b/bundles/org.openhab.binding.tr064/NOTICE
new file mode 100644 (file)
index 0000000..4c20ef4
--- /dev/null
@@ -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 (file)
index 0000000..4296995
--- /dev/null
@@ -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 (file)
index 0000000..8c71c14
--- /dev/null
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.openhab.addons.bundles</groupId>
+    <artifactId>org.openhab.addons.reactor.bundles</artifactId>
+    <version>3.0.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>org.openhab.binding.tr064</artifactId>
+
+  <name>openHAB Add-ons :: Bundles :: TR-064 Binding</name>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.jvnet.jaxb2.maven2</groupId>
+        <artifactId>maven-jaxb2-plugin</artifactId>
+        <version>0.14.0</version>
+        <executions>
+          <execution>
+            <id>generate-jaxb-sources</id>
+            <goals>
+              <goal>generate</goal>
+            </goals>
+          </execution>
+        </executions>
+        <configuration>
+          <schemaDirectory>src/main/resources/xsd</schemaDirectory>
+          <noFileHeader>true</noFileHeader>
+          <locale>en</locale>
+          <episode>false</episode>
+          <extension>true</extension>
+          <args>
+            <arg>-Xxew</arg>
+            <arg>-Xxew:instantiate early</arg>
+          </args>
+          <plugins>
+            <plugin>
+              <groupId>com.github.jaxb-xew-plugin</groupId>
+              <artifactId>jaxb-xew-plugin</artifactId>
+              <version>1.10</version>
+            </plugin>
+          </plugins>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+</project>
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 (file)
index 0000000..9f9ca83
--- /dev/null
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<features name="org.openhab.binding.tr064-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
+       <repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
+
+       <feature name="openhab-binding-tr064" description="TR-064 Binding" version="${project.version}">
+               <feature>openhab-runtime-base</feature>
+               <requirement>openhab.tp;filter:="(feature=jaxb)"</requirement>
+               <feature dependency="true">openhab.tp-jaxb</feature>
+               <requirement>openhab.tp;filter:="(feature=jax-ws)"</requirement>
+               <feature dependency="true">openhab.tp-jaxws</feature>
+               <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.tr064/${project.version}</bundle>
+       </feature>
+</features>
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 (file)
index 0000000..1f8f24f
--- /dev/null
@@ -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 (file)
index 0000000..3629279
--- /dev/null
@@ -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 (file)
index 0000000..9b2c0dd
--- /dev/null
@@ -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 (file)
index 0000000..f9af40c
--- /dev/null
@@ -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 (file)
index 0000000..9abf59e
--- /dev/null
@@ -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<String, String> 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<MimeHeader>) 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<String, String> 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<String, String> 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<ChannelUID, Tr064ChannelConfig> channelConfigMap, ExpiringCacheMap<ChannelUID, State> 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<String, String> 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<ChannelUID, Tr064ChannelConfig> 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 (file)
index 0000000..214a49b
--- /dev/null
@@ -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<String> 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<State> 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("<New>1</New>").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>" + type + "</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 (file)
index 0000000..c8cbaaa
--- /dev/null
@@ -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<ChannelTypeDescription> 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 (file)
index 0000000..d9c6e38
--- /dev/null
@@ -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<ChannelTypeUID, ChannelType> 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<StateChannelTypeBuilder> 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<ChannelType> 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 (file)
index 0000000..8e1bee6
--- /dev/null
@@ -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 (file)
index 0000000..f48c3f6
--- /dev/null
@@ -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<ThingTypeUID> 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<ThingTypeUID> 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<SCPDDeviceType> 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<String, Object> 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 (file)
index 0000000..4cd4fa6
--- /dev/null
@@ -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<ChannelUID, StateDescription> 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 (file)
index 0000000..c40dd50
--- /dev/null
@@ -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<ThingTypeUID> 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 (file)
index 0000000..af03c1d
--- /dev/null
@@ -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<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_GENERIC, THING_TYPE_FRITZBOX);
+    private static final int RETRY_INTERVAL = 60;
+    private static final Set<String> 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<ChannelUID, Tr064ChannelConfig> channels = new HashMap<>();
+    // caching is used to prevent excessive calls to the same action
+    private final ExpiringCacheMap<ChannelUID, State> stateCache = new ExpiringCacheMap<>(2000);
+    private Collection<Phonebook> 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<String, String> 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<SCPDDeviceType> 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<Phonebook> processPhonebookList(SOAPMessage soapMessagePhonebookList,
+            SCPDServiceType scpdService) {
+        SOAPValueConverter soapValueConverter = new SOAPValueConverter(httpClient);
+        return (Collection<Phonebook>) 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<SCPDServiceType> 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.<Phonebook> emptyList();
+            }
+        }).orElse(Collections.emptyList());
+
+        if (phonebooks.isEmpty()) {
+            logger.warn("Could not get phonebooks for thing {}", thing.getUID());
+        }
+    }
+
+    @Override
+    public Optional<Phonebook> getPhonebookByName(String name) {
+        return phonebooks.stream().filter(p -> name.equals(p.getName())).findAny();
+    }
+
+    @Override
+    public Collection<Phonebook> 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<Class<? extends ThingHandlerService>> 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 (file)
index 0000000..ab70fdd
--- /dev/null
@@ -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<ThingTypeUID> 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<ChannelUID, Tr064ChannelConfig> channels = new HashMap<>();
+    // caching is used to prevent excessive calls to the same action
+    private final ExpiringCacheMap<ChannelUID, State> 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<String, String> 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 (file)
index 0000000..cc63ed7
--- /dev/null
@@ -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 (file)
index 0000000..d0f77c8
--- /dev/null
@@ -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 (file)
index 0000000..51b391e
--- /dev/null
@@ -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<String> tamIndices = Collections.emptyList();
+    public List<String> callDeflectionIndices = Collections.emptyList();
+    public List<String> missedCallDays = Collections.emptyList();
+    public List<String> rejectedCallDays = Collections.emptyList();
+    public List<String> inboundCallDays = Collections.emptyList();
+    public List<String> 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 (file)
index 0000000..41e40b7
--- /dev/null
@@ -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<String> 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 (file)
index 0000000..fb7e117
--- /dev/null
@@ -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<String> 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 (file)
index 0000000..33bc639
--- /dev/null
@@ -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<ThingUID, PhonebookProvider> phonebookProviders;
+    private final int matchCount;
+
+    public PhonebookProfile(ProfileCallback callback, ProfileContext context,
+            Map<ThingUID, PhonebookProvider> 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<String> 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 (file)
index 0000000..054e09c
--- /dev/null
@@ -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<ThingUID, PhonebookProvider> phonebookProviders = new ConcurrentHashMap<>();
+
+    @Override
+    public @Nullable Profile createProfile(ProfileTypeUID profileTypeUID, ProfileCallback callback,
+            ProfileContext profileContext) {
+        return new PhonebookProfile(callback, profileContext, phonebookProviders);
+    }
+
+    @Override
+    public Collection<ProfileTypeUID> getSupportedProfileTypeUIDs() {
+        return Collections.singleton(PhonebookProfile.PHONEBOOK_PROFILE_TYPE_UID);
+    }
+
+    @Override
+    public Collection<ProfileType> 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<ParameterOption> createPhonebookList(Map.Entry<ThingUID, PhonebookProvider> entry) {
+        String thingUid = UIDUtils.encode(entry.getKey().toString());
+        String thingName = entry.getValue().getFriendlyName();
+
+        Stream<ParameterOption> 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<ParameterOption> 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 (file)
index 0000000..5a41c01
--- /dev/null
@@ -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<Phonebook> getPhonebookByName(String name);
+
+    Collection<Phonebook> 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 (file)
index 0000000..bf105c3
--- /dev/null
@@ -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<String, String> 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<String> 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 (file)
index 0000000..3de7469
--- /dev/null
@@ -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<SCPDDeviceType> scpdDevicesList = new ArrayList<>();
+    private final Map<String, SCPDScpdType> 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 <T> @Nullable T getAndUnmarshalSCPD(String uri, Class<T> 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<SCPDDeviceType> 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<SCPDDeviceType> 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<SCPDDeviceType> 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<SCPDScpdType> 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 (file)
index 0000000..a3146bc
--- /dev/null
@@ -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<ChannelTypeDescription> readXMLChannelConfig() {
+        try {
+            InputStream resource = Thread.currentThread().getContextClassLoader().getResourceAsStream("channels.xml");
+            JAXBContext context = JAXBContext.newInstance(ChannelTypeDescriptions.class);
+            Unmarshaller um = context.createUnmarshaller();
+            JAXBElement<ChannelTypeDescriptions> 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<ChannelUID, Tr064ChannelConfig> 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<String> 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<String> 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<String> 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<String> 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<String> 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 (file)
index 0000000..c50a9f7
--- /dev/null
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<binding:binding id="tr064" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
+
+       <name>TR-064 Binding</name>
+       <description>This is the binding for TR-064 device support.</description>
+       <author>Jan N. Klug</author>
+
+</binding:binding>
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 (file)
index 0000000..f603d7d
--- /dev/null
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<config-description:config-descriptions
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
+       <config-description uri="profile:transform:PHONEBOOK">
+               <parameter name="phonebook" type="text" required="true">
+                       <label>Phonebook</label>
+                       <description>The name of the the phonebook</description>
+               </parameter>
+               <parameter name="matchCount" type="text" required="false">
+                       <label>Match Count</label>
+                       <description>The number of digits matching the incoming value, counted from far right (default is 0 = all matching)</description>
+               </parameter>
+       </config-description>
+</config-description:config-descriptions>
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 (file)
index 0000000..e419d71
--- /dev/null
@@ -0,0 +1,145 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="tr064"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <bridge-type id="generic">
+               <label>Generic CPE</label>
+
+               <config-description>
+                       <parameter name="host" type="text" required="true">
+                               <label>Host</label>
+                               <description>Host name or IP address.</description>
+                               <context>network-address</context>
+                       </parameter>
+                       <parameter name="user" type="text">
+                               <label>Username</label>
+                               <default>dslf-config</default>
+                       </parameter>
+                       <parameter name="password" type="text" required="true">
+                               <label>Password</label>
+                               <context>password</context>
+                       </parameter>
+                       <parameter name="refresh" type="integer" unit="s">
+                               <label>Refresh Interval</label>
+                               <default>60</default>
+                               <unitLabel>s</unitLabel>
+                       </parameter>
+               </config-description>
+       </bridge-type>
+
+       <bridge-type id="fritzbox">
+               <label>FritzBox</label>
+               <description>A physical FritzBox Device.</description>
+
+               <config-description>
+                       <parameter name="host" type="text" required="true">
+                               <label>Host</label>
+                               <description>Host name or IP address.</description>
+                               <context>network-address</context>
+                       </parameter>
+                       <parameter name="user" type="text">
+                               <label>Username</label>
+                               <default>dslf-config</default>
+                       </parameter>
+                       <parameter name="password" type="text" required="true">
+                               <label>Password</label>
+                               <context>password</context>
+                       </parameter>
+                       <parameter name="refresh" type="integer" unit="s">
+                               <label>Refresh Interval</label>
+                               <default>60</default>
+                               <unitLabel>s</unitLabel>
+                       </parameter>
+                       <parameter name="tamIndices" type="text" multiple="true">
+                               <label>TAM</label>
+                               <description>List of answering machines (starting with 0).</description>
+                               <advanced>true</advanced>
+                       </parameter>
+                       <parameter name="callDeflectionIndices" type="text" multiple="true">
+                               <label>Call Deflection</label>
+                               <description>List of call deflection IDs (starting with 0).</description>
+                               <advanced>true</advanced>
+                       </parameter>
+                       <parameter name="missedCallDays" type="text" multiple="true">
+                               <label>Missed Call Days</label>
+                               <description>List of days for which missed calls should be calculated.</description>
+                               <advanced>true</advanced>
+                       </parameter>
+                       <parameter name="rejectedCallDays" type="text" multiple="true">
+                               <label>Rejected Call Days</label>
+                               <description>List of days for which rejected calls should be calculated.</description>
+                               <advanced>true</advanced>
+                       </parameter>
+                       <parameter name="inboundCallDays" type="text" multiple="true">
+                               <label>Inbound Call Days</label>
+                               <description>List of days for which inbound calls should be calculated.</description>
+                               <advanced>true</advanced>
+                       </parameter>
+                       <parameter name="outboundCallDays" type="text" multiple="true">
+                               <label>Outbound Call Days</label>
+                               <description>List of days for which outbound calls should be calculated.</description>
+                               <advanced>true</advanced>
+                       </parameter>
+                       <parameter name="wanBlockIPs" type="text" multiple="true">
+                               <label>WAN Block IPs</label>
+                               <description>List of IPs that can be blocked for WAN access.</description>
+                               <advanced>true</advanced>
+                       </parameter>
+                       <parameter name="phonebookInterval" type="integer" min="0" unit="s">
+                               <label>Phonebook Interval</label>
+                               <description>The interval for refreshing the phonebook (disabled = 0)</description>
+                               <default>600</default>
+                               <advanced>true</advanced>
+                       </parameter>
+               </config-description>
+       </bridge-type>
+
+       <thing-type id="subdevice">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="generic"/>
+                       <bridge-type-ref id="fritzbox"/>
+               </supported-bridge-type-refs>
+               <label>Sub-Device</label>
+               <description>A virtual sub-device.</description>
+
+               <config-description>
+                       <parameter name="uuid" type="text" required="true">
+                               <label>UUID</label>
+                               <description>UUID of the sub-device</description>
+                       </parameter>
+                       <parameter name="refresh" type="integer" unit="s">
+                               <label>Refresh Interval</label>
+                               <default>60</default>
+                               <unitLabel>s</unitLabel>
+                       </parameter>
+               </config-description>
+       </thing-type>
+
+       <thing-type id="subdeviceLan" listed="false">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="generic"/>
+                       <bridge-type-ref id="fritzbox"/>
+               </supported-bridge-type-refs>
+               <label>Sub-Device (LAN)</label>
+               <description>A virtual Sub-Device (LAN).</description>
+
+               <config-description>
+                       <parameter name="uuid" type="text" required="true">
+                               <label>UUID</label>
+                               <description>UUID of the sub-device</description>
+                       </parameter>
+                       <parameter name="refresh" type="integer" unit="s">
+                               <label>Refresh Interval</label>
+                               <default>60</default>
+                               <unitLabel>s</unitLabel>
+                       </parameter>
+                       <parameter name="macOnline" type="text" multiple="true">
+                               <label>MAC Online</label>
+                               <description>List of MACs for "online" status detection (format: 11:11:11:11:11:11).</description>
+                       </parameter>
+               </config-description>
+       </thing-type>
+
+</thing:thing-descriptions>
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 (file)
index 0000000..c723baf
--- /dev/null
@@ -0,0 +1,253 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
+<channels xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="channelconfig"
+       xsi:noNamespaceSchemaLocation="xsd/channeltypes.xsd">
+       <!-- Root Device -->
+       <channel name="securityPort" label="Security Port"
+               description="The port for connecting via HTTPS to the TR-064 service." advanced="true">
+               <item type="Number"></item>
+               <service deviceType="urn:dslforum-org:device:InternetGatewayDevice:1"
+                       serviceId="urn:DeviceInfo-com:serviceId:DeviceInfo1"></service>
+               <getAction name="GetSecurityPort" argument="NewSecurityPort"/>
+       </channel>
+       <channel name="uptime" label="Uptime">
+               <item type="Number:Time" unit="s" statePattern="%d s"/>
+               <service deviceType="urn:dslforum-org:device:InternetGatewayDevice:1"
+                       serviceId="urn:DeviceInfo-com:serviceId:DeviceInfo1"/>
+               <getAction name="GetInfo" argument="NewUpTime"/>
+       </channel>
+       <channel name="deviceLog" label="Device Log" description="A string containing the last log messages.">
+               <item type="String"/>
+               <service deviceType="urn:dslforum-org:device:InternetGatewayDevice:1"
+                       serviceId="urn:DeviceInfo-com:serviceId:DeviceInfo1"/>
+               <getAction name="GetInfo" argument="NewDeviceLog"/>
+       </channel>
+       <channel name="reboot" label="Reboot">
+               <item type="Switch"/>
+               <service deviceType="urn:dslforum-org:device:InternetGatewayDevice:1"
+                       serviceId="urn:DeviceConfig-com:serviceId:DeviceConfig1"/>
+               <setAction name="Reboot"/>
+       </channel>
+       <channel name="tamEnable" label="TAM" description="Enable/Disable the answering machine with the given index.">
+               <item type="Switch"/>
+               <service deviceType="urn:dslforum-org:device:InternetGatewayDevice:1"
+                       serviceId="urn:X_AVM-DE_TAM-com:serviceId:X_AVM-DE_TAM1"/>
+               <getAction name="GetInfo" argument="NewEnable">
+                       <parameter name="NewIndex" thingParameter="tamIndices"/>
+               </getAction>
+               <setAction name="SetEnable" argument="NewEnable">
+                       <parameter name="NewIndex" thingParameter="tamIndices"/>
+               </setAction>
+       </channel>
+       <channel name="tamNewMessages" label="TAM New Messages"
+               description="The number of new messages of the given answering machine.">
+               <item type="Number"/>
+               <service deviceType="urn:dslforum-org:device:InternetGatewayDevice:1"
+                       serviceId="urn:X_AVM-DE_TAM-com:serviceId:X_AVM-DE_TAM1"/>
+               <getAction name="GetMessageList" argument="NewURL" postProcessor="processTamListURL">
+                       <parameter name="NewIndex" thingParameter="tamIndices"/>
+               </getAction>
+       </channel>
+       <channel name="callDeflectionEnable" label="Call Deflection"
+               description="Enable/Disable the call deflection setup with the given index.">
+               <item type="Switch"/>
+               <service deviceType="urn:dslforum-org:device:InternetGatewayDevice:1"
+                       serviceId="urn:X_AVM-DE_OnTel-com:serviceId:X_AVM-DE_OnTel1"/>
+               <getAction name="GetDeflection" argument="NewEnable">
+                       <parameter name="NewDeflectionId" thingParameter="callDeflectionIndices"/>
+               </getAction>
+               <setAction name="SetDeflectionEnable" argument="NewEnable">
+                       <parameter name="NewDeflectionId" thingParameter="callDeflectionIndices"/>
+               </setAction>
+       </channel>
+       <channel name="wanBlockByIP" label="WAN Block By IP" description="Block/Unblock WAN access with the given IP.">
+               <item type="Switch"/>
+               <service deviceType="urn:dslforum-org:device:InternetGatewayDevice:1"
+                       serviceId="urn:X_AVM-DE_HostFilter-com:serviceId:X_AVM-DE_HostFilter1"/>
+               <getAction name="GetWANAccessByIP" argument="NewDisallow">
+                       <parameter name="NewIPv4Address" thingParameter="wanBlockIPs"/>
+               </getAction>
+               <setAction name="DisallowWANAccessByIP" argument="NewDisallow">
+                       <parameter name="NewIPv4Address" thingParameter="wanBlockIPs"/>
+               </setAction>
+       </channel>
+       <channel name="missedCalls" label="Missed Calls"
+               description="Number of missed calls within the given number of days.">
+               <item type="Number"/>
+               <service deviceType="urn:dslforum-org:device:InternetGatewayDevice:1"
+                       serviceId="urn:X_AVM-DE_OnTel-com:serviceId:X_AVM-DE_OnTel1"/>
+               <getAction name="GetCallList" argument="NewCallListURL" postProcessor="processMissedCalls">
+                       <parameter name="CallDays" thingParameter="missedCallDays" pattern="[0-9]+" internalOnly="true"/>
+               </getAction>
+       </channel>
+       <channel name="rejectedCalls" label="Rejected Calls"
+               description="Number of rejected calls within the given number of days.">
+               <item type="Number"/>
+               <service deviceType="urn:dslforum-org:device:InternetGatewayDevice:1"
+                       serviceId="urn:X_AVM-DE_OnTel-com:serviceId:X_AVM-DE_OnTel1"/>
+               <getAction name="GetCallList" argument="NewCallListURL" postProcessor="processRejectedCalls">
+                       <parameter name="CallDays" thingParameter="rejectedCallDays" pattern="[0-9]+" internalOnly="true"/>
+               </getAction>
+       </channel>
+       <channel name="inboundCalls" label="Inbound Calls"
+               description="Number of inbound calls within the given number of days.">
+               <item type="Number"/>
+               <service deviceType="urn:dslforum-org:device:InternetGatewayDevice:1"
+                       serviceId="urn:X_AVM-DE_OnTel-com:serviceId:X_AVM-DE_OnTel1"/>
+               <getAction name="GetCallList" argument="NewCallListURL" postProcessor="processInboundCalls">
+                       <parameter name="CallDays" thingParameter="inboundCallDays" pattern="[0-9]+" internalOnly="true"/>
+               </getAction>
+       </channel>
+       <channel name="outboundCalls" label="Outbound Calls"
+               description="Number of outbound calls within the given number of days.">
+               <item type="Number"/>
+               <service deviceType="urn:dslforum-org:device:InternetGatewayDevice:1"
+                       serviceId="urn:X_AVM-DE_OnTel-com:serviceId:X_AVM-DE_OnTel1"/>
+               <getAction name="GetCallList" argument="NewCallListURL" postProcessor="processOutboundCalls">
+                       <parameter name="CallDays" thingParameter="outboundCallDays" pattern="[0-9]+" internalOnly="true"/>
+               </getAction>
+       </channel>
+
+       <!-- LAN Device -->
+       <channel name="wifi24GHzEnable" label="WiFi 2.4 GHz" description="Enable/Disable the 2.4 GHz WiFi device.">
+               <item type="Switch"/>
+               <service deviceType="urn:dslforum-org:device:LANDevice:1"
+                       serviceId="urn:WLANConfiguration-com:serviceId:WLANConfiguration1"/>
+               <getAction name="GetInfo" argument="NewEnable"/>
+               <setAction name="SetEnable" argument="NewEnable"/>
+       </channel>
+       <channel name="wifi5GHzEnable" label="WiFi 5 GHz" description="Enable/Disable the 5.0 GHz WiFi device.">
+               <item type="Switch"/>
+               <service deviceType="urn:dslforum-org:device:LANDevice:1"
+                       serviceId="urn:WLANConfiguration-com:serviceId:WLANConfiguration2"/>
+               <getAction name="GetInfo" argument="NewEnable"/>
+               <setAction name="SetEnable" argument="NewEnable"/>
+       </channel>
+       <channel name="wifiGuestEnable" label="WiFi Guest" description="Enable/Disable the guest WiFi.">
+               <item type="Switch"/>
+               <service deviceType="urn:dslforum-org:device:LANDevice:1"
+                       serviceId="urn:WLANConfiguration-com:serviceId:WLANConfiguration3"/>
+               <getAction name="GetInfo" argument="NewEnable"/>
+               <setAction name="SetEnable" argument="NewEnable"/>
+       </channel>
+       <channel name="macOnline" label="MAC Online" description="Online status of the device with the given MAC">
+               <item type="Switch"/>
+               <service deviceType="urn:dslforum-org:device:LANDevice:1" serviceId="urn:LanDeviceHosts-com:serviceId:Hosts1"/>
+               <getAction name="GetSpecificHostEntry" argument="NewActive">
+                       <parameter name="NewMACAddress" thingParameter="macOnline" pattern="([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}"/>
+               </getAction>
+       </channel>
+
+       <!-- WAN Device -->
+       <channel name="wanAccessType" label="Access Type">
+               <item type="String"/>
+               <service deviceType="urn:dslforum-org:device:WANDevice:1"
+                       serviceId="urn:WANCIfConfig-com:serviceId:WANCommonInterfaceConfig1"/>
+               <getAction name="GetCommonLinkProperties" argument="NewWANAccessType"/>
+       </channel>
+       <channel name="wanPhysicalLinkStatus" label="Link Status">
+               <item type="String"/>
+               <service deviceType="urn:dslforum-org:device:WANDevice:1"
+                       serviceId="urn:WANCIfConfig-com:serviceId:WANCommonInterfaceConfig1"/>
+               <getAction name="GetCommonLinkProperties" argument="NewPhysicalLinkStatus"/>
+       </channel>
+       <channel name="wanMaxDownstreamRate" label="Max Downstream Rate">
+               <item type="Number:DataTransferRate" unit="bit/s" statePattern="%.1f Mbit/s"/>
+               <service deviceType="urn:dslforum-org:device:WANDevice:1"
+                       serviceId="urn:WANCIfConfig-com:serviceId:WANCommonInterfaceConfig1"/>
+               <getAction name="GetCommonLinkProperties" argument="NewLayer1DownstreamMaxBitRate"/>
+       </channel>
+       <channel name="wanMaxUpstreamRate" label="Max Upstream Rate">
+               <item type="Number:DataTransferRate" unit="bit/s" statePattern="%.1f Mbit/s"/>
+               <service deviceType="urn:dslforum-org:device:WANDevice:1"
+                       serviceId="urn:WANCIfConfig-com:serviceId:WANCommonInterfaceConfig1"/>
+               <getAction name="GetCommonLinkProperties" argument="NewLayer1UpstreamMaxBitRate"/>
+       </channel>
+       <channel name="wanTotalBytesReceived" label="Total Bytes Received">
+               <item type="Number:DataAmount" unit="B" statePattern="%.3f Gio"/>
+               <service deviceType="urn:dslforum-org:device:WANDevice:1"
+                       serviceId="urn:WANCIfConfig-com:serviceId:WANCommonInterfaceConfig1"/>
+               <getAction name="GetTotalBytesReceived" argument="NewTotalBytesReceived"/>
+       </channel>
+       <channel name="wanTotalBytesSent" label="Total Bytes Send">
+               <item type="Number:DataAmount" unit="B" statePattern="%.3f Gio"/>
+               <service deviceType="urn:dslforum-org:device:WANDevice:1"
+                       serviceId="urn:WANCIfConfig-com:serviceId:WANCommonInterfaceConfig1"/>
+               <getAction name="GetTotalBytesSent" argument="NewTotalBytesSent"/>
+       </channel>
+       <channel name="dslEnable" label="DSL Enable">
+               <item type="Switch"/>
+               <service deviceType="urn:dslforum-org:service:WANDSLInterfaceConfig:1"
+                       serviceId="urn:WANDSLIfConfig-com:serviceId:WANDSLInterfaceConfig1"/>
+               <getAction name="GetInfo" argument="NewEnable"/>
+       </channel>
+       <channel name="dslStatus" label="DSL Status">
+               <item type="Switch"/>
+               <service deviceType="urn:dslforum-org:service:WANDSLInterfaceConfig:1"
+                       serviceId="urn:WANDSLIfConfig-com:serviceId:WANDSLInterfaceConfig1"/>
+               <getAction name="GetInfo" argument="NewStatus"/>
+       </channel>
+       <channel name="dslDownstreamNoiseMargin" label="DSL Downstream Noise Margin">
+               <item type="Number:Dimensionless" unit="dB" statePattern="%.1f dB"/>
+               <service deviceType="urn:dslforum-org:service:WANDSLInterfaceConfig:1"
+                       serviceId="urn:WANDSLIfConfig-com:serviceId:WANDSLInterfaceConfig1"/>
+               <getAction name="GetInfo" argument="NewDownstreamNoiseMargin"/>
+       </channel>
+       <channel name="dslUpstreamNoiseMargin" label="DSL Upstream Noise Margin">
+               <item type="Number:Dimensionless" unit="dB" statePattern="%.1f dB"/>
+               <service deviceType="urn:dslforum-org:service:WANDSLInterfaceConfig:1"
+                       serviceId="urn:WANDSLIfConfig-com:serviceId:WANDSLInterfaceConfig1"/>
+               <getAction name="GetInfo" argument="NewUpstreamNoiseMargin"/>
+       </channel>
+       <channel name="dslDownstreamNoiseMargin" label="DSL Downstream Attenuation">
+               <item type="Number:Dimensionless" unit="dB" statePattern="%.1f dB"/>
+               <service deviceType="urn:dslforum-org:service:WANDSLInterfaceConfig:1"
+                       serviceId="urn:WANDSLIfConfig-com:serviceId:WANDSLInterfaceConfig1"/>
+               <getAction name="GetInfo" argument="NewDownstreamAttenuation"/>
+       </channel>
+       <channel name="dslUpstreamNoiseMargin" label="DSL Upstream Attenuation">
+               <item type="Number:Dimensionless" unit="dB" statePattern="%.1f dB"/>
+               <service deviceType="urn:dslforum-org:service:WANDSLInterfaceConfig:1"
+                       serviceId="urn:WANDSLIfConfig-com:serviceId:WANDSLInterfaceConfig1"/>
+               <getAction name="GetInfo" argument="NewUpstreamAttenuation"/>
+       </channel>
+       <channel name="dslFECErrors" label="DSL FEC Errors">
+               <item type="Number:Dimensionless"/>
+               <service deviceType="urn:dslforum-org:service:WANDSLInterfaceConfig:1"
+                       serviceId="urn:WANDSLIfConfig-com:serviceId:WANDSLInterfaceConfig1"/>
+               <getAction name="GetStatisticsTotal" argument="NewFECErrors"/>
+       </channel>
+       <channel name="dslHECErrors" label="DSL HEC Errors">
+               <item type="Number:Dimensionless"/>
+               <service deviceType="urn:dslforum-org:service:WANDSLInterfaceConfig:1"
+                       serviceId="urn:WANDSLIfConfig-com:serviceId:WANDSLInterfaceConfig1"/>
+               <getAction name="GetStatisticsTotal" argument="NewHECErrors"/>
+       </channel>
+       <channel name="dslCRCErrors" label="DSL CRC Errors">
+               <item type="Number:Dimensionless"/>
+               <service deviceType="urn:dslforum-org:service:WANDSLInterfaceConfig:1"
+                       serviceId="urn:WANDSLIfConfig-com:serviceId:WANDSLInterfaceConfig1"/>
+               <getAction name="GetStatisticsTotal" argument="NeCRCErrors"/>
+       </channel>
+
+       <!-- WAN Connection device -->
+       <channel name="wanIpAddress" label="WAN IP Address">
+               <item type="String"/>
+               <service deviceType="urn:dslforum-org:device:WANConnectionDevice:1"
+                       serviceId="urn:WANIPConnection-com:serviceId:WANIPConnection1"/>
+               <getAction name="GetInfo" argument="NewExternalIPAddress"/>
+       </channel>
+       <channel name="wanConnectionStatus" label="Connection Status">
+               <item type="String"/>
+               <service deviceType="urn:dslforum-org:device:WANConnectionDevice:1"
+                       serviceId="urn:WANIPConnection-com:serviceId:WANIPConnection1"/>
+               <getAction name="GetInfo" argument="NewConnectionStatus"/>
+       </channel>
+       <channel name="uptime" label="Uptime">
+               <item type="Number:Time" unit="s" statePattern="%d s"/>
+               <service deviceType="urn:dslforum-org:device:WANConnectionDevice:1"
+                       serviceId="urn:WANIPConnection-com:serviceId:WANIPConnection1"/>
+               <getAction name="GetInfo" argument="NewUptime"/>
+       </channel>
+
+
+</channels>
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 (file)
index 0000000..7cd2e8d
--- /dev/null
@@ -0,0 +1,39 @@
+<jaxb:bindings xmlns:jaxb="http://java.sun.com/xml/ns/jaxb" xmlns:xjc="http://java.sun.com/xml/ns/jaxb/xjc"
+               version="2.0">
+    <jaxb:globalBindings>
+        <xjc:serializable uid="1"/>
+    </jaxb:globalBindings>
+
+    <jaxb:bindings schemaLocation="phonebook.xsd">
+        <jaxb:schemaBindings>
+            <jaxb:package name="org.openhab.binding.tr064.internal.dto.phonebook"/>
+        </jaxb:schemaBindings>
+    </jaxb:bindings>
+
+    <jaxb:bindings schemaLocation="channeltypes.xsd">
+        <jaxb:schemaBindings>
+            <jaxb:package name="org.openhab.binding.tr064.internal.dto.config"/>
+        </jaxb:schemaBindings>
+    </jaxb:bindings>
+
+    <jaxb:bindings schemaLocation="scpdservice.xsd">
+        <jaxb:schemaBindings>
+            <jaxb:package name="org.openhab.binding.tr064.internal.dto.scpd.service"/>
+            <jaxb:nameXmlTransform>
+                <jaxb:typeName prefix="SCPD"/>
+                <jaxb:anonymousTypeName prefix="SCPD"/>
+            </jaxb:nameXmlTransform>
+        </jaxb:schemaBindings>
+    </jaxb:bindings>
+
+    <jaxb:bindings schemaLocation="scpddevice.xsd">
+        <jaxb:schemaBindings>
+            <jaxb:package name="org.openhab.binding.tr064.internal.dto.scpd.root"/>
+            <jaxb:nameXmlTransform>
+                <jaxb:typeName prefix="SCPD"/>
+                <jaxb:anonymousTypeName prefix="SCPD"/>
+            </jaxb:nameXmlTransform>
+        </jaxb:schemaBindings>
+    </jaxb:bindings>
+
+</jaxb:bindings>
\ 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 (file)
index 0000000..87a7691
--- /dev/null
@@ -0,0 +1,56 @@
+<xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema"
+           xmlns="channelconfig" targetNamespace="channelconfig" xmlns:xa="http://www.w3.org/2001/XMLSchema">
+    <xs:element name="channels" type="channelTypeDescriptions"/>
+    <xs:complexType name="itemType">
+        <xs:simpleContent>
+            <xs:extension base="xs:string">
+                <xs:attribute type="xs:string" name="type" use="required"/>
+                <xs:attribute type="xs:string" name="unit" default=""/>
+                <xs:attribute type="xs:string" name="statePattern"/>
+            </xs:extension>
+        </xs:simpleContent>
+    </xs:complexType>
+    <xs:complexType name="serviceType">
+        <xs:simpleContent>
+            <xs:extension base="xs:string">
+                <xs:attribute type="xs:string" name="deviceType" use="required"/>
+                <xs:attribute type="xs:string" name="serviceId" use="required"/>
+            </xs:extension>
+        </xs:simpleContent>
+    </xs:complexType>
+    <xs:complexType name="parameterType">
+        <xs:simpleContent>
+            <xs:extension base="xs:string">
+                <xs:attribute type="xs:string" name="name" use="required"/>
+                <xs:attribute type="xs:string" name="thingParameter" use="required"/>
+                <xs:attribute type="xs:string" name="pattern"/>
+                <xs:attribute type="xs:boolean" name="internalOnly" default="false"/>
+            </xs:extension>
+        </xs:simpleContent>
+    </xs:complexType>
+    <xs:complexType name="actionType">
+        <xs:sequence>
+            <xs:element type="parameterType" name="parameter" minOccurs="0"/>
+        </xs:sequence>
+        <xs:attribute type="xs:string" name="name" use="required"/>
+        <xs:attribute type="xs:string" name="argument"/>
+        <xs:attribute type="xs:string" name="postProcessor"/>
+    </xs:complexType>
+    <xs:complexType name="channelTypeDescription">
+        <xs:sequence>
+            <xs:element type="itemType" name="item"/>
+            <xs:element type="serviceType" name="service"/>
+            <xs:element type="actionType" name="getAction" minOccurs="0"/>
+            <xs:element type="actionType" name="setAction" minOccurs="0"/>
+        </xs:sequence>
+        <xs:attribute type="xs:string" name="name" use="required"/>
+        <xs:attribute type="xs:string" name="label"/>
+        <xs:attribute type="xs:string" name="description"/>
+        <xs:attribute type="xs:boolean" name="advanced" default="false"/>
+    </xs:complexType>
+    <xs:complexType name="channelTypeDescriptions">
+        <xs:sequence>
+            <xs:element type="channelTypeDescription" name="channel" maxOccurs="unbounded"/>
+        </xs:sequence>
+    </xs:complexType>
+</xs:schema>
\ 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 (file)
index 0000000..f5eaf5a
--- /dev/null
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified"
+           xmlns:xs="http://www.w3.org/2001/XMLSchema">
+    <xs:element name="phonebooks" type="phonebooksType"/>
+    <xs:complexType name="personType">
+        <xs:sequence>
+            <xs:element type="xs:string" name="realName"/>
+        </xs:sequence>
+    </xs:complexType>
+    <xs:complexType name="numberType">
+        <xs:simpleContent>
+            <xs:extension base="xs:string">
+                <xs:attribute type="xs:string" name="type" use="optional"/>
+                <xs:attribute type="xs:string" name="vanity" use="optional"/>
+                <xs:attribute type="xs:string" name="prio" use="optional"/>
+                <xs:attribute type="xs:byte" name="quickdial" use="optional"/>
+            </xs:extension>
+        </xs:simpleContent>
+    </xs:complexType>
+    <xs:complexType name="telephonyType">
+        <xs:sequence>
+            <xs:element type="xs:string" name="services"/>
+            <xs:element type="numberType" name="number" maxOccurs="unbounded" minOccurs="0"/>
+        </xs:sequence>
+    </xs:complexType>
+    <xs:complexType name="contactType">
+        <xs:sequence>
+            <xs:element name="category">
+                <xs:simpleType>
+                    <xs:restriction base="xs:byte">
+                        <xs:enumeration value="0"/>
+                        <xs:enumeration value="1"/>
+                    </xs:restriction>
+                </xs:simpleType>
+            </xs:element>
+            <xs:element type="personType" name="person"/>
+            <xs:element type="xs:byte" name="uniqueid"/>
+            <xs:element type="telephonyType" name="telephony"/>
+        </xs:sequence>
+    </xs:complexType>
+    <xs:complexType name="phonebookType">
+        <xs:sequence>
+            <xs:element type="xs:int" name="timestamp"/>
+            <xs:element type="contactType" name="contact" maxOccurs="unbounded" minOccurs="0"/>
+        </xs:sequence>
+        <xs:attribute type="xs:byte" name="owner"/>
+        <xs:attribute type="xs:string" name="name"/>
+    </xs:complexType>
+    <xs:complexType name="phonebooksType">
+        <xs:sequence>
+            <xs:element type="phonebookType" name="phonebook"/>
+        </xs:sequence>
+    </xs:complexType>
+</xs:schema>
\ 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 (file)
index 0000000..894e7d4
--- /dev/null
@@ -0,0 +1,79 @@
+<xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema"
+           xmlns="urn:dslforum-org:device-1-0" targetNamespace="urn:dslforum-org:device-1-0">
+    <xs:element name="root" type="rootType"/>
+    <xs:complexType name="specVersionType">
+        <xs:sequence>
+            <xs:element type="xs:byte" name="major"/>
+            <xs:element type="xs:byte" name="minor"/>
+        </xs:sequence>
+    </xs:complexType>
+    <xs:complexType name="systemVersionType">
+        <xs:sequence>
+            <xs:element type="xs:short" name="HW"/>
+            <xs:element type="xs:short" name="Major"/>
+            <xs:element type="xs:byte" name="Minor"/>
+            <xs:element type="xs:byte" name="Patch"/>
+            <xs:element type="xs:int" name="Buildnumber"/>
+            <xs:element type="xs:string" name="Display"/>
+        </xs:sequence>
+    </xs:complexType>
+    <xs:complexType name="iconType">
+        <xs:sequence>
+            <xs:element type="xs:string" name="mimetype"/>
+            <xs:element type="xs:byte" name="width"/>
+            <xs:element type="xs:byte" name="height"/>
+            <xs:element type="xs:byte" name="depth"/>
+            <xs:element type="xs:string" name="url"/>
+        </xs:sequence>
+    </xs:complexType>
+    <xs:complexType name="iconListType">
+        <xs:sequence>
+            <xs:element type="iconType" name="icon"/>
+        </xs:sequence>
+    </xs:complexType>
+    <xs:complexType name="serviceType">
+        <xs:sequence>
+            <xs:element type="xs:string" name="serviceType"/>
+            <xs:element type="xs:string" name="serviceId"/>
+            <xs:element type="xs:string" name="controlURL"/>
+            <xs:element type="xs:string" name="eventSubURL"/>
+            <xs:element type="xs:string" name="SCPDURL"/>
+        </xs:sequence>
+    </xs:complexType>
+    <xs:complexType name="serviceListType">
+        <xs:sequence>
+            <xs:element type="serviceType" name="service" maxOccurs="unbounded" minOccurs="0"/>
+        </xs:sequence>
+    </xs:complexType>
+    <xs:complexType name="deviceType">
+        <xs:sequence>
+            <xs:element type="xs:string" name="deviceType"/>
+            <xs:element type="xs:string" name="friendlyName"/>
+            <xs:element type="xs:string" name="manufacturer"/>
+            <xs:element type="xs:anyURI" name="manufacturerURL"/>
+            <xs:element type="xs:string" name="modelDescription"/>
+            <xs:element type="xs:string" name="modelName"/>
+            <xs:element type="xs:string" name="modelNumber"/>
+            <xs:element type="xs:anyURI" name="modelURL"/>
+            <xs:element type="xs:string" name="UDN"/>
+            <xs:element type="xs:string" name="UPC" minOccurs="0"/>
+            <xs:element type="iconListType" name="iconList" minOccurs="0"/>
+            <xs:element type="serviceListType" name="serviceList"/>
+            <xs:element type="deviceListType" name="deviceList" minOccurs="0"/>
+            <xs:element type="xs:anyURI" name="presentationURL" minOccurs="0"/>
+        </xs:sequence>
+    </xs:complexType>
+    <xs:complexType name="deviceListType">
+        <xs:sequence>
+            <xs:element type="deviceType" name="device" maxOccurs="unbounded" minOccurs="0"/>
+        </xs:sequence>
+    </xs:complexType>
+    <xs:complexType name="rootType">
+        <xs:sequence>
+            <xs:element type="specVersionType" name="specVersion"/>
+            <xs:element type="systemVersionType" name="systemVersion"/>
+            <xs:element type="deviceType" name="device"/>
+        </xs:sequence>
+    </xs:complexType>
+</xs:schema>
+
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 (file)
index 0000000..bcf37a2
--- /dev/null
@@ -0,0 +1,73 @@
+<xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema"
+           xmlns="urn:dslforum-org:service-1-0" targetNamespace="urn:dslforum-org:service-1-0">
+    <xs:element name="scpd" type="scpdType"/>
+    <xs:complexType name="specVersionType">
+        <xs:sequence>
+            <xs:element type="xs:byte" name="major"/>
+            <xs:element type="xs:byte" name="minor"/>
+        </xs:sequence>
+    </xs:complexType>
+    <xs:simpleType name="direction">
+        <xs:restriction base="xs:string">
+            <xs:enumeration value="in"/>
+            <xs:enumeration value="out"/>
+        </xs:restriction>
+    </xs:simpleType>
+    <xs:complexType name="argumentType">
+        <xs:sequence>
+            <xs:element type="xs:string" name="name" minOccurs="0"/>
+            <xs:element type="direction" name="direction" minOccurs="0"/>
+            <xs:element type="xs:string" name="relatedStateVariable" minOccurs="0"/>
+        </xs:sequence>
+    </xs:complexType>
+    <xs:complexType name="argumentListType">
+        <xs:sequence>
+            <xs:element type="argumentType" name="argument" maxOccurs="unbounded" minOccurs="0"/>
+        </xs:sequence>
+    </xs:complexType>
+    <xs:complexType name="actionType">
+        <xs:sequence>
+            <xs:element type="xs:string" name="name"/>
+            <xs:element type="argumentListType" name="argumentList"/>
+        </xs:sequence>
+    </xs:complexType>
+    <xs:complexType name="actionListType">
+        <xs:sequence>
+            <xs:element type="actionType" name="action" maxOccurs="unbounded" minOccurs="0"/>
+        </xs:sequence>
+    </xs:complexType>
+    <xs:complexType name="stateVariableType">
+        <xs:sequence>
+            <xs:element type="xs:string" name="name"/>
+            <xs:element type="xs:string" name="dataType"/>
+            <xs:element type="xs:string" name="defaultValue" minOccurs="0"/>
+            <xs:element type="allowedValueRangeType" name="allowedValueRange" minOccurs="0"/>
+            <xs:element type="allowedValueListType" name="allowedValueList" minOccurs="0"/>
+        </xs:sequence>
+        <xs:attribute type="xs:string" name="sendEvents"/>
+    </xs:complexType>
+    <xs:complexType name="allowedValueRangeType">
+        <xs:sequence>
+            <xs:element type="xs:byte" name="minimum"/>
+            <xs:element type="xs:byte" name="maximum"/>
+            <xs:element type="xs:byte" name="step"/>
+        </xs:sequence>
+    </xs:complexType>
+    <xs:complexType name="allowedValueListType">
+        <xs:sequence>
+            <xs:element type="xs:string" name="allowedValue" maxOccurs="unbounded" minOccurs="0"/>
+        </xs:sequence>
+    </xs:complexType>
+    <xs:complexType name="serviceStateTableType">
+        <xs:sequence>
+            <xs:element type="stateVariableType" name="stateVariable" maxOccurs="unbounded" minOccurs="0"/>
+        </xs:sequence>
+    </xs:complexType>
+    <xs:complexType name="scpdType">
+        <xs:sequence>
+            <xs:element type="specVersionType" name="specVersion"/>
+            <xs:element type="actionListType" name="actionList"/>
+            <xs:element type="serviceStateTableType" name="serviceStateTable"/>
+        </xs:sequence>
+    </xs:complexType>
+</xs:schema>
\ 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 (file)
index 0000000..d3267ca
--- /dev/null
@@ -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());
+        }
+    }
+}
index afb37fa9f35af8b4d050a6c70d1d3f28c445460f..73be0d30af36e3d0faf007b7a78e132947c807ab 100644 (file)
     <module>org.openhab.binding.tibber</module>
     <module>org.openhab.binding.touchwand</module>
     <module>org.openhab.binding.tplinksmarthome</module>
+    <module>org.openhab.binding.tr064</module>
     <module>org.openhab.binding.tradfri</module>
     <module>org.openhab.binding.unifi</module>
     <module>org.openhab.binding.unifiedremote</module>