/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
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
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.
### 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.
| `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
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)`
`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
```
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`):
--- /dev/null
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tr064.internal;
+
+import java.io.IOException;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+
+import javax.xml.soap.SOAPMessage;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.openhab.binding.tr064.internal.dto.scpd.root.SCPDServiceType;
+import org.openhab.binding.tr064.internal.phonebook.Phonebook;
+import org.openhab.binding.tr064.internal.soap.SOAPRequest;
+import org.openhab.binding.tr064.internal.util.SCPDUtil;
+import org.openhab.binding.tr064.internal.util.Util;
+import org.openhab.core.automation.annotation.ActionInput;
+import org.openhab.core.automation.annotation.ActionOutput;
+import org.openhab.core.automation.annotation.RuleAction;
+import org.openhab.core.thing.binding.ThingActions;
+import org.openhab.core.thing.binding.ThingActionsScope;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link FritzboxActions} is responsible for handling phone book actions
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@ThingActionsScope(name = "tr064")
+@NonNullByDefault
+public class FritzboxActions implements ThingActions {
+ private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("dd.MM.yyyy_HHmm");
+
+ private final Logger logger = LoggerFactory.getLogger(FritzboxActions.class);
+
+ private @Nullable Tr064RootHandler handler;
+
+ @RuleAction(label = "@text/phonebookLookupActionLabel", description = "@text/phonebookLookupActionDescription")
+ public @ActionOutput(name = "name", label = "@text/phonebookLookupActionOutputLabel", description = "@text/phonebookLookupActionOutputDescription", type = "java.lang.String") String phonebookLookup(
+ @ActionInput(name = "phonenumber", label = "@text/phonebookLookupActionInputPhoneNumberLabel", description = "@text/phonebookLookupActionInputPhoneNumberDescription", type = "java.lang.String", required = true) @Nullable String phonenumber,
+ @ActionInput(name = "matches", label = "@text/phonebookLookupActionInputMatchesLabel", description = "@text/phonebookLookupActionInputMatchesDescription", type = "java.lang.Integer") @Nullable Integer matchCount) {
+ return phonebookLookup(phonenumber, null, matchCount);
+ }
+
+ @RuleAction(label = "@text/phonebookLookupActionLabel", description = "@text/phonebookLookupActionDescription")
+ public @ActionOutput(name = "name", label = "@text/phonebookLookupActionOutputLabel", description = "@text/phonebookLookupActionOutputDescription", type = "java.lang.String") String phonebookLookup(
+ @ActionInput(name = "phonenumber", label = "@text/phonebookLookupActionInputPhoneNumberLabel", description = "@text/phonebookLookupActionInputPhoneNumberDescription", type = "java.lang.String", required = true) @Nullable String phonenumber) {
+ return phonebookLookup(phonenumber, null, null);
+ }
+
+ @RuleAction(label = "@text/phonebookLookupActionLabel", description = "@text/phonebookLookupActionDescription")
+ public @ActionOutput(name = "name", label = "@text/phonebookLookupActionOutputLabel", description = "@text/phonebookLookupActionOutputDescription", type = "java.lang.String") String phonebookLookup(
+ @ActionInput(name = "phonenumber", label = "@text/phonebookLookupActionInputPhoneNumberLabel", description = "@text/phonebookLookupActionInputPhoneNumberDescription", type = "java.lang.String", required = true) @Nullable String phonenumber,
+ @ActionInput(name = "phonebook", label = "@text/phonebookLookupActionInputPhoneBookLabel", description = "@text/phonebookLookupActionInputPhoneBookDescription", type = "java.lang.String") @Nullable String phonebook) {
+ return phonebookLookup(phonenumber, phonebook, null);
+ }
+
+ @RuleAction(label = "@text/phonebookLookupActionLabel", description = "@text/phonebookLookupActionDescription")
+ public @ActionOutput(name = "name", label = "@text/phonebookLookupActionOutputLabel", description = "@text/phonebookLookupActionOutputDescription", type = "java.lang.String") String phonebookLookup(
+ @ActionInput(name = "phonenumber", label = "@text/phonebookLookupActionInputPhoneNumberLabel", description = "@text/phonebookLookupActionInputPhoneNumberDescription", type = "java.lang.String", required = true) @Nullable String phonenumber,
+ @ActionInput(name = "phonebook", label = "@text/phonebookLookupActionInputPhoneBookLabel", description = "@text/phonebookLookupActionInputPhoneBookDescription", type = "java.lang.String") @Nullable String phonebook,
+ @ActionInput(name = "matches", label = "@text/phonebookLookupActionInputMatchesLabel", description = "@text/phonebookLookupActionInputMatchesDescription", type = "java.lang.Integer") @Nullable Integer matchCount) {
+ if (phonenumber == null) {
+ logger.warn("Cannot lookup a missing number.");
+ return "";
+ }
+
+ final Tr064RootHandler handler = this.handler;
+ if (handler == null) {
+ logger.info("Handler is null, cannot lookup number.");
+ return phonenumber;
+ } else {
+ int matchCountInt = matchCount == null ? 0 : matchCount;
+ if (phonebook != null && !phonebook.isEmpty()) {
+ return Objects.requireNonNull(handler.getPhonebookByName(phonebook)
+ .flatMap(p -> p.lookupNumber(phonenumber, matchCountInt)).orElse(phonenumber));
+ } else {
+ Collection<Phonebook> phonebooks = handler.getPhonebooks();
+ return Objects.requireNonNull(phonebooks.stream().map(p -> p.lookupNumber(phonenumber, matchCountInt))
+ .filter(Optional::isPresent).map(Optional::get).findAny().orElse(phonenumber));
+ }
+ }
+ }
+
+ @RuleAction(label = "create configuration backup", description = "Creates a configuration backup")
+ public void createConfigurationBackup() {
+ Tr064RootHandler handler = this.handler;
+
+ if (handler == null) {
+ logger.warn("TR064 action service ThingHandler is null!");
+ return;
+ }
+
+ SCPDUtil scpdUtil = handler.getSCPDUtil();
+ if (scpdUtil == null) {
+ logger.warn("Could not get SCPDUtil, handler seems to be uninitialized.");
+ return;
+ }
+
+ Optional<SCPDServiceType> scpdService = scpdUtil.getDevice("")
+ .flatMap(deviceType -> deviceType.getServiceList().stream().filter(
+ service -> service.getServiceId().equals("urn:DeviceConfig-com:serviceId:DeviceConfig1"))
+ .findFirst());
+ if (scpdService.isEmpty()) {
+ logger.warn("Could not get service.");
+ return;
+ }
+
+ BackupConfiguration configuration = handler.getBackupConfiguration();
+ try {
+ SOAPRequest soapRequest = new SOAPRequest(scpdService.get(), "X_AVM-DE_GetConfigFile",
+ Map.of("NewX_AVM-DE_Password", configuration.password));
+ SOAPMessage soapMessage = handler.getSOAPConnector().doSOAPRequestUncached(soapRequest);
+ String configBackupURL = Util.getSOAPElement(soapMessage, "NewX_AVM-DE_ConfigFileUrl")
+ .orElseThrow(() -> new Tr064CommunicationException("Empty URL"));
+
+ ContentResponse content = handler.getUrl(configBackupURL);
+
+ String fileName = String.format("%s %s.export", handler.getFriendlyName(),
+ DATE_TIME_FORMATTER.format(LocalDateTime.now()));
+ Path filePath = FileSystems.getDefault().getPath(configuration.directory, fileName);
+ Path folder = filePath.getParent();
+ if (folder != null) {
+ Files.createDirectories(folder);
+ }
+ Files.write(filePath, content.getContent());
+ } catch (Tr064CommunicationException e) {
+ logger.warn("Failed to get configuration backup URL: {}", e.getMessage());
+ } catch (InterruptedException | ExecutionException | TimeoutException e) {
+ logger.warn("Failed to get remote backup file: {}", e.getMessage());
+ } catch (IOException e) {
+ logger.warn("Failed to create backup file: {}", e.getMessage());
+ }
+ }
+
+ public static String phonebookLookup(ThingActions actions, @Nullable String phonenumber,
+ @Nullable Integer matchCount) {
+ return phonebookLookup(actions, phonenumber, null, matchCount);
+ }
+
+ public static String phonebookLookup(ThingActions actions, @Nullable String phonenumber) {
+ return phonebookLookup(actions, phonenumber, null, null);
+ }
+
+ public static String phonebookLookup(ThingActions actions, @Nullable String phonenumber,
+ @Nullable String phonebook) {
+ return phonebookLookup(actions, phonenumber, phonebook, null);
+ }
+
+ public static String phonebookLookup(ThingActions actions, @Nullable String phonenumber, @Nullable String phonebook,
+ @Nullable Integer matchCount) {
+ return ((FritzboxActions) actions).phonebookLookup(phonenumber, phonebook, matchCount);
+ }
+
+ public static void createConfigurationBackup(ThingActions actions) {
+ ((FritzboxActions) actions).createConfigurationBackup();
+ }
+
+ @Override
+ public void setThingHandler(@Nullable ThingHandler handler) {
+ this.handler = (Tr064RootHandler) handler;
+ }
+
+ @Override
+ public @Nullable ThingHandler getThingHandler() {
+ return handler;
+ }
+
+ public record BackupConfiguration(String directory, String password) {
+ }
+}
super(s);
this.httpError = httpError;
this.soapError = soapError;
- };
+ }
public String getSoapError() {
return soapError;
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;
@NonNullByDefault
public class Tr064DiscoveryService extends AbstractDiscoveryService implements ThingHandlerService {
private static final int SEARCH_TIME = 5;
- public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_SUBDEVICE);
+ public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_SUBDEVICE);
private final Logger logger = LoggerFactory.getLogger(Tr064DiscoveryService.class);
private @Nullable Tr064RootHandler bridgeHandler;
}
ThingUID thingUID = new ThingUID(thingTypeUID, bridgeUID, UIDUtils.encode(udn));
- Map<String, Object> properties = new HashMap<>(2);
- properties.put("uuid", udn);
- properties.put("deviceType", device.getDeviceType());
-
- DiscoveryResult result = DiscoveryResultBuilder.create(thingUID).withLabel(device.getFriendlyName())
- .withBridge(bridgeHandler.getThing().getUID()).withProperties(properties)
- .withRepresentationProperty("uuid").build();
+ DiscoveryResult result = DiscoveryResultBuilder.create(thingUID) //
+ .withLabel(device.getFriendlyName()) //
+ .withBridge(bridgeUID) //
+ .withProperties(Map.of("uuid", udn, "deviceType", device.getDeviceType())) //
+ .withRepresentationProperty("uuid") //
+ .build();
thingDiscovered(result);
}
});
+++ /dev/null
-/**
- * Copyright (c) 2010-2023 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.tr064.internal;
-
-import java.util.Locale;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
-import org.openhab.core.thing.Channel;
-import org.openhab.core.thing.ChannelUID;
-import org.openhab.core.thing.ThingUID;
-import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
-import org.openhab.core.types.StateDescription;
-import org.osgi.service.component.annotations.Component;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Dynamic channel state description provider.
- * Overrides the state description for the controls, which receive its configuration in the runtime.
- *
- * @author Jan N. Klug - Initial contribution
- */
-@NonNullByDefault
-@Component(service = { DynamicStateDescriptionProvider.class, Tr064DynamicStateDescriptionProvider.class })
-public class Tr064DynamicStateDescriptionProvider implements DynamicStateDescriptionProvider {
- private final Logger logger = LoggerFactory.getLogger(Tr064DynamicStateDescriptionProvider.class);
- private final Map<ChannelUID, StateDescription> descriptions = new ConcurrentHashMap<>();
-
- /**
- * Set a state description for a channel. This description will be used when preparing the channel state by
- * the framework for presentation. A previous description, if existed, will be replaced.
- *
- * @param channelUID channel UID
- * @param description state description for the channel
- */
- public void setDescription(ChannelUID channelUID, StateDescription description) {
- logger.trace("adding state description for channel {}", channelUID);
- descriptions.put(channelUID, description);
- }
-
- /**
- * remove all descriptions for a given thing
- *
- * @param thingUID the thing's UID
- */
- public void removeDescriptionsForThing(ThingUID thingUID) {
- logger.trace("removing state description for thing {}", thingUID);
- descriptions.entrySet().removeIf(entry -> entry.getKey().getThingUID().equals(thingUID));
- }
-
- @Override
- public @Nullable StateDescription getStateDescription(Channel channel,
- @Nullable StateDescription originalStateDescription, @Nullable Locale locale) {
- if (descriptions.containsKey(channel.getUID())) {
- return descriptions.get(channel.getUID());
- } else {
- return null;
- }
- }
-}
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;
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;
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;
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;
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;
private final Logger logger = LoggerFactory.getLogger(Tr064RootHandler.class);
private final HttpClient httpClient;
- private Tr064RootConfiguration config = new Tr064RootConfiguration();
- private String deviceType = "";
-
private @Nullable SCPDUtil scpdUtil;
private SOAPConnector soapConnector;
+
+ // these are set when the config is available
+ private Tr064RootConfiguration config = new Tr064RootConfiguration();
private String endpointBaseURL = "";
+ private int timeout = Tr064RootConfiguration.DEFAULT_HTTP_TIMEOUT;
+
+ private String deviceType = "";
private final Map<ChannelUID, Tr064ChannelConfig> channels = new HashMap<>();
// caching is used to prevent excessive calls to the same action
Tr064RootHandler(Bridge bridge, HttpClient httpClient) {
super(bridge);
this.httpClient = httpClient;
- this.soapConnector = new SOAPConnector(httpClient, endpointBaseURL);
+ this.soapConnector = new SOAPConnector(httpClient, endpointBaseURL, timeout);
}
@Override
}
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);
*/
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);
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());
}
removeConnectScheduler();
uninstallPolling();
stateCache.clear();
+ scpdUtil = null;
super.dispose();
}
* poll remote device for channel values
*/
private void poll() {
- channels.forEach((channelUID, channelConfig) -> {
- if (isLinked(channelUID)) {
- State state = stateCache.putIfAbsentAndGet(channelUID,
- () -> soapConnector.getChannelStateFromDevice(channelConfig, channels, stateCache));
- if (state != null) {
- updateState(channelUID, state);
+ 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
*/
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 {
}
// 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
.orElseThrow(() -> new SCPDException("Action 'GetInfo' not found"));
SOAPMessage soapResponse1 = soapConnector
.doSOAPRequest(new SOAPRequest(deviceService, getInfoAction.getName()));
- SOAPValueConverter soapValueConverter = new SOAPValueConverter(httpClient);
+ SOAPValueConverter soapValueConverter = new SOAPValueConverter(httpClient, timeout);
Map<String, String> properties = editProperties();
PROPERTY_ARGUMENTS.forEach(argumentName -> getInfoAction.getArgumentList().stream()
.filter(argument -> argument.getName().equals(argumentName)).findFirst()
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
*
@SuppressWarnings("unchecked")
private Collection<Phonebook> processPhonebookList(SOAPMessage soapMessagePhonebookList,
SCPDServiceType scpdService) {
- SOAPValueConverter soapValueConverter = new SOAPValueConverter(httpClient);
- return (Collection<Phonebook>) soapValueConverter
+ SOAPValueConverter soapValueConverter = new SOAPValueConverter(httpClient, timeout);
+ Optional<Stream<String>> phonebookStream = soapValueConverter
.getStateFromSOAPValue(soapMessagePhonebookList, "NewPhonebookList", null)
- .map(phonebookList -> Arrays.stream(phonebookList.toString().split(","))).orElse(Stream.empty())
- .map(index -> {
- try {
- SOAPMessage soapMessageURL = soapConnector.doSOAPRequest(
- new SOAPRequest(scpdService, "GetPhonebook", Map.of("NewPhonebookID", index)));
- return soapValueConverter.getStateFromSOAPValue(soapMessageURL, "NewPhonebookURL", null)
- .map(url -> (Phonebook) new Tr064PhonebookImpl(httpClient, url.toString()));
- } catch (Tr064CommunicationException e) {
- logger.warn("Failed to get phonebook with index {}:", index, e);
- }
- return Optional.empty();
- }).filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList());
+ .map(phonebookList -> Arrays.stream(phonebookList.toString().split(",")));
+ return phonebookStream.map(stringStream -> (Collection<Phonebook>) stringStream.map(index -> {
+ try {
+ SOAPMessage soapMessageURL = soapConnector
+ .doSOAPRequest(new SOAPRequest(scpdService, "GetPhonebook", Map.of("NewPhonebookID", index)));
+ return soapValueConverter.getStateFromSOAPValue(soapMessageURL, "NewPhonebookURL", null)
+ .map(url -> (Phonebook) new Tr064PhonebookImpl(httpClient, url.toString(), timeout));
+ } catch (Tr064CommunicationException e) {
+ logger.warn("Failed to get phonebook with index {}:", index, e);
+ }
+ return Optional.empty();
+ }).filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList())).orElseGet(Set::of);
}
private void retrievePhonebooks() {
Optional<SCPDServiceType> scpdService = scpdUtil.getDevice("").flatMap(deviceType -> deviceType.getServiceList()
.stream().filter(service -> service.getServiceId().equals(serviceId)).findFirst());
- phonebooks = scpdService.map(service -> {
+ phonebooks = Objects.requireNonNull(scpdService.map(service -> {
try {
return processPhonebookList(soapConnector.doSOAPRequest(new SOAPRequest(service, "GetPhonebookList")),
service);
} catch (Tr064CommunicationException e) {
return Collections.<Phonebook> emptyList();
}
- }).orElse(List.of());
+ }).orElse(List.of()));
if (phonebooks.isEmpty()) {
logger.warn("Could not get phonebooks for thing {}", thing.getUID());
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
- return Set.of(Tr064DiscoveryService.class, PhonebookActions.class);
+ if (THING_TYPE_FRITZBOX.equals(thing.getThingTypeUID())) {
+ return Set.of(Tr064DiscoveryService.class, FritzboxActions.class);
+ } else {
+ return Set.of(Tr064DiscoveryService.class);
+ }
+ }
+
+ /**
+ * get the backup configuration for this thing (only applies to FritzBox devices
+ *
+ * @return the configuration
+ */
+ public FritzboxActions.BackupConfiguration getBackupConfiguration() {
+ return new FritzboxActions.BackupConfiguration(config.backupDirectory,
+ Objects.requireNonNullElse(config.backupPassword, config.password));
}
}
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;
}
@Override
- @SuppressWarnings("null")
public void handleCommand(ChannelUID channelUID, Command command) {
Tr064ChannelConfig channelConfig = channels.get(channelUID);
if (channelConfig == null) {
}
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) {
return;
}
scheduler.execute(() -> {
+ final SOAPConnector soapConnector = this.soapConnector;
if (soapConnector == null) {
logger.warn("Could not send command because connector not available");
} else {
"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
*/
@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;
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.
*/
@NonNullByDefault
public class Tr064RootConfiguration extends Tr064BaseThingConfiguration {
+ public static final int DEFAULT_HTTP_TIMEOUT = 5; // in s
+
public String host = "";
public String user = "dslf-config";
public String password = "";
+ public int timeout = DEFAULT_HTTP_TIMEOUT;
/* following parameters only available in fritzbox thing */
public List<String> tamIndices = List.of();
public List<String> wanBlockIPs = List.of();
public int phonebookInterval = 600;
+ // Backup data
+ public String backupDirectory = OpenHAB.getUserDataFolder();
+ public @Nullable String backupPassword;
+
public boolean isValid() {
return !host.isEmpty() && !user.isEmpty() && !password.isEmpty();
}
+++ /dev/null
-/**
- * Copyright (c) 2010-2023 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.tr064.internal.phonebook;
-
-import java.util.Collection;
-import java.util.Optional;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
-import org.openhab.binding.tr064.internal.Tr064RootHandler;
-import org.openhab.core.automation.annotation.ActionInput;
-import org.openhab.core.automation.annotation.ActionOutput;
-import org.openhab.core.automation.annotation.RuleAction;
-import org.openhab.core.thing.binding.ThingActions;
-import org.openhab.core.thing.binding.ThingActionsScope;
-import org.openhab.core.thing.binding.ThingHandler;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * The {@link PhonebookActions} is responsible for handling phonebook actions
- *
- * @author Jan N. Klug - Initial contribution
- */
-@ThingActionsScope(name = "tr064")
-@NonNullByDefault
-public class PhonebookActions implements ThingActions {
- private final Logger logger = LoggerFactory.getLogger(PhonebookActions.class);
-
- private @Nullable Tr064RootHandler handler;
-
- @RuleAction(label = "@text/phonebookLookupActionLabel", description = "@text/phonebookLookupActionDescription")
- public @ActionOutput(name = "name", label = "@text/phonebookLookupActionOutputLabel", description = "@text/phonebookLookupActionOutputDescription", type = "java.lang.String") String phonebookLookup(
- @ActionInput(name = "phonenumber", label = "@text/phonebookLookupActionInputPhoneNumberLabel", description = "@text/phonebookLookupActionInputPhoneNumberDescription", type = "java.lang.String", required = true) @Nullable String phonenumber,
- @ActionInput(name = "matches", label = "@text/phonebookLookupActionInputMatchesLabel", description = "@text/phonebookLookupActionInputMatchesDescription", type = "java.lang.Integer") @Nullable Integer matchCount) {
- return phonebookLookup(phonenumber, null, matchCount);
- }
-
- @RuleAction(label = "@text/phonebookLookupActionLabel", description = "@text/phonebookLookupActionDescription")
- public @ActionOutput(name = "name", label = "@text/phonebookLookupActionOutputLabel", description = "@text/phonebookLookupActionOutputDescription", type = "java.lang.String") String phonebookLookup(
- @ActionInput(name = "phonenumber", label = "@text/phonebookLookupActionInputPhoneNumberLabel", description = "@text/phonebookLookupActionInputPhoneNumberDescription", type = "java.lang.String", required = true) @Nullable String phonenumber) {
- return phonebookLookup(phonenumber, null, null);
- }
-
- @RuleAction(label = "@text/phonebookLookupActionLabel", description = "@text/phonebookLookupActionDescription")
- public @ActionOutput(name = "name", label = "@text/phonebookLookupActionOutputLabel", description = "@text/phonebookLookupActionOutputDescription", type = "java.lang.String") String phonebookLookup(
- @ActionInput(name = "phonenumber", label = "@text/phonebookLookupActionInputPhoneNumberLabel", description = "@text/phonebookLookupActionInputPhoneNumberDescription", type = "java.lang.String", required = true) @Nullable String phonenumber,
- @ActionInput(name = "phonebook", label = "@text/phonebookLookupActionInputPhoneBookLabel", description = "@text/phonebookLookupActionInputPhoneBookDescription", type = "java.lang.String") @Nullable String phonebook) {
- return phonebookLookup(phonenumber, phonebook, null);
- }
-
- @RuleAction(label = "@text/phonebookLookupActionLabel", description = "@text/phonebookLookupActionDescription")
- public @ActionOutput(name = "name", label = "@text/phonebookLookupActionOutputLabel", description = "@text/phonebookLookupActionOutputDescription", type = "java.lang.String") String phonebookLookup(
- @ActionInput(name = "phonenumber", label = "@text/phonebookLookupActionInputPhoneNumberLabel", description = "@text/phonebookLookupActionInputPhoneNumberDescription", type = "java.lang.String", required = true) @Nullable String phonenumber,
- @ActionInput(name = "phonebook", label = "@text/phonebookLookupActionInputPhoneBookLabel", description = "@text/phonebookLookupActionInputPhoneBookDescription", type = "java.lang.String") @Nullable String phonebook,
- @ActionInput(name = "matches", label = "@text/phonebookLookupActionInputMatchesLabel", description = "@text/phonebookLookupActionInputMatchesDescription", type = "java.lang.Integer") @Nullable Integer matchCount) {
- if (phonenumber == null) {
- logger.warn("Cannot lookup a missing number.");
- return "";
- }
-
- final Tr064RootHandler handler = this.handler;
- if (handler == null) {
- logger.info("Handler is null, cannot lookup number.");
- return phonenumber;
- } else {
- int matchCountInt = matchCount == null ? 0 : matchCount;
- if (phonebook != null && !phonebook.isEmpty()) {
- return handler.getPhonebookByName(phonebook).flatMap(p -> p.lookupNumber(phonenumber, matchCountInt))
- .orElse(phonenumber);
- } else {
- Collection<Phonebook> phonebooks = handler.getPhonebooks();
- return phonebooks.stream().map(p -> p.lookupNumber(phonenumber, matchCountInt))
- .filter(Optional::isPresent).map(Optional::get).findAny().orElse(phonenumber);
- }
- }
- }
-
- public static String phonebookLookup(ThingActions actions, @Nullable String phonenumber,
- @Nullable Integer matchCount) {
- return phonebookLookup(actions, phonenumber, null, matchCount);
- }
-
- public static String phonebookLookup(ThingActions actions, @Nullable String phonenumber) {
- return phonebookLookup(actions, phonenumber, null, null);
- }
-
- public static String phonebookLookup(ThingActions actions, @Nullable String phonenumber,
- @Nullable String phonebook) {
- return phonebookLookup(actions, phonenumber, phonebook, null);
- }
-
- public static String phonebookLookup(ThingActions actions, @Nullable String phonenumber, @Nullable String phonebook,
- @Nullable Integer matchCount) {
- return ((PhonebookActions) actions).phonebookLookup(phonenumber, phonebook, matchCount);
- }
-
- @Override
- public void setThingHandler(@Nullable ThingHandler handler) {
- if (handler instanceof Tr064RootHandler) {
- this.handler = (Tr064RootHandler) handler;
- }
- }
-
- @Override
- public @Nullable ThingHandler getThingHandler() {
- return handler;
- }
-}
import java.math.BigDecimal;
import java.util.Map;
+import java.util.Objects;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
}
if (state instanceof StringType) {
Optional<String> match = resolveNumber(state.toString());
- State newState = match.map(name -> (State) new StringType(name)).orElse(state);
- // Compare by reference to check if the name is mapped to the same state
- if (newState == state) {
+ State newState = Objects.requireNonNull(match.map(name -> (State) new StringType(name)).orElse(state));
+ if (newState.equals(state)) {
logger.debug("Number '{}' not found in phonebook '{}' from provider '{}'", state, phonebookName,
thingUID);
}
callback.sendUpdate(newState);
- } else if (state instanceof StringListType) {
- StringListType stringList = (StringListType) state;
+ } else if (state instanceof StringListType stringList) {
try {
String phoneNumber = stringList.getValue(phoneNumberIndex);
Optional<String> match = resolveNumber(phoneNumber);
public class Tr064PhonebookImpl implements Phonebook {
private final Logger logger = LoggerFactory.getLogger(Tr064PhonebookImpl.class);
- private Map<String, String> phonebook = new HashMap<>();
+ protected Map<String, String> phonebook = new HashMap<>();
private final HttpClient httpClient;
private final String phonebookUrl;
+ private final int httpTimeout;
private String phonebookName = "";
- public Tr064PhonebookImpl(HttpClient httpClient, String phonebookUrl) {
+ public Tr064PhonebookImpl(HttpClient httpClient, String phonebookUrl, int httpTimeout) {
this.httpClient = httpClient;
this.phonebookUrl = phonebookUrl;
+ this.httpTimeout = httpTimeout;
getPhonebook();
}
private void getPhonebook() {
- PhonebooksType phonebooksType = Util.getAndUnmarshalXML(httpClient, phonebookUrl, PhonebooksType.class);
- if (phonebooksType != null) {
- phonebookName = phonebooksType.getPhonebook().getName();
- phonebook = phonebooksType.getPhonebook().getContact().stream().map(contact -> {
- String contactName = contact.getPerson().getRealName();
- return contact.getTelephony().getNumber().stream()
- .collect(Collectors.toMap(number -> normalizeNumber(number.getValue()), number -> contactName,
- this::mergeSameContactNames));
- }).collect(HashMap::new, HashMap::putAll, HashMap::putAll);
- logger.debug("Downloaded phonebook {}: {}", phonebookName, phonebook);
+ PhonebooksType phonebooksType = Util.getAndUnmarshalXML(httpClient, phonebookUrl, PhonebooksType.class,
+ httpTimeout);
+ if (phonebooksType == null) {
+ logger.warn("Failed to get phonebook with URL '{}'", phonebookUrl);
+ return;
}
+ phonebookName = phonebooksType.getPhonebook().getName();
+
+ phonebook = phonebooksType.getPhonebook().getContact().stream().map(contact -> {
+ String contactName = contact.getPerson().getRealName();
+ if (contactName == null || contactName.isBlank()) {
+ return new HashMap<String, String>();
+ }
+ return contact.getTelephony().getNumber().stream().collect(Collectors.toMap(
+ number -> normalizeNumber(number.getValue()), number -> contactName, this::mergeSameContactNames));
+ }).collect(HashMap::new, HashMap::putAll, HashMap::putAll);
+ logger.debug("Downloaded phonebook {}: {}", phonebookName, phonebook);
}
// in case there are multiple phone entries with same number -> name mapping, i.e. in phonebooks exported from
@Override
public Optional<String> lookupNumber(String number, int matchCount) {
String normalized = normalizeNumber(number);
- String matchString = matchCount > 0 && matchCount < normalized.length()
- ? normalized.substring(normalized.length() - matchCount)
- : normalized;
+ String matchString;
+ if (matchCount > 0 && matchCount < normalized.length()) {
+ matchString = normalized.substring(normalized.length() - matchCount);
+ } else if (matchCount < 0 && (-matchCount) < normalized.length()) {
+ matchString = normalized.substring(-matchCount);
+ } else {
+ matchString = normalized;
+ }
logger.trace("Normalized '{}' to '{}', matchString is '{}'", number, normalized, matchString);
return matchString.isBlank() ? Optional.empty()
: phonebook.keySet().stream().filter(n -> n.endsWith(matchString)).findFirst().map(phonebook::get);
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\\*\\+]", "");
}
}
*/
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;
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;
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;
}
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"),
/**
*
- * 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
*/
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;
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;
*/
@NonNullByDefault
public class SOAPConnector {
- private static final int SOAP_TIMEOUT = 5; // in
private final Logger logger = LoggerFactory.getLogger(SOAPConnector.class);
private final HttpClient httpClient;
private final String endpointBaseURL;
private final SOAPValueConverter soapValueConverter;
+ private final int timeout;
private final ExpiringCacheMap<SOAPRequest, SOAPMessage> soapMessageCache = new ExpiringCacheMap<>(
Duration.ofMillis(2000));
- public SOAPConnector(HttpClient httpClient, String endpointBaseURL) {
+ public SOAPConnector(HttpClient httpClient, String endpointBaseURL, int timeout) {
this.httpClient = httpClient;
this.endpointBaseURL = endpointBaseURL;
- this.soapValueConverter = new SOAPValueConverter(httpClient);
+ this.timeout = timeout;
+ this.soapValueConverter = new SOAPValueConverter(httpClient, timeout);
}
/**
// create Request and add headers and content
Request request = httpClient.newRequest(endpointBaseURL + soapRequest.service.getControlURL())
.method(HttpMethod.POST);
- ((Iterator<MimeHeader>) soapMessage.getMimeHeaders().getAllHeaders())
+ soapMessage.getMimeHeaders().getAllHeaders()
.forEachRemaining(header -> request.header(header.getName(), header.getValue()));
try (final ByteArrayOutputStream os = new ByteArrayOutputStream()) {
soapMessage.writeTo(os);
*/
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())));
}
// 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())) {
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
} 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
if (o == null || getClass() != o.getClass()) {
return false;
}
-
SOAPRequest that = (SOAPRequest) o;
if (!service.equals(that.service)) {
if (!soapAction.equals(that.soapAction)) {
return false;
}
+
return arguments.equals(that.arguments);
}
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;
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;
}
/**
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)) {
@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;
}).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
*
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
*
@SuppressWarnings("unused")
private State processTamListURL(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException {
try {
- ContentResponse response = httpClient.newRequest(state.toString()).timeout(1500, TimeUnit.MILLISECONDS)
+ ContentResponse response = httpClient.newRequest(state.toString()).timeout(timeout, TimeUnit.MILLISECONDS)
.send();
String responseContent = response.getContentAsString();
int messageCount = responseContent.split("<New>1</New>").length - 1;
return new DecimalType(messageCount);
- } catch (TimeoutException e) {
- throw new PostProcessingException("Failed to get TAM list due to time out from URL " + state.toString(), e);
- } catch (InterruptedException | ExecutionException e) {
- throw new PostProcessingException("Failed to get TAM list from URL " + state.toString(), e);
+ } catch (InterruptedException | TimeoutException | ExecutionException e) {
+ throw new PostProcessingException("Failed to get TAM list from URL " + state, e);
}
}
*/
private State processCallList(State state, @Nullable String days, CallListType type)
throws PostProcessingException {
- Root callListRoot = Util.getAndUnmarshalXML(httpClient, state.toString() + "&days=" + days, Root.class);
+ Root callListRoot = Util.getAndUnmarshalXML(httpClient, state + "&days=" + days, Root.class, timeout);
if (callListRoot == null) {
- throw new PostProcessingException("Failed to get call list from URL " + state.toString());
+ throw new PostProcessingException("Failed to get call list from URL " + state);
}
List<Call> calls = callListRoot.getCall();
switch (type) {
- case INBOUND_COUNT:
- case MISSED_COUNT:
- case OUTBOUND_COUNT:
- case REJECTED_COUNT:
+ case INBOUND_COUNT, MISSED_COUNT, OUTBOUND_COUNT, REJECTED_COUNT -> {
long callCount = calls.stream().filter(call -> type.typeString().equals(call.getType())).count();
return new DecimalType(callCount);
- case JSON_LIST:
+ }
+ case JSON_LIST -> {
Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ssX").serializeNulls().create();
List<CallListEntry> callListEntries = calls.stream().map(CallListEntry::new)
.collect(Collectors.toList());
return new StringType(gson.toJson(callListEntries));
+ }
}
return UnDefType.UNDEF;
}
*/
@NonNullByDefault
public class SCPDUtil {
- private SCPDRootType scpdRoot;
+ private final SCPDRootType scpdRoot;
private final List<SCPDDeviceType> scpdDevicesList = new ArrayList<>();
private final Map<String, SCPDScpdType> serviceMap = new HashMap<>();
- public SCPDUtil(HttpClient httpClient, String endpoint) throws SCPDException {
- SCPDRootType scpdRoot = Util.getAndUnmarshalXML(httpClient, endpoint + "/tr64desc.xml", SCPDRootType.class);
+ public SCPDUtil(HttpClient httpClient, String endpoint, int timeout) throws SCPDException {
+ SCPDRootType scpdRoot = Util.getAndUnmarshalXML(httpClient, endpoint + "/tr64desc.xml", SCPDRootType.class,
+ timeout);
if (scpdRoot == null) {
throw new SCPDException("could not get SCPD root");
}
this.scpdRoot = scpdRoot;
- scpdDevicesList.addAll(flatDeviceList(scpdRoot.getDevice()).collect(Collectors.toList()));
+ scpdDevicesList.addAll(flatDeviceList(scpdRoot.getDevice()).toList());
for (SCPDDeviceType device : scpdDevicesList) {
for (SCPDServiceType service : device.getServiceList()) {
SCPDScpdType scpd = serviceMap.computeIfAbsent(service.getServiceId(), serviceId -> Util
- .getAndUnmarshalXML(httpClient, endpoint + service.getSCPDURL(), SCPDScpdType.class));
+ .getAndUnmarshalXML(httpClient, endpoint + service.getSCPDURL(), SCPDScpdType.class, timeout));
if (scpd == null) {
throw new SCPDException("could not get SCPD service");
}
}
/**
- * get a single device by it's UDN
+ * get a single device by its UDN
*
* @param udn the device UDN
* @return the device
}
/**
- * get a single service by it's serviceId
+ * get a single service by its serviceId
*
* @param serviceId the service id
* @return the service
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;
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;
@NonNullByDefault
public class Util {
private static final Logger LOGGER = LoggerFactory.getLogger(Util.class);
- private static final int HTTP_REQUEST_TIMEOUT = 5; // in s
// cache XML content for 5s
private static final ExpiringCacheMap<String, Object> XML_OBJECT_CACHE = new ExpiringCacheMap<>(
Duration.ofMillis(3000));
* @param deviceType the (SCPD) device-type for this thing
* @param channels a (mutable) channel list for storing all channels
*/
- public static void checkAvailableChannels(Thing thing, ThingBuilder thingBuilder, SCPDUtil scpdUtil,
- String deviceId, String deviceType, Map<ChannelUID, Tr064ChannelConfig> channels) {
+ public static void checkAvailableChannels(Thing thing, ThingHandlerCallback callback, ThingBuilder thingBuilder,
+ SCPDUtil scpdUtil, String deviceId, String deviceType, Map<ChannelUID, Tr064ChannelConfig> channels) {
Tr064BaseThingConfiguration thingConfig = Tr064RootHandler.SUPPORTED_THING_TYPES
.contains(thing.getThingTypeUID()) ? thing.getConfiguration().as(Tr064RootConfiguration.class)
: thing.getConfiguration().as(Tr064SubConfiguration.class);
.forEach(channelTypeDescription -> {
String channelId = channelTypeDescription.getName();
String serviceId = channelTypeDescription.getService().getServiceId();
- String typeId = channelTypeDescription.getTypeId();
- Map<String, String> channelProperties = new HashMap<String, String>();
-
- if (typeId != null) {
- channelProperties.put("typeId", typeId);
- }
-
Set<String> parameters = new HashSet<>();
try {
SCPDServiceType deviceService = scpdUtil.getDevice(deviceId)
deviceService);
// get
+ boolean fixedValue = false;
ActionType getAction = channelTypeDescription.getGetAction();
if (getAction != null) {
String actionName = getAction.getName();
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());
}
}
// 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 -> {
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);
SCPDScpdType serviceRoot, Tr064BaseThingConfiguration thingConfig) throws ChannelConfigException {
ParameterType parameter = action.getParameter();
if (parameter == null) {
- return Collections.emptySet();
+ return Set.of();
}
+ if (parameter.getFixedValue() != null) {
+ return Set.of(parameter.getFixedValue());
+ }
+ // process list of thing parameters
try {
Set<String> parameters = new HashSet<>();
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;
*
* @param uri the uri of the XML file
* @param clazz the class describing the XML file
+ * @param timeout timeout in s
* @return unmarshalling result
*/
@SuppressWarnings("unchecked")
- public static <T> @Nullable T getAndUnmarshalXML(HttpClient httpClient, String uri, Class<T> clazz) {
+ public static <T> @Nullable T getAndUnmarshalXML(HttpClient httpClient, String uri, Class<T> clazz, int timeout) {
try {
T returnValue = (T) XML_OBJECT_CACHE.putIfAbsentAndGet(uri, () -> {
try {
LOGGER.trace("Refreshing cache for '{}'", uri);
- ContentResponse contentResponse = httpClient.newRequest(uri)
- .timeout(HTTP_REQUEST_TIMEOUT, TimeUnit.SECONDS).method(HttpMethod.GET).send();
+ ContentResponse contentResponse = httpClient.newRequest(uri).timeout(timeout, TimeUnit.SECONDS)
+ .method(HttpMethod.GET).send();
byte[] response = contentResponse.getContent();
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("XML = {}", new String(response));
<label>Phone Book</label>
<description>The name of the the phone book.</description>
</parameter>
- <parameter name="matchCount" type="integer" min="0" step="1">
+ <parameter name="matchCount" type="integer" step="1">
<label>Match Count</label>
- <description>The number of digits matching the incoming value, counted from far right (default is 0 = all matching)</description>
+ <description>The number of digits matching the incoming value, counted from far right (default is 0 = all matching).
+ Negative numbers skip digits from the left.</description>
<default>0</default>
</parameter>
<parameter name="phoneNumberIndex" type="integer" min="0" max="1" step="1">
# 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
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.
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
<label>Refresh Interval</label>
<default>60</default>
</parameter>
+ <parameter name="timeout" type="integer" unit="s">
+ <label>Timeout</label>
+ <description>Timeout for all requests (SOAP requests, phone book retrieval, call lists, ...).</description>
+ <default>5</default>
+ <advanced>true</advanced>
+ </parameter>
</config-description>
</bridge-type>
<label>Refresh Interval</label>
<default>60</default>
</parameter>
+ <parameter name="timeout" type="integer" unit="s">
+ <label>Timeout</label>
+ <description>Timeout for all requests (SOAP requests, phone book retrieval, call lists, ...).</description>
+ <default>5</default>
+ <advanced>true</advanced>
+ </parameter>
<parameter name="tamIndices" type="text" multiple="true">
<label>TAM</label>
<description>List of answering machines (starting with 0).</description>
<default>600</default>
<advanced>true</advanced>
</parameter>
+ <parameter name="backupDirectory" type="text">
+ <label>Backup Directory</label>
+ <description>The directory where configuration backups are stored (default to userdata directory).</description>
+ <advanced>true</advanced>
+ </parameter>
+ <parameter name="backupPassword" type="text">
+ <label>Backup Password</label>
+ <description>The password used to encrypt the backup data.</description>
+ <context>password</context>
+ <advanced>true</advanced>
+ </parameter>
</config-description>
</bridge-type>
serviceId="urn:X_AVM-DE_HostFilter-com:serviceId:X_AVM-DE_HostFilter1"/>
<getAction name="GetWANAccessByIP" argument="NewDisallow">
<parameter name="NewIPv4Address" thingParameter="wanBlockIPs"
- pattern="^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)(\.(?!$)|$)){4}$"/>
+ pattern="((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)(\.(?!([\s#]|$))|([\s#]|$))){4}(\s*#.*)*"/>
</getAction>
<setAction name="DisallowWANAccessByIP" argument="NewDisallow">
<parameter name="NewIPv4Address" thingParameter="wanBlockIPs"/>
<getAction name="GetInfo" argument="NewEnable"/>
<setAction name="SetEnable" argument="NewEnable"/>
</channel>
- <channel name="macOnline" label="MAC Online" description="Online status of the device with the given MAC">
+ <channel name="macOnline" label="MAC Online" description="Online status of the device with the given MAC"
+ advanced="true">
<item type="Switch"/>
<service deviceType="urn:dslforum-org:device:LANDevice:1" serviceId="urn:LanDeviceHosts-com:serviceId:Hosts1"/>
<getAction name="GetSpecificHostEntry" argument="NewActive">
pattern="([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}(\s*#.*)*"/>
</getAction>
</channel>
- <channel name="macIP" label="MAC IP" description="IP of the device with the given MAC">
+ <channel name="macOnlineIpAddress" label="MAC Online IP"
+ description="IP of the device with the given MAC (see macOnline)" advanced="true">
<item type="String"/>
<service deviceType="urn:dslforum-org:device:LANDevice:1" serviceId="urn:LanDeviceHosts-com:serviceId:Hosts1"/>
<getAction name="GetSpecificHostEntry" argument="NewIPAddress">
pattern="([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}(\s*#.*)*"/>
</getAction>
</channel>
-
<!-- WLAN Config 1 - 2.4 Ghz -->
<channel name="macSignalStrength1" label="MAC Wifi Signal Strength 2.4Ghz"
description="Wifi Signal Strength of the device with
the given MAC. This is set in case the Device is connected to 2.4Ghz"
- typeId="system.signal-strength">
+ typeId="system:signal-strength">
<item type="Number"/>
<service deviceType="urn:dslforum-org:device:LANDevice:1"
serviceId="urn:WLANConfiguration-com:serviceId:WLANConfiguration1"/>
</channel>
<channel name="macSpeed1" label="MAC Wifi Speed 2.4Ghz"
description="Wifi Speed of the device with
- the given MAC. This is set in case the Device is connected to 2.4Ghz">
+ the given MAC (see macOnline). This is set in case the Device is connected to 2.4Ghz">
<item type="Number:DataTransferRate" unit="Mbit/s" statePattern="%d Mbit/s"/>
<service deviceType="urn:dslforum-org:device:LANDevice:1"
serviceId="urn:WLANConfiguration-com:serviceId:WLANConfiguration1"/>
<!-- WLAN Config 2 - 5 Ghz -->
<channel name="macSignalStrength2" label="MAC Wifi Signal Strength 5Ghz"
description="Wifi Signal Strength of the device with
- the given MAC. This is set in case the Device is connected to 5Ghz"
- typeId="system.signal-strength">
+ the given MAC (see macOnline). This is set in case the Device is connected to 5Ghz"
+ typeId="system:signal-strength">
<item type="Number"/>
<service deviceType="urn:dslforum-org:device:LANDevice:1"
serviceId="urn:WLANConfiguration-com:serviceId:WLANConfiguration2"/>
</channel>
<channel name="macSpeed2" label="MAC Wifi Speed 5Ghz"
description="Wifi Speed of the device with
- the given MAC. This is set in case the Device is connected to 5Ghz">
+ the given MAC (see macOnline). This is set in case the Device is connected to 5Ghz">
<item type="Number:DataTransferRate" unit="Mbit/s" statePattern="%d Mbit/s"/>
<service deviceType="urn:dslforum-org:device:LANDevice:1"
serviceId="urn:WLANConfiguration-com:serviceId:WLANConfiguration2"/>
serviceId="urn:WANCIfConfig-com:serviceId:WANCommonInterfaceConfig1"/>
<getAction name="GetTotalBytesSent" argument="NewTotalBytesSent"/>
</channel>
+ <channel name="wanCurrentDownstreamBitrate" label="Current Downstream Rate">
+ <item type="Number:DataTransferRate" unit="kbit/s" statePattern="%.1f Mbit/s"/>
+ <service deviceType="urn:dslforum-org:device:WANDevice:1"
+ serviceId="urn:WANCIfConfig-com:serviceId:WANCommonInterfaceConfig1"/>
+ <getAction name="X_AVM-DE_GetOnlineMonitor" argument="Newds_current_bps"
+ postProcessor="processCurrentBitrate">
+ <parameter name="NewSyncGroupIndex" fixedValue="0"/>
+ </getAction>
+ </channel>
+ <channel name="wanCurrentUpstreamBitrate" label="Current Upstream Rate">
+ <item type="Number:DataTransferRate" unit="kbit/s" statePattern="%.1f Mbit/s"/>
+ <service deviceType="urn:dslforum-org:device:WANDevice:1"
+ serviceId="urn:WANCIfConfig-com:serviceId:WANCommonInterfaceConfig1"/>
+ <getAction name="X_AVM-DE_GetOnlineMonitor" argument="Newus_current_bps"
+ postProcessor="processCurrentBitrate">
+ <parameter name="NewSyncGroupIndex" fixedValue="0"/>
+ </getAction>
+ </channel>
<channel name="dslEnable" label="DSL Enable">
<item type="Switch"/>
<service deviceType="urn:dslforum-org:device:WANDevice:1"
<getAction name="GetInfo" argument="NewUpstreamCurrRate"/>
</channel>
<channel name="dslDownstreamNoiseMargin" label="DSL Downstream Noise Margin">
- <item type="Number:Dimensionless" unit="dB" statePattern="%.1f dB"/>
+ <item type="Number:Dimensionless" unit="dB" factor="0.1" statePattern="%.1f dB"/>
<service deviceType="urn:dslforum-org:device:WANDevice:1"
serviceId="urn:WANDSLIfConfig-com:serviceId:WANDSLInterfaceConfig1"/>
- <getAction name="GetInfo" argument="NewDownstreamNoiseMargin" postProcessor="processDecaDecibel"/>
+ <getAction name="GetInfo" argument="NewDownstreamNoiseMargin"/>
</channel>
<channel name="dslUpstreamNoiseMargin" label="DSL Upstream Noise Margin">
- <item type="Number:Dimensionless" unit="dB" statePattern="%.1f dB"/>
+ <item type="Number:Dimensionless" unit="dB" factor="0.1" statePattern="%.1f dB"/>
<service deviceType="urn:dslforum-org:device:WANDevice:1"
serviceId="urn:WANDSLIfConfig-com:serviceId:WANDSLInterfaceConfig1"/>
- <getAction name="GetInfo" argument="NewUpstreamNoiseMargin" postProcessor="processDecaDecibel"/>
+ <getAction name="GetInfo" argument="NewUpstreamNoiseMargin"/>
</channel>
<channel name="dslDownstreamAttenuation" label="DSL Downstream Attenuation">
- <item type="Number:Dimensionless" unit="dB" statePattern="%.1f dB"/>
+ <item type="Number:Dimensionless" unit="dB" factor="0.1" statePattern="%.1f dB"/>
<service deviceType="urn:dslforum-org:device:WANDevice:1"
serviceId="urn:WANDSLIfConfig-com:serviceId:WANDSLInterfaceConfig1"/>
- <getAction name="GetInfo" argument="NewDownstreamAttenuation" postProcessor="processDecaDecibel"/>
+ <getAction name="GetInfo" argument="NewDownstreamAttenuation"/>
</channel>
<channel name="dslUpstreamAttenuation" label="DSL Upstream Attenuation">
- <item type="Number:Dimensionless" unit="dB" statePattern="%.1f dB"/>
+ <item type="Number:Dimensionless" unit="dB" factor="0.1" statePattern="%.1f dB"/>
<service deviceType="urn:dslforum-org:device:WANDevice:1"
serviceId="urn:WANDSLIfConfig-com:serviceId:WANDSLInterfaceConfig1"/>
- <getAction name="GetInfo" argument="NewUpstreamAttenuation" postProcessor="processDecaDecibel"/>
+ <getAction name="GetInfo" argument="NewUpstreamAttenuation"/>
</channel>
<channel name="dslFECErrors" label="DSL FEC Errors">
<item type="Number:Dimensionless"/>
<xs:extension base="xs:string">
<xs:attribute type="xs:string" name="type" use="required"/>
<xs:attribute type="xs:string" name="unit" default=""/>
+ <xs:attribute type="xs:decimal" name="factor"/>
<xs:attribute type="xs:string" name="statePattern"/>
</xs:extension>
</xs:simpleContent>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:string" name="name" use="required"/>
- <xs:attribute type="xs:string" name="thingParameter" use="required"/>
+ <xs:attribute type="xs:string" name="thingParameter" />
+ <xs:attribute type="xs:string" name="fixedValue" />
<xs:attribute type="xs:string" name="pattern"/>
<xs:attribute type="xs:boolean" name="internalOnly" default="false"/>
</xs:extension>
@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";
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;
private final Phonebook phonebook = new Phonebook() {
@Override
public Optional<String> lookupNumber(String number, int matchCount) {
- switch (number) {
- case JOHN_DOES_PHONE_NUMBER:
- return Optional.of(JOHN_DOES_NAME);
- default:
- return Optional.empty();
- }
+ return switch (number) {
+ case JOHN_DOES_PHONE_NUMBER -> Optional.of(JOHN_DOES_NAME);
+ default -> Optional.empty();
+ };
}
@Override
--- /dev/null
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tr064.internal.phonebook;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.HttpClient;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+
+/**
+ * The {@link Tr064PhonebookImplTest} class implements test cases for the {@link Tr064PhonebookImpl} class
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+@MockitoSettings(strictness = Strictness.WARN)
+@ExtendWith(MockitoExtension.class)
+public class Tr064PhonebookImplTest {
+ @Mock
+ private @NonNullByDefault({}) HttpClient httpClient;
+
+ // key -> input, value -> output
+ public static Collection<Map.Entry<String, String>> phoneNumbers() {
+ return List.of( //
+ Map.entry("**820", "**820"), //
+ Map.entry("49200123456", "49200123456"), //
+ Map.entry("+49-200-123456", "+49200123456"), //
+ Map.entry("49 (200) 123456", "49200123456"), //
+ Map.entry("+49 200/123456", "+49200123456"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("phoneNumbers")
+ public void testNormalization(Map.Entry<String, String> input) {
+ when(httpClient.newRequest((String) any())).thenThrow(new IllegalArgumentException("testing"));
+ Tr064PhonebookImpl testPhonebook = new Tr064PhonebookImpl(httpClient, "", 0);
+ assertEquals(input.getValue(), testPhonebook.normalizeNumber(input.getKey()));
+ }
+
+ @Test
+ public void testLookup() {
+ when(httpClient.newRequest((String) any())).thenThrow(new IllegalArgumentException("testing"));
+ TestPhonebook testPhonebook = new TestPhonebook(httpClient, "", 0);
+ testPhonebook.setPhonebook(Map.of("+491238007001", "foo", "+4933998005671", "bar"));
+
+ Optional<String> result = testPhonebook.lookupNumber("01238007001", 0);
+ assertEquals(Optional.empty(), result);
+
+ result = testPhonebook.lookupNumber("01238007001", 10);
+ assertEquals("foo", result.get());
+
+ result = testPhonebook.lookupNumber("033998005671", -1);
+ assertEquals("bar", result.get());
+ }
+
+ private static class TestPhonebook extends Tr064PhonebookImpl {
+ public TestPhonebook(HttpClient httpClient, String phonebookUrl, int httpTimeout) {
+ super(httpClient, phonebookUrl, httpTimeout);
+ }
+
+ public void setPhonebook(Map<String, String> phonebook) {
+ this.phonebook = phonebook;
+ }
+ }
+}