[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.touchwand/ @roieg
/bundles/org.openhab.binding.tplinkrouter/ @olivierkeke /bundles/org.openhab.binding.tplinkrouter/ @olivierkeke
/bundles/org.openhab.binding.tplinksmarthome/ @Hilbrand /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.tradfri/ @cweitkamp @kaikreuzer
/bundles/org.openhab.binding.twitter/ @computergeek1507 /bundles/org.openhab.binding.twitter/ @computergeek1507
/bundles/org.openhab.binding.unifi/ @mgbowman @Hilbrand /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. The second credential parameter is `password`, which is mandatory.
For security reasons it is highly recommended to set both, username and password. 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` ### `fritzbox`
The `fritzbox` devices can give additional informations in dedicated channels, controlled The `fritzbox` devices can give additional informations in dedicated channels, controlled
@ -76,6 +80,13 @@ 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`). 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. 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` ### `subdevice`, `subdeviceLan`
Additional informations (i.e. channels) are available in subdevices of the bridge. Additional informations (i.e. channels) are available in subdevices of the bridge.
@ -118,17 +129,16 @@ The call-types are the same as provided by the FritzBox, i.e. `1` (inbound), `2`
### LAN `subdeviceLan` channels ### LAN `subdeviceLan` channels
| channel | item-type | advanced | description | | channel | item-type | advanced | description |
|----------------------------|---------------------------|:--------:|----------------------------------------------------------------| |----------------------|---------------------------|:--------:|--------------------------------------------------------------------------------------------------------------|
| `wifi24GHzEnable` | `Switch` | | Enable/Disable the 2.4 GHz WiFi device. | | `wifi24GHzEnable` | `Switch` | | Enable/Disable the 2.4 GHz WiFi device. |
| `wifi5GHzEnable` | `Switch` | | Enable/Disable the 5.0 GHz WiFi device. | | `wifi5GHzEnable` | `Switch` | | Enable/Disable the 5.0 GHz WiFi device. |
| `wifiGuestEnable` | `Switch` | | Enable/Disable the guest WiFi. | | `wifiGuestEnable` | `Switch` | | Enable/Disable the guest WiFi. |
| `macOnline` | `Switch` | x | Online status of the device with the given MAC | | `macOnline` | `Switch` | x | Online status of the device with the given MAC |
| `macIP` | `String` | x | IP 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 | | `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 | | `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 | | `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 | | `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. Older FritzBox devices may not support 5 GHz WiFi.
In this case you have to use the `wifi5GHzEnable` channel for switching the guest WiFi. In this case you have to use the `wifi5GHzEnable` channel for switching the guest WiFi.
@ -155,7 +165,7 @@ In this case you have to use the `wifi5GHzEnable` channel for switching the gues
| `dslEnable` | `Switch` | | DSL Enable | | `dslEnable` | `Switch` | | DSL Enable |
| `dslFECErrors` | `Number:Dimensionless` | x | DSL FEC Errors | | `dslFECErrors` | `Number:Dimensionless` | x | DSL FEC Errors |
| `dslHECErrors` | `Number:Dimensionless` | x | DSL HEC Errors | | `dslHECErrors` | `Number:Dimensionless` | x | DSL HEC Errors |
| `dslStatus` | `Switch` | | DSL Status | | `dslStatus` | `String` | | DSL Status |
| `dslUpstreamMaxRate` | `Number:DataTransferRate` | x | DSL Max Upstream Rate | | `dslUpstreamMaxRate` | `Number:DataTransferRate` | x | DSL Max Upstream Rate |
| `dslUpstreamCurrRate` | `Number:DataTransferRate` | x | DSL Curr. Upstream Rate | | `dslUpstreamCurrRate` | `Number:DataTransferRate` | x | DSL Curr. Upstream Rate |
| `dslUpstreamNoiseMargin` | `Number:Dimensionless` | x | DSL Upstream Noise Margin | | `dslUpstreamNoiseMargin` | `Number:Dimensionless` | x | DSL Upstream Noise Margin |
@ -163,11 +173,13 @@ In this case you have to use the `wifi5GHzEnable` channel for switching the gues
| `wanAccessType` | `String` | x | Access Type | | `wanAccessType` | `String` | x | Access Type |
| `wanMaxDownstreamRate` | `Number:DataTransferRate` | x | Max. Downstream Rate | | `wanMaxDownstreamRate` | `Number:DataTransferRate` | x | Max. Downstream Rate |
| `wanMaxUpstreamRate` | `Number:DataTransferRate` | x | Max. Upstream 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 | | `wanPhysicalLinkStatus` | `String` | x | Link Status |
| `wanTotalBytesReceived` | `Number:DataAmount` | x | Total Bytes Received | | `wanTotalBytesReceived` | `Number:DataAmount` | x | Total Bytes Received |
| `wanTotalBytesSent` | `Number:DataAmount` | x | Total Bytes Sent | | `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 ## `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. 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. 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. 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". 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. 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. 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 ## Rule Action
### Phonebook lookup
The phonebooks of a `fritzbox` thing can be used to lookup a number from rules via a thing action: 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)` `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. `phonebook` and `matchCount` are optional parameters.
You can omit one or both of these 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. 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" and is used as default if no other value is given. 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. 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. The return value is either the phonebook entry (if found) or the input number.
Example (use all phonebooks, match 5 digits from right): Example (use all phonebooks, match 5 digits from right):
``` ```java
val tr064Actions = getActions("tr064","tr064:fritzbox:2a28aee1ee") val tr064Actions = getActions("tr064","tr064:fritzbox:2a28aee1ee")
val result = tr064Actions.phonebookLookup("49157712341234", 5) 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 ## 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 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 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"} 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`): 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 * 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.Collection;
import java.util.Map;
import java.util.Objects;
import java.util.Optional; 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.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; 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.ActionInput;
import org.openhab.core.automation.annotation.ActionOutput; import org.openhab.core.automation.annotation.ActionOutput;
import org.openhab.core.automation.annotation.RuleAction; import org.openhab.core.automation.annotation.RuleAction;
@ -28,14 +45,16 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; 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 * @author Jan N. Klug - Initial contribution
*/ */
@ThingActionsScope(name = "tr064") @ThingActionsScope(name = "tr064")
@NonNullByDefault @NonNullByDefault
public class PhonebookActions implements ThingActions { public class FritzboxActions implements ThingActions {
private final Logger logger = LoggerFactory.getLogger(PhonebookActions.class); 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; private @Nullable Tr064RootHandler handler;
@ -76,16 +95,67 @@ public class PhonebookActions implements ThingActions {
} else { } else {
int matchCountInt = matchCount == null ? 0 : matchCount; int matchCountInt = matchCount == null ? 0 : matchCount;
if (phonebook != null && !phonebook.isEmpty()) { if (phonebook != null && !phonebook.isEmpty()) {
return handler.getPhonebookByName(phonebook).flatMap(p -> p.lookupNumber(phonenumber, matchCountInt)) return Objects.requireNonNull(handler.getPhonebookByName(phonebook)
.orElse(phonenumber); .flatMap(p -> p.lookupNumber(phonenumber, matchCountInt)).orElse(phonenumber));
} else { } else {
Collection<Phonebook> phonebooks = handler.getPhonebooks(); Collection<Phonebook> phonebooks = handler.getPhonebooks();
return phonebooks.stream().map(p -> p.lookupNumber(phonenumber, matchCountInt)) return Objects.requireNonNull(phonebooks.stream().map(p -> p.lookupNumber(phonenumber, matchCountInt))
.filter(Optional::isPresent).map(Optional::get).findAny().orElse(phonenumber); .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, public static String phonebookLookup(ThingActions actions, @Nullable String phonenumber,
@Nullable Integer matchCount) { @Nullable Integer matchCount) {
return phonebookLookup(actions, phonenumber, null, 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, public static String phonebookLookup(ThingActions actions, @Nullable String phonenumber, @Nullable String phonebook,
@Nullable Integer matchCount) { @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 @Override
public void setThingHandler(@Nullable ThingHandler handler) { public void setThingHandler(@Nullable ThingHandler handler) {
if (handler instanceof Tr064RootHandler) {
this.handler = (Tr064RootHandler) handler; this.handler = (Tr064RootHandler) handler;
} }
}
@Override @Override
public @Nullable ThingHandler getThingHandler() { public @Nullable ThingHandler getThingHandler() {
return handler; return handler;
} }
public record BackupConfiguration(String directory, String password) {
}
} }

View File

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

View File

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

View File

@ -23,10 +23,13 @@ import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; 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.HttpClient;
import org.eclipse.jetty.client.api.Authentication; import org.eclipse.jetty.client.api.Authentication;
import org.eclipse.jetty.client.api.AuthenticationStore; import org.eclipse.jetty.client.api.AuthenticationStore;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.util.DigestAuthentication; import org.eclipse.jetty.client.util.DigestAuthentication;
import org.openhab.binding.tr064.internal.config.Tr064ChannelConfig; import org.openhab.binding.tr064.internal.config.Tr064ChannelConfig;
import org.openhab.binding.tr064.internal.config.Tr064RootConfiguration; 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.root.SCPDServiceType;
import org.openhab.binding.tr064.internal.dto.scpd.service.SCPDActionType; 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.Phonebook;
import org.openhab.binding.tr064.internal.phonebook.PhonebookActions;
import org.openhab.binding.tr064.internal.phonebook.PhonebookProvider; import org.openhab.binding.tr064.internal.phonebook.PhonebookProvider;
import org.openhab.binding.tr064.internal.phonebook.Tr064PhonebookImpl; import org.openhab.binding.tr064.internal.phonebook.Tr064PhonebookImpl;
import org.openhab.binding.tr064.internal.soap.SOAPConnector; 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.ThingTypeUID;
import org.openhab.core.thing.ThingUID; import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseBridgeHandler; 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.ThingHandlerService;
import org.openhab.core.thing.binding.builder.ThingBuilder; import org.openhab.core.thing.binding.builder.ThingBuilder;
import org.openhab.core.types.Command; 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 Logger logger = LoggerFactory.getLogger(Tr064RootHandler.class);
private final HttpClient httpClient; private final HttpClient httpClient;
private Tr064RootConfiguration config = new Tr064RootConfiguration();
private String deviceType = "";
private @Nullable SCPDUtil scpdUtil; private @Nullable SCPDUtil scpdUtil;
private SOAPConnector soapConnector; private SOAPConnector soapConnector;
// these are set when the config is available
private Tr064RootConfiguration config = new Tr064RootConfiguration();
private String endpointBaseURL = ""; private String endpointBaseURL = "";
private int timeout = Tr064RootConfiguration.DEFAULT_HTTP_TIMEOUT;
private String deviceType = "";
private final Map<ChannelUID, Tr064ChannelConfig> channels = new HashMap<>(); private final Map<ChannelUID, Tr064ChannelConfig> channels = new HashMap<>();
// caching is used to prevent excessive calls to the same action // 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) { Tr064RootHandler(Bridge bridge, HttpClient httpClient) {
super(bridge); super(bridge);
this.httpClient = httpClient; this.httpClient = httpClient;
this.soapConnector = new SOAPConnector(httpClient, endpointBaseURL); this.soapConnector = new SOAPConnector(httpClient, endpointBaseURL, timeout);
} }
@Override @Override
@ -147,7 +154,8 @@ public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProv
} }
endpointBaseURL = "http://" + config.host + ":49000"; endpointBaseURL = "http://" + config.host + ":49000";
soapConnector = new SOAPConnector(httpClient, endpointBaseURL); soapConnector = new SOAPConnector(httpClient, endpointBaseURL, timeout);
timeout = config.timeout;
updateStatus(ThingStatus.UNKNOWN); updateStatus(ThingStatus.UNKNOWN);
connectFuture = scheduler.scheduleWithFixedDelay(this::internalInitialize, 0, RETRY_INTERVAL, TimeUnit.SECONDS); connectFuture = scheduler.scheduleWithFixedDelay(this::internalInitialize, 0, RETRY_INTERVAL, TimeUnit.SECONDS);
@ -158,7 +166,7 @@ public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProv
*/ */
private void internalInitialize() { private void internalInitialize() {
try { try {
scpdUtil = new SCPDUtil(httpClient, endpointBaseURL); scpdUtil = new SCPDUtil(httpClient, endpointBaseURL, timeout);
} catch (SCPDException e) { } catch (SCPDException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"could not get device definitions from " + config.host); "could not get device definitions from " + config.host);
@ -172,8 +180,9 @@ public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProv
ThingBuilder thingBuilder = editThing(); ThingBuilder thingBuilder = editThing();
thingBuilder.withoutChannels(thing.getChannels()); thingBuilder.withoutChannels(thing.getChannels());
final SCPDUtil scpdUtil = this.scpdUtil; final SCPDUtil scpdUtil = this.scpdUtil;
if (scpdUtil != null) { final ThingHandlerCallback callback = getCallback();
Util.checkAvailableChannels(thing, thingBuilder, scpdUtil, "", deviceType, channels); if (scpdUtil != null && callback != null) {
Util.checkAvailableChannels(thing, callback, thingBuilder, scpdUtil, "", deviceType, channels);
updateThing(thingBuilder.build()); updateThing(thingBuilder.build());
} }
@ -197,6 +206,7 @@ public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProv
removeConnectScheduler(); removeConnectScheduler();
uninstallPolling(); uninstallPolling();
stateCache.clear(); stateCache.clear();
scpdUtil = null;
super.dispose(); super.dispose();
} }
@ -205,6 +215,7 @@ public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProv
* poll remote device for channel values * poll remote device for channel values
*/ */
private void poll() { private void poll() {
try {
channels.forEach((channelUID, channelConfig) -> { channels.forEach((channelUID, channelConfig) -> {
if (isLinked(channelUID)) { if (isLinked(channelUID)) {
State state = stateCache.putIfAbsentAndGet(channelUID, State state = stateCache.putIfAbsentAndGet(channelUID,
@ -214,10 +225,15 @@ public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProv
} }
} }
}); });
} 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 * @return true if successful
*/ */
@ -238,11 +254,11 @@ public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProv
SOAPMessage soapResponse = soapConnector SOAPMessage soapResponse = soapConnector
.doSOAPRequest(new SOAPRequest(deviceService, "GetSecurityPort")); .doSOAPRequest(new SOAPRequest(deviceService, "GetSecurityPort"));
if (!soapResponse.getSOAPBody().hasFault()) { if (!soapResponse.getSOAPBody().hasFault()) {
SOAPValueConverter soapValueConverter = new SOAPValueConverter(httpClient); SOAPValueConverter soapValueConverter = new SOAPValueConverter(httpClient, timeout);
soapValueConverter.getStateFromSOAPValue(soapResponse, "NewSecurityPort", null) soapValueConverter.getStateFromSOAPValue(soapResponse, "NewSecurityPort", null)
.ifPresentOrElse(port -> { .ifPresentOrElse(port -> {
endpointBaseURL = "https://" + config.host + ":" + port.toString(); endpointBaseURL = "https://" + config.host + ":" + port;
soapConnector = new SOAPConnector(httpClient, endpointBaseURL); soapConnector = new SOAPConnector(httpClient, endpointBaseURL, timeout);
logger.debug("endpointBaseURL is now '{}'", endpointBaseURL); logger.debug("endpointBaseURL is now '{}'", endpointBaseURL);
}, () -> logger.warn("Could not determine secure port, disabling https")); }, () -> logger.warn("Could not determine secure port, disabling https"));
} else { } else {
@ -250,9 +266,10 @@ public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProv
} }
// clear auth cache and force re-auth // clear auth cache and force re-auth
httpClient.getAuthenticationStore().clearAuthenticationResults(); AuthenticationStore authStore = httpClient.getAuthenticationStore();
AuthenticationStore auth = httpClient.getAuthenticationStore(); authStore.clearAuthentications();
auth.addAuthentication(new DigestAuthentication(new URI(endpointBaseURL), Authentication.ANY_REALM, authStore.clearAuthenticationResults();
authStore.addAuthentication(new DigestAuthentication(new URI(endpointBaseURL), Authentication.ANY_REALM,
config.user, config.password)); config.user, config.password));
// check & update properties // check & update properties
@ -263,7 +280,7 @@ public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProv
.orElseThrow(() -> new SCPDException("Action 'GetInfo' not found")); .orElseThrow(() -> new SCPDException("Action 'GetInfo' not found"));
SOAPMessage soapResponse1 = soapConnector SOAPMessage soapResponse1 = soapConnector
.doSOAPRequest(new SOAPRequest(deviceService, getInfoAction.getName())); .doSOAPRequest(new SOAPRequest(deviceService, getInfoAction.getName()));
SOAPValueConverter soapValueConverter = new SOAPValueConverter(httpClient); SOAPValueConverter soapValueConverter = new SOAPValueConverter(httpClient, timeout);
Map<String, String> properties = editProperties(); Map<String, String> properties = editProperties();
PROPERTY_ARGUMENTS.forEach(argumentName -> getInfoAction.getArgumentList().stream() PROPERTY_ARGUMENTS.forEach(argumentName -> getInfoAction.getArgumentList().stream()
.filter(argument -> argument.getName().equals(argumentName)).findFirst() .filter(argument -> argument.getName().equals(argumentName)).findFirst()
@ -301,6 +318,22 @@ public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProv
return soapConnector; 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 * get the SCPD processing utility
* *
@ -341,21 +374,21 @@ public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProv
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private Collection<Phonebook> processPhonebookList(SOAPMessage soapMessagePhonebookList, private Collection<Phonebook> processPhonebookList(SOAPMessage soapMessagePhonebookList,
SCPDServiceType scpdService) { SCPDServiceType scpdService) {
SOAPValueConverter soapValueConverter = new SOAPValueConverter(httpClient); SOAPValueConverter soapValueConverter = new SOAPValueConverter(httpClient, timeout);
return (Collection<Phonebook>) soapValueConverter Optional<Stream<String>> phonebookStream = soapValueConverter
.getStateFromSOAPValue(soapMessagePhonebookList, "NewPhonebookList", null) .getStateFromSOAPValue(soapMessagePhonebookList, "NewPhonebookList", null)
.map(phonebookList -> Arrays.stream(phonebookList.toString().split(","))).orElse(Stream.empty()) .map(phonebookList -> Arrays.stream(phonebookList.toString().split(",")));
.map(index -> { return phonebookStream.map(stringStream -> (Collection<Phonebook>) stringStream.map(index -> {
try { try {
SOAPMessage soapMessageURL = soapConnector.doSOAPRequest( SOAPMessage soapMessageURL = soapConnector
new SOAPRequest(scpdService, "GetPhonebook", Map.of("NewPhonebookID", index))); .doSOAPRequest(new SOAPRequest(scpdService, "GetPhonebook", Map.of("NewPhonebookID", index)));
return soapValueConverter.getStateFromSOAPValue(soapMessageURL, "NewPhonebookURL", null) return soapValueConverter.getStateFromSOAPValue(soapMessageURL, "NewPhonebookURL", null)
.map(url -> (Phonebook) new Tr064PhonebookImpl(httpClient, url.toString())); .map(url -> (Phonebook) new Tr064PhonebookImpl(httpClient, url.toString(), timeout));
} catch (Tr064CommunicationException e) { } catch (Tr064CommunicationException e) {
logger.warn("Failed to get phonebook with index {}:", index, e); logger.warn("Failed to get phonebook with index {}:", index, e);
} }
return Optional.empty(); return Optional.empty();
}).filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList()); }).filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList())).orElseGet(Set::of);
} }
private void retrievePhonebooks() { private void retrievePhonebooks() {
@ -368,14 +401,14 @@ public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProv
Optional<SCPDServiceType> scpdService = scpdUtil.getDevice("").flatMap(deviceType -> deviceType.getServiceList() Optional<SCPDServiceType> scpdService = scpdUtil.getDevice("").flatMap(deviceType -> deviceType.getServiceList()
.stream().filter(service -> service.getServiceId().equals(serviceId)).findFirst()); .stream().filter(service -> service.getServiceId().equals(serviceId)).findFirst());
phonebooks = scpdService.map(service -> { phonebooks = Objects.requireNonNull(scpdService.map(service -> {
try { try {
return processPhonebookList(soapConnector.doSOAPRequest(new SOAPRequest(service, "GetPhonebookList")), return processPhonebookList(soapConnector.doSOAPRequest(new SOAPRequest(service, "GetPhonebookList")),
service); service);
} catch (Tr064CommunicationException e) { } catch (Tr064CommunicationException e) {
return Collections.<Phonebook> emptyList(); return Collections.<Phonebook> emptyList();
} }
}).orElse(List.of()); }).orElse(List.of()));
if (phonebooks.isEmpty()) { if (phonebooks.isEmpty()) {
logger.warn("Could not get phonebooks for thing {}", thing.getUID()); logger.warn("Could not get phonebooks for thing {}", thing.getUID());
@ -405,6 +438,20 @@ public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProv
@Override @Override
public Collection<Class<? extends ThingHandlerService>> getServices() { 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.ThingStatusInfo;
import org.openhab.core.thing.ThingTypeUID; import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandler; 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.thing.binding.builder.ThingBuilder;
import org.openhab.core.types.Command; import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType; import org.openhab.core.types.RefreshType;
@ -77,7 +78,6 @@ public class Tr064SubHandler extends BaseThingHandler {
} }
@Override @Override
@SuppressWarnings("null")
public void handleCommand(ChannelUID channelUID, Command command) { public void handleCommand(ChannelUID channelUID, Command command) {
Tr064ChannelConfig channelConfig = channels.get(channelUID); Tr064ChannelConfig channelConfig = channels.get(channelUID);
if (channelConfig == null) { if (channelConfig == null) {
@ -86,6 +86,7 @@ public class Tr064SubHandler extends BaseThingHandler {
} }
if (command instanceof RefreshType) { if (command instanceof RefreshType) {
final SOAPConnector soapConnector = this.soapConnector;
State state = stateCache.putIfAbsentAndGet(channelUID, () -> soapConnector == null ? UnDefType.UNDEF State state = stateCache.putIfAbsentAndGet(channelUID, () -> soapConnector == null ? UnDefType.UNDEF
: soapConnector.getChannelStateFromDevice(channelConfig, channels, stateCache)); : soapConnector.getChannelStateFromDevice(channelConfig, channels, stateCache));
if (state != null) { if (state != null) {
@ -99,6 +100,7 @@ public class Tr064SubHandler extends BaseThingHandler {
return; return;
} }
scheduler.execute(() -> { scheduler.execute(() -> {
final SOAPConnector soapConnector = this.soapConnector;
if (soapConnector == null) { if (soapConnector == null) {
logger.warn("Could not send command because connector not available"); logger.warn("Could not send command because connector not available");
} else { } else {
@ -141,12 +143,16 @@ public class Tr064SubHandler extends BaseThingHandler {
"Could not get device definitions"); "Could not get device definitions");
return; return;
} }
final ThingHandlerCallback callback = getCallback();
if (callback == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Could not get callback");
return;
}
if (checkProperties(scpdUtil)) { if (checkProperties(scpdUtil)) {
// properties set, check channels // properties set, check channels
ThingBuilder thingBuilder = editThing(); ThingBuilder thingBuilder = editThing();
thingBuilder.withoutChannels(thing.getChannels()); 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()); updateThing(thingBuilder.build());
// remove connect scheduler // remove connect scheduler

View File

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

View File

@ -15,6 +15,8 @@ package org.openhab.binding.tr064.internal.config;
import java.util.List; import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault; 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. * The {@link Tr064RootConfiguration} class contains fields mapping thing configuration parameters.
@ -23,9 +25,12 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
*/ */
@NonNullByDefault @NonNullByDefault
public class Tr064RootConfiguration extends Tr064BaseThingConfiguration { public class Tr064RootConfiguration extends Tr064BaseThingConfiguration {
public static final int DEFAULT_HTTP_TIMEOUT = 5; // in s
public String host = ""; public String host = "";
public String user = "dslf-config"; public String user = "dslf-config";
public String password = ""; public String password = "";
public int timeout = DEFAULT_HTTP_TIMEOUT;
/* following parameters only available in fritzbox thing */ /* following parameters only available in fritzbox thing */
public List<String> tamIndices = List.of(); public List<String> tamIndices = List.of();
@ -38,6 +43,10 @@ public class Tr064RootConfiguration extends Tr064BaseThingConfiguration {
public List<String> wanBlockIPs = List.of(); public List<String> wanBlockIPs = List.of();
public int phonebookInterval = 600; public int phonebookInterval = 600;
// Backup data
public String backupDirectory = OpenHAB.getUserDataFolder();
public @Nullable String backupPassword;
public boolean isValid() { public boolean isValid() {
return !host.isEmpty() && !user.isEmpty() && !password.isEmpty(); 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.math.BigDecimal;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
@ -139,15 +140,13 @@ public class PhonebookProfile implements StateProfile {
} }
if (state instanceof StringType) { if (state instanceof StringType) {
Optional<String> match = resolveNumber(state.toString()); Optional<String> match = resolveNumber(state.toString());
State newState = match.map(name -> (State) new StringType(name)).orElse(state); State newState = Objects.requireNonNull(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.equals(state)) {
if (newState == state) {
logger.debug("Number '{}' not found in phonebook '{}' from provider '{}'", state, phonebookName, logger.debug("Number '{}' not found in phonebook '{}' from provider '{}'", state, phonebookName,
thingUID); thingUID);
} }
callback.sendUpdate(newState); callback.sendUpdate(newState);
} else if (state instanceof StringListType) { } else if (state instanceof StringListType stringList) {
StringListType stringList = (StringListType) state;
try { try {
String phoneNumber = stringList.getValue(phoneNumberIndex); String phoneNumber = stringList.getValue(phoneNumberIndex);
Optional<String> match = resolveNumber(phoneNumber); Optional<String> match = resolveNumber(phoneNumber);

View File

@ -33,32 +33,40 @@ import org.slf4j.LoggerFactory;
public class Tr064PhonebookImpl implements Phonebook { public class Tr064PhonebookImpl implements Phonebook {
private final Logger logger = LoggerFactory.getLogger(Tr064PhonebookImpl.class); 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 HttpClient httpClient;
private final String phonebookUrl; private final String phonebookUrl;
private final int httpTimeout;
private String phonebookName = ""; private String phonebookName = "";
public Tr064PhonebookImpl(HttpClient httpClient, String phonebookUrl) { public Tr064PhonebookImpl(HttpClient httpClient, String phonebookUrl, int httpTimeout) {
this.httpClient = httpClient; this.httpClient = httpClient;
this.phonebookUrl = phonebookUrl; this.phonebookUrl = phonebookUrl;
this.httpTimeout = httpTimeout;
getPhonebook(); getPhonebook();
} }
private void getPhonebook() { private void getPhonebook() {
PhonebooksType phonebooksType = Util.getAndUnmarshalXML(httpClient, phonebookUrl, PhonebooksType.class); PhonebooksType phonebooksType = Util.getAndUnmarshalXML(httpClient, phonebookUrl, PhonebooksType.class,
if (phonebooksType != null) { httpTimeout);
if (phonebooksType == null) {
logger.warn("Failed to get phonebook with URL '{}'", phonebookUrl);
return;
}
phonebookName = phonebooksType.getPhonebook().getName(); phonebookName = phonebooksType.getPhonebook().getName();
phonebook = phonebooksType.getPhonebook().getContact().stream().map(contact -> { phonebook = phonebooksType.getPhonebook().getContact().stream().map(contact -> {
String contactName = contact.getPerson().getRealName(); String contactName = contact.getPerson().getRealName();
return contact.getTelephony().getNumber().stream() if (contactName == null || contactName.isBlank()) {
.collect(Collectors.toMap(number -> normalizeNumber(number.getValue()), number -> contactName, return new HashMap<String, String>();
this::mergeSameContactNames)); }
return contact.getTelephony().getNumber().stream().collect(Collectors.toMap(
number -> normalizeNumber(number.getValue()), number -> contactName, this::mergeSameContactNames));
}).collect(HashMap::new, HashMap::putAll, HashMap::putAll); }).collect(HashMap::new, HashMap::putAll, HashMap::putAll);
logger.debug("Downloaded phonebook {}: {}", phonebookName, phonebook); logger.debug("Downloaded phonebook {}: {}", phonebookName, phonebook);
} }
}
// in case there are multiple phone entries with same number -> name mapping, i.e. in phonebooks exported from // in case there are multiple phone entries with same number -> name mapping, i.e. in phonebooks exported from
// mobiles containing multiple accounts like: local, cloudprovider1, messenger1, messenger2,... // mobiles containing multiple accounts like: local, cloudprovider1, messenger1, messenger2,...
@ -78,9 +86,14 @@ public class Tr064PhonebookImpl implements Phonebook {
@Override @Override
public Optional<String> lookupNumber(String number, int matchCount) { public Optional<String> lookupNumber(String number, int matchCount) {
String normalized = normalizeNumber(number); String normalized = normalizeNumber(number);
String matchString = matchCount > 0 && matchCount < normalized.length() String matchString;
? normalized.substring(normalized.length() - matchCount) if (matchCount > 0 && matchCount < normalized.length()) {
: normalized; 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); logger.trace("Normalized '{}' to '{}', matchString is '{}'", number, normalized, matchString);
return matchString.isBlank() ? Optional.empty() return matchString.isBlank() ? Optional.empty()
: phonebook.keySet().stream().filter(n -> n.endsWith(matchString)).findFirst().map(phonebook::get); : 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 + '}'; 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\\*\\+]", ""); return number.replaceAll("[^0-9\\*\\+]", "");
} }
} }

View File

@ -12,10 +12,8 @@
*/ */
package org.openhab.binding.tr064.internal.soap; package org.openhab.binding.tr064.internal.soap;
import java.time.LocalDateTime; import java.text.ParseException;
import java.time.ZoneId; import java.text.SimpleDateFormat;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Date; import java.util.Date;
import org.eclipse.jdt.annotation.NonNullByDefault; 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; 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 * lists
* *
* @author Jan N. Klug - Initial contribution * @author Jan N. Klug - Initial contribution
*/ */
@NonNullByDefault @NonNullByDefault
public class CallListEntry { 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 localNumber;
public @Nullable String remoteNumber; public @Nullable String remoteNumber;
public @Nullable Date date; public @Nullable Date date;
@ -39,9 +37,10 @@ public class CallListEntry {
public CallListEntry(Call call) { public CallListEntry(Call call) {
try { try {
date = Date.from( synchronized (DATE_FORMAT_PARSER) {
LocalDateTime.parse(call.getDate(), DATE_FORMAT_PARSER).atZone(ZoneId.systemDefault()).toInstant()); date = DATE_FORMAT_PARSER.parse(call.getDate());
} catch (DateTimeParseException e) { }
} catch (ParseException e) {
// ignore parsing error // ignore parsing error
date = null; date = null;
} }

View File

@ -15,11 +15,10 @@ package org.openhab.binding.tr064.internal.soap;
import org.eclipse.jdt.annotation.NonNullByDefault; 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 * @author Jan N. Klug - Initial contribution
*/ */
@NonNullByDefault @NonNullByDefault
public enum CallListType { public enum CallListType {
MISSED_COUNT("2"), 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 * The {@link PostProcessingException} is an Exception that is thrown in case of conversion errors during
* processing * post-processing
* *
* @author Jan N. Klug - Initial contribution * @author Jan N. Klug - Initial contribution
*/ */

View File

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

View File

@ -48,7 +48,6 @@ public class SOAPRequest {
if (o == null || getClass() != o.getClass()) { if (o == null || getClass() != o.getClass()) {
return false; return false;
} }
SOAPRequest that = (SOAPRequest) o; SOAPRequest that = (SOAPRequest) o;
if (!service.equals(that.service)) { if (!service.equals(that.service)) {
@ -57,6 +56,7 @@ public class SOAPRequest {
if (!soapAction.equals(that.soapAction)) { if (!soapAction.equals(that.soapAction)) {
return false; return false;
} }
return arguments.equals(that.arguments); 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.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
@ -57,9 +58,11 @@ import com.google.gson.GsonBuilder;
public class SOAPValueConverter { public class SOAPValueConverter {
private final Logger logger = LoggerFactory.getLogger(SOAPValueConverter.class); private final Logger logger = LoggerFactory.getLogger(SOAPValueConverter.class);
private final HttpClient httpClient; private final HttpClient httpClient;
private final int timeout;
public SOAPValueConverter(HttpClient httpClient) { public SOAPValueConverter(HttpClient httpClient, int timeout) {
this.httpClient = httpClient; this.httpClient = httpClient;
this.timeout = timeout;
} }
/** /**
@ -83,24 +86,26 @@ public class SOAPValueConverter {
return Optional.empty(); return Optional.empty();
} }
switch (dataType) { switch (dataType) {
case "ui1": case "ui1", "ui2" -> {
case "ui2":
return Optional.of(String.valueOf(value.shortValue())); return Optional.of(String.valueOf(value.shortValue()));
case "i4": }
case "ui4": case "i4", "ui4" -> {
return Optional.of(String.valueOf(value.intValue())); return Optional.of(String.valueOf(value.intValue()));
default: }
default -> {
}
} }
} else if (command instanceof DecimalType) { } else if (command instanceof DecimalType) {
BigDecimal value = ((DecimalType) command).toBigDecimal(); BigDecimal value = ((DecimalType) command).toBigDecimal();
switch (dataType) { switch (dataType) {
case "ui1": case "ui1", "ui2" -> {
case "ui2":
return Optional.of(String.valueOf(value.shortValue())); return Optional.of(String.valueOf(value.shortValue()));
case "i4": }
case "ui4": case "i4", "ui4" -> {
return Optional.of(String.valueOf(value.intValue())); return Optional.of(String.valueOf(value.intValue()));
default: }
default -> {
}
} }
} else if (command instanceof StringType) { } else if (command instanceof StringType) {
if ("string".equals(dataType)) { if ("string".equals(dataType)) {
@ -127,28 +132,35 @@ public class SOAPValueConverter {
@Nullable Tr064ChannelConfig channelConfig) { @Nullable Tr064ChannelConfig channelConfig) {
String dataType = channelConfig != null ? channelConfig.getDataType() : "string"; String dataType = channelConfig != null ? channelConfig.getDataType() : "string";
String unit = channelConfig != null ? channelConfig.getChannelTypeDescription().getItem().getUnit() : ""; String unit = channelConfig != null ? channelConfig.getChannelTypeDescription().getItem().getUnit() : "";
BigDecimal factor = channelConfig != null ? channelConfig.getChannelTypeDescription().getItem().getFactor()
: null;
return getSOAPElement(soapMessage, element).map(rawValue -> { return getSOAPElement(soapMessage, element).map(rawValue -> {
// map rawValue to State // map rawValue to State
switch (dataType) { switch (dataType) {
case "boolean": case "boolean" -> {
return rawValue.equals("0") ? OnOffType.OFF : OnOffType.ON; return rawValue.equals("0") ? OnOffType.OFF : OnOffType.ON;
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);
} }
default: case "string" -> {
return new StringType(rawValue);
}
case "ui1", "ui2", "i4", "ui4" -> {
BigDecimal decimalValue = new BigDecimal(rawValue);
if (factor != null) {
decimalValue = decimalValue.multiply(factor);
}
if (!unit.isEmpty()) {
return new QuantityType<>(decimalValue + " " + unit);
} else {
return new DecimalType(decimalValue);
}
}
default -> {
return null; return null;
} }
}
}).map(state -> { }).map(state -> {
// check if we need post processing // check if we need post-processing
if (channelConfig == null if (channelConfig == null
|| channelConfig.getChannelTypeDescription().getGetAction().getPostProcessor() == null) { || channelConfig.getChannelTypeDescription().getGetAction().getPostProcessor() == null) {
return state; return state;
@ -172,6 +184,26 @@ public class SOAPValueConverter {
}).or(Optional::empty); }).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 * post processor to map mac device signal strength to system.signal-strength 0-4
* *
@ -201,20 +233,6 @@ public class SOAPValueConverter {
return mappedSignalStrength; 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 * post processor for answering machine new messages channel
* *
@ -226,16 +244,14 @@ public class SOAPValueConverter {
@SuppressWarnings("unused") @SuppressWarnings("unused")
private State processTamListURL(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException { private State processTamListURL(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException {
try { try {
ContentResponse response = httpClient.newRequest(state.toString()).timeout(1500, TimeUnit.MILLISECONDS) ContentResponse response = httpClient.newRequest(state.toString()).timeout(timeout, TimeUnit.MILLISECONDS)
.send(); .send();
String responseContent = response.getContentAsString(); String responseContent = response.getContentAsString();
int messageCount = responseContent.split("<New>1</New>").length - 1; int messageCount = responseContent.split("<New>1</New>").length - 1;
return new DecimalType(messageCount); return new DecimalType(messageCount);
} catch (TimeoutException e) { } catch (InterruptedException | TimeoutException | ExecutionException e) {
throw new PostProcessingException("Failed to get TAM list due to time out from URL " + state.toString(), e); throw new PostProcessingException("Failed to get TAM list from URL " + state, e);
} catch (InterruptedException | ExecutionException e) {
throw new PostProcessingException("Failed to get TAM list from URL " + state.toString(), e);
} }
} }
@ -315,24 +331,23 @@ public class SOAPValueConverter {
*/ */
private State processCallList(State state, @Nullable String days, CallListType type) private State processCallList(State state, @Nullable String days, CallListType type)
throws PostProcessingException { 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) { 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(); List<Call> calls = callListRoot.getCall();
switch (type) { switch (type) {
case INBOUND_COUNT: case INBOUND_COUNT, MISSED_COUNT, OUTBOUND_COUNT, REJECTED_COUNT -> {
case MISSED_COUNT:
case OUTBOUND_COUNT:
case REJECTED_COUNT:
long callCount = calls.stream().filter(call -> type.typeString().equals(call.getType())).count(); long callCount = calls.stream().filter(call -> type.typeString().equals(call.getType())).count();
return new DecimalType(callCount); return new DecimalType(callCount);
case JSON_LIST: }
case JSON_LIST -> {
Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ssX").serializeNulls().create(); Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ssX").serializeNulls().create();
List<CallListEntry> callListEntries = calls.stream().map(CallListEntry::new) List<CallListEntry> callListEntries = calls.stream().map(CallListEntry::new)
.collect(Collectors.toList()); .collect(Collectors.toList());
return new StringType(gson.toJson(callListEntries)); return new StringType(gson.toJson(callListEntries));
} }
}
return UnDefType.UNDEF; return UnDefType.UNDEF;
} }
} }

View File

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

View File

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

View File

@ -9,9 +9,10 @@
<label>Phone Book</label> <label>Phone Book</label>
<description>The name of the the phone book.</description> <description>The name of the the phone book.</description>
</parameter> </parameter>
<parameter name="matchCount" type="integer" min="0" step="1"> <parameter name="matchCount" type="integer" step="1">
<label>Match Count</label> <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> <default>0</default>
</parameter> </parameter>
<parameter name="phoneNumberIndex" type="integer" min="0" max="1" step="1"> <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 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.label = Call Deflection
thing-type.config.tr064.fritzbox.callDeflectionIndices.description = List of call deflection IDs (starting with 0). 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.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.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.label = TAM
thing-type.config.tr064.fritzbox.tamIndices.description = List of answering machines (starting with 0). 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.user.label = Username
thing-type.config.tr064.fritzbox.wanBlockIPs.label = WAN Block IPs 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.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.host.description = Host name or IP address.
thing-type.config.tr064.generic.password.label = Password thing-type.config.tr064.generic.password.label = Password
thing-type.config.tr064.generic.refresh.label = Refresh Interval 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.generic.user.label = Username
thing-type.config.tr064.subdevice.refresh.label = Refresh Interval thing-type.config.tr064.subdevice.refresh.label = Refresh Interval
thing-type.config.tr064.subdevice.uuid.label = UUID thing-type.config.tr064.subdevice.uuid.label = UUID

View File

@ -27,6 +27,12 @@
<label>Refresh Interval</label> <label>Refresh Interval</label>
<default>60</default> <default>60</default>
</parameter> </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> </config-description>
</bridge-type> </bridge-type>
@ -54,6 +60,12 @@
<label>Refresh Interval</label> <label>Refresh Interval</label>
<default>60</default> <default>60</default>
</parameter> </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"> <parameter name="tamIndices" type="text" multiple="true">
<label>TAM</label> <label>TAM</label>
<description>List of answering machines (starting with 0).</description> <description>List of answering machines (starting with 0).</description>
@ -100,6 +112,17 @@
<default>600</default> <default>600</default>
<advanced>true</advanced> <advanced>true</advanced>
</parameter> </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> </config-description>
</bridge-type> </bridge-type>

View File

@ -65,7 +65,7 @@
serviceId="urn:X_AVM-DE_HostFilter-com:serviceId:X_AVM-DE_HostFilter1"/> serviceId="urn:X_AVM-DE_HostFilter-com:serviceId:X_AVM-DE_HostFilter1"/>
<getAction name="GetWANAccessByIP" argument="NewDisallow"> <getAction name="GetWANAccessByIP" argument="NewDisallow">
<parameter name="NewIPv4Address" thingParameter="wanBlockIPs" <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> </getAction>
<setAction name="DisallowWANAccessByIP" argument="NewDisallow"> <setAction name="DisallowWANAccessByIP" argument="NewDisallow">
<parameter name="NewIPv4Address" thingParameter="wanBlockIPs"/> <parameter name="NewIPv4Address" thingParameter="wanBlockIPs"/>
@ -138,7 +138,8 @@
<getAction name="GetInfo" argument="NewEnable"/> <getAction name="GetInfo" argument="NewEnable"/>
<setAction name="SetEnable" argument="NewEnable"/> <setAction name="SetEnable" argument="NewEnable"/>
</channel> </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"/> <item type="Switch"/>
<service deviceType="urn:dslforum-org:device:LANDevice:1" serviceId="urn:LanDeviceHosts-com:serviceId:Hosts1"/> <service deviceType="urn:dslforum-org:device:LANDevice:1" serviceId="urn:LanDeviceHosts-com:serviceId:Hosts1"/>
<getAction name="GetSpecificHostEntry" argument="NewActive"> <getAction name="GetSpecificHostEntry" argument="NewActive">
@ -146,7 +147,8 @@
pattern="([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}(\s*#.*)*"/> pattern="([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}(\s*#.*)*"/>
</getAction> </getAction>
</channel> </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"/> <item type="String"/>
<service deviceType="urn:dslforum-org:device:LANDevice:1" serviceId="urn:LanDeviceHosts-com:serviceId:Hosts1"/> <service deviceType="urn:dslforum-org:device:LANDevice:1" serviceId="urn:LanDeviceHosts-com:serviceId:Hosts1"/>
<getAction name="GetSpecificHostEntry" argument="NewIPAddress"> <getAction name="GetSpecificHostEntry" argument="NewIPAddress">
@ -154,12 +156,11 @@
pattern="([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}(\s*#.*)*"/> pattern="([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}(\s*#.*)*"/>
</getAction> </getAction>
</channel> </channel>
<!-- WLAN Config 1 - 2.4 Ghz --> <!-- WLAN Config 1 - 2.4 Ghz -->
<channel name="macSignalStrength1" label="MAC Wifi Signal Strength 2.4Ghz" <channel name="macSignalStrength1" label="MAC Wifi Signal Strength 2.4Ghz"
description="Wifi Signal Strength of the device with description="Wifi Signal Strength of the device with
the given MAC. This is set in case the Device is connected to 2.4Ghz" 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"/> <item type="Number"/>
<service deviceType="urn:dslforum-org:device:LANDevice:1" <service deviceType="urn:dslforum-org:device:LANDevice:1"
serviceId="urn:WLANConfiguration-com:serviceId:WLANConfiguration1"/> serviceId="urn:WLANConfiguration-com:serviceId:WLANConfiguration1"/>
@ -171,7 +172,7 @@
</channel> </channel>
<channel name="macSpeed1" label="MAC Wifi Speed 2.4Ghz" <channel name="macSpeed1" label="MAC Wifi Speed 2.4Ghz"
description="Wifi Speed of the device with 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"/> <item type="Number:DataTransferRate" unit="Mbit/s" statePattern="%d Mbit/s"/>
<service deviceType="urn:dslforum-org:device:LANDevice:1" <service deviceType="urn:dslforum-org:device:LANDevice:1"
serviceId="urn:WLANConfiguration-com:serviceId:WLANConfiguration1"/> serviceId="urn:WLANConfiguration-com:serviceId:WLANConfiguration1"/>
@ -184,8 +185,8 @@
<!-- WLAN Config 2 - 5 Ghz --> <!-- WLAN Config 2 - 5 Ghz -->
<channel name="macSignalStrength2" label="MAC Wifi Signal Strength 5Ghz" <channel name="macSignalStrength2" label="MAC Wifi Signal Strength 5Ghz"
description="Wifi Signal Strength of the device with description="Wifi Signal Strength 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"
typeId="system.signal-strength"> typeId="system:signal-strength">
<item type="Number"/> <item type="Number"/>
<service deviceType="urn:dslforum-org:device:LANDevice:1" <service deviceType="urn:dslforum-org:device:LANDevice:1"
serviceId="urn:WLANConfiguration-com:serviceId:WLANConfiguration2"/> serviceId="urn:WLANConfiguration-com:serviceId:WLANConfiguration2"/>
@ -197,7 +198,7 @@
</channel> </channel>
<channel name="macSpeed2" label="MAC Wifi Speed 5Ghz" <channel name="macSpeed2" label="MAC Wifi Speed 5Ghz"
description="Wifi Speed of the device with 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"/> <item type="Number:DataTransferRate" unit="Mbit/s" statePattern="%d Mbit/s"/>
<service deviceType="urn:dslforum-org:device:LANDevice:1" <service deviceType="urn:dslforum-org:device:LANDevice:1"
serviceId="urn:WLANConfiguration-com:serviceId:WLANConfiguration2"/> serviceId="urn:WLANConfiguration-com:serviceId:WLANConfiguration2"/>
@ -244,6 +245,24 @@
serviceId="urn:WANCIfConfig-com:serviceId:WANCommonInterfaceConfig1"/> serviceId="urn:WANCIfConfig-com:serviceId:WANCommonInterfaceConfig1"/>
<getAction name="GetTotalBytesSent" argument="NewTotalBytesSent"/> <getAction name="GetTotalBytesSent" argument="NewTotalBytesSent"/>
</channel> </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"> <channel name="dslEnable" label="DSL Enable">
<item type="Switch"/> <item type="Switch"/>
<service deviceType="urn:dslforum-org:device:WANDevice:1" <service deviceType="urn:dslforum-org:device:WANDevice:1"
@ -281,28 +300,28 @@
<getAction name="GetInfo" argument="NewUpstreamCurrRate"/> <getAction name="GetInfo" argument="NewUpstreamCurrRate"/>
</channel> </channel>
<channel name="dslDownstreamNoiseMargin" label="DSL Downstream Noise Margin"> <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" <service deviceType="urn:dslforum-org:device:WANDevice:1"
serviceId="urn:WANDSLIfConfig-com:serviceId:WANDSLInterfaceConfig1"/> serviceId="urn:WANDSLIfConfig-com:serviceId:WANDSLInterfaceConfig1"/>
<getAction name="GetInfo" argument="NewDownstreamNoiseMargin" postProcessor="processDecaDecibel"/> <getAction name="GetInfo" argument="NewDownstreamNoiseMargin"/>
</channel> </channel>
<channel name="dslUpstreamNoiseMargin" label="DSL Upstream Noise Margin"> <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" <service deviceType="urn:dslforum-org:device:WANDevice:1"
serviceId="urn:WANDSLIfConfig-com:serviceId:WANDSLInterfaceConfig1"/> serviceId="urn:WANDSLIfConfig-com:serviceId:WANDSLInterfaceConfig1"/>
<getAction name="GetInfo" argument="NewUpstreamNoiseMargin" postProcessor="processDecaDecibel"/> <getAction name="GetInfo" argument="NewUpstreamNoiseMargin"/>
</channel> </channel>
<channel name="dslDownstreamAttenuation" label="DSL Downstream Attenuation"> <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" <service deviceType="urn:dslforum-org:device:WANDevice:1"
serviceId="urn:WANDSLIfConfig-com:serviceId:WANDSLInterfaceConfig1"/> serviceId="urn:WANDSLIfConfig-com:serviceId:WANDSLInterfaceConfig1"/>
<getAction name="GetInfo" argument="NewDownstreamAttenuation" postProcessor="processDecaDecibel"/> <getAction name="GetInfo" argument="NewDownstreamAttenuation"/>
</channel> </channel>
<channel name="dslUpstreamAttenuation" label="DSL Upstream Attenuation"> <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" <service deviceType="urn:dslforum-org:device:WANDevice:1"
serviceId="urn:WANDSLIfConfig-com:serviceId:WANDSLInterfaceConfig1"/> serviceId="urn:WANDSLIfConfig-com:serviceId:WANDSLInterfaceConfig1"/>
<getAction name="GetInfo" argument="NewUpstreamAttenuation" postProcessor="processDecaDecibel"/> <getAction name="GetInfo" argument="NewUpstreamAttenuation"/>
</channel> </channel>
<channel name="dslFECErrors" label="DSL FEC Errors"> <channel name="dslFECErrors" label="DSL FEC Errors">
<item type="Number:Dimensionless"/> <item type="Number:Dimensionless"/>

View File

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

View File

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