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