From: J-N-K Date: Fri, 24 Feb 2023 15:06:53 +0000 (+0100) Subject: [tr064] Enhancements, code improvements and fixes (#14468) X-Git-Url: https://git.basschouten.com/?a=commitdiff_plain;h=cb31f420ff459502ba46645fb5d9b504cb9a740b;p=openhab-addons.git [tr064] Enhancements, code improvements and fixes (#14468) Signed-off-by: Jan N. Klug --- diff --git a/CODEOWNERS b/CODEOWNERS index 34ff5b5d4b..3aa65f2842 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -339,7 +339,7 @@ /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 diff --git a/bundles/org.openhab.binding.tr064/README.md b/bundles/org.openhab.binding.tr064/README.md index d154350b9a..8f777e4275 100644 --- a/bundles/org.openhab.binding.tr064/README.md +++ b/bundles/org.openhab.binding.tr064/README.md @@ -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` | x | WAN IP Address | +| `wanPppIpAddress` | `String` | x | 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` | 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 | | `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 index 0000000000..c905ae6ae1 --- /dev/null +++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/FritzboxActions.java @@ -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 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 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) { + } +} diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064CommunicationException.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064CommunicationException.java index 089c65f505..c144aa855a 100644 --- a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064CommunicationException.java +++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064CommunicationException.java @@ -37,7 +37,7 @@ public class Tr064CommunicationException extends Exception { super(s); this.httpError = httpError; this.soapError = soapError; - }; + } public String getSoapError() { return soapError; diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064DiscoveryService.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064DiscoveryService.java index 7084f16028..f209416d04 100644 --- a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064DiscoveryService.java +++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064DiscoveryService.java @@ -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 SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_SUBDEVICE); + public static final Set 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 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 index 2f900ce714..0000000000 --- a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064DynamicStateDescriptionProvider.java +++ /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 descriptions = new ConcurrentHashMap<>(); - - /** - * Set a state description for a channel. This description will be used when preparing the channel state by - * the framework for presentation. A previous description, if existed, will be replaced. - * - * @param channelUID channel UID - * @param description state description for the channel - */ - public void setDescription(ChannelUID channelUID, StateDescription description) { - logger.trace("adding state description for channel {}", channelUID); - descriptions.put(channelUID, description); - } - - /** - * remove all descriptions for a given thing - * - * @param thingUID the thing's UID - */ - public void removeDescriptionsForThing(ThingUID thingUID) { - logger.trace("removing state description for thing {}", thingUID); - descriptions.entrySet().removeIf(entry -> entry.getKey().getThingUID().equals(thingUID)); - } - - @Override - public @Nullable StateDescription getStateDescription(Channel channel, - @Nullable StateDescription originalStateDescription, @Nullable Locale locale) { - if (descriptions.containsKey(channel.getUID())) { - return descriptions.get(channel.getUID()); - } else { - return null; - } - } -} diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064HandlerFactory.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064HandlerFactory.java index cfddb8f9bd..a23ae32648 100644 --- a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064HandlerFactory.java +++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064HandlerFactory.java @@ -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; diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064RootHandler.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064RootHandler.java index 0023b87da1..a80dd8e6ec 100644 --- a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064RootHandler.java +++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064RootHandler.java @@ -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 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 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 processPhonebookList(SOAPMessage soapMessagePhonebookList, SCPDServiceType scpdService) { - SOAPValueConverter soapValueConverter = new SOAPValueConverter(httpClient); - return (Collection) soapValueConverter + SOAPValueConverter soapValueConverter = new SOAPValueConverter(httpClient, timeout); + Optional> 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) 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 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. 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> 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)); } } diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064SubHandler.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064SubHandler.java index 4823a60b92..75040d959b 100644 --- a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064SubHandler.java +++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064SubHandler.java @@ -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 diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/config/Tr064ChannelConfig.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/config/Tr064ChannelConfig.java index 6b1239bae8..2e67ccbe33 100644 --- a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/config/Tr064ChannelConfig.java +++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/config/Tr064ChannelConfig.java @@ -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; diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/config/Tr064RootConfiguration.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/config/Tr064RootConfiguration.java index 5e24dde354..2b619af26f 100644 --- a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/config/Tr064RootConfiguration.java +++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/config/Tr064RootConfiguration.java @@ -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 tamIndices = List.of(); @@ -38,6 +43,10 @@ public class Tr064RootConfiguration extends Tr064BaseThingConfiguration { public List 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 index 7a907277ff..0000000000 --- a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/PhonebookActions.java +++ /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 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; - } -} diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/PhonebookProfile.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/PhonebookProfile.java index 783cce5e33..d0292a2a9e 100644 --- a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/PhonebookProfile.java +++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/PhonebookProfile.java @@ -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 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 match = resolveNumber(phoneNumber); diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/Tr064PhonebookImpl.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/Tr064PhonebookImpl.java index f255108194..11ffc3ed74 100644 --- a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/Tr064PhonebookImpl.java +++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/Tr064PhonebookImpl.java @@ -33,31 +33,39 @@ import org.slf4j.LoggerFactory; public class Tr064PhonebookImpl implements Phonebook { private final Logger logger = LoggerFactory.getLogger(Tr064PhonebookImpl.class); - private Map phonebook = new HashMap<>(); + protected Map 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(); + } + 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 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\\*\\+]", ""); } } diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/soap/CallListEntry.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/soap/CallListEntry.java index 445a6f3c41..24e0cc978a 100644 --- a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/soap/CallListEntry.java +++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/soap/CallListEntry.java @@ -12,10 +12,8 @@ */ 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; } diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/soap/CallListType.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/soap/CallListType.java index ce172e315c..7026ab6086 100644 --- a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/soap/CallListType.java +++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/soap/CallListType.java @@ -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"), diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/soap/PostProcessingException.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/soap/PostProcessingException.java index ef41671cf8..d97acbb895 100644 --- a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/soap/PostProcessingException.java +++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/soap/PostProcessingException.java @@ -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 */ diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/soap/SOAPConnector.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/soap/SOAPConnector.java index 515f33e46a..3aa372d58f 100644 --- a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/soap/SOAPConnector.java +++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/soap/SOAPConnector.java @@ -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 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) 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 diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/soap/SOAPRequest.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/soap/SOAPRequest.java index 2b50ef6447..5e4944234f 100644 --- a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/soap/SOAPRequest.java +++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/soap/SOAPRequest.java @@ -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); } diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/soap/SOAPValueConverter.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/soap/SOAPValueConverter.java index 1822bb7ba8..7324ccf838 100644 --- a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/soap/SOAPValueConverter.java +++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/soap/SOAPValueConverter.java @@ -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("1").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 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 callListEntries = calls.stream().map(CallListEntry::new) .collect(Collectors.toList()); return new StringType(gson.toJson(callListEntries)); + } } return UnDefType.UNDEF; } diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/util/SCPDUtil.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/util/SCPDUtil.java index 34b6f49e13..f5540299da 100644 --- a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/util/SCPDUtil.java +++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/util/SCPDUtil.java @@ -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 scpdDevicesList = new ArrayList<>(); private final Map 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 diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/util/Util.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/util/Util.java index ea4129399f..dc4c7c37b3 100644 --- a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/util/Util.java +++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/util/Util.java @@ -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 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 channels) { + public static void checkAvailableChannels(Thing thing, ThingHandlerCallback callback, ThingBuilder thingBuilder, + SCPDUtil scpdUtil, String deviceId, String deviceType, Map channels) { Tr064BaseThingConfiguration thingConfig = Tr064RootHandler.SUPPORTED_THING_TYPES .contains(thing.getThingTypeUID()) ? thing.getConfiguration().as(Tr064RootConfiguration.class) : thing.getConfiguration().as(Tr064SubConfiguration.class); @@ -179,13 +178,6 @@ public class Util { .forEach(channelTypeDescription -> { String channelId = channelTypeDescription.getName(); String serviceId = channelTypeDescription.getService().getServiceId(); - String typeId = channelTypeDescription.getTypeId(); - Map channelProperties = new HashMap(); - - if (typeId != null) { - channelProperties.put("typeId", typeId); - } - Set 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 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 @Nullable T getAndUnmarshalXML(HttpClient httpClient, String uri, Class clazz) { + public static @Nullable T getAndUnmarshalXML(HttpClient httpClient, String uri, Class 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)); diff --git a/bundles/org.openhab.binding.tr064/src/main/resources/OH-INF/config/phonebookProfile.xml b/bundles/org.openhab.binding.tr064/src/main/resources/OH-INF/config/phonebookProfile.xml index 32de7b2667..c0e8804aea 100644 --- a/bundles/org.openhab.binding.tr064/src/main/resources/OH-INF/config/phonebookProfile.xml +++ b/bundles/org.openhab.binding.tr064/src/main/resources/OH-INF/config/phonebookProfile.xml @@ -9,9 +9,10 @@ The name of the the phone book. - + - The number of digits matching the incoming value, counted from far right (default is 0 = all matching) + The number of digits matching the incoming value, counted from far right (default is 0 = all matching). + Negative numbers skip digits from the left. 0 diff --git a/bundles/org.openhab.binding.tr064/src/main/resources/OH-INF/i18n/tr064.properties b/bundles/org.openhab.binding.tr064/src/main/resources/OH-INF/i18n/tr064.properties index de1394b2b0..80141aeb90 100644 --- a/bundles/org.openhab.binding.tr064/src/main/resources/OH-INF/i18n/tr064.properties +++ b/bundles/org.openhab.binding.tr064/src/main/resources/OH-INF/i18n/tr064.properties @@ -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 diff --git a/bundles/org.openhab.binding.tr064/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.tr064/src/main/resources/OH-INF/thing/thing-types.xml index 50cad82689..72a762b984 100644 --- a/bundles/org.openhab.binding.tr064/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.tr064/src/main/resources/OH-INF/thing/thing-types.xml @@ -27,6 +27,12 @@ 60 + + + Timeout for all requests (SOAP requests, phone book retrieval, call lists, ...). + 5 + true + @@ -54,6 +60,12 @@ 60 + + + Timeout for all requests (SOAP requests, phone book retrieval, call lists, ...). + 5 + true + List of answering machines (starting with 0). @@ -100,6 +112,17 @@ 600 true + + + The directory where configuration backups are stored (default to userdata directory). + true + + + + The password used to encrypt the backup data. + password + true + diff --git a/bundles/org.openhab.binding.tr064/src/main/resources/channels.xml b/bundles/org.openhab.binding.tr064/src/main/resources/channels.xml index 80e310c409..7db46f24e9 100644 --- a/bundles/org.openhab.binding.tr064/src/main/resources/channels.xml +++ b/bundles/org.openhab.binding.tr064/src/main/resources/channels.xml @@ -65,7 +65,7 @@ serviceId="urn:X_AVM-DE_HostFilter-com:serviceId:X_AVM-DE_HostFilter1"/> + pattern="((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)(\.(?!([\s#]|$))|([\s#]|$))){4}(\s*#.*)*"/> @@ -138,7 +138,8 @@ - + @@ -146,7 +147,8 @@ pattern="([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}(\s*#.*)*"/> - + @@ -154,12 +156,11 @@ pattern="([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}(\s*#.*)*"/> - + typeId="system:signal-strength"> @@ -171,7 +172,7 @@ + the given MAC (see macOnline). This is set in case the Device is connected to 2.4Ghz"> @@ -184,8 +185,8 @@ + the given MAC (see macOnline). This is set in case the Device is connected to 5Ghz" + typeId="system:signal-strength"> @@ -197,7 +198,7 @@ + the given MAC (see macOnline). This is set in case the Device is connected to 5Ghz"> @@ -244,6 +245,24 @@ serviceId="urn:WANCIfConfig-com:serviceId:WANCommonInterfaceConfig1"/> + + + + + + + + + + + + + + - + - + - + - + - + - + - + - + diff --git a/bundles/org.openhab.binding.tr064/src/main/resources/xsd/channeltypes.xsd b/bundles/org.openhab.binding.tr064/src/main/resources/xsd/channeltypes.xsd index cbc7c26c0b..85e1381cd4 100644 --- a/bundles/org.openhab.binding.tr064/src/main/resources/xsd/channeltypes.xsd +++ b/bundles/org.openhab.binding.tr064/src/main/resources/xsd/channeltypes.xsd @@ -6,6 +6,7 @@ + @@ -22,7 +23,8 @@ - + + diff --git a/bundles/org.openhab.binding.tr064/src/test/java/org/openhab/binding/tr064/internal/phonebook/PhonebookProfileTest.java b/bundles/org.openhab.binding.tr064/src/test/java/org/openhab/binding/tr064/internal/phonebook/PhonebookProfileTest.java index e6827db2b5..ed802b347f 100644 --- a/bundles/org.openhab.binding.tr064/src/test/java/org/openhab/binding/tr064/internal/phonebook/PhonebookProfileTest.java +++ b/bundles/org.openhab.binding.tr064/src/test/java/org/openhab/binding/tr064/internal/phonebook/PhonebookProfileTest.java @@ -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 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 index 0000000000..2c31a9b968 --- /dev/null +++ b/bundles/org.openhab.binding.tr064/src/test/java/org/openhab/binding/tr064/internal/phonebook/Tr064PhonebookImplTest.java @@ -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> 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 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 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 phonebook) { + this.phonebook = phonebook; + } + } +}