]> git.basschouten.com Git - openhab-addons.git/commitdiff
[tr064] Enhancements, code improvements and fixes (#14468)
authorJ-N-K <github@klug.nrw>
Fri, 24 Feb 2023 15:06:53 +0000 (16:06 +0100)
committerGitHub <noreply@github.com>
Fri, 24 Feb 2023 15:06:53 +0000 (16:06 +0100)
Signed-off-by: Jan N. Klug <github@klug.nrw>
29 files changed:
CODEOWNERS
bundles/org.openhab.binding.tr064/README.md
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/FritzboxActions.java [new file with mode: 0644]
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064CommunicationException.java
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064DiscoveryService.java
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064DynamicStateDescriptionProvider.java [deleted file]
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064HandlerFactory.java
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064RootHandler.java
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064SubHandler.java
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/config/Tr064ChannelConfig.java
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/config/Tr064RootConfiguration.java
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/PhonebookActions.java [deleted file]
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/PhonebookProfile.java
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/Tr064PhonebookImpl.java
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/soap/CallListEntry.java
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/soap/CallListType.java
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/soap/PostProcessingException.java
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/soap/SOAPConnector.java
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/soap/SOAPRequest.java
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/soap/SOAPValueConverter.java
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/util/SCPDUtil.java
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/util/Util.java
bundles/org.openhab.binding.tr064/src/main/resources/OH-INF/config/phonebookProfile.xml
bundles/org.openhab.binding.tr064/src/main/resources/OH-INF/i18n/tr064.properties
bundles/org.openhab.binding.tr064/src/main/resources/OH-INF/thing/thing-types.xml
bundles/org.openhab.binding.tr064/src/main/resources/channels.xml
bundles/org.openhab.binding.tr064/src/main/resources/xsd/channeltypes.xsd
bundles/org.openhab.binding.tr064/src/test/java/org/openhab/binding/tr064/internal/phonebook/PhonebookProfileTest.java
bundles/org.openhab.binding.tr064/src/test/java/org/openhab/binding/tr064/internal/phonebook/Tr064PhonebookImplTest.java [new file with mode: 0644]

index 34ff5b5d4b6f437be7768119bff55f3bf1a4bc96..3aa65f284207b5c58048c17ac91133931f83f4fd 100644 (file)
 /bundles/org.openhab.binding.touchwand/ @roieg
 /bundles/org.openhab.binding.tplinkrouter/ @olivierkeke
 /bundles/org.openhab.binding.tplinksmarthome/ @Hilbrand
-/bundles/org.openhab.binding.tr064/ @openhab/add-ons-maintainers
+/bundles/org.openhab.binding.tr064/ @J-N-K
 /bundles/org.openhab.binding.tradfri/ @cweitkamp @kaikreuzer
 /bundles/org.openhab.binding.twitter/ @computergeek1507
 /bundles/org.openhab.binding.unifi/ @mgbowman @Hilbrand
index d154350b9a4fb8de6b9b52ed9b0480e1abed9bfe..8f777e4275e8ea50bb8422f0cb3dcc246c7dfd0e 100644 (file)
@@ -38,6 +38,10 @@ If you only configured password authentication for your device, the `user` param
 The second credential parameter is `password`, which is mandatory.
 For security reasons it is highly recommended to set both, username and password.
 
+Another optional and advanced configuration parameter is `timeout`.
+This parameter applies to all requests to the device (SOAP requests, phonebook retrieval, call lists, ...).
+It only needs to be changed from the default value of `5` seconds when the remote device is unexpectedly slow and does not respond within that time.
+
 ### `fritzbox`
 
 The `fritzbox` devices can give additional informations in dedicated channels, controlled 
@@ -70,12 +74,19 @@ If the `PHONEBOOK` profile shall be used, it is necessary to retrieve the phoneb
 The `phonebookInterval` is used to set the refresh cycle for phonebooks.
 It defaults to 600 seconds, and it can be set to 0 if phonebooks are not used.
 
-Some parameters (e.g. `macOnline`, `wanBlockIPs`) accept lists. 
+Some parameters (e.g. `macOnline`, `wanBlockIPs`) accept lists.
 List items are configured one per line in the UI, or are comma separated values when using textual config.
 These parameters that accept list can also contain comments.
 Comments are separated from the value with a '#' (e.g. `192.168.0.77 # Daughter's iPhone`).
 The full string is used for the channel label.
 
+Two more advanced parameters are used for the backup thing action.
+The `backupDirectory` is the directory where the backup files are stored.
+The default value is the userdata directory.
+The `backupPassword` is used to encrypt the backup file.
+This is equivalent to setting a password in the UI.
+If no password is given, the user password (parameter `password`) is used.
+
 ### `subdevice`, `subdeviceLan`
 
 Additional informations (i.e. channels) are available in subdevices of the bridge.
@@ -117,18 +128,17 @@ The call-types are the same as provided by the FritzBox, i.e. `1` (inbound), `2`
 
 ### LAN `subdeviceLan` channels
 
-| channel                    | item-type                 | advanced | description                                                    |
-|----------------------------|---------------------------|:--------:|----------------------------------------------------------------|
-| `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.                                 |
-| `macOnline`                | `Switch`                  |     x    | Online status of the device with the given MAC                 |
-| `macIP`                    | `String`                  |     x    | IP of the device with the given MAC                            |
-| `macSignalStrength1`       | `Number`                  |     x    | Wifi Signal Strength of the device with the given MAC. This is set in case the Device is connected to 2.4Ghz  |
-| `macSpeed1`                | `Number:DataTransferRate` |     x    | Wifi Speed of the device with the given MAC. This is set in case the Device is connected to 2.4Ghz            |
-| `macSignalStrength2`       | `Number`                  |     x    | Wifi Signal Strength of the device with the given MAC. This is set in case the Device is connected to 5Ghz  |
-| `macSpeed2`                | `Number:DataTransferRate` |     x    | Wifi Speed of the device with the given MAC. This is set in case the Device is connected to 5Ghz            |
-
+| channel              | item-type                 | advanced | description                                                                                                  |
+|----------------------|---------------------------|:--------:|--------------------------------------------------------------------------------------------------------------|
+| `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.                                                                               |
+| `macOnline`          | `Switch`                  |    x     | Online status of the device with the given MAC                                                               |
+| `macOnlineIpAddress` | `String`                  |    x     | IP of the MAC (uses same parameter as `macOnline`)                                                           |
+| `macSignalStrength1` | `Number`                  |    x     | Wifi Signal Strength of the device with the given MAC. This is set in case the Device is connected to 2.4Ghz |
+| `macSpeed1`          | `Number:DataTransferRate` |    x     | Wifi Speed of the device with the given MAC. This is set in case the Device is connected to 2.4Ghz           |
+| `macSignalStrength2` | `Number`                  |    x     | Wifi Signal Strength of the device with the given MAC. This is set in case the Device is connected to 5Ghz   |
+| `macSpeed2`          | `Number:DataTransferRate` |    x     | Wifi Speed of the device with the given MAC. This is set in case the Device is connected to 5Ghz             |
 Older FritzBox devices may not support 5 GHz WiFi.
 In this case you have to use the `wifi5GHzEnable` channel for switching the guest WiFi.
 
@@ -140,34 +150,36 @@ In this case you have to use the `wifi5GHzEnable` channel for switching the gues
 | `pppUptime`                | `Number:Time`             |          | Uptime (if using PPP)                                          |
 | `wanConnectionStatus`      | `String`                  |          | Connection Status                                              |
 | `wanPppConnectionStatus`   | `String`                  |          | Connection Status (if using PPP)                               |
-| `wanIpAddress`             | `String`                  |     x    | WAN IP Address                                                 |
-| `wanPppIpAddress`          | `String`                  |     x    | WAN IP Address (if using PPP)                                  |
+| `wanIpAddress`             | `String`                  |        | WAN IP Address                                                 |
+| `wanPppIpAddress`          | `String`                  |        | WAN IP Address (if using PPP)                                  |
 
 ### WAN `subdevice` channels
 
 | channel                    | item-type                 | advanced | description                                                    |
 |----------------------------|---------------------------|:--------:|----------------------------------------------------------------|
-| `dslCRCErrors`             | `Number:Dimensionless`    |     x    | DSL CRC Errors                                                 |
-| `dslDownstreamMaxRate`     | `Number:DataTransferRate` |     x    | DSL Max Downstream Rate                                        |
-| `dslDownstreamCurrRate`    | `Number:DataTransferRate` |     x    | DSL Curr. Downstream Rate                                      |
-| `dslDownstreamNoiseMargin` | `Number:Dimensionless`    |     x    | DSL Downstream Noise Margin                                    |
-| `dslDownstreamAttenuation` | `Number:Dimensionless`    |     x    | DSL Downstream Attenuation                                     |
+| `dslCRCErrors`             | `Number:Dimensionless`    |        | DSL CRC Errors                                                 |
+| `dslDownstreamMaxRate`     | `Number:DataTransferRate` |        | DSL Max Downstream Rate                                        |
+| `dslDownstreamCurrRate`    | `Number:DataTransferRate` |        | DSL Curr. Downstream Rate                                      |
+| `dslDownstreamNoiseMargin` | `Number:Dimensionless`    |        | DSL Downstream Noise Margin                                    |
+| `dslDownstreamAttenuation` | `Number:Dimensionless`    |        | 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                                                     |
-| `dslUpstreamMaxRate`       | `Number:DataTransferRate` |     x    | DSL Max Upstream Rate                                          |
-| `dslUpstreamCurrRate`      | `Number:DataTransferRate` |     x    | DSL Curr. Upstream Rate                                        |
-| `dslUpstreamNoiseMargin`   | `Number:Dimensionless`    |     x    | DSL Upstream Noise Margin                                      |
-| `dslUpstreamAttenuation`   | `Number:Dimensionless`    |     x    | DSL Upstream Attenuation                                       |
-| `wanAccessType`            | `String`                  |     x    | Access Type                                                    |
-| `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 Sent                                               |
+| `dslFECErrors`             | `Number:Dimensionless`    |    x     | DSL FEC Errors                                                 |
+| `dslHECErrors`             | `Number:Dimensionless`    |    x     | DSL HEC Errors                                                 |
+| `dslStatus`                | `String`                  |          | DSL Status                                                     |
+| `dslUpstreamMaxRate`       | `Number:DataTransferRate` |    x     | DSL Max Upstream Rate                                          |
+| `dslUpstreamCurrRate`      | `Number:DataTransferRate` |    x     | DSL Curr. Upstream Rate                                        |
+| `dslUpstreamNoiseMargin`   | `Number:Dimensionless`    |    x     | DSL Upstream Noise Margin                                      |
+| `dslUpstreamAttenuation`   | `Number:Dimensionless`    |    x     | DSL Upstream Attenuation                                       |
+| `wanAccessType`            | `String`                  |    x     | Access Type                                                    |
+| `wanMaxDownstreamRate`     | `Number:DataTransferRate` |    x     | Max. Downstream Rate                                           |
+| `wanMaxUpstreamRate`       | `Number:DataTransferRate` |    x     | Max. Upstream Rate                                             |
+| `wanCurrentDownstreamRate` | `Number:DataTransferRate` |    x     | Current Downstream Rate (average last 15 seconds)              |
+| `wanCurrentUpstreamRate`   | `Number:DataTransferRate` |    x     | Current Upstream Rate (average last 15 seconds)                |
+| `wanPhysicalLinkStatus`    | `String`                  |    x     | Link Status                                                    |
+| `wanTotalBytesReceived`    | `Number:DataAmount`       |    x     | Total Bytes Received                                           |
+| `wanTotalBytesSent`        | `Number:DataAmount`       |    x     | Total Bytes Sent                                               |
  
-**Note:** AVM Fritzbox devices use 4-byte-unsigned-integers for `wanTotalBytesReceived` and `wanTotalBytesSent`, because of that the counters are reset after around 4GB data.
+**Note:** AVM FritzBox devices use 4-byte-unsigned-integers for `wanTotalBytesReceived` and `wanTotalBytesSent`, because of that the counters are reset after around 4GB data.
 
 ## `PHONEBOOK` Profile
 
@@ -179,12 +191,15 @@ If only a specific phonebook from the device should be used, this can be specifi
 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.
+Negative `matchCount` values skip digits from the left (e.g. if the input number is `033998005671` a `matchCount` of `-1` would remove the leading `0` ).
 A `matchCount` of `0` is considered as "match everything".
 Matching is done on normalized versions of the numbers that have all characters except digits, '+' and '*' removed.
 There is an optional configuration parameter called `phoneNumberIndex` that should be used when linking to a channel with item type `StringListType` (like `Call` in the example below), which determines which number to be picked, i.e. to or from.
 
 ## Rule Action
 
+### Phonebook lookup
+
 The phonebooks of a `fritzbox` thing can be used to lookup a number from rules via a thing action:
 
 `String name = phonebookLookup(String number, String phonebook, int matchCount)`
@@ -192,17 +207,36 @@ The phonebooks of a `fritzbox` thing can be used to lookup a number from rules v
 `phonebook` and `matchCount` are optional parameters.
 You can omit one or both of these parameters.
 The configured `matchCount` is counted from the right end and denotes the number of matching characters needed to consider this number as matching.
-A `matchCount` of `0` is considered as "match everything" and is used as default if no other value is given. 
+Negative `matchCount` values skip digits from the left (e.g. if the input number is `033998005671` a `matchCount` of `-1` would remove the leading `0` ).
+A `matchCount` of `0` is considered as "match everything" and is used as default if no other value is given.
 As in the phonebook profile, matching is done on normalized versions of the numbers that have all characters except digits, '+' and '*' removed.
 The return value is either the phonebook entry (if found) or the input number.
 
 Example (use all phonebooks, match 5 digits from right):
 
-```
+```java
 val tr064Actions = getActions("tr064","tr064:fritzbox:2a28aee1ee")
 val result = tr064Actions.phonebookLookup("49157712341234", 5)
 ```
 
+### Fritz!Box Backup
+
+The `fritzbox` things can create configuration backups of the Fritz!Box.
+
+The default configuration of the Fritz!Boxes requires 2-factor-authentication for creating backups.
+If you see a `Failed to get configuration backup URL: HTTP-Response-Code 500 (Internal Server Error), SOAP-Fault: 866 (second factor authentication required)` warning, you need to disable 2-actor authentication.
+But beware: depending on your configuration this might be a security issue.
+The setting can be found under "System -> FRITZ!Box Users -> Login to the Home Network -> Confirm".
+
+When executed, the action requests a backup file with the given password in the configured path.
+The backup file is names as `ThingFriendlyName dd.mm.yyyy HHMM.export` (e.g. `My FritzBox 18.06.2021 1720.export`).
+Files with the same name will be overwritten, so make sure that you trigger the rules at different times if your devices have the same friendly name.
+
+```java
+val tr064Actions = getActions("tr064","tr064:fritzbox:2a28aee1ee")
+tr064Actions.createConfigurationBackup()
+```
+
 ## A note on textual configuration
 
 Textual configuration through a `.things` file is possible but, at present, strongly discouraged because it is significantly more error-prone
@@ -230,7 +264,6 @@ The channel are automatically generated and it is simpler to use the Main User I
 ```
 Switch PresXX "[%s]" {channel="tr064:subdeviceLan:rootuid:LAN:macOnline_XX_3AXX_3AXX_3AXX_3AXX_3AXX"}
 Switch PresYY "[%s]" {channel="tr064:subdeviceLan:rootuid:LAN:macOnline_YY_3AYY_3AYY_3AYY_3AYY_3AYY"}
-
 ```
 
 Example `*.items` file using the `PHONEBOOK` profile for storing the name of a caller in an item. it matches 8 digits from the right of the "from" number (note the escaping of `:` to `_3A`):
diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/FritzboxActions.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/FritzboxActions.java
new file mode 100644 (file)
index 0000000..c905ae6
--- /dev/null
@@ -0,0 +1,194 @@
+/**
+ * Copyright (c) 2010-2023 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.io.IOException;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.ExecutionException;
+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.api.ContentResponse;
+import org.openhab.binding.tr064.internal.dto.scpd.root.SCPDServiceType;
+import org.openhab.binding.tr064.internal.phonebook.Phonebook;
+import org.openhab.binding.tr064.internal.soap.SOAPRequest;
+import org.openhab.binding.tr064.internal.util.SCPDUtil;
+import org.openhab.binding.tr064.internal.util.Util;
+import org.openhab.core.automation.annotation.ActionInput;
+import org.openhab.core.automation.annotation.ActionOutput;
+import org.openhab.core.automation.annotation.RuleAction;
+import org.openhab.core.thing.binding.ThingActions;
+import org.openhab.core.thing.binding.ThingActionsScope;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link FritzboxActions} is responsible for handling phone book actions
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@ThingActionsScope(name = "tr064")
+@NonNullByDefault
+public class FritzboxActions implements ThingActions {
+    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("dd.MM.yyyy_HHmm");
+
+    private final Logger logger = LoggerFactory.getLogger(FritzboxActions.class);
+
+    private @Nullable Tr064RootHandler handler;
+
+    @RuleAction(label = "@text/phonebookLookupActionLabel", description = "@text/phonebookLookupActionDescription")
+    public @ActionOutput(name = "name", label = "@text/phonebookLookupActionOutputLabel", description = "@text/phonebookLookupActionOutputDescription", type = "java.lang.String") String phonebookLookup(
+            @ActionInput(name = "phonenumber", label = "@text/phonebookLookupActionInputPhoneNumberLabel", description = "@text/phonebookLookupActionInputPhoneNumberDescription", type = "java.lang.String", required = true) @Nullable String phonenumber,
+            @ActionInput(name = "matches", label = "@text/phonebookLookupActionInputMatchesLabel", description = "@text/phonebookLookupActionInputMatchesDescription", type = "java.lang.Integer") @Nullable Integer matchCount) {
+        return phonebookLookup(phonenumber, null, matchCount);
+    }
+
+    @RuleAction(label = "@text/phonebookLookupActionLabel", description = "@text/phonebookLookupActionDescription")
+    public @ActionOutput(name = "name", label = "@text/phonebookLookupActionOutputLabel", description = "@text/phonebookLookupActionOutputDescription", type = "java.lang.String") String phonebookLookup(
+            @ActionInput(name = "phonenumber", label = "@text/phonebookLookupActionInputPhoneNumberLabel", description = "@text/phonebookLookupActionInputPhoneNumberDescription", type = "java.lang.String", required = true) @Nullable String phonenumber) {
+        return phonebookLookup(phonenumber, null, null);
+    }
+
+    @RuleAction(label = "@text/phonebookLookupActionLabel", description = "@text/phonebookLookupActionDescription")
+    public @ActionOutput(name = "name", label = "@text/phonebookLookupActionOutputLabel", description = "@text/phonebookLookupActionOutputDescription", type = "java.lang.String") String phonebookLookup(
+            @ActionInput(name = "phonenumber", label = "@text/phonebookLookupActionInputPhoneNumberLabel", description = "@text/phonebookLookupActionInputPhoneNumberDescription", type = "java.lang.String", required = true) @Nullable String phonenumber,
+            @ActionInput(name = "phonebook", label = "@text/phonebookLookupActionInputPhoneBookLabel", description = "@text/phonebookLookupActionInputPhoneBookDescription", type = "java.lang.String") @Nullable String phonebook) {
+        return phonebookLookup(phonenumber, phonebook, null);
+    }
+
+    @RuleAction(label = "@text/phonebookLookupActionLabel", description = "@text/phonebookLookupActionDescription")
+    public @ActionOutput(name = "name", label = "@text/phonebookLookupActionOutputLabel", description = "@text/phonebookLookupActionOutputDescription", type = "java.lang.String") String phonebookLookup(
+            @ActionInput(name = "phonenumber", label = "@text/phonebookLookupActionInputPhoneNumberLabel", description = "@text/phonebookLookupActionInputPhoneNumberDescription", type = "java.lang.String", required = true) @Nullable String phonenumber,
+            @ActionInput(name = "phonebook", label = "@text/phonebookLookupActionInputPhoneBookLabel", description = "@text/phonebookLookupActionInputPhoneBookDescription", type = "java.lang.String") @Nullable String phonebook,
+            @ActionInput(name = "matches", label = "@text/phonebookLookupActionInputMatchesLabel", description = "@text/phonebookLookupActionInputMatchesDescription", type = "java.lang.Integer") @Nullable Integer matchCount) {
+        if (phonenumber == null) {
+            logger.warn("Cannot lookup a missing number.");
+            return "";
+        }
+
+        final Tr064RootHandler handler = this.handler;
+        if (handler == null) {
+            logger.info("Handler is null, cannot lookup number.");
+            return phonenumber;
+        } else {
+            int matchCountInt = matchCount == null ? 0 : matchCount;
+            if (phonebook != null && !phonebook.isEmpty()) {
+                return Objects.requireNonNull(handler.getPhonebookByName(phonebook)
+                        .flatMap(p -> p.lookupNumber(phonenumber, matchCountInt)).orElse(phonenumber));
+            } else {
+                Collection<Phonebook> phonebooks = handler.getPhonebooks();
+                return Objects.requireNonNull(phonebooks.stream().map(p -> p.lookupNumber(phonenumber, matchCountInt))
+                        .filter(Optional::isPresent).map(Optional::get).findAny().orElse(phonenumber));
+            }
+        }
+    }
+
+    @RuleAction(label = "create configuration backup", description = "Creates a configuration backup")
+    public void createConfigurationBackup() {
+        Tr064RootHandler handler = this.handler;
+
+        if (handler == null) {
+            logger.warn("TR064 action service ThingHandler is null!");
+            return;
+        }
+
+        SCPDUtil scpdUtil = handler.getSCPDUtil();
+        if (scpdUtil == null) {
+            logger.warn("Could not get SCPDUtil, handler seems to be uninitialized.");
+            return;
+        }
+
+        Optional<SCPDServiceType> scpdService = scpdUtil.getDevice("")
+                .flatMap(deviceType -> deviceType.getServiceList().stream().filter(
+                        service -> service.getServiceId().equals("urn:DeviceConfig-com:serviceId:DeviceConfig1"))
+                        .findFirst());
+        if (scpdService.isEmpty()) {
+            logger.warn("Could not get service.");
+            return;
+        }
+
+        BackupConfiguration configuration = handler.getBackupConfiguration();
+        try {
+            SOAPRequest soapRequest = new SOAPRequest(scpdService.get(), "X_AVM-DE_GetConfigFile",
+                    Map.of("NewX_AVM-DE_Password", configuration.password));
+            SOAPMessage soapMessage = handler.getSOAPConnector().doSOAPRequestUncached(soapRequest);
+            String configBackupURL = Util.getSOAPElement(soapMessage, "NewX_AVM-DE_ConfigFileUrl")
+                    .orElseThrow(() -> new Tr064CommunicationException("Empty URL"));
+
+            ContentResponse content = handler.getUrl(configBackupURL);
+
+            String fileName = String.format("%s %s.export", handler.getFriendlyName(),
+                    DATE_TIME_FORMATTER.format(LocalDateTime.now()));
+            Path filePath = FileSystems.getDefault().getPath(configuration.directory, fileName);
+            Path folder = filePath.getParent();
+            if (folder != null) {
+                Files.createDirectories(folder);
+            }
+            Files.write(filePath, content.getContent());
+        } catch (Tr064CommunicationException e) {
+            logger.warn("Failed to get configuration backup URL: {}", e.getMessage());
+        } catch (InterruptedException | ExecutionException | TimeoutException e) {
+            logger.warn("Failed to get remote backup file: {}", e.getMessage());
+        } catch (IOException e) {
+            logger.warn("Failed to create backup file: {}", e.getMessage());
+        }
+    }
+
+    public static String phonebookLookup(ThingActions actions, @Nullable String phonenumber,
+            @Nullable Integer matchCount) {
+        return phonebookLookup(actions, phonenumber, null, matchCount);
+    }
+
+    public static String phonebookLookup(ThingActions actions, @Nullable String phonenumber) {
+        return phonebookLookup(actions, phonenumber, null, null);
+    }
+
+    public static String phonebookLookup(ThingActions actions, @Nullable String phonenumber,
+            @Nullable String phonebook) {
+        return phonebookLookup(actions, phonenumber, phonebook, null);
+    }
+
+    public static String phonebookLookup(ThingActions actions, @Nullable String phonenumber, @Nullable String phonebook,
+            @Nullable Integer matchCount) {
+        return ((FritzboxActions) actions).phonebookLookup(phonenumber, phonebook, matchCount);
+    }
+
+    public static void createConfigurationBackup(ThingActions actions) {
+        ((FritzboxActions) actions).createConfigurationBackup();
+    }
+
+    @Override
+    public void setThingHandler(@Nullable ThingHandler handler) {
+        this.handler = (Tr064RootHandler) handler;
+    }
+
+    @Override
+    public @Nullable ThingHandler getThingHandler() {
+        return handler;
+    }
+
+    public record BackupConfiguration(String directory, String password) {
+    }
+}
index 089c65f50534ab4b00f7bf89bc29461871319fb4..c144aa855ad69bd5c0b839a086766f2cc7feea46 100644 (file)
@@ -37,7 +37,7 @@ public class Tr064CommunicationException extends Exception {
         super(s);
         this.httpError = httpError;
         this.soapError = soapError;
-    };
+    }
 
     public String getSoapError() {
         return soapError;
index 7084f1602829d2ac45a6b883a9ffcc0bd3e77445..f209416d04481d383ffcc7b9c301acfa1272d23a 100644 (file)
@@ -14,9 +14,7 @@ package org.openhab.binding.tr064.internal;
 
 import static org.openhab.binding.tr064.internal.Tr064BindingConstants.*;
 
-import java.util.Collections;
 import java.util.Date;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -44,7 +42,7 @@ import org.slf4j.LoggerFactory;
 @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);
+    public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_SUBDEVICE);
 
     private final Logger logger = LoggerFactory.getLogger(Tr064DiscoveryService.class);
     private @Nullable Tr064RootHandler bridgeHandler;
@@ -101,13 +99,12 @@ public class Tr064DiscoveryService extends AbstractDiscoveryService implements T
                 }
                 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();
+                DiscoveryResult result = DiscoveryResultBuilder.create(thingUID) //
+                        .withLabel(device.getFriendlyName()) //
+                        .withBridge(bridgeUID) //
+                        .withProperties(Map.of("uuid", udn, "deviceType", device.getDeviceType())) //
+                        .withRepresentationProperty("uuid") //
+                        .build();
                 thingDiscovered(result);
             }
         });
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
deleted file mode 100644 (file)
index 2f900ce..0000000
+++ /dev/null
@@ -1,73 +0,0 @@
-/**
- * Copyright (c) 2010-2023 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;
-        }
-    }
-}
index cfddb8f9bd4904e5ca75ec76cbace41a5356796b..a23ae326485e16b7070685cce1b092943f2e789c 100644 (file)
@@ -98,7 +98,7 @@ public class Tr064HandlerFactory extends BaseThingHandlerFactory {
 
         if (Tr064RootHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) {
             Tr064RootHandler handler = new Tr064RootHandler((Bridge) thing, httpClient);
-            if (thingTypeUID.equals(THING_TYPE_FRITZBOX)) {
+            if (THING_TYPE_FRITZBOX.equals(thingTypeUID)) {
                 phonebookProfileFactory.registerPhonebookProvider(handler);
             }
             return handler;
index 0023b87da183006b6dcdf7649ea17d963af8c232..a80dd8e6ec768fe9d089904355403cd8502b0737 100644 (file)
@@ -23,10 +23,13 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
+import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -38,6 +41,7 @@ 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.api.ContentResponse;
 import org.eclipse.jetty.client.util.DigestAuthentication;
 import org.openhab.binding.tr064.internal.config.Tr064ChannelConfig;
 import org.openhab.binding.tr064.internal.config.Tr064RootConfiguration;
@@ -45,7 +49,6 @@ 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.PhonebookActions;
 import org.openhab.binding.tr064.internal.phonebook.PhonebookProvider;
 import org.openhab.binding.tr064.internal.phonebook.Tr064PhonebookImpl;
 import org.openhab.binding.tr064.internal.soap.SOAPConnector;
@@ -61,6 +64,7 @@ import org.openhab.core.thing.ThingStatusDetail;
 import org.openhab.core.thing.ThingTypeUID;
 import org.openhab.core.thing.ThingUID;
 import org.openhab.core.thing.binding.BaseBridgeHandler;
+import org.openhab.core.thing.binding.ThingHandlerCallback;
 import org.openhab.core.thing.binding.ThingHandlerService;
 import org.openhab.core.thing.binding.builder.ThingBuilder;
 import org.openhab.core.types.Command;
@@ -85,12 +89,15 @@ public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProv
     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;
+
+    // these are set when the config is available
+    private Tr064RootConfiguration config = new Tr064RootConfiguration();
     private String endpointBaseURL = "";
+    private int timeout = Tr064RootConfiguration.DEFAULT_HTTP_TIMEOUT;
+
+    private String deviceType = "";
 
     private final Map<ChannelUID, Tr064ChannelConfig> channels = new HashMap<>();
     // caching is used to prevent excessive calls to the same action
@@ -106,7 +113,7 @@ public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProv
     Tr064RootHandler(Bridge bridge, HttpClient httpClient) {
         super(bridge);
         this.httpClient = httpClient;
-        this.soapConnector = new SOAPConnector(httpClient, endpointBaseURL);
+        this.soapConnector = new SOAPConnector(httpClient, endpointBaseURL, timeout);
     }
 
     @Override
@@ -147,7 +154,8 @@ public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProv
         }
 
         endpointBaseURL = "http://" + config.host + ":49000";
-        soapConnector = new SOAPConnector(httpClient, endpointBaseURL);
+        soapConnector = new SOAPConnector(httpClient, endpointBaseURL, timeout);
+        timeout = config.timeout;
         updateStatus(ThingStatus.UNKNOWN);
 
         connectFuture = scheduler.scheduleWithFixedDelay(this::internalInitialize, 0, RETRY_INTERVAL, TimeUnit.SECONDS);
@@ -158,7 +166,7 @@ public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProv
      */
     private void internalInitialize() {
         try {
-            scpdUtil = new SCPDUtil(httpClient, endpointBaseURL);
+            scpdUtil = new SCPDUtil(httpClient, endpointBaseURL, timeout);
         } catch (SCPDException e) {
             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
                     "could not get device definitions from " + config.host);
@@ -172,8 +180,9 @@ public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProv
             ThingBuilder thingBuilder = editThing();
             thingBuilder.withoutChannels(thing.getChannels());
             final SCPDUtil scpdUtil = this.scpdUtil;
-            if (scpdUtil != null) {
-                Util.checkAvailableChannels(thing, thingBuilder, scpdUtil, "", deviceType, channels);
+            final ThingHandlerCallback callback = getCallback();
+            if (scpdUtil != null && callback != null) {
+                Util.checkAvailableChannels(thing, callback, thingBuilder, scpdUtil, "", deviceType, channels);
                 updateThing(thingBuilder.build());
             }
 
@@ -197,6 +206,7 @@ public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProv
         removeConnectScheduler();
         uninstallPolling();
         stateCache.clear();
+        scpdUtil = null;
 
         super.dispose();
     }
@@ -205,19 +215,25 @@ public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProv
      * 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);
+        try {
+            channels.forEach((channelUID, channelConfig) -> {
+                if (isLinked(channelUID)) {
+                    State state = stateCache.putIfAbsentAndGet(channelUID,
+                            () -> soapConnector.getChannelStateFromDevice(channelConfig, channels, stateCache));
+                    if (state != null) {
+                        updateState(channelUID, state);
+                    }
                 }
-            }
-        });
+            });
+        } catch (RuntimeException e) {
+            logger.warn("Exception while refreshing remote data for thing '{}':", thing.getUID(), e);
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                    "Refresh exception: " + e.getMessage());
+        }
     }
 
     /**
-     * establish the connection - get secure port (if avallable), install authentication, get device properties
+     * establish the connection - get secure port (if available), install authentication, get device properties
      *
      * @return true if successful
      */
@@ -238,11 +254,11 @@ public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProv
                 SOAPMessage soapResponse = soapConnector
                         .doSOAPRequest(new SOAPRequest(deviceService, "GetSecurityPort"));
                 if (!soapResponse.getSOAPBody().hasFault()) {
-                    SOAPValueConverter soapValueConverter = new SOAPValueConverter(httpClient);
+                    SOAPValueConverter soapValueConverter = new SOAPValueConverter(httpClient, timeout);
                     soapValueConverter.getStateFromSOAPValue(soapResponse, "NewSecurityPort", null)
                             .ifPresentOrElse(port -> {
-                                endpointBaseURL = "https://" + config.host + ":" + port.toString();
-                                soapConnector = new SOAPConnector(httpClient, endpointBaseURL);
+                                endpointBaseURL = "https://" + config.host + ":" + port;
+                                soapConnector = new SOAPConnector(httpClient, endpointBaseURL, timeout);
                                 logger.debug("endpointBaseURL is now '{}'", endpointBaseURL);
                             }, () -> logger.warn("Could not determine secure port, disabling https"));
                 } else {
@@ -250,9 +266,10 @@ public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProv
                 }
 
                 // clear auth cache and force re-auth
-                httpClient.getAuthenticationStore().clearAuthenticationResults();
-                AuthenticationStore auth = httpClient.getAuthenticationStore();
-                auth.addAuthentication(new DigestAuthentication(new URI(endpointBaseURL), Authentication.ANY_REALM,
+                AuthenticationStore authStore = httpClient.getAuthenticationStore();
+                authStore.clearAuthentications();
+                authStore.clearAuthenticationResults();
+                authStore.addAuthentication(new DigestAuthentication(new URI(endpointBaseURL), Authentication.ANY_REALM,
                         config.user, config.password));
 
                 // check & update properties
@@ -263,7 +280,7 @@ public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProv
                         .orElseThrow(() -> new SCPDException("Action 'GetInfo' not found"));
                 SOAPMessage soapResponse1 = soapConnector
                         .doSOAPRequest(new SOAPRequest(deviceService, getInfoAction.getName()));
-                SOAPValueConverter soapValueConverter = new SOAPValueConverter(httpClient);
+                SOAPValueConverter soapValueConverter = new SOAPValueConverter(httpClient, timeout);
                 Map<String, String> properties = editProperties();
                 PROPERTY_ARGUMENTS.forEach(argumentName -> getInfoAction.getArgumentList().stream()
                         .filter(argument -> argument.getName().equals(argumentName)).findFirst()
@@ -301,6 +318,22 @@ public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProv
         return soapConnector;
     }
 
+    /**
+     * return the result of an (authenticated) GET request
+     *
+     * @param url the requested URL
+     *
+     * @return a {@link ContentResponse} with the result of the request
+     * @throws ExecutionException
+     * @throws InterruptedException
+     * @throws TimeoutException
+     */
+    public ContentResponse getUrl(String url) throws ExecutionException, InterruptedException, TimeoutException {
+        httpClient.getAuthenticationStore().addAuthentication(
+                new DigestAuthentication(URI.create(url), Authentication.ANY_REALM, config.user, config.password));
+        return httpClient.GET(URI.create(url));
+    }
+
     /**
      * get the SCPD processing utility
      *
@@ -341,21 +374,21 @@ public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProv
     @SuppressWarnings("unchecked")
     private Collection<Phonebook> processPhonebookList(SOAPMessage soapMessagePhonebookList,
             SCPDServiceType scpdService) {
-        SOAPValueConverter soapValueConverter = new SOAPValueConverter(httpClient);
-        return (Collection<Phonebook>) soapValueConverter
+        SOAPValueConverter soapValueConverter = new SOAPValueConverter(httpClient, timeout);
+        Optional<Stream<String>> phonebookStream = soapValueConverter
                 .getStateFromSOAPValue(soapMessagePhonebookList, "NewPhonebookList", null)
-                .map(phonebookList -> Arrays.stream(phonebookList.toString().split(","))).orElse(Stream.empty())
-                .map(index -> {
-                    try {
-                        SOAPMessage soapMessageURL = soapConnector.doSOAPRequest(
-                                new SOAPRequest(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());
+                .map(phonebookList -> Arrays.stream(phonebookList.toString().split(",")));
+        return phonebookStream.map(stringStream -> (Collection<Phonebook>) stringStream.map(index -> {
+            try {
+                SOAPMessage soapMessageURL = soapConnector
+                        .doSOAPRequest(new SOAPRequest(scpdService, "GetPhonebook", Map.of("NewPhonebookID", index)));
+                return soapValueConverter.getStateFromSOAPValue(soapMessageURL, "NewPhonebookURL", null)
+                        .map(url -> (Phonebook) new Tr064PhonebookImpl(httpClient, url.toString(), timeout));
+            } 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())).orElseGet(Set::of);
     }
 
     private void retrievePhonebooks() {
@@ -368,14 +401,14 @@ public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProv
         Optional<SCPDServiceType> scpdService = scpdUtil.getDevice("").flatMap(deviceType -> deviceType.getServiceList()
                 .stream().filter(service -> service.getServiceId().equals(serviceId)).findFirst());
 
-        phonebooks = scpdService.map(service -> {
+        phonebooks = Objects.requireNonNull(scpdService.map(service -> {
             try {
                 return processPhonebookList(soapConnector.doSOAPRequest(new SOAPRequest(service, "GetPhonebookList")),
                         service);
             } catch (Tr064CommunicationException e) {
                 return Collections.<Phonebook> emptyList();
             }
-        }).orElse(List.of());
+        }).orElse(List.of()));
 
         if (phonebooks.isEmpty()) {
             logger.warn("Could not get phonebooks for thing {}", thing.getUID());
@@ -405,6 +438,20 @@ public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProv
 
     @Override
     public Collection<Class<? extends ThingHandlerService>> getServices() {
-        return Set.of(Tr064DiscoveryService.class, PhonebookActions.class);
+        if (THING_TYPE_FRITZBOX.equals(thing.getThingTypeUID())) {
+            return Set.of(Tr064DiscoveryService.class, FritzboxActions.class);
+        } else {
+            return Set.of(Tr064DiscoveryService.class);
+        }
+    }
+
+    /**
+     * get the backup configuration for this thing (only applies to FritzBox devices
+     *
+     * @return the configuration
+     */
+    public FritzboxActions.BackupConfiguration getBackupConfiguration() {
+        return new FritzboxActions.BackupConfiguration(config.backupDirectory,
+                Objects.requireNonNullElse(config.backupPassword, config.password));
     }
 }
index 4823a60b92d55ecee0694e05b85ce13714c3ff34..75040d959b771e4ebaa51983683a1161a717b96a 100644 (file)
@@ -37,6 +37,7 @@ import org.openhab.core.thing.ThingStatusDetail;
 import org.openhab.core.thing.ThingStatusInfo;
 import org.openhab.core.thing.ThingTypeUID;
 import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerCallback;
 import org.openhab.core.thing.binding.builder.ThingBuilder;
 import org.openhab.core.types.Command;
 import org.openhab.core.types.RefreshType;
@@ -77,7 +78,6 @@ public class Tr064SubHandler extends BaseThingHandler {
     }
 
     @Override
-    @SuppressWarnings("null")
     public void handleCommand(ChannelUID channelUID, Command command) {
         Tr064ChannelConfig channelConfig = channels.get(channelUID);
         if (channelConfig == null) {
@@ -86,6 +86,7 @@ public class Tr064SubHandler extends BaseThingHandler {
         }
 
         if (command instanceof RefreshType) {
+            final SOAPConnector soapConnector = this.soapConnector;
             State state = stateCache.putIfAbsentAndGet(channelUID, () -> soapConnector == null ? UnDefType.UNDEF
                     : soapConnector.getChannelStateFromDevice(channelConfig, channels, stateCache));
             if (state != null) {
@@ -99,6 +100,7 @@ public class Tr064SubHandler extends BaseThingHandler {
             return;
         }
         scheduler.execute(() -> {
+            final SOAPConnector soapConnector = this.soapConnector;
             if (soapConnector == null) {
                 logger.warn("Could not send command because connector not available");
             } else {
@@ -141,12 +143,16 @@ public class Tr064SubHandler extends BaseThingHandler {
                     "Could not get device definitions");
             return;
         }
-
+        final ThingHandlerCallback callback = getCallback();
+        if (callback == null) {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Could not get callback");
+            return;
+        }
         if (checkProperties(scpdUtil)) {
             // properties set, check channels
             ThingBuilder thingBuilder = editThing();
             thingBuilder.withoutChannels(thing.getChannels());
-            Util.checkAvailableChannels(thing, thingBuilder, scpdUtil, config.uuid, deviceType, channels);
+            Util.checkAvailableChannels(thing, callback, thingBuilder, scpdUtil, config.uuid, deviceType, channels);
             updateThing(thingBuilder.build());
 
             // remove connect scheduler
index 6b1239bae8285062ce6ff4f5c7205d63a85231b5..2e67ccbe3367d6144ae2f159f1fda0d2dd64b8e1 100644 (file)
@@ -25,8 +25,8 @@ import org.openhab.binding.tr064.internal.dto.scpd.service.SCPDActionType;
  */
 @NonNullByDefault
 public class Tr064ChannelConfig {
-    private ChannelTypeDescription channelTypeDescription;
-    private SCPDServiceType service;
+    private final ChannelTypeDescription channelTypeDescription;
+    private final SCPDServiceType service;
     private @Nullable SCPDActionType getAction;
     private String dataType = "";
     private @Nullable String parameter;
index 5e24dde3543ded4c65749639fba324714d8722da..2b619af26f14c54ec41c6951004d9737173fbb78 100644 (file)
@@ -15,6 +15,8 @@ package org.openhab.binding.tr064.internal.config;
 import java.util.List;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.OpenHAB;
 
 /**
  * The {@link Tr064RootConfiguration} class contains fields mapping thing configuration parameters.
@@ -23,9 +25,12 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
  */
 @NonNullByDefault
 public class Tr064RootConfiguration extends Tr064BaseThingConfiguration {
+    public static final int DEFAULT_HTTP_TIMEOUT = 5; // in s
+
     public String host = "";
     public String user = "dslf-config";
     public String password = "";
+    public int timeout = DEFAULT_HTTP_TIMEOUT;
 
     /* following parameters only available in fritzbox thing */
     public List<String> tamIndices = List.of();
@@ -38,6 +43,10 @@ public class Tr064RootConfiguration extends Tr064BaseThingConfiguration {
     public List<String> wanBlockIPs = List.of();
     public int phonebookInterval = 600;
 
+    // Backup data
+    public String backupDirectory = OpenHAB.getUserDataFolder();
+    public @Nullable String backupPassword;
+
     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/phonebook/PhonebookActions.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/PhonebookActions.java
deleted file mode 100644 (file)
index 7a90727..0000000
+++ /dev/null
@@ -1,119 +0,0 @@
-/**
- * Copyright (c) 2010-2023 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.eclipse.jdt.annotation.Nullable;
-import org.openhab.binding.tr064.internal.Tr064RootHandler;
-import org.openhab.core.automation.annotation.ActionInput;
-import org.openhab.core.automation.annotation.ActionOutput;
-import org.openhab.core.automation.annotation.RuleAction;
-import org.openhab.core.thing.binding.ThingActions;
-import org.openhab.core.thing.binding.ThingActionsScope;
-import org.openhab.core.thing.binding.ThingHandler;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * The {@link PhonebookActions} is responsible for handling phonebook actions
- *
- * @author Jan N. Klug - Initial contribution
- */
-@ThingActionsScope(name = "tr064")
-@NonNullByDefault
-public class PhonebookActions implements ThingActions {
-    private final Logger logger = LoggerFactory.getLogger(PhonebookActions.class);
-
-    private @Nullable Tr064RootHandler handler;
-
-    @RuleAction(label = "@text/phonebookLookupActionLabel", description = "@text/phonebookLookupActionDescription")
-    public @ActionOutput(name = "name", label = "@text/phonebookLookupActionOutputLabel", description = "@text/phonebookLookupActionOutputDescription", type = "java.lang.String") String phonebookLookup(
-            @ActionInput(name = "phonenumber", label = "@text/phonebookLookupActionInputPhoneNumberLabel", description = "@text/phonebookLookupActionInputPhoneNumberDescription", type = "java.lang.String", required = true) @Nullable String phonenumber,
-            @ActionInput(name = "matches", label = "@text/phonebookLookupActionInputMatchesLabel", description = "@text/phonebookLookupActionInputMatchesDescription", type = "java.lang.Integer") @Nullable Integer matchCount) {
-        return phonebookLookup(phonenumber, null, matchCount);
-    }
-
-    @RuleAction(label = "@text/phonebookLookupActionLabel", description = "@text/phonebookLookupActionDescription")
-    public @ActionOutput(name = "name", label = "@text/phonebookLookupActionOutputLabel", description = "@text/phonebookLookupActionOutputDescription", type = "java.lang.String") String phonebookLookup(
-            @ActionInput(name = "phonenumber", label = "@text/phonebookLookupActionInputPhoneNumberLabel", description = "@text/phonebookLookupActionInputPhoneNumberDescription", type = "java.lang.String", required = true) @Nullable String phonenumber) {
-        return phonebookLookup(phonenumber, null, null);
-    }
-
-    @RuleAction(label = "@text/phonebookLookupActionLabel", description = "@text/phonebookLookupActionDescription")
-    public @ActionOutput(name = "name", label = "@text/phonebookLookupActionOutputLabel", description = "@text/phonebookLookupActionOutputDescription", type = "java.lang.String") String phonebookLookup(
-            @ActionInput(name = "phonenumber", label = "@text/phonebookLookupActionInputPhoneNumberLabel", description = "@text/phonebookLookupActionInputPhoneNumberDescription", type = "java.lang.String", required = true) @Nullable String phonenumber,
-            @ActionInput(name = "phonebook", label = "@text/phonebookLookupActionInputPhoneBookLabel", description = "@text/phonebookLookupActionInputPhoneBookDescription", type = "java.lang.String") @Nullable String phonebook) {
-        return phonebookLookup(phonenumber, phonebook, null);
-    }
-
-    @RuleAction(label = "@text/phonebookLookupActionLabel", description = "@text/phonebookLookupActionDescription")
-    public @ActionOutput(name = "name", label = "@text/phonebookLookupActionOutputLabel", description = "@text/phonebookLookupActionOutputDescription", type = "java.lang.String") String phonebookLookup(
-            @ActionInput(name = "phonenumber", label = "@text/phonebookLookupActionInputPhoneNumberLabel", description = "@text/phonebookLookupActionInputPhoneNumberDescription", type = "java.lang.String", required = true) @Nullable String phonenumber,
-            @ActionInput(name = "phonebook", label = "@text/phonebookLookupActionInputPhoneBookLabel", description = "@text/phonebookLookupActionInputPhoneBookDescription", type = "java.lang.String") @Nullable String phonebook,
-            @ActionInput(name = "matches", label = "@text/phonebookLookupActionInputMatchesLabel", description = "@text/phonebookLookupActionInputMatchesDescription", type = "java.lang.Integer") @Nullable Integer matchCount) {
-        if (phonenumber == null) {
-            logger.warn("Cannot lookup a missing number.");
-            return "";
-        }
-
-        final Tr064RootHandler handler = this.handler;
-        if (handler == null) {
-            logger.info("Handler is null, cannot lookup number.");
-            return phonenumber;
-        } else {
-            int matchCountInt = matchCount == null ? 0 : matchCount;
-            if (phonebook != null && !phonebook.isEmpty()) {
-                return handler.getPhonebookByName(phonebook).flatMap(p -> p.lookupNumber(phonenumber, matchCountInt))
-                        .orElse(phonenumber);
-            } else {
-                Collection<Phonebook> phonebooks = handler.getPhonebooks();
-                return phonebooks.stream().map(p -> p.lookupNumber(phonenumber, matchCountInt))
-                        .filter(Optional::isPresent).map(Optional::get).findAny().orElse(phonenumber);
-            }
-        }
-    }
-
-    public static String phonebookLookup(ThingActions actions, @Nullable String phonenumber,
-            @Nullable Integer matchCount) {
-        return phonebookLookup(actions, phonenumber, null, matchCount);
-    }
-
-    public static String phonebookLookup(ThingActions actions, @Nullable String phonenumber) {
-        return phonebookLookup(actions, phonenumber, null, null);
-    }
-
-    public static String phonebookLookup(ThingActions actions, @Nullable String phonenumber,
-            @Nullable String phonebook) {
-        return phonebookLookup(actions, phonenumber, phonebook, null);
-    }
-
-    public static String phonebookLookup(ThingActions actions, @Nullable String phonenumber, @Nullable String phonebook,
-            @Nullable Integer matchCount) {
-        return ((PhonebookActions) actions).phonebookLookup(phonenumber, phonebook, matchCount);
-    }
-
-    @Override
-    public void setThingHandler(@Nullable ThingHandler handler) {
-        if (handler instanceof Tr064RootHandler) {
-            this.handler = (Tr064RootHandler) handler;
-        }
-    }
-
-    @Override
-    public @Nullable ThingHandler getThingHandler() {
-        return handler;
-    }
-}
index 783cce5e33c7cc38c24d741610eafb0e4a61ca64..d0292a2a9e5a599fc96251e3a24f68d6570e5460 100644 (file)
@@ -14,6 +14,7 @@ package org.openhab.binding.tr064.internal.phonebook;
 
 import java.math.BigDecimal;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Optional;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
@@ -139,15 +140,13 @@ public class PhonebookProfile implements StateProfile {
         }
         if (state instanceof StringType) {
             Optional<String> match = resolveNumber(state.toString());
-            State newState = match.map(name -> (State) new StringType(name)).orElse(state);
-            // Compare by reference to check if the name is mapped to the same state
-            if (newState == state) {
+            State newState = Objects.requireNonNull(match.map(name -> (State) new StringType(name)).orElse(state));
+            if (newState.equals(state)) {
                 logger.debug("Number '{}' not found in phonebook '{}' from provider '{}'", state, phonebookName,
                         thingUID);
             }
             callback.sendUpdate(newState);
-        } else if (state instanceof StringListType) {
-            StringListType stringList = (StringListType) state;
+        } else if (state instanceof StringListType stringList) {
             try {
                 String phoneNumber = stringList.getValue(phoneNumberIndex);
                 Optional<String> match = resolveNumber(phoneNumber);
index f25510819496dbf746d9b7ed99d7c65d5b12fdf2..11ffc3ed74d93759c00bb7d83ab6f2ee7ab3fc26 100644 (file)
@@ -33,31 +33,39 @@ import org.slf4j.LoggerFactory;
 public class Tr064PhonebookImpl implements Phonebook {
     private final Logger logger = LoggerFactory.getLogger(Tr064PhonebookImpl.class);
 
-    private Map<String, String> phonebook = new HashMap<>();
+    protected Map<String, String> phonebook = new HashMap<>();
 
     private final HttpClient httpClient;
     private final String phonebookUrl;
+    private final int httpTimeout;
 
     private String phonebookName = "";
 
-    public Tr064PhonebookImpl(HttpClient httpClient, String phonebookUrl) {
+    public Tr064PhonebookImpl(HttpClient httpClient, String phonebookUrl, int httpTimeout) {
         this.httpClient = httpClient;
         this.phonebookUrl = phonebookUrl;
+        this.httpTimeout = httpTimeout;
         getPhonebook();
     }
 
     private void getPhonebook() {
-        PhonebooksType phonebooksType = Util.getAndUnmarshalXML(httpClient, phonebookUrl, PhonebooksType.class);
-        if (phonebooksType != null) {
-            phonebookName = phonebooksType.getPhonebook().getName();
-            phonebook = phonebooksType.getPhonebook().getContact().stream().map(contact -> {
-                String contactName = contact.getPerson().getRealName();
-                return contact.getTelephony().getNumber().stream()
-                        .collect(Collectors.toMap(number -> normalizeNumber(number.getValue()), number -> contactName,
-                                this::mergeSameContactNames));
-            }).collect(HashMap::new, HashMap::putAll, HashMap::putAll);
-            logger.debug("Downloaded phonebook {}: {}", phonebookName, phonebook);
+        PhonebooksType phonebooksType = Util.getAndUnmarshalXML(httpClient, phonebookUrl, PhonebooksType.class,
+                httpTimeout);
+        if (phonebooksType == null) {
+            logger.warn("Failed to get phonebook with URL '{}'", phonebookUrl);
+            return;
         }
+        phonebookName = phonebooksType.getPhonebook().getName();
+
+        phonebook = phonebooksType.getPhonebook().getContact().stream().map(contact -> {
+            String contactName = contact.getPerson().getRealName();
+            if (contactName == null || contactName.isBlank()) {
+                return new HashMap<String, String>();
+            }
+            return contact.getTelephony().getNumber().stream().collect(Collectors.toMap(
+                    number -> normalizeNumber(number.getValue()), number -> contactName, this::mergeSameContactNames));
+        }).collect(HashMap::new, HashMap::putAll, HashMap::putAll);
+        logger.debug("Downloaded phonebook {}: {}", phonebookName, phonebook);
     }
 
     // in case there are multiple phone entries with same number -> name mapping, i.e. in phonebooks exported from
@@ -78,9 +86,14 @@ public class Tr064PhonebookImpl implements Phonebook {
     @Override
     public Optional<String> lookupNumber(String number, int matchCount) {
         String normalized = normalizeNumber(number);
-        String matchString = matchCount > 0 && matchCount < normalized.length()
-                ? normalized.substring(normalized.length() - matchCount)
-                : normalized;
+        String matchString;
+        if (matchCount > 0 && matchCount < normalized.length()) {
+            matchString = normalized.substring(normalized.length() - matchCount);
+        } else if (matchCount < 0 && (-matchCount) < normalized.length()) {
+            matchString = normalized.substring(-matchCount);
+        } else {
+            matchString = normalized;
+        }
         logger.trace("Normalized '{}' to '{}', matchString is '{}'", number, normalized, matchString);
         return matchString.isBlank() ? Optional.empty()
                 : phonebook.keySet().stream().filter(n -> n.endsWith(matchString)).findFirst().map(phonebook::get);
@@ -91,8 +104,13 @@ public class Tr064PhonebookImpl implements Phonebook {
         return "Phonebook{" + "phonebookName='" + phonebookName + "', phonebook=" + phonebook + '}';
     }
 
-    private String normalizeNumber(String number) {
-        // Naive normalization: remove all non-digit characters
+    /**
+     * normalize a phone number (remove everything except digits and *) for comparison
+     *
+     * @param number the input phone number string
+     * @return normalized phone number string
+     */
+    public final String normalizeNumber(String number) {
         return number.replaceAll("[^0-9\\*\\+]", "");
     }
 }
index 445a6f3c415b3500bd95ee11a4927634e736ce06..24e0cc978a5a3cc2845f58a9ab6d88b9082e278b 100644 (file)
  */
 package org.openhab.binding.tr064.internal.soap;
 
-import java.time.LocalDateTime;
-import java.time.ZoneId;
-import java.time.format.DateTimeFormatter;
-import java.time.format.DateTimeParseException;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
 import java.util.Date;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
@@ -23,14 +21,14 @@ import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.tr064.internal.dto.additions.Call;
 
 /**
- * The {@link CallListEntry} is used for post processing the retrieved call
+ * The {@link CallListEntry} is used for post-processing the retrieved call
  * lists
  *
  * @author Jan N. Klug - Initial contribution
  */
 @NonNullByDefault
 public class CallListEntry {
-    private static final DateTimeFormatter DATE_FORMAT_PARSER = DateTimeFormatter.ofPattern("dd.MM.yy HH:mm");
+    private static final SimpleDateFormat DATE_FORMAT_PARSER = new SimpleDateFormat("dd.MM.yy HH:mm");
     public @Nullable String localNumber;
     public @Nullable String remoteNumber;
     public @Nullable Date date;
@@ -39,9 +37,10 @@ public class CallListEntry {
 
     public CallListEntry(Call call) {
         try {
-            date = Date.from(
-                    LocalDateTime.parse(call.getDate(), DATE_FORMAT_PARSER).atZone(ZoneId.systemDefault()).toInstant());
-        } catch (DateTimeParseException e) {
+            synchronized (DATE_FORMAT_PARSER) {
+                date = DATE_FORMAT_PARSER.parse(call.getDate());
+            }
+        } catch (ParseException e) {
             // ignore parsing error
             date = null;
         }
index ce172e315c41025700ff34f9d99ded77f834ae1d..7026ab60860cb8a288df435b2dba8f41bbf79618 100644 (file)
@@ -15,11 +15,10 @@ package org.openhab.binding.tr064.internal.soap;
 import org.eclipse.jdt.annotation.NonNullByDefault;
 
 /**
- * The {@link CallListType} is used for post processing the retrieved call list
+ * The {@link CallListType} is used for post-processing the retrieved call list
  *
  * @author Jan N. Klug - Initial contribution
  */
-
 @NonNullByDefault
 public enum CallListType {
     MISSED_COUNT("2"),
index ef41671cf86f9cc44bc700e1b8967d1fb39042d7..d97acbb895a36d67fed63ea760731710a91e705a 100644 (file)
@@ -16,8 +16,8 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
 
 /**
  *
- * The{@link PostProcessingException} is a catched Exception that is thrown in case of conversion errors during post
- * processing
+ * The {@link PostProcessingException} is an Exception that is thrown in case of conversion errors during
+ * post-processing
  *
  * @author Jan N. Klug - Initial contribution
  */
index 515f33e46aca253c7258ab32ee167455d6d69b2a..3aa372d58f28a79cff691ad3c04d56e479dc45a6 100644 (file)
@@ -19,7 +19,6 @@ import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.time.Duration;
 import java.util.HashMap;
-import java.util.Iterator;
 import java.util.Map;
 import java.util.Objects;
 import java.util.concurrent.ExecutionException;
@@ -28,7 +27,6 @@ import java.util.concurrent.TimeoutException;
 import java.util.stream.Collectors;
 
 import javax.xml.soap.MessageFactory;
-import javax.xml.soap.MimeHeader;
 import javax.xml.soap.MimeHeaders;
 import javax.xml.soap.SOAPBody;
 import javax.xml.soap.SOAPElement;
@@ -67,19 +65,20 @@ import org.slf4j.LoggerFactory;
  */
 @NonNullByDefault
 public class SOAPConnector {
-    private static final int SOAP_TIMEOUT = 5; // in
     private final Logger logger = LoggerFactory.getLogger(SOAPConnector.class);
     private final HttpClient httpClient;
     private final String endpointBaseURL;
     private final SOAPValueConverter soapValueConverter;
+    private final int timeout;
 
     private final ExpiringCacheMap<SOAPRequest, SOAPMessage> soapMessageCache = new ExpiringCacheMap<>(
             Duration.ofMillis(2000));
 
-    public SOAPConnector(HttpClient httpClient, String endpointBaseURL) {
+    public SOAPConnector(HttpClient httpClient, String endpointBaseURL, int timeout) {
         this.httpClient = httpClient;
         this.endpointBaseURL = endpointBaseURL;
-        this.soapValueConverter = new SOAPValueConverter(httpClient);
+        this.timeout = timeout;
+        this.soapValueConverter = new SOAPValueConverter(httpClient, timeout);
     }
 
     /**
@@ -118,7 +117,7 @@ public class SOAPConnector {
         // create Request and add headers and content
         Request request = httpClient.newRequest(endpointBaseURL + soapRequest.service.getControlURL())
                 .method(HttpMethod.POST);
-        ((Iterator<MimeHeader>) soapMessage.getMimeHeaders().getAllHeaders())
+        soapMessage.getMimeHeaders().getAllHeaders()
                 .forEachRemaining(header -> request.header(header.getName(), header.getValue()));
         try (final ByteArrayOutputStream os = new ByteArrayOutputStream()) {
             soapMessage.writeTo(os);
@@ -169,7 +168,7 @@ public class SOAPConnector {
      */
     public synchronized SOAPMessage doSOAPRequestUncached(SOAPRequest soapRequest) throws Tr064CommunicationException {
         try {
-            Request request = prepareSOAPRequest(soapRequest).timeout(SOAP_TIMEOUT, TimeUnit.SECONDS);
+            Request request = prepareSOAPRequest(soapRequest).timeout(timeout, TimeUnit.SECONDS);
             if (logger.isTraceEnabled()) {
                 request.getContent().forEach(buffer -> logger.trace("Request: {}", new String(buffer.array())));
             }
@@ -179,7 +178,7 @@ public class SOAPConnector {
                 // retry once if authentication expired
                 logger.trace("Re-Auth needed.");
                 httpClient.getAuthenticationStore().clearAuthenticationResults();
-                request = prepareSOAPRequest(soapRequest).timeout(SOAP_TIMEOUT, TimeUnit.SECONDS);
+                request = prepareSOAPRequest(soapRequest).timeout(timeout, TimeUnit.SECONDS);
                 response = request.send();
             }
             try (final ByteArrayInputStream is = new ByteArrayInputStream(response.getContent())) {
@@ -247,14 +246,11 @@ public class SOAPConnector {
             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;
-                }
+                return switch (channelConfig.getDataType()) {
+                    case "boolean" -> OnOffType.OFF;
+                    case "string" -> StringType.EMPTY;
+                    default -> UnDefType.UNDEF;
+                };
             }
 
             // get value(s) from remote device
@@ -290,11 +286,13 @@ public class SOAPConnector {
         } catch (Tr064CommunicationException e) {
             if (e.getHttpError() == 500) {
                 switch (e.getSoapError()) {
-                    case "714":
+                    case "714" -> {
                         // NoSuchEntryInArray usually is an unknown entry in the MAC list
                         logger.debug("Failed to get {}: {}", channelConfig, e.getMessage());
                         return UnDefType.UNDEF;
-                    default:
+                    }
+                    default -> {
+                    }
                 }
             }
             // all other cases are an error
index 2b50ef644766f4573adc702aa519d3aa071d7553..5e4944234f4df1b5895ea34fc91d7f0620a14675 100644 (file)
@@ -48,7 +48,6 @@ public class SOAPRequest {
         if (o == null || getClass() != o.getClass()) {
             return false;
         }
-
         SOAPRequest that = (SOAPRequest) o;
 
         if (!service.equals(that.service)) {
@@ -57,6 +56,7 @@ public class SOAPRequest {
         if (!soapAction.equals(that.soapAction)) {
             return false;
         }
+
         return arguments.equals(that.arguments);
     }
 
index 1822bb7ba8b238da8e112fefcaf91a3bde9856fe..7324ccf838ce7f73273afa73aac577a4bdd3b4fc 100644 (file)
@@ -17,6 +17,7 @@ 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.Arrays;
 import java.util.List;
 import java.util.Optional;
 import java.util.concurrent.ExecutionException;
@@ -57,9 +58,11 @@ import com.google.gson.GsonBuilder;
 public class SOAPValueConverter {
     private final Logger logger = LoggerFactory.getLogger(SOAPValueConverter.class);
     private final HttpClient httpClient;
+    private final int timeout;
 
-    public SOAPValueConverter(HttpClient httpClient) {
+    public SOAPValueConverter(HttpClient httpClient, int timeout) {
         this.httpClient = httpClient;
+        this.timeout = timeout;
     }
 
     /**
@@ -83,24 +86,26 @@ public class SOAPValueConverter {
                 return Optional.empty();
             }
             switch (dataType) {
-                case "ui1":
-                case "ui2":
+                case "ui1", "ui2" -> {
                     return Optional.of(String.valueOf(value.shortValue()));
-                case "i4":
-                case "ui4":
+                }
+                case "i4", "ui4" -> {
                     return Optional.of(String.valueOf(value.intValue()));
-                default:
+                }
+                default -> {
+                }
             }
         } else if (command instanceof DecimalType) {
             BigDecimal value = ((DecimalType) command).toBigDecimal();
             switch (dataType) {
-                case "ui1":
-                case "ui2":
+                case "ui1", "ui2" -> {
                     return Optional.of(String.valueOf(value.shortValue()));
-                case "i4":
-                case "ui4":
+                }
+                case "i4", "ui4" -> {
                     return Optional.of(String.valueOf(value.intValue()));
-                default:
+                }
+                default -> {
+                }
             }
         } else if (command instanceof StringType) {
             if ("string".equals(dataType)) {
@@ -127,28 +132,35 @@ public class SOAPValueConverter {
             @Nullable Tr064ChannelConfig channelConfig) {
         String dataType = channelConfig != null ? channelConfig.getDataType() : "string";
         String unit = channelConfig != null ? channelConfig.getChannelTypeDescription().getItem().getUnit() : "";
+        BigDecimal factor = channelConfig != null ? channelConfig.getChannelTypeDescription().getItem().getFactor()
+                : null;
 
         return getSOAPElement(soapMessage, element).map(rawValue -> {
             // map rawValue to State
             switch (dataType) {
-                case "boolean":
+                case "boolean" -> {
                     return rawValue.equals("0") ? OnOffType.OFF : OnOffType.ON;
-                case "string":
+                }
+                case "string" -> {
                     return new StringType(rawValue);
-                case "ui1":
-                case "ui2":
-                case "i4":
-                case "ui4":
+                }
+                case "ui1", "ui2", "i4", "ui4" -> {
+                    BigDecimal decimalValue = new BigDecimal(rawValue);
+                    if (factor != null) {
+                        decimalValue = decimalValue.multiply(factor);
+                    }
                     if (!unit.isEmpty()) {
-                        return new QuantityType<>(rawValue + " " + unit);
+                        return new QuantityType<>(decimalValue + " " + unit);
                     } else {
-                        return new DecimalType(rawValue);
+                        return new DecimalType(decimalValue);
                     }
-                default:
+                }
+                default -> {
                     return null;
+                }
             }
         }).map(state -> {
-            // check if we need post processing
+            // check if we need post-processing
             if (channelConfig == null
                     || channelConfig.getChannelTypeDescription().getGetAction().getPostProcessor() == null) {
                 return state;
@@ -172,6 +184,26 @@ public class SOAPValueConverter {
         }).or(Optional::empty);
     }
 
+    /**
+     * post processor for current bitrate
+     */
+    @SuppressWarnings("unused")
+    private State processCurrentBitrate(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException {
+        Double bps = Arrays.stream(state.toString().split(",")).mapToDouble(s -> {
+            try {
+                return Double.parseDouble(s);
+            } catch (NumberFormatException e) {
+                return 0.0;
+            }
+        }).limit(3).average().orElse(Double.NaN);
+
+        if (bps.equals(Double.NaN)) {
+            return UnDefType.UNDEF;
+        } else {
+            return new QuantityType<>(bps * 8.0 / 1024.0, Units.KILOBIT_PER_SECOND);
+        }
+    }
+
     /**
      * post processor to map mac device signal strength to system.signal-strength 0-4
      *
@@ -201,20 +233,6 @@ public class SOAPValueConverter {
         return mappedSignalStrength;
     }
 
-    /**
-     * post processor for decibel values (which are served as deca decibel)
-     *
-     * @param state the channel value in deca decibel
-     * @param channelConfig channel config of the channel
-     * @return the state converted to decibel
-     */
-    @SuppressWarnings("unused")
-    private State processDecaDecibel(State state, Tr064ChannelConfig channelConfig) {
-        Float value = state.as(DecimalType.class).floatValue() / 10;
-
-        return new QuantityType<>(value, Units.DECIBEL);
-    }
-
     /**
      * post processor for answering machine new messages channel
      *
@@ -226,16 +244,14 @@ public class SOAPValueConverter {
     @SuppressWarnings("unused")
     private State processTamListURL(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException {
         try {
-            ContentResponse response = httpClient.newRequest(state.toString()).timeout(1500, TimeUnit.MILLISECONDS)
+            ContentResponse response = httpClient.newRequest(state.toString()).timeout(timeout, TimeUnit.MILLISECONDS)
                     .send();
             String responseContent = response.getContentAsString();
             int messageCount = responseContent.split("<New>1</New>").length - 1;
 
             return new DecimalType(messageCount);
-        } catch (TimeoutException e) {
-            throw new PostProcessingException("Failed to get TAM list due to time out from URL " + state.toString(), e);
-        } catch (InterruptedException | ExecutionException e) {
-            throw new PostProcessingException("Failed to get TAM list from URL " + state.toString(), e);
+        } catch (InterruptedException | TimeoutException | ExecutionException e) {
+            throw new PostProcessingException("Failed to get TAM list from URL " + state, e);
         }
     }
 
@@ -315,23 +331,22 @@ public class SOAPValueConverter {
      */
     private State processCallList(State state, @Nullable String days, CallListType type)
             throws PostProcessingException {
-        Root callListRoot = Util.getAndUnmarshalXML(httpClient, state.toString() + "&days=" + days, Root.class);
+        Root callListRoot = Util.getAndUnmarshalXML(httpClient, state + "&days=" + days, Root.class, timeout);
         if (callListRoot == null) {
-            throw new PostProcessingException("Failed to get call list from URL " + state.toString());
+            throw new PostProcessingException("Failed to get call list from URL " + state);
         }
         List<Call> calls = callListRoot.getCall();
         switch (type) {
-            case INBOUND_COUNT:
-            case MISSED_COUNT:
-            case OUTBOUND_COUNT:
-            case REJECTED_COUNT:
+            case INBOUND_COUNT, MISSED_COUNT, OUTBOUND_COUNT, REJECTED_COUNT -> {
                 long callCount = calls.stream().filter(call -> type.typeString().equals(call.getType())).count();
                 return new DecimalType(callCount);
-            case JSON_LIST:
+            }
+            case JSON_LIST -> {
                 Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ssX").serializeNulls().create();
                 List<CallListEntry> callListEntries = calls.stream().map(CallListEntry::new)
                         .collect(Collectors.toList());
                 return new StringType(gson.toJson(callListEntries));
+            }
         }
         return UnDefType.UNDEF;
     }
index 34b6f49e1316e1342090c3a49285195fd263b1a1..f5540299daa00e53850a85a2086d383e8fe2ff98 100644 (file)
@@ -36,22 +36,23 @@ import org.openhab.binding.tr064.internal.dto.scpd.service.SCPDScpdType;
  */
 @NonNullByDefault
 public class SCPDUtil {
-    private SCPDRootType scpdRoot;
+    private final 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 {
-        SCPDRootType scpdRoot = Util.getAndUnmarshalXML(httpClient, endpoint + "/tr64desc.xml", SCPDRootType.class);
+    public SCPDUtil(HttpClient httpClient, String endpoint, int timeout) throws SCPDException {
+        SCPDRootType scpdRoot = Util.getAndUnmarshalXML(httpClient, endpoint + "/tr64desc.xml", SCPDRootType.class,
+                timeout);
         if (scpdRoot == null) {
             throw new SCPDException("could not get SCPD root");
         }
         this.scpdRoot = scpdRoot;
 
-        scpdDevicesList.addAll(flatDeviceList(scpdRoot.getDevice()).collect(Collectors.toList()));
+        scpdDevicesList.addAll(flatDeviceList(scpdRoot.getDevice()).toList());
         for (SCPDDeviceType device : scpdDevicesList) {
             for (SCPDServiceType service : device.getServiceList()) {
                 SCPDScpdType scpd = serviceMap.computeIfAbsent(service.getServiceId(), serviceId -> Util
-                        .getAndUnmarshalXML(httpClient, endpoint + service.getSCPDURL(), SCPDScpdType.class));
+                        .getAndUnmarshalXML(httpClient, endpoint + service.getSCPDURL(), SCPDScpdType.class, timeout));
                 if (scpd == null) {
                     throw new SCPDException("could not get SCPD service");
                 }
@@ -80,7 +81,7 @@ public class SCPDUtil {
     }
 
     /**
-     * get a single device by it's UDN
+     * get a single device by its UDN
      *
      * @param udn the device UDN
      * @return the device
@@ -94,7 +95,7 @@ public class SCPDUtil {
     }
 
     /**
-     * get a single service by it's serviceId
+     * get a single service by its serviceId
      *
      * @param serviceId the service id
      * @return the service
index ea4129399f4ce22f4a9654ca5f47e2d2d0cb721d..dc4c7c37b33d849e70b03eaa6411c064cc9a9052 100644 (file)
@@ -18,11 +18,10 @@ import java.io.ByteArrayInputStream;
 import java.io.InputStream;
 import java.lang.reflect.Field;
 import java.time.Duration;
-import java.util.Collections;
-import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
@@ -64,9 +63,10 @@ import org.openhab.binding.tr064.internal.dto.scpd.service.SCPDDirection;
 import org.openhab.binding.tr064.internal.dto.scpd.service.SCPDScpdType;
 import org.openhab.binding.tr064.internal.dto.scpd.service.SCPDStateVariableType;
 import org.openhab.core.cache.ExpiringCacheMap;
+import org.openhab.core.thing.Channel;
 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.ThingHandlerCallback;
 import org.openhab.core.thing.binding.builder.ThingBuilder;
 import org.openhab.core.thing.type.ChannelTypeUID;
 import org.openhab.core.util.UIDUtils;
@@ -82,7 +82,6 @@ import org.w3c.dom.NodeList;
 @NonNullByDefault
 public class Util {
     private static final Logger LOGGER = LoggerFactory.getLogger(Util.class);
-    private static final int HTTP_REQUEST_TIMEOUT = 5; // in s
     // cache XML content for 5s
     private static final ExpiringCacheMap<String, Object> XML_OBJECT_CACHE = new ExpiringCacheMap<>(
             Duration.ofMillis(3000));
@@ -169,8 +168,8 @@ public class Util {
      * @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) {
+    public static void checkAvailableChannels(Thing thing, ThingHandlerCallback callback, 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);
@@ -179,13 +178,6 @@ public class Util {
                 .forEach(channelTypeDescription -> {
                     String channelId = channelTypeDescription.getName();
                     String serviceId = channelTypeDescription.getService().getServiceId();
-                    String typeId = channelTypeDescription.getTypeId();
-                    Map<String, String> channelProperties = new HashMap<String, String>();
-
-                    if (typeId != null) {
-                        channelProperties.put("typeId", typeId);
-                    }
-
                     Set<String> parameters = new HashSet<>();
                     try {
                         SCPDServiceType deviceService = scpdUtil.getDevice(deviceId)
@@ -199,6 +191,7 @@ public class Util {
                                 deviceService);
 
                         // get
+                        boolean fixedValue = false;
                         ActionType getAction = channelTypeDescription.getGetAction();
                         if (getAction != null) {
                             String actionName = getAction.getName();
@@ -208,7 +201,9 @@ public class Util {
                             SCPDStateVariableType relatedStateVariable = getStateVariable(serviceRoot, scpdArgument);
                             parameters.addAll(
                                     getAndCheckParameters(channelId, getAction, scpdAction, serviceRoot, thingConfig));
-
+                            if (getAction.getParameter() != null && getAction.getParameter().getFixedValue() != null) {
+                                fixedValue = true;
+                            }
                             channelConfig.setGetAction(scpdAction);
                             channelConfig.setDataType(relatedStateVariable.getDataType());
                         }
@@ -233,16 +228,20 @@ public class Util {
                         }
 
                         // everything is available, create the channel
-                        ChannelTypeUID channelTypeUID = new ChannelTypeUID(BINDING_ID,
-                                channelTypeDescription.getName());
-                        if (parameters.isEmpty()) {
+                        String channelType = Objects.requireNonNullElse(channelTypeDescription.getTypeId(), "");
+                        ChannelTypeUID channelTypeUID = channelType.isBlank()
+                                ? new ChannelTypeUID(BINDING_ID, channelTypeDescription.getName())
+                                : new ChannelTypeUID(channelType);
+                        if (parameters.isEmpty() || fixedValue) {
                             // 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).withProperties(channelProperties);
-                            thingBuilder.withChannel(channelBuilder.build());
-                            channels.put(channelUID, channelConfig);
+                            Channel channel = callback.createChannelBuilder(channelUID, channelTypeUID).build();
+                            thingBuilder.withChannel(channel);
+                            Tr064ChannelConfig channelConfig1 = new Tr064ChannelConfig(channelConfig);
+                            if (fixedValue) {
+                                channelConfig1.setParameter(parameters.iterator().next());
+                            }
+                            channels.put(channelUID, channelConfig1);
                         } else {
                             // create a channel for each parameter
                             parameters.forEach(parameter -> {
@@ -252,11 +251,9 @@ public class Util {
                                 String normalizedParameter = UIDUtils.encode(rawParameter);
                                 ChannelUID channelUID = new ChannelUID(thing.getUID(),
                                         channelId + "_" + normalizedParameter);
-                                ChannelBuilder channelBuilder = ChannelBuilder
-                                        .create(channelUID, channelTypeDescription.getItem().getType())
-                                        .withType(channelTypeUID).withProperties(channelProperties)
-                                        .withLabel(channelTypeDescription.getLabel() + " " + parameter);
-                                thingBuilder.withChannel(channelBuilder.build());
+                                Channel channel = callback.createChannelBuilder(channelUID, channelTypeUID)
+                                        .withLabel(channelTypeDescription.getLabel() + " " + parameter).build();
+                                thingBuilder.withChannel(channel);
                                 Tr064ChannelConfig channelConfig1 = new Tr064ChannelConfig(channelConfig);
                                 channelConfig1.setParameter(rawParameter);
                                 channels.put(channelUID, channelConfig1);
@@ -272,8 +269,12 @@ public class Util {
             SCPDScpdType serviceRoot, Tr064BaseThingConfiguration thingConfig) throws ChannelConfigException {
         ParameterType parameter = action.getParameter();
         if (parameter == null) {
-            return Collections.emptySet();
+            return Set.of();
         }
+        if (parameter.getFixedValue() != null) {
+            return Set.of(parameter.getFixedValue());
+        }
+        // process list of thing parameters
         try {
             Set<String> parameters = new HashSet<>();
 
@@ -292,9 +293,12 @@ public class Util {
             String parameterPattern = parameter.getPattern();
             if (parameterPattern != null) {
                 parameters.removeIf(param -> {
-                    if (!param.matches(parameterPattern)) {
-                        LOGGER.warn("Removing {} while processing {}, does not match pattern {}, check config.", param,
-                                channelId, parameterPattern);
+                    if (param.isBlank()) {
+                        LOGGER.debug("Removing empty parameter while processing '{}'.", channelId);
+                        return true;
+                    } else if (!param.matches(parameterPattern)) {
+                        LOGGER.warn("Removing '{}' while processing '{}', does not match pattern '{}', check config.",
+                                param, channelId, parameterPattern);
                         return true;
                     } else {
                         return false;
@@ -344,16 +348,17 @@ public class Util {
      *
      * @param uri the uri of the XML file
      * @param clazz the class describing the XML file
+     * @param timeout timeout in s
      * @return unmarshalling result
      */
     @SuppressWarnings("unchecked")
-    public static <T> @Nullable T getAndUnmarshalXML(HttpClient httpClient, String uri, Class<T> clazz) {
+    public static <T> @Nullable T getAndUnmarshalXML(HttpClient httpClient, String uri, Class<T> clazz, int timeout) {
         try {
             T returnValue = (T) XML_OBJECT_CACHE.putIfAbsentAndGet(uri, () -> {
                 try {
                     LOGGER.trace("Refreshing cache for '{}'", uri);
-                    ContentResponse contentResponse = httpClient.newRequest(uri)
-                            .timeout(HTTP_REQUEST_TIMEOUT, TimeUnit.SECONDS).method(HttpMethod.GET).send();
+                    ContentResponse contentResponse = httpClient.newRequest(uri).timeout(timeout, TimeUnit.SECONDS)
+                            .method(HttpMethod.GET).send();
                     byte[] response = contentResponse.getContent();
                     if (LOGGER.isTraceEnabled()) {
                         LOGGER.trace("XML = {}", new String(response));
index 32de7b2667ecd5fa557b8b50df20eb3f115b6315..c0e8804aea0e39e1a307845ca016d5d87bd59178 100644 (file)
@@ -9,9 +9,10 @@
                        <label>Phone Book</label>
                        <description>The name of the the phone book.</description>
                </parameter>
-               <parameter name="matchCount" type="integer" min="0" step="1">
+               <parameter name="matchCount" type="integer" step="1">
                        <label>Match Count</label>
-                       <description>The number of digits matching the incoming value, counted from far right (default is 0 = all matching)</description>
+                       <description>The number of digits matching the incoming value, counted from far right (default is 0 = all matching).
+                               Negative numbers skip digits from the left.</description>
                        <default>0</default>
                </parameter>
                <parameter name="phoneNumberIndex" type="integer" min="0" max="1" step="1">
index de1394b2b0b50aa875c7c5e2769dc55862606368..80141aeb90756472b556a29808b13975e123aeb7 100644 (file)
@@ -15,6 +15,10 @@ thing-type.tr064.subdeviceLan.description = A virtual Sub-Device (LAN).
 
 # thing types config
 
+thing-type.config.tr064.fritzbox.backupDirectory.label = Backup Directory
+thing-type.config.tr064.fritzbox.backupDirectory.description = The directory where configuration backups are stored (default to userdata directory).
+thing-type.config.tr064.fritzbox.backupPassword.label = Backup Password
+thing-type.config.tr064.fritzbox.backupPassword.description = The password used to encrypt the backup data.
 thing-type.config.tr064.fritzbox.callDeflectionIndices.label = Call Deflection
 thing-type.config.tr064.fritzbox.callDeflectionIndices.description = List of call deflection IDs (starting with 0).
 thing-type.config.tr064.fritzbox.callListDays.label = Call List Days
@@ -35,6 +39,8 @@ thing-type.config.tr064.fritzbox.rejectedCallDays.label = Rejected Call Days
 thing-type.config.tr064.fritzbox.rejectedCallDays.description = List of days for which rejected calls should be calculated.
 thing-type.config.tr064.fritzbox.tamIndices.label = TAM
 thing-type.config.tr064.fritzbox.tamIndices.description = List of answering machines (starting with 0).
+thing-type.config.tr064.fritzbox.timeout.label = Timeout
+thing-type.config.tr064.fritzbox.timeout.description = Timeout for all requests (SOAP requests, phone book retrieval, call lists, ...).
 thing-type.config.tr064.fritzbox.user.label = Username
 thing-type.config.tr064.fritzbox.wanBlockIPs.label = WAN Block IPs
 thing-type.config.tr064.fritzbox.wanBlockIPs.description = List of IPs that can be blocked for WAN access.
@@ -42,6 +48,8 @@ thing-type.config.tr064.generic.host.label = Host
 thing-type.config.tr064.generic.host.description = Host name or IP address.
 thing-type.config.tr064.generic.password.label = Password
 thing-type.config.tr064.generic.refresh.label = Refresh Interval
+thing-type.config.tr064.generic.timeout.label = Timeout
+thing-type.config.tr064.generic.timeout.description = Timeout for all requests (SOAP requests, phone book retrieval, call lists, ...).
 thing-type.config.tr064.generic.user.label = Username
 thing-type.config.tr064.subdevice.refresh.label = Refresh Interval
 thing-type.config.tr064.subdevice.uuid.label = UUID
index 50cad826894cbc5846026e9fb9324a35a8032b76..72a762b984f0afe5d69b4ecec21112bb9fff4638 100644 (file)
                                <label>Refresh Interval</label>
                                <default>60</default>
                        </parameter>
+                       <parameter name="timeout" type="integer" unit="s">
+                               <label>Timeout</label>
+                               <description>Timeout for all requests (SOAP requests, phone book retrieval, call lists, ...).</description>
+                               <default>5</default>
+                               <advanced>true</advanced>
+                       </parameter>
                </config-description>
        </bridge-type>
 
                                <label>Refresh Interval</label>
                                <default>60</default>
                        </parameter>
+                       <parameter name="timeout" type="integer" unit="s">
+                               <label>Timeout</label>
+                               <description>Timeout for all requests (SOAP requests, phone book retrieval, call lists, ...).</description>
+                               <default>5</default>
+                               <advanced>true</advanced>
+                       </parameter>
                        <parameter name="tamIndices" type="text" multiple="true">
                                <label>TAM</label>
                                <description>List of answering machines (starting with 0).</description>
                                <default>600</default>
                                <advanced>true</advanced>
                        </parameter>
+                       <parameter name="backupDirectory" type="text">
+                               <label>Backup Directory</label>
+                               <description>The directory where configuration backups are stored (default to userdata directory).</description>
+                               <advanced>true</advanced>
+                       </parameter>
+                       <parameter name="backupPassword" type="text">
+                               <label>Backup Password</label>
+                               <description>The password used to encrypt the backup data.</description>
+                               <context>password</context>
+                               <advanced>true</advanced>
+                       </parameter>
                </config-description>
        </bridge-type>
 
index 80e310c4096cf7e73cec7e6018037a329232254c..7db46f24e9f21c3b7e20995c2681d9536a2e8681 100644 (file)
@@ -65,7 +65,7 @@
                        serviceId="urn:X_AVM-DE_HostFilter-com:serviceId:X_AVM-DE_HostFilter1"/>
                <getAction name="GetWANAccessByIP" argument="NewDisallow">
                        <parameter name="NewIPv4Address" thingParameter="wanBlockIPs"
-                               pattern="^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)(\.(?!$)|$)){4}$"/>
+                               pattern="((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)(\.(?!([\s#]|$))|([\s#]|$))){4}(\s*#.*)*"/>
                </getAction>
                <setAction name="DisallowWANAccessByIP" argument="NewDisallow">
                        <parameter name="NewIPv4Address" thingParameter="wanBlockIPs"/>
                <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">
+       <channel name="macOnline" label="MAC Online" description="Online status of the device with the given MAC"
+               advanced="true">
                <item type="Switch"/>
                <service deviceType="urn:dslforum-org:device:LANDevice:1" serviceId="urn:LanDeviceHosts-com:serviceId:Hosts1"/>
                <getAction name="GetSpecificHostEntry" argument="NewActive">
                                pattern="([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}(\s*#.*)*"/>
                </getAction>
        </channel>
-       <channel name="macIP" label="MAC IP" description="IP of the device with the given MAC">
+       <channel name="macOnlineIpAddress" label="MAC Online IP"
+               description="IP of the device with the given MAC (see macOnline)" advanced="true">
                <item type="String"/>
                <service deviceType="urn:dslforum-org:device:LANDevice:1" serviceId="urn:LanDeviceHosts-com:serviceId:Hosts1"/>
                <getAction name="GetSpecificHostEntry" argument="NewIPAddress">
                                pattern="([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}(\s*#.*)*"/>
                </getAction>
        </channel>
-
        <!-- WLAN Config 1 - 2.4 Ghz -->
        <channel name="macSignalStrength1" label="MAC Wifi Signal Strength 2.4Ghz"
                description="Wifi Signal Strength of the device with
                the given MAC. This is set in case the Device is connected to 2.4Ghz"
-               typeId="system.signal-strength">
+               typeId="system:signal-strength">
                <item type="Number"/>
                <service deviceType="urn:dslforum-org:device:LANDevice:1"
                        serviceId="urn:WLANConfiguration-com:serviceId:WLANConfiguration1"/>
        </channel>
        <channel name="macSpeed1" label="MAC Wifi Speed 2.4Ghz"
                description="Wifi Speed of the device with
-               the given MAC. This is set in case the Device is connected to 2.4Ghz">
+               the given MAC (see macOnline). This is set in case the Device is connected to 2.4Ghz">
                <item type="Number:DataTransferRate" unit="Mbit/s" statePattern="%d Mbit/s"/>
                <service deviceType="urn:dslforum-org:device:LANDevice:1"
                        serviceId="urn:WLANConfiguration-com:serviceId:WLANConfiguration1"/>
        <!-- WLAN Config 2 - 5 Ghz -->
        <channel name="macSignalStrength2" label="MAC Wifi Signal Strength 5Ghz"
                description="Wifi Signal Strength of the device with
-               the given MAC. This is set in case the Device is connected to 5Ghz"
-               typeId="system.signal-strength">
+               the given MAC (see macOnline). This is set in case the Device is connected to 5Ghz"
+               typeId="system:signal-strength">
                <item type="Number"/>
                <service deviceType="urn:dslforum-org:device:LANDevice:1"
                        serviceId="urn:WLANConfiguration-com:serviceId:WLANConfiguration2"/>
        </channel>
        <channel name="macSpeed2" label="MAC Wifi Speed 5Ghz"
                description="Wifi Speed of the device with
-               the given MAC. This is set in case the Device is connected to 5Ghz">
+               the given MAC (see macOnline). This is set in case the Device is connected to 5Ghz">
                <item type="Number:DataTransferRate" unit="Mbit/s" statePattern="%d Mbit/s"/>
                <service deviceType="urn:dslforum-org:device:LANDevice:1"
                        serviceId="urn:WLANConfiguration-com:serviceId:WLANConfiguration2"/>
                        serviceId="urn:WANCIfConfig-com:serviceId:WANCommonInterfaceConfig1"/>
                <getAction name="GetTotalBytesSent" argument="NewTotalBytesSent"/>
        </channel>
+       <channel name="wanCurrentDownstreamBitrate" label="Current Downstream Rate">
+               <item type="Number:DataTransferRate" unit="kbit/s" statePattern="%.1f Mbit/s"/>
+               <service deviceType="urn:dslforum-org:device:WANDevice:1"
+                       serviceId="urn:WANCIfConfig-com:serviceId:WANCommonInterfaceConfig1"/>
+               <getAction name="X_AVM-DE_GetOnlineMonitor" argument="Newds_current_bps"
+                       postProcessor="processCurrentBitrate">
+                       <parameter name="NewSyncGroupIndex" fixedValue="0"/>
+               </getAction>
+       </channel>
+       <channel name="wanCurrentUpstreamBitrate" label="Current Upstream Rate">
+               <item type="Number:DataTransferRate" unit="kbit/s" statePattern="%.1f Mbit/s"/>
+               <service deviceType="urn:dslforum-org:device:WANDevice:1"
+                       serviceId="urn:WANCIfConfig-com:serviceId:WANCommonInterfaceConfig1"/>
+               <getAction name="X_AVM-DE_GetOnlineMonitor" argument="Newus_current_bps"
+                       postProcessor="processCurrentBitrate">
+                       <parameter name="NewSyncGroupIndex" fixedValue="0"/>
+               </getAction>
+       </channel>
        <channel name="dslEnable" label="DSL Enable">
                <item type="Switch"/>
                <service deviceType="urn:dslforum-org:device:WANDevice:1"
                <getAction name="GetInfo" argument="NewUpstreamCurrRate"/>
        </channel>
        <channel name="dslDownstreamNoiseMargin" label="DSL Downstream Noise Margin">
-               <item type="Number:Dimensionless" unit="dB" statePattern="%.1f dB"/>
+               <item type="Number:Dimensionless" unit="dB" factor="0.1" statePattern="%.1f dB"/>
                <service deviceType="urn:dslforum-org:device:WANDevice:1"
                        serviceId="urn:WANDSLIfConfig-com:serviceId:WANDSLInterfaceConfig1"/>
-               <getAction name="GetInfo" argument="NewDownstreamNoiseMargin" postProcessor="processDecaDecibel"/>
+               <getAction name="GetInfo" argument="NewDownstreamNoiseMargin"/>
        </channel>
        <channel name="dslUpstreamNoiseMargin" label="DSL Upstream Noise Margin">
-               <item type="Number:Dimensionless" unit="dB" statePattern="%.1f dB"/>
+               <item type="Number:Dimensionless" unit="dB" factor="0.1" statePattern="%.1f dB"/>
                <service deviceType="urn:dslforum-org:device:WANDevice:1"
                        serviceId="urn:WANDSLIfConfig-com:serviceId:WANDSLInterfaceConfig1"/>
-               <getAction name="GetInfo" argument="NewUpstreamNoiseMargin" postProcessor="processDecaDecibel"/>
+               <getAction name="GetInfo" argument="NewUpstreamNoiseMargin"/>
        </channel>
        <channel name="dslDownstreamAttenuation" label="DSL Downstream Attenuation">
-               <item type="Number:Dimensionless" unit="dB" statePattern="%.1f dB"/>
+               <item type="Number:Dimensionless" unit="dB" factor="0.1" statePattern="%.1f dB"/>
                <service deviceType="urn:dslforum-org:device:WANDevice:1"
                        serviceId="urn:WANDSLIfConfig-com:serviceId:WANDSLInterfaceConfig1"/>
-               <getAction name="GetInfo" argument="NewDownstreamAttenuation" postProcessor="processDecaDecibel"/>
+               <getAction name="GetInfo" argument="NewDownstreamAttenuation"/>
        </channel>
        <channel name="dslUpstreamAttenuation" label="DSL Upstream Attenuation">
-               <item type="Number:Dimensionless" unit="dB" statePattern="%.1f dB"/>
+               <item type="Number:Dimensionless" unit="dB" factor="0.1" statePattern="%.1f dB"/>
                <service deviceType="urn:dslforum-org:device:WANDevice:1"
                        serviceId="urn:WANDSLIfConfig-com:serviceId:WANDSLInterfaceConfig1"/>
-               <getAction name="GetInfo" argument="NewUpstreamAttenuation" postProcessor="processDecaDecibel"/>
+               <getAction name="GetInfo" argument="NewUpstreamAttenuation"/>
        </channel>
        <channel name="dslFECErrors" label="DSL FEC Errors">
                <item type="Number:Dimensionless"/>
index cbc7c26c0b24e7506d0a42542460e6fad9a11b8e..85e1381cd4c5ad1da65384251cff623b726fc134 100644 (file)
@@ -6,6 +6,7 @@
             <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:decimal" name="factor"/>
                 <xs:attribute type="xs:string" name="statePattern"/>
             </xs:extension>
         </xs:simpleContent>
@@ -22,7 +23,8 @@
         <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="thingParameter" />
+                <xs:attribute type="xs:string" name="fixedValue" />
                 <xs:attribute type="xs:string" name="pattern"/>
                 <xs:attribute type="xs:boolean" name="internalOnly" default="false"/>
             </xs:extension>
index e6827db2b569db4ac78724ea6c9d7ee7437ac97b..ed802b347fe135922ad7ac8b501fc74b12045227 100644 (file)
@@ -53,7 +53,6 @@ import org.openhab.core.util.UIDUtils;
 @MockitoSettings(strictness = Strictness.LENIENT)
 @NonNullByDefault
 class PhonebookProfileTest {
-
     private static final String INTERNAL_PHONE_NUMBER = "999";
     private static final String OTHER_PHONE_NUMBER = "555-456";
     private static final String JOHN_DOES_PHONE_NUMBER = "12345";
@@ -61,6 +60,7 @@ class PhonebookProfileTest {
     private static final ThingUID THING_UID = new ThingUID(BINDING_ID, THING_TYPE_FRITZBOX.getId(), "test");
     private static final String MY_PHONEBOOK = UIDUtils.encode(THING_UID.getAsString()) + ":MyPhonebook";
 
+    @NonNullByDefault
     public static class ParameterSet {
         public final State state;
         public final State resultingState;
@@ -108,12 +108,10 @@ class PhonebookProfileTest {
     private final Phonebook phonebook = new Phonebook() {
         @Override
         public Optional<String> lookupNumber(String number, int matchCount) {
-            switch (number) {
-                case JOHN_DOES_PHONE_NUMBER:
-                    return Optional.of(JOHN_DOES_NAME);
-                default:
-                    return Optional.empty();
-            }
+            return switch (number) {
+                case JOHN_DOES_PHONE_NUMBER -> Optional.of(JOHN_DOES_NAME);
+                default -> Optional.empty();
+            };
         }
 
         @Override
diff --git a/bundles/org.openhab.binding.tr064/src/test/java/org/openhab/binding/tr064/internal/phonebook/Tr064PhonebookImplTest.java b/bundles/org.openhab.binding.tr064/src/test/java/org/openhab/binding/tr064/internal/phonebook/Tr064PhonebookImplTest.java
new file mode 100644 (file)
index 0000000..2c31a9b
--- /dev/null
@@ -0,0 +1,90 @@
+/**
+ * Copyright (c) 2010-2023 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 static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.HttpClient;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+
+/**
+ * The {@link Tr064PhonebookImplTest} class implements test cases for the {@link Tr064PhonebookImpl} class
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+@MockitoSettings(strictness = Strictness.WARN)
+@ExtendWith(MockitoExtension.class)
+public class Tr064PhonebookImplTest {
+    @Mock
+    private @NonNullByDefault({}) HttpClient httpClient;
+
+    // key -> input, value -> output
+    public static Collection<Map.Entry<String, String>> phoneNumbers() {
+        return List.of( //
+                Map.entry("**820", "**820"), //
+                Map.entry("49200123456", "49200123456"), //
+                Map.entry("+49-200-123456", "+49200123456"), //
+                Map.entry("49 (200) 123456", "49200123456"), //
+                Map.entry("+49 200/123456", "+49200123456"));
+    }
+
+    @ParameterizedTest
+    @MethodSource("phoneNumbers")
+    public void testNormalization(Map.Entry<String, String> input) {
+        when(httpClient.newRequest((String) any())).thenThrow(new IllegalArgumentException("testing"));
+        Tr064PhonebookImpl testPhonebook = new Tr064PhonebookImpl(httpClient, "", 0);
+        assertEquals(input.getValue(), testPhonebook.normalizeNumber(input.getKey()));
+    }
+
+    @Test
+    public void testLookup() {
+        when(httpClient.newRequest((String) any())).thenThrow(new IllegalArgumentException("testing"));
+        TestPhonebook testPhonebook = new TestPhonebook(httpClient, "", 0);
+        testPhonebook.setPhonebook(Map.of("+491238007001", "foo", "+4933998005671", "bar"));
+
+        Optional<String> result = testPhonebook.lookupNumber("01238007001", 0);
+        assertEquals(Optional.empty(), result);
+
+        result = testPhonebook.lookupNumber("01238007001", 10);
+        assertEquals("foo", result.get());
+
+        result = testPhonebook.lookupNumber("033998005671", -1);
+        assertEquals("bar", result.get());
+    }
+
+    private static class TestPhonebook extends Tr064PhonebookImpl {
+        public TestPhonebook(HttpClient httpClient, String phonebookUrl, int httpTimeout) {
+            super(httpClient, phonebookUrl, httpTimeout);
+        }
+
+        public void setPhonebook(Map<String, String> phonebook) {
+            this.phonebook = phonebook;
+        }
+    }
+}