[tr064] Enhancements, code improvements and fixes (#14468)

Signed-off-by: Jan N. Klug <github@klug.nrw>
This commit is contained in:
J-N-K 2023-02-24 16:06:53 +01:00 committed by GitHub
parent 561eb84f65
commit cb31f420ff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 623 additions and 354 deletions

View File

@ -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

View File

@ -38,6 +38,10 @@ If you only configured password authentication for your device, the `user` param
The second credential parameter is `password`, which is mandatory.
For security reasons it is highly recommended to set both, username and password.
Another optional and advanced configuration parameter is `timeout`.
This parameter applies to all requests to the device (SOAP requests, phonebook retrieval, call lists, ...).
It only needs to be changed from the default value of `5` seconds when the remote device is unexpectedly slow and does not respond within that time.
### `fritzbox`
The `fritzbox` devices can give additional informations in dedicated channels, controlled
@ -70,12 +74,19 @@ If the `PHONEBOOK` profile shall be used, it is necessary to retrieve the phoneb
The `phonebookInterval` is used to set the refresh cycle for phonebooks.
It defaults to 600 seconds, and it can be set to 0 if phonebooks are not used.
Some parameters (e.g. `macOnline`, `wanBlockIPs`) accept lists.
Some parameters (e.g. `macOnline`, `wanBlockIPs`) accept lists.
List items are configured one per line in the UI, or are comma separated values when using textual config.
These parameters that accept list can also contain comments.
Comments are separated from the value with a '#' (e.g. `192.168.0.77 # Daughter's iPhone`).
The full string is used for the channel label.
Two more advanced parameters are used for the backup thing action.
The `backupDirectory` is the directory where the backup files are stored.
The default value is the userdata directory.
The `backupPassword` is used to encrypt the backup file.
This is equivalent to setting a password in the UI.
If no password is given, the user password (parameter `password`) is used.
### `subdevice`, `subdeviceLan`
Additional informations (i.e. channels) are available in subdevices of the bridge.
@ -117,18 +128,17 @@ The call-types are the same as provided by the FritzBox, i.e. `1` (inbound), `2`
### LAN `subdeviceLan` channels
| channel | item-type | advanced | description |
|----------------------------|---------------------------|:--------:|----------------------------------------------------------------|
| `wifi24GHzEnable` | `Switch` | | Enable/Disable the 2.4 GHz WiFi device. |
| `wifi5GHzEnable` | `Switch` | | Enable/Disable the 5.0 GHz WiFi device. |
| `wifiGuestEnable` | `Switch` | | Enable/Disable the guest WiFi. |
| `macOnline` | `Switch` | x | Online status of the device with the given MAC |
| `macIP` | `String` | x | IP of the device with the given MAC |
| `macSignalStrength1` | `Number` | x | Wifi Signal Strength of the device with the given MAC. This is set in case the Device is connected to 2.4Ghz |
| `macSpeed1` | `Number:DataTransferRate` | x | Wifi Speed of the device with the given MAC. This is set in case the Device is connected to 2.4Ghz |
| `macSignalStrength2` | `Number` | x | Wifi Signal Strength of the device with the given MAC. This is set in case the Device is connected to 5Ghz |
| `macSpeed2` | `Number:DataTransferRate` | x | Wifi Speed of the device with the given MAC. This is set in case the Device is connected to 5Ghz |
| channel | item-type | advanced | description |
|----------------------|---------------------------|:--------:|--------------------------------------------------------------------------------------------------------------|
| `wifi24GHzEnable` | `Switch` | | Enable/Disable the 2.4 GHz WiFi device. |
| `wifi5GHzEnable` | `Switch` | | Enable/Disable the 5.0 GHz WiFi device. |
| `wifiGuestEnable` | `Switch` | | Enable/Disable the guest WiFi. |
| `macOnline` | `Switch` | x | Online status of the device with the given MAC |
| `macOnlineIpAddress` | `String` | x | IP of the MAC (uses same parameter as `macOnline`) |
| `macSignalStrength1` | `Number` | x | Wifi Signal Strength of the device with the given MAC. This is set in case the Device is connected to 2.4Ghz |
| `macSpeed1` | `Number:DataTransferRate` | x | Wifi Speed of the device with the given MAC. This is set in case the Device is connected to 2.4Ghz |
| `macSignalStrength2` | `Number` | x | Wifi Signal Strength of the device with the given MAC. This is set in case the Device is connected to 5Ghz |
| `macSpeed2` | `Number:DataTransferRate` | x | Wifi Speed of the device with the given MAC. This is set in case the Device is connected to 5Ghz |
Older FritzBox devices may not support 5 GHz WiFi.
In this case you have to use the `wifi5GHzEnable` channel for switching the guest WiFi.
@ -140,34 +150,36 @@ In this case you have to use the `wifi5GHzEnable` channel for switching the gues
| `pppUptime` | `Number:Time` | | Uptime (if using PPP) |
| `wanConnectionStatus` | `String` | | Connection Status |
| `wanPppConnectionStatus` | `String` | | Connection Status (if using PPP) |
| `wanIpAddress` | `String` | x | WAN IP Address |
| `wanPppIpAddress` | `String` | x | WAN IP Address (if using PPP) |
| `wanIpAddress` | `String` | 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`):

View File

@ -10,14 +10,31 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.tr064.internal.phonebook;
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.openhab.binding.tr064.internal.Tr064RootHandler;
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;
@ -28,14 +45,16 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link PhonebookActions} is responsible for handling phonebook actions
* The {@link FritzboxActions} is responsible for handling phone book actions
*
* @author Jan N. Klug - Initial contribution
*/
@ThingActionsScope(name = "tr064")
@NonNullByDefault
public class PhonebookActions implements ThingActions {
private final Logger logger = LoggerFactory.getLogger(PhonebookActions.class);
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;
@ -76,16 +95,67 @@ public class PhonebookActions implements ThingActions {
} else {
int matchCountInt = matchCount == null ? 0 : matchCount;
if (phonebook != null && !phonebook.isEmpty()) {
return handler.getPhonebookByName(phonebook).flatMap(p -> p.lookupNumber(phonenumber, matchCountInt))
.orElse(phonenumber);
return Objects.requireNonNull(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);
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);
@ -102,18 +172,23 @@ public class PhonebookActions implements ThingActions {
public static String phonebookLookup(ThingActions actions, @Nullable String phonenumber, @Nullable String phonebook,
@Nullable Integer matchCount) {
return ((PhonebookActions) actions).phonebookLookup(phonenumber, phonebook, matchCount);
return ((FritzboxActions) actions).phonebookLookup(phonenumber, phonebook, matchCount);
}
public static void createConfigurationBackup(ThingActions actions) {
((FritzboxActions) actions).createConfigurationBackup();
}
@Override
public void setThingHandler(@Nullable ThingHandler handler) {
if (handler instanceof Tr064RootHandler) {
this.handler = (Tr064RootHandler) handler;
}
this.handler = (Tr064RootHandler) handler;
}
@Override
public @Nullable ThingHandler getThingHandler() {
return handler;
}
public record BackupConfiguration(String directory, String password) {
}
}

View File

@ -37,7 +37,7 @@ public class Tr064CommunicationException extends Exception {
super(s);
this.httpError = httpError;
this.soapError = soapError;
};
}
public String getSoapError() {
return soapError;

View File

@ -14,9 +14,7 @@ package org.openhab.binding.tr064.internal;
import static org.openhab.binding.tr064.internal.Tr064BindingConstants.*;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ -44,7 +42,7 @@ import org.slf4j.LoggerFactory;
@NonNullByDefault
public class Tr064DiscoveryService extends AbstractDiscoveryService implements ThingHandlerService {
private static final int SEARCH_TIME = 5;
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_SUBDEVICE);
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_SUBDEVICE);
private final Logger logger = LoggerFactory.getLogger(Tr064DiscoveryService.class);
private @Nullable Tr064RootHandler bridgeHandler;
@ -101,13 +99,12 @@ public class Tr064DiscoveryService extends AbstractDiscoveryService implements T
}
ThingUID thingUID = new ThingUID(thingTypeUID, bridgeUID, UIDUtils.encode(udn));
Map<String, Object> properties = new HashMap<>(2);
properties.put("uuid", udn);
properties.put("deviceType", device.getDeviceType());
DiscoveryResult result = DiscoveryResultBuilder.create(thingUID).withLabel(device.getFriendlyName())
.withBridge(bridgeHandler.getThing().getUID()).withProperties(properties)
.withRepresentationProperty("uuid").build();
DiscoveryResult result = DiscoveryResultBuilder.create(thingUID) //
.withLabel(device.getFriendlyName()) //
.withBridge(bridgeUID) //
.withProperties(Map.of("uuid", udn, "deviceType", device.getDeviceType())) //
.withRepresentationProperty("uuid") //
.build();
thingDiscovered(result);
}
});

View File

@ -1,73 +0,0 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.tr064.internal;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
import org.openhab.core.types.StateDescription;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Dynamic channel state description provider.
* Overrides the state description for the controls, which receive its configuration in the runtime.
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
@Component(service = { DynamicStateDescriptionProvider.class, Tr064DynamicStateDescriptionProvider.class })
public class Tr064DynamicStateDescriptionProvider implements DynamicStateDescriptionProvider {
private final Logger logger = LoggerFactory.getLogger(Tr064DynamicStateDescriptionProvider.class);
private final Map<ChannelUID, StateDescription> descriptions = new ConcurrentHashMap<>();
/**
* Set a state description for a channel. This description will be used when preparing the channel state by
* the framework for presentation. A previous description, if existed, will be replaced.
*
* @param channelUID channel UID
* @param description state description for the channel
*/
public void setDescription(ChannelUID channelUID, StateDescription description) {
logger.trace("adding state description for channel {}", channelUID);
descriptions.put(channelUID, description);
}
/**
* remove all descriptions for a given thing
*
* @param thingUID the thing's UID
*/
public void removeDescriptionsForThing(ThingUID thingUID) {
logger.trace("removing state description for thing {}", thingUID);
descriptions.entrySet().removeIf(entry -> entry.getKey().getThingUID().equals(thingUID));
}
@Override
public @Nullable StateDescription getStateDescription(Channel channel,
@Nullable StateDescription originalStateDescription, @Nullable Locale locale) {
if (descriptions.containsKey(channel.getUID())) {
return descriptions.get(channel.getUID());
} else {
return null;
}
}
}

View File

@ -98,7 +98,7 @@ public class Tr064HandlerFactory extends BaseThingHandlerFactory {
if (Tr064RootHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) {
Tr064RootHandler handler = new Tr064RootHandler((Bridge) thing, httpClient);
if (thingTypeUID.equals(THING_TYPE_FRITZBOX)) {
if (THING_TYPE_FRITZBOX.equals(thingTypeUID)) {
phonebookProfileFactory.registerPhonebookProvider(handler);
}
return handler;

View File

@ -23,10 +23,13 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -38,6 +41,7 @@ import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.Authentication;
import org.eclipse.jetty.client.api.AuthenticationStore;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.util.DigestAuthentication;
import org.openhab.binding.tr064.internal.config.Tr064ChannelConfig;
import org.openhab.binding.tr064.internal.config.Tr064RootConfiguration;
@ -45,7 +49,6 @@ import org.openhab.binding.tr064.internal.dto.scpd.root.SCPDDeviceType;
import org.openhab.binding.tr064.internal.dto.scpd.root.SCPDServiceType;
import org.openhab.binding.tr064.internal.dto.scpd.service.SCPDActionType;
import org.openhab.binding.tr064.internal.phonebook.Phonebook;
import org.openhab.binding.tr064.internal.phonebook.PhonebookActions;
import org.openhab.binding.tr064.internal.phonebook.PhonebookProvider;
import org.openhab.binding.tr064.internal.phonebook.Tr064PhonebookImpl;
import org.openhab.binding.tr064.internal.soap.SOAPConnector;
@ -61,6 +64,7 @@ import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.thing.binding.ThingHandlerCallback;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.thing.binding.builder.ThingBuilder;
import org.openhab.core.types.Command;
@ -85,12 +89,15 @@ public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProv
private final Logger logger = LoggerFactory.getLogger(Tr064RootHandler.class);
private final HttpClient httpClient;
private Tr064RootConfiguration config = new Tr064RootConfiguration();
private String deviceType = "";
private @Nullable SCPDUtil scpdUtil;
private SOAPConnector soapConnector;
// these are set when the config is available
private Tr064RootConfiguration config = new Tr064RootConfiguration();
private String endpointBaseURL = "";
private int timeout = Tr064RootConfiguration.DEFAULT_HTTP_TIMEOUT;
private String deviceType = "";
private final Map<ChannelUID, Tr064ChannelConfig> channels = new HashMap<>();
// caching is used to prevent excessive calls to the same action
@ -106,7 +113,7 @@ public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProv
Tr064RootHandler(Bridge bridge, HttpClient httpClient) {
super(bridge);
this.httpClient = httpClient;
this.soapConnector = new SOAPConnector(httpClient, endpointBaseURL);
this.soapConnector = new SOAPConnector(httpClient, endpointBaseURL, timeout);
}
@Override
@ -147,7 +154,8 @@ public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProv
}
endpointBaseURL = "http://" + config.host + ":49000";
soapConnector = new SOAPConnector(httpClient, endpointBaseURL);
soapConnector = new SOAPConnector(httpClient, endpointBaseURL, timeout);
timeout = config.timeout;
updateStatus(ThingStatus.UNKNOWN);
connectFuture = scheduler.scheduleWithFixedDelay(this::internalInitialize, 0, RETRY_INTERVAL, TimeUnit.SECONDS);
@ -158,7 +166,7 @@ public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProv
*/
private void internalInitialize() {
try {
scpdUtil = new SCPDUtil(httpClient, endpointBaseURL);
scpdUtil = new SCPDUtil(httpClient, endpointBaseURL, timeout);
} catch (SCPDException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"could not get device definitions from " + config.host);
@ -172,8 +180,9 @@ public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProv
ThingBuilder thingBuilder = editThing();
thingBuilder.withoutChannels(thing.getChannels());
final SCPDUtil scpdUtil = this.scpdUtil;
if (scpdUtil != null) {
Util.checkAvailableChannels(thing, thingBuilder, scpdUtil, "", deviceType, channels);
final ThingHandlerCallback callback = getCallback();
if (scpdUtil != null && callback != null) {
Util.checkAvailableChannels(thing, callback, thingBuilder, scpdUtil, "", deviceType, channels);
updateThing(thingBuilder.build());
}
@ -197,6 +206,7 @@ public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProv
removeConnectScheduler();
uninstallPolling();
stateCache.clear();
scpdUtil = null;
super.dispose();
}
@ -205,19 +215,25 @@ public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProv
* poll remote device for channel values
*/
private void poll() {
channels.forEach((channelUID, channelConfig) -> {
if (isLinked(channelUID)) {
State state = stateCache.putIfAbsentAndGet(channelUID,
() -> soapConnector.getChannelStateFromDevice(channelConfig, channels, stateCache));
if (state != null) {
updateState(channelUID, state);
try {
channels.forEach((channelUID, channelConfig) -> {
if (isLinked(channelUID)) {
State state = stateCache.putIfAbsentAndGet(channelUID,
() -> soapConnector.getChannelStateFromDevice(channelConfig, channels, stateCache));
if (state != null) {
updateState(channelUID, state);
}
}
}
});
});
} catch (RuntimeException e) {
logger.warn("Exception while refreshing remote data for thing '{}':", thing.getUID(), e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Refresh exception: " + e.getMessage());
}
}
/**
* establish the connection - get secure port (if avallable), install authentication, get device properties
* establish the connection - get secure port (if available), install authentication, get device properties
*
* @return true if successful
*/
@ -238,11 +254,11 @@ public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProv
SOAPMessage soapResponse = soapConnector
.doSOAPRequest(new SOAPRequest(deviceService, "GetSecurityPort"));
if (!soapResponse.getSOAPBody().hasFault()) {
SOAPValueConverter soapValueConverter = new SOAPValueConverter(httpClient);
SOAPValueConverter soapValueConverter = new SOAPValueConverter(httpClient, timeout);
soapValueConverter.getStateFromSOAPValue(soapResponse, "NewSecurityPort", null)
.ifPresentOrElse(port -> {
endpointBaseURL = "https://" + config.host + ":" + port.toString();
soapConnector = new SOAPConnector(httpClient, endpointBaseURL);
endpointBaseURL = "https://" + config.host + ":" + port;
soapConnector = new SOAPConnector(httpClient, endpointBaseURL, timeout);
logger.debug("endpointBaseURL is now '{}'", endpointBaseURL);
}, () -> logger.warn("Could not determine secure port, disabling https"));
} else {
@ -250,9 +266,10 @@ public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProv
}
// clear auth cache and force re-auth
httpClient.getAuthenticationStore().clearAuthenticationResults();
AuthenticationStore auth = httpClient.getAuthenticationStore();
auth.addAuthentication(new DigestAuthentication(new URI(endpointBaseURL), Authentication.ANY_REALM,
AuthenticationStore authStore = httpClient.getAuthenticationStore();
authStore.clearAuthentications();
authStore.clearAuthenticationResults();
authStore.addAuthentication(new DigestAuthentication(new URI(endpointBaseURL), Authentication.ANY_REALM,
config.user, config.password));
// check & update properties
@ -263,7 +280,7 @@ public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProv
.orElseThrow(() -> new SCPDException("Action 'GetInfo' not found"));
SOAPMessage soapResponse1 = soapConnector
.doSOAPRequest(new SOAPRequest(deviceService, getInfoAction.getName()));
SOAPValueConverter soapValueConverter = new SOAPValueConverter(httpClient);
SOAPValueConverter soapValueConverter = new SOAPValueConverter(httpClient, timeout);
Map<String, String> properties = editProperties();
PROPERTY_ARGUMENTS.forEach(argumentName -> getInfoAction.getArgumentList().stream()
.filter(argument -> argument.getName().equals(argumentName)).findFirst()
@ -301,6 +318,22 @@ public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProv
return soapConnector;
}
/**
* return the result of an (authenticated) GET request
*
* @param url the requested URL
*
* @return a {@link ContentResponse} with the result of the request
* @throws ExecutionException
* @throws InterruptedException
* @throws TimeoutException
*/
public ContentResponse getUrl(String url) throws ExecutionException, InterruptedException, TimeoutException {
httpClient.getAuthenticationStore().addAuthentication(
new DigestAuthentication(URI.create(url), Authentication.ANY_REALM, config.user, config.password));
return httpClient.GET(URI.create(url));
}
/**
* get the SCPD processing utility
*
@ -341,21 +374,21 @@ public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProv
@SuppressWarnings("unchecked")
private Collection<Phonebook> processPhonebookList(SOAPMessage soapMessagePhonebookList,
SCPDServiceType scpdService) {
SOAPValueConverter soapValueConverter = new SOAPValueConverter(httpClient);
return (Collection<Phonebook>) soapValueConverter
SOAPValueConverter soapValueConverter = new SOAPValueConverter(httpClient, timeout);
Optional<Stream<String>> phonebookStream = soapValueConverter
.getStateFromSOAPValue(soapMessagePhonebookList, "NewPhonebookList", null)
.map(phonebookList -> Arrays.stream(phonebookList.toString().split(","))).orElse(Stream.empty())
.map(index -> {
try {
SOAPMessage soapMessageURL = soapConnector.doSOAPRequest(
new SOAPRequest(scpdService, "GetPhonebook", Map.of("NewPhonebookID", index)));
return soapValueConverter.getStateFromSOAPValue(soapMessageURL, "NewPhonebookURL", null)
.map(url -> (Phonebook) new Tr064PhonebookImpl(httpClient, url.toString()));
} catch (Tr064CommunicationException e) {
logger.warn("Failed to get phonebook with index {}:", index, e);
}
return Optional.empty();
}).filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList());
.map(phonebookList -> Arrays.stream(phonebookList.toString().split(",")));
return phonebookStream.map(stringStream -> (Collection<Phonebook>) stringStream.map(index -> {
try {
SOAPMessage soapMessageURL = soapConnector
.doSOAPRequest(new SOAPRequest(scpdService, "GetPhonebook", Map.of("NewPhonebookID", index)));
return soapValueConverter.getStateFromSOAPValue(soapMessageURL, "NewPhonebookURL", null)
.map(url -> (Phonebook) new Tr064PhonebookImpl(httpClient, url.toString(), timeout));
} catch (Tr064CommunicationException e) {
logger.warn("Failed to get phonebook with index {}:", index, e);
}
return Optional.empty();
}).filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList())).orElseGet(Set::of);
}
private void retrievePhonebooks() {
@ -368,14 +401,14 @@ public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProv
Optional<SCPDServiceType> scpdService = scpdUtil.getDevice("").flatMap(deviceType -> deviceType.getServiceList()
.stream().filter(service -> service.getServiceId().equals(serviceId)).findFirst());
phonebooks = scpdService.map(service -> {
phonebooks = Objects.requireNonNull(scpdService.map(service -> {
try {
return processPhonebookList(soapConnector.doSOAPRequest(new SOAPRequest(service, "GetPhonebookList")),
service);
} catch (Tr064CommunicationException e) {
return Collections.<Phonebook> emptyList();
}
}).orElse(List.of());
}).orElse(List.of()));
if (phonebooks.isEmpty()) {
logger.warn("Could not get phonebooks for thing {}", thing.getUID());
@ -405,6 +438,20 @@ public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProv
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Set.of(Tr064DiscoveryService.class, PhonebookActions.class);
if (THING_TYPE_FRITZBOX.equals(thing.getThingTypeUID())) {
return Set.of(Tr064DiscoveryService.class, FritzboxActions.class);
} else {
return Set.of(Tr064DiscoveryService.class);
}
}
/**
* get the backup configuration for this thing (only applies to FritzBox devices
*
* @return the configuration
*/
public FritzboxActions.BackupConfiguration getBackupConfiguration() {
return new FritzboxActions.BackupConfiguration(config.backupDirectory,
Objects.requireNonNullElse(config.backupPassword, config.password));
}
}

View File

@ -37,6 +37,7 @@ import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingStatusInfo;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.ThingHandlerCallback;
import org.openhab.core.thing.binding.builder.ThingBuilder;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
@ -77,7 +78,6 @@ public class Tr064SubHandler extends BaseThingHandler {
}
@Override
@SuppressWarnings("null")
public void handleCommand(ChannelUID channelUID, Command command) {
Tr064ChannelConfig channelConfig = channels.get(channelUID);
if (channelConfig == null) {
@ -86,6 +86,7 @@ public class Tr064SubHandler extends BaseThingHandler {
}
if (command instanceof RefreshType) {
final SOAPConnector soapConnector = this.soapConnector;
State state = stateCache.putIfAbsentAndGet(channelUID, () -> soapConnector == null ? UnDefType.UNDEF
: soapConnector.getChannelStateFromDevice(channelConfig, channels, stateCache));
if (state != null) {
@ -99,6 +100,7 @@ public class Tr064SubHandler extends BaseThingHandler {
return;
}
scheduler.execute(() -> {
final SOAPConnector soapConnector = this.soapConnector;
if (soapConnector == null) {
logger.warn("Could not send command because connector not available");
} else {
@ -141,12 +143,16 @@ public class Tr064SubHandler extends BaseThingHandler {
"Could not get device definitions");
return;
}
final ThingHandlerCallback callback = getCallback();
if (callback == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Could not get callback");
return;
}
if (checkProperties(scpdUtil)) {
// properties set, check channels
ThingBuilder thingBuilder = editThing();
thingBuilder.withoutChannels(thing.getChannels());
Util.checkAvailableChannels(thing, thingBuilder, scpdUtil, config.uuid, deviceType, channels);
Util.checkAvailableChannels(thing, callback, thingBuilder, scpdUtil, config.uuid, deviceType, channels);
updateThing(thingBuilder.build());
// remove connect scheduler

View File

@ -25,8 +25,8 @@ import org.openhab.binding.tr064.internal.dto.scpd.service.SCPDActionType;
*/
@NonNullByDefault
public class Tr064ChannelConfig {
private ChannelTypeDescription channelTypeDescription;
private SCPDServiceType service;
private final ChannelTypeDescription channelTypeDescription;
private final SCPDServiceType service;
private @Nullable SCPDActionType getAction;
private String dataType = "";
private @Nullable String parameter;

View File

@ -15,6 +15,8 @@ package org.openhab.binding.tr064.internal.config;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.OpenHAB;
/**
* The {@link Tr064RootConfiguration} class contains fields mapping thing configuration parameters.
@ -23,9 +25,12 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
*/
@NonNullByDefault
public class Tr064RootConfiguration extends Tr064BaseThingConfiguration {
public static final int DEFAULT_HTTP_TIMEOUT = 5; // in s
public String host = "";
public String user = "dslf-config";
public String password = "";
public int timeout = DEFAULT_HTTP_TIMEOUT;
/* following parameters only available in fritzbox thing */
public List<String> tamIndices = List.of();
@ -38,6 +43,10 @@ public class Tr064RootConfiguration extends Tr064BaseThingConfiguration {
public List<String> wanBlockIPs = List.of();
public int phonebookInterval = 600;
// Backup data
public String backupDirectory = OpenHAB.getUserDataFolder();
public @Nullable String backupPassword;
public boolean isValid() {
return !host.isEmpty() && !user.isEmpty() && !password.isEmpty();
}

View File

@ -14,6 +14,7 @@ package org.openhab.binding.tr064.internal.phonebook;
import java.math.BigDecimal;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
@ -139,15 +140,13 @@ public class PhonebookProfile implements StateProfile {
}
if (state instanceof StringType) {
Optional<String> match = resolveNumber(state.toString());
State newState = match.map(name -> (State) new StringType(name)).orElse(state);
// Compare by reference to check if the name is mapped to the same state
if (newState == state) {
State newState = Objects.requireNonNull(match.map(name -> (State) new StringType(name)).orElse(state));
if (newState.equals(state)) {
logger.debug("Number '{}' not found in phonebook '{}' from provider '{}'", state, phonebookName,
thingUID);
}
callback.sendUpdate(newState);
} else if (state instanceof StringListType) {
StringListType stringList = (StringListType) state;
} else if (state instanceof StringListType stringList) {
try {
String phoneNumber = stringList.getValue(phoneNumberIndex);
Optional<String> match = resolveNumber(phoneNumber);

View File

@ -33,31 +33,39 @@ import org.slf4j.LoggerFactory;
public class Tr064PhonebookImpl implements Phonebook {
private final Logger logger = LoggerFactory.getLogger(Tr064PhonebookImpl.class);
private Map<String, String> phonebook = new HashMap<>();
protected Map<String, String> phonebook = new HashMap<>();
private final HttpClient httpClient;
private final String phonebookUrl;
private final int httpTimeout;
private String phonebookName = "";
public Tr064PhonebookImpl(HttpClient httpClient, String phonebookUrl) {
public Tr064PhonebookImpl(HttpClient httpClient, String phonebookUrl, int httpTimeout) {
this.httpClient = httpClient;
this.phonebookUrl = phonebookUrl;
this.httpTimeout = httpTimeout;
getPhonebook();
}
private void getPhonebook() {
PhonebooksType phonebooksType = Util.getAndUnmarshalXML(httpClient, phonebookUrl, PhonebooksType.class);
if (phonebooksType != null) {
phonebookName = phonebooksType.getPhonebook().getName();
phonebook = phonebooksType.getPhonebook().getContact().stream().map(contact -> {
String contactName = contact.getPerson().getRealName();
return contact.getTelephony().getNumber().stream()
.collect(Collectors.toMap(number -> normalizeNumber(number.getValue()), number -> contactName,
this::mergeSameContactNames));
}).collect(HashMap::new, HashMap::putAll, HashMap::putAll);
logger.debug("Downloaded phonebook {}: {}", phonebookName, phonebook);
PhonebooksType phonebooksType = Util.getAndUnmarshalXML(httpClient, phonebookUrl, PhonebooksType.class,
httpTimeout);
if (phonebooksType == null) {
logger.warn("Failed to get phonebook with URL '{}'", phonebookUrl);
return;
}
phonebookName = phonebooksType.getPhonebook().getName();
phonebook = phonebooksType.getPhonebook().getContact().stream().map(contact -> {
String contactName = contact.getPerson().getRealName();
if (contactName == null || contactName.isBlank()) {
return new HashMap<String, String>();
}
return contact.getTelephony().getNumber().stream().collect(Collectors.toMap(
number -> normalizeNumber(number.getValue()), number -> contactName, this::mergeSameContactNames));
}).collect(HashMap::new, HashMap::putAll, HashMap::putAll);
logger.debug("Downloaded phonebook {}: {}", phonebookName, phonebook);
}
// in case there are multiple phone entries with same number -> name mapping, i.e. in phonebooks exported from
@ -78,9 +86,14 @@ public class Tr064PhonebookImpl implements Phonebook {
@Override
public Optional<String> lookupNumber(String number, int matchCount) {
String normalized = normalizeNumber(number);
String matchString = matchCount > 0 && matchCount < normalized.length()
? normalized.substring(normalized.length() - matchCount)
: normalized;
String matchString;
if (matchCount > 0 && matchCount < normalized.length()) {
matchString = normalized.substring(normalized.length() - matchCount);
} else if (matchCount < 0 && (-matchCount) < normalized.length()) {
matchString = normalized.substring(-matchCount);
} else {
matchString = normalized;
}
logger.trace("Normalized '{}' to '{}', matchString is '{}'", number, normalized, matchString);
return matchString.isBlank() ? Optional.empty()
: phonebook.keySet().stream().filter(n -> n.endsWith(matchString)).findFirst().map(phonebook::get);
@ -91,8 +104,13 @@ public class Tr064PhonebookImpl implements Phonebook {
return "Phonebook{" + "phonebookName='" + phonebookName + "', phonebook=" + phonebook + '}';
}
private String normalizeNumber(String number) {
// Naive normalization: remove all non-digit characters
/**
* normalize a phone number (remove everything except digits and *) for comparison
*
* @param number the input phone number string
* @return normalized phone number string
*/
public final String normalizeNumber(String number) {
return number.replaceAll("[^0-9\\*\\+]", "");
}
}

View File

@ -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;
}

View File

@ -15,11 +15,10 @@ package org.openhab.binding.tr064.internal.soap;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link CallListType} is used for post processing the retrieved call list
* The {@link CallListType} is used for post-processing the retrieved call list
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public enum CallListType {
MISSED_COUNT("2"),

View File

@ -16,8 +16,8 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
/**
*
* The{@link PostProcessingException} is a catched Exception that is thrown in case of conversion errors during post
* processing
* The {@link PostProcessingException} is an Exception that is thrown in case of conversion errors during
* post-processing
*
* @author Jan N. Klug - Initial contribution
*/

View File

@ -19,7 +19,6 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.time.Duration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
@ -28,7 +27,6 @@ import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import javax.xml.soap.MessageFactory;
import javax.xml.soap.MimeHeader;
import javax.xml.soap.MimeHeaders;
import javax.xml.soap.SOAPBody;
import javax.xml.soap.SOAPElement;
@ -67,19 +65,20 @@ import org.slf4j.LoggerFactory;
*/
@NonNullByDefault
public class SOAPConnector {
private static final int SOAP_TIMEOUT = 5; // in
private final Logger logger = LoggerFactory.getLogger(SOAPConnector.class);
private final HttpClient httpClient;
private final String endpointBaseURL;
private final SOAPValueConverter soapValueConverter;
private final int timeout;
private final ExpiringCacheMap<SOAPRequest, SOAPMessage> soapMessageCache = new ExpiringCacheMap<>(
Duration.ofMillis(2000));
public SOAPConnector(HttpClient httpClient, String endpointBaseURL) {
public SOAPConnector(HttpClient httpClient, String endpointBaseURL, int timeout) {
this.httpClient = httpClient;
this.endpointBaseURL = endpointBaseURL;
this.soapValueConverter = new SOAPValueConverter(httpClient);
this.timeout = timeout;
this.soapValueConverter = new SOAPValueConverter(httpClient, timeout);
}
/**
@ -118,7 +117,7 @@ public class SOAPConnector {
// create Request and add headers and content
Request request = httpClient.newRequest(endpointBaseURL + soapRequest.service.getControlURL())
.method(HttpMethod.POST);
((Iterator<MimeHeader>) soapMessage.getMimeHeaders().getAllHeaders())
soapMessage.getMimeHeaders().getAllHeaders()
.forEachRemaining(header -> request.header(header.getName(), header.getValue()));
try (final ByteArrayOutputStream os = new ByteArrayOutputStream()) {
soapMessage.writeTo(os);
@ -169,7 +168,7 @@ public class SOAPConnector {
*/
public synchronized SOAPMessage doSOAPRequestUncached(SOAPRequest soapRequest) throws Tr064CommunicationException {
try {
Request request = prepareSOAPRequest(soapRequest).timeout(SOAP_TIMEOUT, TimeUnit.SECONDS);
Request request = prepareSOAPRequest(soapRequest).timeout(timeout, TimeUnit.SECONDS);
if (logger.isTraceEnabled()) {
request.getContent().forEach(buffer -> logger.trace("Request: {}", new String(buffer.array())));
}
@ -179,7 +178,7 @@ public class SOAPConnector {
// retry once if authentication expired
logger.trace("Re-Auth needed.");
httpClient.getAuthenticationStore().clearAuthenticationResults();
request = prepareSOAPRequest(soapRequest).timeout(SOAP_TIMEOUT, TimeUnit.SECONDS);
request = prepareSOAPRequest(soapRequest).timeout(timeout, TimeUnit.SECONDS);
response = request.send();
}
try (final ByteArrayInputStream is = new ByteArrayInputStream(response.getContent())) {
@ -247,14 +246,11 @@ public class SOAPConnector {
final SCPDActionType getAction = channelConfig.getGetAction();
if (getAction == null) {
// channel has no get action, return a default
switch (channelConfig.getDataType()) {
case "boolean":
return OnOffType.OFF;
case "string":
return StringType.EMPTY;
default:
return UnDefType.UNDEF;
}
return switch (channelConfig.getDataType()) {
case "boolean" -> OnOffType.OFF;
case "string" -> StringType.EMPTY;
default -> UnDefType.UNDEF;
};
}
// get value(s) from remote device
@ -290,11 +286,13 @@ public class SOAPConnector {
} catch (Tr064CommunicationException e) {
if (e.getHttpError() == 500) {
switch (e.getSoapError()) {
case "714":
case "714" -> {
// NoSuchEntryInArray usually is an unknown entry in the MAC list
logger.debug("Failed to get {}: {}", channelConfig, e.getMessage());
return UnDefType.UNDEF;
default:
}
default -> {
}
}
}
// all other cases are an error

View File

@ -48,7 +48,6 @@ public class SOAPRequest {
if (o == null || getClass() != o.getClass()) {
return false;
}
SOAPRequest that = (SOAPRequest) o;
if (!service.equals(that.service)) {
@ -57,6 +56,7 @@ public class SOAPRequest {
if (!soapAction.equals(that.soapAction)) {
return false;
}
return arguments.equals(that.arguments);
}

View File

@ -17,6 +17,7 @@ import static org.openhab.binding.tr064.internal.util.Util.getSOAPElement;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
@ -57,9 +58,11 @@ import com.google.gson.GsonBuilder;
public class SOAPValueConverter {
private final Logger logger = LoggerFactory.getLogger(SOAPValueConverter.class);
private final HttpClient httpClient;
private final int timeout;
public SOAPValueConverter(HttpClient httpClient) {
public SOAPValueConverter(HttpClient httpClient, int timeout) {
this.httpClient = httpClient;
this.timeout = timeout;
}
/**
@ -83,24 +86,26 @@ public class SOAPValueConverter {
return Optional.empty();
}
switch (dataType) {
case "ui1":
case "ui2":
case "ui1", "ui2" -> {
return Optional.of(String.valueOf(value.shortValue()));
case "i4":
case "ui4":
}
case "i4", "ui4" -> {
return Optional.of(String.valueOf(value.intValue()));
default:
}
default -> {
}
}
} else if (command instanceof DecimalType) {
BigDecimal value = ((DecimalType) command).toBigDecimal();
switch (dataType) {
case "ui1":
case "ui2":
case "ui1", "ui2" -> {
return Optional.of(String.valueOf(value.shortValue()));
case "i4":
case "ui4":
}
case "i4", "ui4" -> {
return Optional.of(String.valueOf(value.intValue()));
default:
}
default -> {
}
}
} else if (command instanceof StringType) {
if ("string".equals(dataType)) {
@ -127,28 +132,35 @@ public class SOAPValueConverter {
@Nullable Tr064ChannelConfig channelConfig) {
String dataType = channelConfig != null ? channelConfig.getDataType() : "string";
String unit = channelConfig != null ? channelConfig.getChannelTypeDescription().getItem().getUnit() : "";
BigDecimal factor = channelConfig != null ? channelConfig.getChannelTypeDescription().getItem().getFactor()
: null;
return getSOAPElement(soapMessage, element).map(rawValue -> {
// map rawValue to State
switch (dataType) {
case "boolean":
case "boolean" -> {
return rawValue.equals("0") ? OnOffType.OFF : OnOffType.ON;
case "string":
}
case "string" -> {
return new StringType(rawValue);
case "ui1":
case "ui2":
case "i4":
case "ui4":
if (!unit.isEmpty()) {
return new QuantityType<>(rawValue + " " + unit);
} else {
return new DecimalType(rawValue);
}
case "ui1", "ui2", "i4", "ui4" -> {
BigDecimal decimalValue = new BigDecimal(rawValue);
if (factor != null) {
decimalValue = decimalValue.multiply(factor);
}
default:
if (!unit.isEmpty()) {
return new QuantityType<>(decimalValue + " " + unit);
} else {
return new DecimalType(decimalValue);
}
}
default -> {
return null;
}
}
}).map(state -> {
// check if we need post processing
// check if we need post-processing
if (channelConfig == null
|| channelConfig.getChannelTypeDescription().getGetAction().getPostProcessor() == null) {
return state;
@ -172,6 +184,26 @@ public class SOAPValueConverter {
}).or(Optional::empty);
}
/**
* post processor for current bitrate
*/
@SuppressWarnings("unused")
private State processCurrentBitrate(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException {
Double bps = Arrays.stream(state.toString().split(",")).mapToDouble(s -> {
try {
return Double.parseDouble(s);
} catch (NumberFormatException e) {
return 0.0;
}
}).limit(3).average().orElse(Double.NaN);
if (bps.equals(Double.NaN)) {
return UnDefType.UNDEF;
} else {
return new QuantityType<>(bps * 8.0 / 1024.0, Units.KILOBIT_PER_SECOND);
}
}
/**
* post processor to map mac device signal strength to system.signal-strength 0-4
*
@ -201,20 +233,6 @@ public class SOAPValueConverter {
return mappedSignalStrength;
}
/**
* post processor for decibel values (which are served as deca decibel)
*
* @param state the channel value in deca decibel
* @param channelConfig channel config of the channel
* @return the state converted to decibel
*/
@SuppressWarnings("unused")
private State processDecaDecibel(State state, Tr064ChannelConfig channelConfig) {
Float value = state.as(DecimalType.class).floatValue() / 10;
return new QuantityType<>(value, Units.DECIBEL);
}
/**
* post processor for answering machine new messages channel
*
@ -226,16 +244,14 @@ public class SOAPValueConverter {
@SuppressWarnings("unused")
private State processTamListURL(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException {
try {
ContentResponse response = httpClient.newRequest(state.toString()).timeout(1500, TimeUnit.MILLISECONDS)
ContentResponse response = httpClient.newRequest(state.toString()).timeout(timeout, TimeUnit.MILLISECONDS)
.send();
String responseContent = response.getContentAsString();
int messageCount = responseContent.split("<New>1</New>").length - 1;
return new DecimalType(messageCount);
} catch (TimeoutException e) {
throw new PostProcessingException("Failed to get TAM list due to time out from URL " + state.toString(), e);
} catch (InterruptedException | ExecutionException e) {
throw new PostProcessingException("Failed to get TAM list from URL " + state.toString(), e);
} catch (InterruptedException | TimeoutException | ExecutionException e) {
throw new PostProcessingException("Failed to get TAM list from URL " + state, e);
}
}
@ -315,23 +331,22 @@ public class SOAPValueConverter {
*/
private State processCallList(State state, @Nullable String days, CallListType type)
throws PostProcessingException {
Root callListRoot = Util.getAndUnmarshalXML(httpClient, state.toString() + "&days=" + days, Root.class);
Root callListRoot = Util.getAndUnmarshalXML(httpClient, state + "&days=" + days, Root.class, timeout);
if (callListRoot == null) {
throw new PostProcessingException("Failed to get call list from URL " + state.toString());
throw new PostProcessingException("Failed to get call list from URL " + state);
}
List<Call> calls = callListRoot.getCall();
switch (type) {
case INBOUND_COUNT:
case MISSED_COUNT:
case OUTBOUND_COUNT:
case REJECTED_COUNT:
case INBOUND_COUNT, MISSED_COUNT, OUTBOUND_COUNT, REJECTED_COUNT -> {
long callCount = calls.stream().filter(call -> type.typeString().equals(call.getType())).count();
return new DecimalType(callCount);
case JSON_LIST:
}
case JSON_LIST -> {
Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ssX").serializeNulls().create();
List<CallListEntry> callListEntries = calls.stream().map(CallListEntry::new)
.collect(Collectors.toList());
return new StringType(gson.toJson(callListEntries));
}
}
return UnDefType.UNDEF;
}

View File

@ -36,22 +36,23 @@ import org.openhab.binding.tr064.internal.dto.scpd.service.SCPDScpdType;
*/
@NonNullByDefault
public class SCPDUtil {
private SCPDRootType scpdRoot;
private final SCPDRootType scpdRoot;
private final List<SCPDDeviceType> scpdDevicesList = new ArrayList<>();
private final Map<String, SCPDScpdType> serviceMap = new HashMap<>();
public SCPDUtil(HttpClient httpClient, String endpoint) throws SCPDException {
SCPDRootType scpdRoot = Util.getAndUnmarshalXML(httpClient, endpoint + "/tr64desc.xml", SCPDRootType.class);
public SCPDUtil(HttpClient httpClient, String endpoint, int timeout) throws SCPDException {
SCPDRootType scpdRoot = Util.getAndUnmarshalXML(httpClient, endpoint + "/tr64desc.xml", SCPDRootType.class,
timeout);
if (scpdRoot == null) {
throw new SCPDException("could not get SCPD root");
}
this.scpdRoot = scpdRoot;
scpdDevicesList.addAll(flatDeviceList(scpdRoot.getDevice()).collect(Collectors.toList()));
scpdDevicesList.addAll(flatDeviceList(scpdRoot.getDevice()).toList());
for (SCPDDeviceType device : scpdDevicesList) {
for (SCPDServiceType service : device.getServiceList()) {
SCPDScpdType scpd = serviceMap.computeIfAbsent(service.getServiceId(), serviceId -> Util
.getAndUnmarshalXML(httpClient, endpoint + service.getSCPDURL(), SCPDScpdType.class));
.getAndUnmarshalXML(httpClient, endpoint + service.getSCPDURL(), SCPDScpdType.class, timeout));
if (scpd == null) {
throw new SCPDException("could not get SCPD service");
}
@ -80,7 +81,7 @@ public class SCPDUtil {
}
/**
* get a single device by it's UDN
* get a single device by its UDN
*
* @param udn the device UDN
* @return the device
@ -94,7 +95,7 @@ public class SCPDUtil {
}
/**
* get a single service by it's serviceId
* get a single service by its serviceId
*
* @param serviceId the service id
* @return the service

View File

@ -18,11 +18,10 @@ import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.time.Duration;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutionException;
@ -64,9 +63,10 @@ import org.openhab.binding.tr064.internal.dto.scpd.service.SCPDDirection;
import org.openhab.binding.tr064.internal.dto.scpd.service.SCPDScpdType;
import org.openhab.binding.tr064.internal.dto.scpd.service.SCPDStateVariableType;
import org.openhab.core.cache.ExpiringCacheMap;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.thing.binding.ThingHandlerCallback;
import org.openhab.core.thing.binding.builder.ThingBuilder;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.util.UIDUtils;
@ -82,7 +82,6 @@ import org.w3c.dom.NodeList;
@NonNullByDefault
public class Util {
private static final Logger LOGGER = LoggerFactory.getLogger(Util.class);
private static final int HTTP_REQUEST_TIMEOUT = 5; // in s
// cache XML content for 5s
private static final ExpiringCacheMap<String, Object> XML_OBJECT_CACHE = new ExpiringCacheMap<>(
Duration.ofMillis(3000));
@ -169,8 +168,8 @@ public class Util {
* @param deviceType the (SCPD) device-type for this thing
* @param channels a (mutable) channel list for storing all channels
*/
public static void checkAvailableChannels(Thing thing, ThingBuilder thingBuilder, SCPDUtil scpdUtil,
String deviceId, String deviceType, Map<ChannelUID, Tr064ChannelConfig> channels) {
public static void checkAvailableChannels(Thing thing, ThingHandlerCallback callback, ThingBuilder thingBuilder,
SCPDUtil scpdUtil, String deviceId, String deviceType, Map<ChannelUID, Tr064ChannelConfig> channels) {
Tr064BaseThingConfiguration thingConfig = Tr064RootHandler.SUPPORTED_THING_TYPES
.contains(thing.getThingTypeUID()) ? thing.getConfiguration().as(Tr064RootConfiguration.class)
: thing.getConfiguration().as(Tr064SubConfiguration.class);
@ -179,13 +178,6 @@ public class Util {
.forEach(channelTypeDescription -> {
String channelId = channelTypeDescription.getName();
String serviceId = channelTypeDescription.getService().getServiceId();
String typeId = channelTypeDescription.getTypeId();
Map<String, String> channelProperties = new HashMap<String, String>();
if (typeId != null) {
channelProperties.put("typeId", typeId);
}
Set<String> parameters = new HashSet<>();
try {
SCPDServiceType deviceService = scpdUtil.getDevice(deviceId)
@ -199,6 +191,7 @@ public class Util {
deviceService);
// get
boolean fixedValue = false;
ActionType getAction = channelTypeDescription.getGetAction();
if (getAction != null) {
String actionName = getAction.getName();
@ -208,7 +201,9 @@ public class Util {
SCPDStateVariableType relatedStateVariable = getStateVariable(serviceRoot, scpdArgument);
parameters.addAll(
getAndCheckParameters(channelId, getAction, scpdAction, serviceRoot, thingConfig));
if (getAction.getParameter() != null && getAction.getParameter().getFixedValue() != null) {
fixedValue = true;
}
channelConfig.setGetAction(scpdAction);
channelConfig.setDataType(relatedStateVariable.getDataType());
}
@ -233,16 +228,20 @@ public class Util {
}
// everything is available, create the channel
ChannelTypeUID channelTypeUID = new ChannelTypeUID(BINDING_ID,
channelTypeDescription.getName());
if (parameters.isEmpty()) {
String channelType = Objects.requireNonNullElse(channelTypeDescription.getTypeId(), "");
ChannelTypeUID channelTypeUID = channelType.isBlank()
? new ChannelTypeUID(BINDING_ID, channelTypeDescription.getName())
: new ChannelTypeUID(channelType);
if (parameters.isEmpty() || fixedValue) {
// we have no parameters, so create a single channel
ChannelUID channelUID = new ChannelUID(thing.getUID(), channelId);
ChannelBuilder channelBuilder = ChannelBuilder
.create(channelUID, channelTypeDescription.getItem().getType())
.withType(channelTypeUID).withProperties(channelProperties);
thingBuilder.withChannel(channelBuilder.build());
channels.put(channelUID, channelConfig);
Channel channel = callback.createChannelBuilder(channelUID, channelTypeUID).build();
thingBuilder.withChannel(channel);
Tr064ChannelConfig channelConfig1 = new Tr064ChannelConfig(channelConfig);
if (fixedValue) {
channelConfig1.setParameter(parameters.iterator().next());
}
channels.put(channelUID, channelConfig1);
} else {
// create a channel for each parameter
parameters.forEach(parameter -> {
@ -252,11 +251,9 @@ public class Util {
String normalizedParameter = UIDUtils.encode(rawParameter);
ChannelUID channelUID = new ChannelUID(thing.getUID(),
channelId + "_" + normalizedParameter);
ChannelBuilder channelBuilder = ChannelBuilder
.create(channelUID, channelTypeDescription.getItem().getType())
.withType(channelTypeUID).withProperties(channelProperties)
.withLabel(channelTypeDescription.getLabel() + " " + parameter);
thingBuilder.withChannel(channelBuilder.build());
Channel channel = callback.createChannelBuilder(channelUID, channelTypeUID)
.withLabel(channelTypeDescription.getLabel() + " " + parameter).build();
thingBuilder.withChannel(channel);
Tr064ChannelConfig channelConfig1 = new Tr064ChannelConfig(channelConfig);
channelConfig1.setParameter(rawParameter);
channels.put(channelUID, channelConfig1);
@ -272,8 +269,12 @@ public class Util {
SCPDScpdType serviceRoot, Tr064BaseThingConfiguration thingConfig) throws ChannelConfigException {
ParameterType parameter = action.getParameter();
if (parameter == null) {
return Collections.emptySet();
return Set.of();
}
if (parameter.getFixedValue() != null) {
return Set.of(parameter.getFixedValue());
}
// process list of thing parameters
try {
Set<String> parameters = new HashSet<>();
@ -292,9 +293,12 @@ public class Util {
String parameterPattern = parameter.getPattern();
if (parameterPattern != null) {
parameters.removeIf(param -> {
if (!param.matches(parameterPattern)) {
LOGGER.warn("Removing {} while processing {}, does not match pattern {}, check config.", param,
channelId, parameterPattern);
if (param.isBlank()) {
LOGGER.debug("Removing empty parameter while processing '{}'.", channelId);
return true;
} else if (!param.matches(parameterPattern)) {
LOGGER.warn("Removing '{}' while processing '{}', does not match pattern '{}', check config.",
param, channelId, parameterPattern);
return true;
} else {
return false;
@ -344,16 +348,17 @@ public class Util {
*
* @param uri the uri of the XML file
* @param clazz the class describing the XML file
* @param timeout timeout in s
* @return unmarshalling result
*/
@SuppressWarnings("unchecked")
public static <T> @Nullable T getAndUnmarshalXML(HttpClient httpClient, String uri, Class<T> clazz) {
public static <T> @Nullable T getAndUnmarshalXML(HttpClient httpClient, String uri, Class<T> clazz, int timeout) {
try {
T returnValue = (T) XML_OBJECT_CACHE.putIfAbsentAndGet(uri, () -> {
try {
LOGGER.trace("Refreshing cache for '{}'", uri);
ContentResponse contentResponse = httpClient.newRequest(uri)
.timeout(HTTP_REQUEST_TIMEOUT, TimeUnit.SECONDS).method(HttpMethod.GET).send();
ContentResponse contentResponse = httpClient.newRequest(uri).timeout(timeout, TimeUnit.SECONDS)
.method(HttpMethod.GET).send();
byte[] response = contentResponse.getContent();
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("XML = {}", new String(response));

View File

@ -9,9 +9,10 @@
<label>Phone Book</label>
<description>The name of the the phone book.</description>
</parameter>
<parameter name="matchCount" type="integer" min="0" step="1">
<parameter name="matchCount" type="integer" step="1">
<label>Match Count</label>
<description>The number of digits matching the incoming value, counted from far right (default is 0 = all matching)</description>
<description>The number of digits matching the incoming value, counted from far right (default is 0 = all matching).
Negative numbers skip digits from the left.</description>
<default>0</default>
</parameter>
<parameter name="phoneNumberIndex" type="integer" min="0" max="1" step="1">

View File

@ -15,6 +15,10 @@ thing-type.tr064.subdeviceLan.description = A virtual Sub-Device (LAN).
# thing types config
thing-type.config.tr064.fritzbox.backupDirectory.label = Backup Directory
thing-type.config.tr064.fritzbox.backupDirectory.description = The directory where configuration backups are stored (default to userdata directory).
thing-type.config.tr064.fritzbox.backupPassword.label = Backup Password
thing-type.config.tr064.fritzbox.backupPassword.description = The password used to encrypt the backup data.
thing-type.config.tr064.fritzbox.callDeflectionIndices.label = Call Deflection
thing-type.config.tr064.fritzbox.callDeflectionIndices.description = List of call deflection IDs (starting with 0).
thing-type.config.tr064.fritzbox.callListDays.label = Call List Days
@ -35,6 +39,8 @@ thing-type.config.tr064.fritzbox.rejectedCallDays.label = Rejected Call Days
thing-type.config.tr064.fritzbox.rejectedCallDays.description = List of days for which rejected calls should be calculated.
thing-type.config.tr064.fritzbox.tamIndices.label = TAM
thing-type.config.tr064.fritzbox.tamIndices.description = List of answering machines (starting with 0).
thing-type.config.tr064.fritzbox.timeout.label = Timeout
thing-type.config.tr064.fritzbox.timeout.description = Timeout for all requests (SOAP requests, phone book retrieval, call lists, ...).
thing-type.config.tr064.fritzbox.user.label = Username
thing-type.config.tr064.fritzbox.wanBlockIPs.label = WAN Block IPs
thing-type.config.tr064.fritzbox.wanBlockIPs.description = List of IPs that can be blocked for WAN access.
@ -42,6 +48,8 @@ thing-type.config.tr064.generic.host.label = Host
thing-type.config.tr064.generic.host.description = Host name or IP address.
thing-type.config.tr064.generic.password.label = Password
thing-type.config.tr064.generic.refresh.label = Refresh Interval
thing-type.config.tr064.generic.timeout.label = Timeout
thing-type.config.tr064.generic.timeout.description = Timeout for all requests (SOAP requests, phone book retrieval, call lists, ...).
thing-type.config.tr064.generic.user.label = Username
thing-type.config.tr064.subdevice.refresh.label = Refresh Interval
thing-type.config.tr064.subdevice.uuid.label = UUID

View File

@ -27,6 +27,12 @@
<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>
@ -54,6 +60,12 @@
<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>
@ -100,6 +112,17 @@
<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>

View File

@ -65,7 +65,7 @@
serviceId="urn:X_AVM-DE_HostFilter-com:serviceId:X_AVM-DE_HostFilter1"/>
<getAction name="GetWANAccessByIP" argument="NewDisallow">
<parameter name="NewIPv4Address" thingParameter="wanBlockIPs"
pattern="^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)(\.(?!$)|$)){4}$"/>
pattern="((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)(\.(?!([\s#]|$))|([\s#]|$))){4}(\s*#.*)*"/>
</getAction>
<setAction name="DisallowWANAccessByIP" argument="NewDisallow">
<parameter name="NewIPv4Address" thingParameter="wanBlockIPs"/>
@ -138,7 +138,8 @@
<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">
@ -146,7 +147,8 @@
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">
@ -154,12 +156,11 @@
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"/>
@ -171,7 +172,7 @@
</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"/>
@ -184,8 +185,8 @@
<!-- 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"/>
@ -197,7 +198,7 @@
</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"/>
@ -244,6 +245,24 @@
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"
@ -281,28 +300,28 @@
<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"/>

View File

@ -6,6 +6,7 @@
<xs:extension base="xs:string">
<xs:attribute type="xs:string" name="type" use="required"/>
<xs:attribute type="xs:string" name="unit" default=""/>
<xs:attribute type="xs:decimal" name="factor"/>
<xs:attribute type="xs:string" name="statePattern"/>
</xs:extension>
</xs:simpleContent>
@ -22,7 +23,8 @@
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:string" name="name" use="required"/>
<xs:attribute type="xs:string" name="thingParameter" use="required"/>
<xs:attribute type="xs:string" name="thingParameter" />
<xs:attribute type="xs:string" name="fixedValue" />
<xs:attribute type="xs:string" name="pattern"/>
<xs:attribute type="xs:boolean" name="internalOnly" default="false"/>
</xs:extension>

View File

@ -53,7 +53,6 @@ import org.openhab.core.util.UIDUtils;
@MockitoSettings(strictness = Strictness.LENIENT)
@NonNullByDefault
class PhonebookProfileTest {
private static final String INTERNAL_PHONE_NUMBER = "999";
private static final String OTHER_PHONE_NUMBER = "555-456";
private static final String JOHN_DOES_PHONE_NUMBER = "12345";
@ -61,6 +60,7 @@ class PhonebookProfileTest {
private static final ThingUID THING_UID = new ThingUID(BINDING_ID, THING_TYPE_FRITZBOX.getId(), "test");
private static final String MY_PHONEBOOK = UIDUtils.encode(THING_UID.getAsString()) + ":MyPhonebook";
@NonNullByDefault
public static class ParameterSet {
public final State state;
public final State resultingState;
@ -108,12 +108,10 @@ class PhonebookProfileTest {
private final Phonebook phonebook = new Phonebook() {
@Override
public Optional<String> lookupNumber(String number, int matchCount) {
switch (number) {
case JOHN_DOES_PHONE_NUMBER:
return Optional.of(JOHN_DOES_NAME);
default:
return Optional.empty();
}
return switch (number) {
case JOHN_DOES_PHONE_NUMBER -> Optional.of(JOHN_DOES_NAME);
default -> Optional.empty();
};
}
@Override

View File

@ -0,0 +1,90 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.tr064.internal.phonebook;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
/**
* The {@link Tr064PhonebookImplTest} class implements test cases for the {@link Tr064PhonebookImpl} class
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
@MockitoSettings(strictness = Strictness.WARN)
@ExtendWith(MockitoExtension.class)
public class Tr064PhonebookImplTest {
@Mock
private @NonNullByDefault({}) HttpClient httpClient;
// key -> input, value -> output
public static Collection<Map.Entry<String, String>> phoneNumbers() {
return List.of( //
Map.entry("**820", "**820"), //
Map.entry("49200123456", "49200123456"), //
Map.entry("+49-200-123456", "+49200123456"), //
Map.entry("49 (200) 123456", "49200123456"), //
Map.entry("+49 200/123456", "+49200123456"));
}
@ParameterizedTest
@MethodSource("phoneNumbers")
public void testNormalization(Map.Entry<String, String> input) {
when(httpClient.newRequest((String) any())).thenThrow(new IllegalArgumentException("testing"));
Tr064PhonebookImpl testPhonebook = new Tr064PhonebookImpl(httpClient, "", 0);
assertEquals(input.getValue(), testPhonebook.normalizeNumber(input.getKey()));
}
@Test
public void testLookup() {
when(httpClient.newRequest((String) any())).thenThrow(new IllegalArgumentException("testing"));
TestPhonebook testPhonebook = new TestPhonebook(httpClient, "", 0);
testPhonebook.setPhonebook(Map.of("+491238007001", "foo", "+4933998005671", "bar"));
Optional<String> result = testPhonebook.lookupNumber("01238007001", 0);
assertEquals(Optional.empty(), result);
result = testPhonebook.lookupNumber("01238007001", 10);
assertEquals("foo", result.get());
result = testPhonebook.lookupNumber("033998005671", -1);
assertEquals("bar", result.get());
}
private static class TestPhonebook extends Tr064PhonebookImpl {
public TestPhonebook(HttpClient httpClient, String phonebookUrl, int httpTimeout) {
super(httpClient, phonebookUrl, httpTimeout);
}
public void setPhonebook(Map<String, String> phonebook) {
this.phonebook = phonebook;
}
}
}