[tr064] Initial contribution (#8523)

* Initial contribution

Signed-off-by: Jan N. Klug <jan.n.klug@rub.de>
This commit is contained in:
J-N-K 2020-11-03 06:36:19 +01:00 committed by GitHub
parent 5c4c89a0c9
commit a3d33fa60b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 3767 additions and 0 deletions

View File

@ -245,6 +245,7 @@
/bundles/org.openhab.binding.tibber/ @kjoglum
/bundles/org.openhab.binding.touchwand /@roieg
/bundles/org.openhab.binding.tplinksmarthome/ @Hilbrand
/bundles/org.openhab.binding.tr064/ @J-N-K
/bundles/org.openhab.binding.tradfri/ @cweitkamp @kaikreuzer
/bundles/org.openhab.binding.unifi/ @mgbowman
/bundles/org.openhab.binding.unifiedremote/ @GiviMAD

View File

@ -1211,6 +1211,11 @@
<artifactId>org.openhab.binding.tplinksmarthome</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.tr064</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.tradfri</artifactId>

View File

@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab2-addons

View File

@ -0,0 +1,119 @@
# TR-064 Binding
This binding brings support for internet gateway devices that support the TR-064 protocol.
It can be used to gather information from the device and/or re-configure it.
## Supported Things
Four thing types are supported:
- `generic`: the internet gateway device itself (generic device)
- `fritzbox`: similar to `generic` with extensions for AVM FritzBox devices
- `subDevice`: a sub-device of a `rootDevice` (e.g. a WAN interface)
- `subDeviceLan`: a special type of sub-device that supports MAC-detection
## Discovery
The gateway device needs to be added manually.
After that, sub-devices are detected automatically.
## Thing Configuration
All thing types have a `refresh` parameter.
It sets the refresh-interval in seconds for each device channel.
The default value is 60.
### `generic`, `fritzbox`
The `host` parameter is required to communicate with the device.
It can be a hostname or an IP address.
For accessing the device you need to supply credentials.
If you only configured password authentication for your device, the `user` parameter must be skipped and it will default to `dslf-config`.
The second credential parameter is `password`, which is mandatory.
For security reasons it is highly recommended to set both, username and password.
### `fritzbox`
All additional parameters for `fritzbox` devices (i.e. except those that are shared with `generic`) are advanced parameters.
One or more TAM (telephone answering machines) are supported by most devices.
By setting the `tamIndices` parameter you can instruct the binding to add channels for these devices to the thing.
Values start with `0`.
This is an optional parameter and multiple values are allowed.
Most devices allow to configure call deflections.
If the `callDeflectionIndices` parameter is set, channels for the status of the pre-configured call deflections are added.
Values start with `0`, including the number of "Call Blocks" (two configured call-blocks -> first deflection is `2`).
This is an optional parameter and multiple values are allowed.
Most devices support call lists.
The binding can analyze these call lists and provide channels for the number of missed calls, inbound calls, outbound calls and rejected (blocked) calls.
The days for which this analysis takes place can be controlled with the `missedCallDays`, `rejectedCallDays`, `inboundCallDays` and `outboundCallDays`
This is an optional parameter and multiple values are allowed.
Since FritzOS! 7.20 WAN access of local devices can be controlled by their IPs.
If the `wanBlockIPs` parameter is set, a channel for each IP is created to block/unblock WAN access for this IP.
Values need to be IPv4 addresses in the format `a.b.c.d`.
This is an optional parameter and multiple values are allowed.
If the `PHONEBOOK` profile shall be used, it is necessary to retrieve the phonebooks from the FritzBox.
The `phonebookInterval` is uses to set the refresh cycle for phonebooks.
### `subdevice`, `subdeviceLan`
Besides the bridge that the thing is attached to, sub-devices have a `uuid` parameter.
This is the UUID/UDN of the device and a mandatory parameter.
Since the value can only be determined by examining the SCPD of the root device, the simplest way to get hold of them is through auto-discovery.
For `subdeviceLan` devices (type is detected automatically during discovery) the parameter `macOnline` can be defined.
It adds a channel for each MAC (format 11:11:11:11:11:11) that shows the online status of the respective device.
This is an optional parameter and multiple values are allowed.
## Channels
| channel | item-type | advanced | description |
|----------------------------|---------------------------|:--------:|----------------------------------------------------------------|
| `callDeflectionEnable` | `Switch` | | Enable/Disable the call deflection setup with the given index. |
| `deviceLog` | `String` | x | A string containing the last log messages. |
| `dslCRCErrors` | `Number:Dimensionless` | x | DSL CRC Errors |
| `dslDownstreamNoiseMargin` | `Number:Dimensionless` | x | DSL Downstream Noise Margin |
| `dslDownstreamNoiseMargin` | `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 |
| `dslUpstreamNoiseMargin` | `Number:Dimensionless` | x | DSL Upstream Noise Margin |
| `dslUpstreamNoiseMargin` | `Number:Dimensionless` | x | DSL Upstream Attenuation |
| `inboundCalls` | `Number` | x | Number of inbound calls within the given number of days. |
| `macOnline` | `Switch` | x | Online status of the device with the given MAC |
| `missedCalls` | `Number` | | Number of missed calls within the given number of days. |
| `outboundCalls` | `Number` | x | Number of outbound calls within the given number of days. |
| `reboot` | `Switch` | | Reboot |
| `rejectedCalls` | `Number` | x | Number of rejected calls within the given number of days. |
| `securityPort` | `Number` | x | The port for connecting via HTTPS to the TR-064 service. |
| `tamEnable` | `Switch` | | Enable/Disable the answering machine with the given index. |
| `tamNewMessages` | `Number` | | The number of new messages of the given answering machine. |
| `uptime` | `Number:Time` | | Uptime |
| `wanAccessType` | `String` | x | Access Type |
| `wanConnectionStatus` | `String` | | Connection Status |
| `wanIpAddress` | `String` | x | WAN IP Address |
| `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 Send |
| `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. |
## `PHONEBOOK` Profile
The binding provides a profile for using the FritzBox phonebooks for resolving numbers to names.
The `PHONEBOOK` profile takes strings containing the number as input and provides strings with the caller's name, if found.
The parameter `thingUid` with the UID of the phonebook providing thing is a mandatory parameter.
If only a specific phonebook from the device should be used, this can be specified with the `phonebookName` parameter.
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.

View File

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.tr064</artifactId>
<name>openHAB Add-ons :: Bundles :: TR-064 Binding</name>
<build>
<plugins>
<plugin>
<groupId>org.jvnet.jaxb2.maven2</groupId>
<artifactId>maven-jaxb2-plugin</artifactId>
<version>0.14.0</version>
<executions>
<execution>
<id>generate-jaxb-sources</id>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
<configuration>
<schemaDirectory>src/main/resources/xsd</schemaDirectory>
<noFileHeader>true</noFileHeader>
<locale>en</locale>
<episode>false</episode>
<extension>true</extension>
<args>
<arg>-Xxew</arg>
<arg>-Xxew:instantiate early</arg>
</args>
<plugins>
<plugin>
<groupId>com.github.jaxb-xew-plugin</groupId>
<artifactId>jaxb-xew-plugin</artifactId>
<version>1.10</version>
</plugin>
</plugins>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.tr064-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-tr064" description="TR-064 Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<requirement>openhab.tp;filter:="(feature=jaxb)"</requirement>
<feature dependency="true">openhab.tp-jaxb</feature>
<requirement>openhab.tp;filter:="(feature=jax-ws)"</requirement>
<feature dependency="true">openhab.tp-jaxws</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.tr064/${project.version}</bundle>
</feature>
</features>

View File

@ -0,0 +1,40 @@
/**
* Copyright (c) 2010-2020 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 javax.net.ssl.X509ExtendedTrustManager;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.io.net.http.TlsTrustManagerProvider;
import org.openhab.core.io.net.http.TrustAllTrustManager;
import org.osgi.service.component.annotations.Component;
/**
* Provides a TrustManager to allow secure connections to any FRITZ!Box
*
* @author Christoph Weitkamp - Initial Contribution
*/
@Component
@NonNullByDefault
public class AvmFritzTlsTrustManagerProvider implements TlsTrustManagerProvider {
@Override
public String getHostName() {
return "fritz.box";
}
@Override
public X509ExtendedTrustManager getTrustManager() {
return TrustAllTrustManager.getInstance();
}
}

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2020 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 org.eclipse.jdt.annotation.NonNullByDefault;
/**
*
* The{@link ChannelConfigException} is a catched Exception that is thrown during channel configuration
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class ChannelConfigException extends Exception {
private static final long serialVersionUID = 1L;
public ChannelConfigException(String message) {
super(message);
}
}

View File

@ -0,0 +1,31 @@
/**
* Copyright (c) 2010-2020 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 org.eclipse.jdt.annotation.NonNullByDefault;
/**
*
* The{@link PostProcessingException} is a catched Exception that is thrown in case of conversion errors during post
* processing
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class PostProcessingException extends Exception {
private static final long serialVersionUID = 1L;
public PostProcessingException(String message, Throwable t) {
super(message, t);
}
}

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2020 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 org.eclipse.jdt.annotation.NonNullByDefault;
/**
*
* The{@link SCPDException} is a catched Exception that is thrown in case of errors during SCPD processing
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class SCPDException extends Exception {
private static final long serialVersionUID = 1L;
public SCPDException(String message) {
super(message);
}
}

View File

@ -0,0 +1,254 @@
/**
* Copyright (c) 2010-2020 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 static org.openhab.binding.tr064.internal.util.Util.getSOAPElement;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import javax.xml.soap.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.BytesContentProvider;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.tr064.internal.config.Tr064ChannelConfig;
import org.openhab.binding.tr064.internal.dto.config.ActionType;
import org.openhab.binding.tr064.internal.dto.config.ChannelTypeDescription;
import org.openhab.binding.tr064.internal.dto.scpd.root.SCPDServiceType;
import org.openhab.binding.tr064.internal.dto.scpd.service.SCPDActionType;
import org.openhab.core.cache.ExpiringCacheMap;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link SOAPConnector} provides communication with a remote SOAP device
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class SOAPConnector {
private static final int SOAP_TIMEOUT = 2000; // in ms
private final Logger logger = LoggerFactory.getLogger(SOAPConnector.class);
private final HttpClient httpClient;
private final String endpointBaseURL;
private final SOAPValueConverter soapValueConverter;
public SOAPConnector(HttpClient httpClient, String endpointBaseURL) {
this.httpClient = httpClient;
this.endpointBaseURL = endpointBaseURL;
this.soapValueConverter = new SOAPValueConverter(httpClient);
}
/**
* prepare a SOAP request for an action request to a service
*
* @param service the service
* @param soapAction the action to send
* @param arguments arguments to send along with the request
* @return a jetty Request containing the full SOAP message
* @throws IOException if a problem while writing the SOAP message to the Request occurs
* @throws SOAPException if a problem with creating the SOAP message occurs
*/
private Request prepareSOAPRequest(SCPDServiceType service, String soapAction, Map<String, String> arguments)
throws IOException, SOAPException {
MessageFactory messageFactory = MessageFactory.newInstance();
SOAPMessage soapMessage = messageFactory.createMessage();
SOAPPart soapPart = soapMessage.getSOAPPart();
SOAPEnvelope envelope = soapPart.getEnvelope();
envelope.setEncodingStyle("http://schemas.xmlsoap.org/soap/encoding/");
// SOAP body
SOAPBody soapBody = envelope.getBody();
SOAPElement soapBodyElem = soapBody.addChildElement(soapAction, "u", service.getServiceType());
arguments.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach(argument -> {
try {
soapBodyElem.addChildElement(argument.getKey()).setTextContent(argument.getValue());
} catch (SOAPException e) {
logger.warn("Could not add {}:{} to SOAP Request: {}", argument.getKey(), argument.getValue(),
e.getMessage());
}
});
// SOAP headers
MimeHeaders headers = soapMessage.getMimeHeaders();
headers.addHeader("SOAPAction", service.getServiceType() + "#" + soapAction);
soapMessage.saveChanges();
// create Request and add headers and content
Request request = httpClient.newRequest(endpointBaseURL + service.getControlURL()).method(HttpMethod.POST);
((Iterator<MimeHeader>) soapMessage.getMimeHeaders().getAllHeaders())
.forEachRemaining(header -> request.header(header.getName(), header.getValue()));
try (final ByteArrayOutputStream os = new ByteArrayOutputStream()) {
soapMessage.writeTo(os);
byte[] content = os.toByteArray();
request.content(new BytesContentProvider(content));
}
return request;
}
/**
* execute a SOAP request
*
* @param service the service to send the action to
* @param soapAction the action itself
* @param arguments arguments to send along with the request
* @return the SOAPMessage answer from the remote host
* @throws Tr064CommunicationException if an error occurs during the request
*/
public synchronized SOAPMessage doSOAPRequest(SCPDServiceType service, String soapAction,
Map<String, String> arguments) throws Tr064CommunicationException {
try {
Request request = prepareSOAPRequest(service, soapAction, arguments).timeout(SOAP_TIMEOUT,
TimeUnit.MILLISECONDS);
if (logger.isTraceEnabled()) {
request.getContent().forEach(buffer -> logger.trace("Request: {}", new String(buffer.array())));
}
ContentResponse response = request.send();
if (response.getStatus() == HttpStatus.UNAUTHORIZED_401) {
// retry once if authentication expired
logger.trace("Re-Auth needed.");
httpClient.getAuthenticationStore().clearAuthenticationResults();
request = prepareSOAPRequest(service, soapAction, arguments).timeout(SOAP_TIMEOUT,
TimeUnit.MILLISECONDS);
response = request.send();
}
try (final ByteArrayInputStream is = new ByteArrayInputStream(response.getContent())) {
logger.trace("Received response: {}", response.getContentAsString());
SOAPMessage soapMessage = MessageFactory.newInstance().createMessage(null, is);
if (soapMessage.getSOAPBody().hasFault()) {
String soapError = getSOAPElement(soapMessage, "errorCode").orElse("unknown");
String soapReason = getSOAPElement(soapMessage, "errorDescription").orElse("unknown");
String error = String.format("HTTP-Response-Code %d (%s), SOAP-Fault: %s (%s)",
response.getStatus(), response.getReason(), soapError, soapReason);
throw new Tr064CommunicationException(error);
}
return soapMessage;
}
} catch (IOException | SOAPException | InterruptedException | TimeoutException | ExecutionException e) {
throw new Tr064CommunicationException(e);
}
}
/**
* send a command to the remote device
*
* @param channelConfig the channel config containing all information
* @param command the command to send
*/
public void sendChannelCommandToDevice(Tr064ChannelConfig channelConfig, Command command) {
soapValueConverter.getSOAPValueFromCommand(command, channelConfig.getDataType(),
channelConfig.getChannelTypeDescription().getItem().getUnit()).ifPresentOrElse(value -> {
final ChannelTypeDescription channelTypeDescription = channelConfig.getChannelTypeDescription();
final SCPDServiceType service = channelConfig.getService();
logger.debug("Sending {} as {} to {}/{}", command, value, service.getServiceId(),
channelTypeDescription.getSetAction().getName());
try {
Map<String, String> arguments = new HashMap<>();
if (channelTypeDescription.getSetAction().getArgument() != null) {
arguments.put(channelTypeDescription.getSetAction().getArgument(), value);
}
String parameter = channelConfig.getParameter();
if (parameter != null) {
arguments.put(
channelConfig.getChannelTypeDescription().getGetAction().getParameter().getName(),
parameter);
}
doSOAPRequest(service, channelTypeDescription.getSetAction().getName(), arguments);
} catch (Tr064CommunicationException e) {
logger.warn("Could not send command {}: {}", command, e.getMessage());
}
}, () -> logger.warn("Could not convert {} to SOAP value", command));
}
/**
* get a value from the remote device - updates state cache for all possible channels
*
* @param channelConfig the channel config containing all information
* @param channelConfigMap map of all channels in the device
* @param stateCache the ExpiringCacheMap for states of the device
* @return the value for the requested channel
*/
public State getChannelStateFromDevice(final Tr064ChannelConfig channelConfig,
Map<ChannelUID, Tr064ChannelConfig> channelConfigMap, ExpiringCacheMap<ChannelUID, State> stateCache) {
try {
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;
}
}
// get value(s) from remote device
Map<String, String> arguments = new HashMap<>();
String parameter = channelConfig.getParameter();
ActionType action = channelConfig.getChannelTypeDescription().getGetAction();
if (parameter != null && !action.getParameter().isInternalOnly()) {
arguments.put(action.getParameter().getName(), parameter);
}
SOAPMessage soapResponse = doSOAPRequest(channelConfig.getService(), getAction.getName(), arguments);
String argumentName = channelConfig.getChannelTypeDescription().getGetAction().getArgument();
// find all other channels with the same action that are already in cache, so we can update them
Map<ChannelUID, Tr064ChannelConfig> channelsInRequest = channelConfigMap.entrySet().stream()
.filter(map -> getAction.equals(map.getValue().getGetAction())
&& stateCache.containsKey(map.getKey())
&& !argumentName
.equals(map.getValue().getChannelTypeDescription().getGetAction().getArgument()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
channelsInRequest
.forEach(
(channelUID,
channelConfig1) -> soapValueConverter
.getStateFromSOAPValue(soapResponse,
channelConfig1.getChannelTypeDescription().getGetAction()
.getArgument(),
channelConfig1)
.ifPresent(state -> stateCache.putValue(channelUID, state)));
return soapValueConverter.getStateFromSOAPValue(soapResponse, argumentName, channelConfig)
.orElseThrow(() -> new Tr064CommunicationException("failed to transform '"
+ channelConfig.getChannelTypeDescription().getGetAction().getArgument() + "'"));
} catch (Tr064CommunicationException e) {
logger.info("Failed to get {}: {}", channelConfig, e.getMessage());
return UnDefType.UNDEF;
}
}
}

View File

@ -0,0 +1,255 @@
/**
* Copyright (c) 2010-2020 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 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.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.xml.soap.SOAPMessage;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.openhab.binding.tr064.internal.config.Tr064ChannelConfig;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link SOAPValueConverter} converts SOAP values and openHAB states
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class SOAPValueConverter {
private static final int REQUEST_TIMEOUT = 5000; // in ms
private final Logger logger = LoggerFactory.getLogger(SOAPValueConverter.class);
private final HttpClient httpClient;
public SOAPValueConverter(HttpClient httpClient) {
this.httpClient = httpClient;
}
/**
* convert an openHAB command to a SOAP value
*
* @param command the command to be converted
* @param dataType the datatype to send
* @param unit if available, the unit of the converted value
* @return a string optional containing the converted value
*/
public Optional<String> getSOAPValueFromCommand(Command command, String dataType, String unit) {
if (dataType.isEmpty()) {
// we don't have data to send
return Optional.of("");
}
if (command instanceof QuantityType) {
QuantityType<?> value = (unit.isEmpty()) ? ((QuantityType<?>) command)
: ((QuantityType<?>) command).toUnit(unit);
if (value == null) {
logger.warn("Could not convert {} to unit {}", command, unit);
return Optional.empty();
}
switch (dataType) {
case "ui2":
return Optional.of(String.valueOf(value.shortValue()));
case "ui4":
return Optional.of(String.valueOf(value.intValue()));
default:
}
} else if (command instanceof DecimalType) {
BigDecimal value = ((DecimalType) command).toBigDecimal();
switch (dataType) {
case "ui2":
return Optional.of(String.valueOf(value.shortValue()));
case "ui4":
return Optional.of(String.valueOf(value.intValue()));
default:
}
} else if (command instanceof StringType) {
if (dataType.equals("string")) {
return Optional.of(command.toString());
}
} else if (command instanceof OnOffType) {
if (dataType.equals("boolean")) {
return Optional.of(OnOffType.ON.equals(command) ? "1" : "0");
}
}
return Optional.empty();
}
/**
* convert the value from a SOAP message to an openHAB value
*
* @param soapMessage the inbound SOAP message
* @param element the element that needs to be extracted
* @param channelConfig the channel config containing additional information (if null a data-type "string" and
* missing unit is assumed)
* @return an Optional of State containing the converted value
*/
public Optional<State> getStateFromSOAPValue(SOAPMessage soapMessage, String element,
@Nullable Tr064ChannelConfig channelConfig) {
String dataType = channelConfig != null ? channelConfig.getDataType() : "string";
String unit = channelConfig != null ? channelConfig.getChannelTypeDescription().getItem().getUnit() : "";
return getSOAPElement(soapMessage, element).map(rawValue -> {
// map rawValue to State
switch (dataType) {
case "boolean":
return rawValue.equals("0") ? OnOffType.OFF : OnOffType.ON;
case "string":
return new StringType(rawValue);
case "ui2":
case "ui4":
if (!unit.isEmpty()) {
return new QuantityType<>(rawValue + " " + unit);
} else {
return new DecimalType(rawValue);
}
default:
return null;
}
}).map(state -> {
// check if we need post processing
if (channelConfig == null
|| channelConfig.getChannelTypeDescription().getGetAction().getPostProcessor() == null) {
return state;
}
String postProcessor = channelConfig.getChannelTypeDescription().getGetAction().getPostProcessor();
try {
Method method = SOAPValueConverter.class.getDeclaredMethod(postProcessor, State.class,
Tr064ChannelConfig.class);
Object o = method.invoke(this, state, channelConfig);
if (o instanceof State) {
return (State) o;
}
} catch (NoSuchMethodException | IllegalAccessException e) {
logger.warn("Postprocessor {} not found, this most likely is a programming error", postProcessor, e);
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
logger.info("Postprocessor {} failed: {}", postProcessor,
cause != null ? cause.getMessage() : e.getMessage());
}
return null;
}).or(Optional::empty);
}
/**
* post processor for answering machine new messages channel
*
* @param state the message list URL
* @param channelConfig channel config of the TAM new message channel
* @return the number of new messages
* @throws PostProcessingException if the message list could not be retrieved
*/
@SuppressWarnings("unused")
private State processTamListURL(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException {
try {
ContentResponse response = httpClient.newRequest(state.toString()).timeout(1000, TimeUnit.MILLISECONDS)
.send();
String responseContent = response.getContentAsString();
int messageCount = responseContent.split("<New>1</New>").length - 1;
return new DecimalType(messageCount);
} catch (InterruptedException | TimeoutException | ExecutionException e) {
throw new PostProcessingException("Failed to get TAM list from URL " + state.toString(), e);
}
}
/**
* post processor for missed calls
*
* @param state the call list URL
* @param channelConfig channel config of the missed call channel (contains day number)
* @return the number of missed calls
* @throws PostProcessingException if call list could not be retrieved
*/
@SuppressWarnings("unused")
private State processMissedCalls(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException {
return processCallList(state, channelConfig.getParameter(), "2");
}
/**
* post processor for inbound calls
*
* @param state the call list URL
* @param channelConfig channel config of the inbound call channel (contains day number)
* @return the number of inbound calls
* @throws PostProcessingException if call list could not be retrieved
*/
@SuppressWarnings("unused")
private State processInboundCalls(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException {
return processCallList(state, channelConfig.getParameter(), "1");
}
/**
* post processor for rejected calls
*
* @param state the call list URL
* @param channelConfig channel config of the rejected call channel (contains day number)
* @return the number of rejected calls
* @throws PostProcessingException if call list could not be retrieved
*/
@SuppressWarnings("unused")
private State processRejectedCalls(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException {
return processCallList(state, channelConfig.getParameter(), "3");
}
/**
* post processor for outbound calls
*
* @param state the call list URL
* @param channelConfig channel config of the outbound call channel (contains day number)
* @return the number of outbound calls
* @throws PostProcessingException if call list could not be retrieved
*/
@SuppressWarnings("unused")
private State processOutboundCalls(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException {
return processCallList(state, channelConfig.getParameter(), "4");
}
/**
* internal helper for call list post processors
*
* @param state the call list URL
* @param days number of days to get
* @param type type of call (1=missed 2=inbound 3=rejected 4=outbund)
* @return the quantity of calls of the given type within the given number of days
* @throws PostProcessingException if the call list could not be retrieved
*/
private State processCallList(State state, @Nullable String days, String type) throws PostProcessingException {
try {
ContentResponse response = httpClient.newRequest(state.toString() + "&days=" + days)
.timeout(REQUEST_TIMEOUT, TimeUnit.MILLISECONDS).send();
String responseContent = response.getContentAsString();
int callCount = responseContent.split("<Type>" + type + "</Type>").length - 1;
return new DecimalType(callCount);
} catch (InterruptedException | TimeoutException | ExecutionException e) {
throw new PostProcessingException("Failed to get call list from URL " + state.toString(), e);
}
}
}

View File

@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2020 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.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.tr064.internal.dto.config.ChannelTypeDescription;
import org.openhab.binding.tr064.internal.util.Util;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link Tr064BindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class Tr064BindingConstants {
public static final String BINDING_ID = "tr064";
public static final ThingTypeUID THING_TYPE_GENERIC = new ThingTypeUID(BINDING_ID, "generic");
public static final ThingTypeUID THING_TYPE_FRITZBOX = new ThingTypeUID(BINDING_ID, "fritzbox");
public static final ThingTypeUID THING_TYPE_SUBDEVICE = new ThingTypeUID(BINDING_ID, "subdevice");
public static final ThingTypeUID THING_TYPE_SUBDEVICE_LAN = new ThingTypeUID(BINDING_ID, "subdeviceLan");
public static final List<ChannelTypeDescription> CHANNEL_TYPES = Util.readXMLChannelConfig();
}

View File

@ -0,0 +1,72 @@
/**
* Copyright (c) 2010-2020 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 static org.openhab.binding.tr064.internal.Tr064BindingConstants.CHANNEL_TYPES;
import java.util.Collection;
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.type.*;
import org.openhab.core.types.StateDescriptionFragmentBuilder;
import org.osgi.service.component.annotations.Component;
/**
* The {@link Tr064ChannelTypeProvider} is used for providing dynamic channel types
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
@Component(service = { ChannelTypeProvider.class, Tr064ChannelTypeProvider.class })
public class Tr064ChannelTypeProvider implements ChannelTypeProvider {
private final Map<ChannelTypeUID, ChannelType> channelTypeMap = new ConcurrentHashMap<>();
public Tr064ChannelTypeProvider() {
CHANNEL_TYPES.forEach(channelTypeDescription -> {
ChannelTypeUID channelTypeUID = new ChannelTypeUID(Tr064BindingConstants.BINDING_ID,
channelTypeDescription.getName());
// create state description
StateDescriptionFragmentBuilder stateDescriptionFragmentBuilder = StateDescriptionFragmentBuilder.create()
.withReadOnly(channelTypeDescription.getSetAction() == null);
if (channelTypeDescription.getItem().getStatePattern() != null) {
stateDescriptionFragmentBuilder.withPattern(channelTypeDescription.getItem().getStatePattern());
}
// create channel type
ChannelTypeBuilder<StateChannelTypeBuilder> channelTypeBuilder = ChannelTypeBuilder
.state(channelTypeUID, channelTypeDescription.getLabel(),
channelTypeDescription.getItem().getType())
.withStateDescriptionFragment(stateDescriptionFragmentBuilder.build())
.isAdvanced(channelTypeDescription.isAdvanced());
if (channelTypeDescription.getDescription() != null) {
channelTypeBuilder.withDescription(channelTypeDescription.getDescription());
}
channelTypeMap.put(channelTypeUID, channelTypeBuilder.build());
});
}
@Override
public Collection<ChannelType> getChannelTypes(@Nullable Locale locale) {
return channelTypeMap.values();
}
@Override
public @Nullable ChannelType getChannelType(ChannelTypeUID channelTypeUID, @Nullable Locale locale) {
return channelTypeMap.get(channelTypeUID);
}
}

View File

@ -0,0 +1,33 @@
/**
* Copyright (c) 2010-2020 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 org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link Tr064CommunicationException} is thrown for communication errors
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class Tr064CommunicationException extends Exception {
private static final long serialVersionUID = 1L;
public Tr064CommunicationException(Exception e) {
super(e);
}
public Tr064CommunicationException(String s) {
super(s);
}
}

View File

@ -0,0 +1,117 @@
/**
* Copyright (c) 2010-2020 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 static org.openhab.binding.tr064.internal.Tr064BindingConstants.THING_TYPE_SUBDEVICE;
import static org.openhab.binding.tr064.internal.Tr064BindingConstants.THING_TYPE_SUBDEVICE_LAN;
import java.util.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.tr064.internal.dto.scpd.root.SCPDDeviceType;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BridgeHandler;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.util.UIDUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link Tr064DiscoveryService} discovers sub devices of a root device.
*
* @author Jan N. Klug - Initial contribution
*/
@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);
private final Logger logger = LoggerFactory.getLogger(Tr064DiscoveryService.class);
private @Nullable Tr064RootHandler bridgeHandler;
public Tr064DiscoveryService() {
super(SEARCH_TIME);
}
@Override
public void setThingHandler(ThingHandler thingHandler) {
if (thingHandler instanceof Tr064RootHandler) {
this.bridgeHandler = (Tr064RootHandler) thingHandler;
}
}
@Override
public @Nullable ThingHandler getThingHandler() {
return bridgeHandler;
}
@Override
public void deactivate() {
BridgeHandler bridgeHandler = this.bridgeHandler;
if (bridgeHandler == null) {
logger.warn("Bridgehandler not found, could not cleanup discovery results.");
return;
}
removeOlderResults(new Date().getTime(), bridgeHandler.getThing().getUID());
}
@Override
public Set<ThingTypeUID> getSupportedThingTypes() {
return SUPPORTED_THING_TYPES;
}
@Override
public void startScan() {
Tr064RootHandler bridgeHandler = this.bridgeHandler;
if (bridgeHandler == null) {
logger.warn("Could not start discovery, bridge handler not set");
return;
}
List<SCPDDeviceType> devices = bridgeHandler.getAllSubDevices();
ThingUID bridgeUID = bridgeHandler.getThing().getUID();
devices.forEach(device -> {
logger.trace("Trying to add {} to discovery results on {}", device, bridgeUID);
String udn = device.getUDN();
if (udn != null) {
ThingTypeUID thingTypeUID;
if ("urn:dslforum-org:device:LANDevice:1".equals(device.getDeviceType())) {
thingTypeUID = THING_TYPE_SUBDEVICE_LAN;
} else {
thingTypeUID = THING_TYPE_SUBDEVICE;
}
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();
thingDiscovered(result);
}
});
}
@Override
protected synchronized void stopScan() {
super.stopScan();
removeOlderResults(getTimestampOfLastScan());
}
}

View File

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

View File

@ -0,0 +1,95 @@
/**
* Copyright (c) 2010-2020 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 static org.openhab.binding.tr064.internal.Tr064BindingConstants.THING_TYPE_FRITZBOX;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.tr064.internal.phonebook.PhonebookProfileFactory;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link Tr064HandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
@Component(service = { ThingHandlerFactory.class }, configurationPid = "binding.tr064")
public class Tr064HandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Stream
.of(Tr064RootHandler.SUPPORTED_THING_TYPES, Tr064SubHandler.SUPPORTED_THING_TYPES).flatMap(Set::stream)
.collect(Collectors.toSet());
private final HttpClient httpClient;
private final PhonebookProfileFactory phonebookProfileFactory;
// the Tr064ChannelTypeProvider is needed for creating the channels and
// referenced here to make sure it is available before things are
// initialized
@SuppressWarnings("unused")
private final Tr064ChannelTypeProvider channelTypeProvider;
@Activate
public Tr064HandlerFactory(@Reference HttpClientFactory httpClientFactory,
@Reference Tr064ChannelTypeProvider channelTypeProvider,
@Reference PhonebookProfileFactory phonebookProfileFactory) {
httpClient = httpClientFactory.getCommonHttpClient();
this.channelTypeProvider = channelTypeProvider;
this.phonebookProfileFactory = phonebookProfileFactory;
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (Tr064RootHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) {
Tr064RootHandler handler = new Tr064RootHandler((Bridge) thing, httpClient);
if (thingTypeUID.equals(THING_TYPE_FRITZBOX)) {
phonebookProfileFactory.registerPhonebookProvider(handler);
}
return handler;
} else if (Tr064SubHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) {
return new Tr064SubHandler(thing);
}
return null;
}
@Override
protected void removeHandler(ThingHandler thingHandler) {
if (thingHandler instanceof Tr064RootHandler) {
phonebookProfileFactory.unregisterPhonebookProvider((Tr064RootHandler) thingHandler);
}
}
}

View File

@ -0,0 +1,386 @@
/**
* Copyright (c) 2010-2020 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 static org.openhab.binding.tr064.internal.Tr064BindingConstants.THING_TYPE_FRITZBOX;
import static org.openhab.binding.tr064.internal.Tr064BindingConstants.THING_TYPE_GENERIC;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPMessage;
import org.eclipse.jdt.annotation.NonNullByDefault;
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.util.DigestAuthentication;
import org.openhab.binding.tr064.internal.config.Tr064ChannelConfig;
import org.openhab.binding.tr064.internal.config.Tr064RootConfiguration;
import org.openhab.binding.tr064.internal.dto.scpd.root.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.PhonebookProvider;
import org.openhab.binding.tr064.internal.phonebook.Tr064PhonebookImpl;
import org.openhab.binding.tr064.internal.util.SCPDUtil;
import org.openhab.binding.tr064.internal.util.Util;
import org.openhab.core.cache.ExpiringCacheMap;
import org.openhab.core.thing.*;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.thing.binding.builder.ThingBuilder;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link Tr064RootHandler} is responsible for handling commands, which are
* sent to one of the channels and update channel values
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProvider {
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_GENERIC, THING_TYPE_FRITZBOX);
private static final int RETRY_INTERVAL = 60;
private static final Set<String> PROPERTY_ARGUMENTS = Set.of("NewSerialNumber", "NewSoftwareVersion",
"NewModelName");
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;
private String endpointBaseURL = "http://fritz.box:49000";
private final Map<ChannelUID, Tr064ChannelConfig> channels = new HashMap<>();
// caching is used to prevent excessive calls to the same action
private final ExpiringCacheMap<ChannelUID, State> stateCache = new ExpiringCacheMap<>(2000);
private Collection<Phonebook> phonebooks = Collections.emptyList();
private @Nullable ScheduledFuture<?> connectFuture;
private @Nullable ScheduledFuture<?> pollFuture;
private @Nullable ScheduledFuture<?> phonebookFuture;
Tr064RootHandler(Bridge bridge, HttpClient httpClient) {
super(bridge);
this.httpClient = httpClient;
soapConnector = new SOAPConnector(httpClient, endpointBaseURL);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
Tr064ChannelConfig channelConfig = channels.get(channelUID);
if (channelConfig == null) {
logger.trace("Channel {} not supported.", channelUID);
return;
}
if (command instanceof RefreshType) {
SOAPConnector soapConnector = this.soapConnector;
State state = stateCache.putIfAbsentAndGet(channelUID,
() -> soapConnector.getChannelStateFromDevice(channelConfig, channels, stateCache));
if (state != null) {
updateState(channelUID, state);
}
return;
}
if (channelConfig.getChannelTypeDescription().getSetAction() == null) {
logger.debug("Discarding command {} to {}, read-only channel", command, channelUID);
return;
}
scheduler.execute(() -> soapConnector.sendChannelCommandToDevice(channelConfig, command));
}
@Override
public void initialize() {
config = getConfigAs(Tr064RootConfiguration.class);
if (!config.isValid()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"At least one mandatory configuration field is empty");
return;
}
endpointBaseURL = "http://" + config.host + ":49000";
updateStatus(ThingStatus.UNKNOWN);
connectFuture = scheduler.scheduleWithFixedDelay(this::internalInitialize, 0, RETRY_INTERVAL, TimeUnit.SECONDS);
}
/**
* internal thing initializer (sets SCPDUtil and connects to remote device)
*/
private void internalInitialize() {
try {
scpdUtil = new SCPDUtil(httpClient, endpointBaseURL);
} catch (SCPDException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"could not get device definitions from " + config.host);
return;
}
if (establishSecureConnectionAndUpdateProperties()) {
removeConnectScheduler();
// connection successful, check channels
ThingBuilder thingBuilder = editThing();
thingBuilder.withoutChannels(thing.getChannels());
final SCPDUtil scpdUtil = this.scpdUtil;
if (scpdUtil != null) {
Util.checkAvailableChannels(thing, thingBuilder, scpdUtil, "", deviceType, channels);
updateThing(thingBuilder.build());
}
installPolling();
updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
}
}
private void removeConnectScheduler() {
final ScheduledFuture<?> connectFuture = this.connectFuture;
if (connectFuture != null) {
connectFuture.cancel(true);
this.connectFuture = null;
}
}
@Override
public void dispose() {
removeConnectScheduler();
uninstallPolling();
stateCache.clear();
super.dispose();
}
/**
* poll remote device for channel values
*/
private void poll() {
channels.forEach((channelUID, channelConfig) -> {
if (isLinked(channelUID)) {
State state = stateCache.putIfAbsentAndGet(channelUID,
() -> soapConnector.getChannelStateFromDevice(channelConfig, channels, stateCache));
if (state != null) {
updateState(channelUID, state);
}
}
});
}
/**
* establish the connection - get secure port (if avallable), install authentication, get device properties
*
* @return true if successful
*/
private boolean establishSecureConnectionAndUpdateProperties() {
final SCPDUtil scpdUtil = this.scpdUtil;
if (scpdUtil != null) {
try {
SCPDDeviceType device = scpdUtil.getDevice("")
.orElseThrow(() -> new SCPDException("Root device not found"));
SCPDServiceType deviceService = device.getServiceList().stream()
.filter(service -> service.getServiceId().equals("urn:DeviceInfo-com:serviceId:DeviceInfo1"))
.findFirst().orElseThrow(() -> new SCPDException(
"service 'urn:DeviceInfo-com:serviceId:DeviceInfo1' not found"));
this.deviceType = device.getDeviceType();
// try to get security (https) port
SOAPMessage soapResponse = soapConnector.doSOAPRequest(deviceService, "GetSecurityPort",
Collections.emptyMap());
if (!soapResponse.getSOAPBody().hasFault()) {
SOAPValueConverter soapValueConverter = new SOAPValueConverter(httpClient);
soapValueConverter.getStateFromSOAPValue(soapResponse, "NewSecurityPort", null)
.ifPresentOrElse(port -> {
endpointBaseURL = "https://" + config.host + ":" + port.toString();
soapConnector = new SOAPConnector(httpClient, endpointBaseURL);
logger.debug("endpointBaseURL is now '{}'", endpointBaseURL);
}, () -> logger.warn("Could not determine secure port, disabling https"));
} else {
logger.warn("Could not determine secure port, disabling https");
}
// clear auth cache and force re-auth
httpClient.getAuthenticationStore().clearAuthenticationResults();
AuthenticationStore auth = httpClient.getAuthenticationStore();
auth.addAuthentication(new DigestAuthentication(new URI(endpointBaseURL), Authentication.ANY_REALM,
config.user, config.password));
// check & update properties
SCPDActionType getInfoAction = scpdUtil.getService(deviceService.getServiceId())
.orElseThrow(() -> new SCPDException(
"Could not get service definition for 'urn:DeviceInfo-com:serviceId:DeviceInfo1'"))
.getActionList().stream().filter(action -> action.getName().equals("GetInfo")).findFirst()
.orElseThrow(() -> new SCPDException("Action 'GetInfo' not found"));
SOAPMessage soapResponse1 = soapConnector.doSOAPRequest(deviceService, getInfoAction.getName(),
Collections.emptyMap());
SOAPValueConverter soapValueConverter = new SOAPValueConverter(httpClient);
Map<String, String> properties = editProperties();
PROPERTY_ARGUMENTS.forEach(argumentName -> getInfoAction.getArgumentList().stream()
.filter(argument -> argument.getName().equals(argumentName)).findFirst()
.ifPresent(argument -> soapValueConverter
.getStateFromSOAPValue(soapResponse1, argumentName, null).ifPresent(value -> properties
.put(argument.getRelatedStateVariable(), value.toString()))));
properties.put("deviceType", device.getDeviceType());
updateProperties(properties);
return true;
} catch (SCPDException | SOAPException | Tr064CommunicationException | URISyntaxException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
return false;
}
}
return false;
}
/**
* get all sub devices of this root device (used for discovery)
*
* @return the list
*/
public List<SCPDDeviceType> getAllSubDevices() {
final SCPDUtil scpdUtil = this.scpdUtil;
return (scpdUtil == null) ? Collections.emptyList() : scpdUtil.getAllSubDevices();
}
/**
* get the SOAP connector (used by sub devices for communication with the remote device)
*
* @return the SOAP connector
*/
public SOAPConnector getSOAPConnector() {
return soapConnector;
}
/**
* get the SCPD processing utility
*
* @return the SCPD utility (or null if not available)
*/
public @Nullable SCPDUtil getSCPDUtil() {
return scpdUtil;
}
/**
* uninstall the polling
*/
private void uninstallPolling() {
final ScheduledFuture<?> pollFuture = this.pollFuture;
if (pollFuture != null) {
pollFuture.cancel(true);
this.pollFuture = null;
}
final ScheduledFuture<?> phonebookFuture = this.phonebookFuture;
if (phonebookFuture != null) {
phonebookFuture.cancel(true);
this.phonebookFuture = null;
}
}
/**
* install the polling
*/
private void installPolling() {
uninstallPolling();
pollFuture = scheduler.scheduleWithFixedDelay(this::poll, 0, config.refresh, TimeUnit.SECONDS);
if (config.phonebookInterval > 0) {
phonebookFuture = scheduler.scheduleWithFixedDelay(this::retrievePhonebooks, 0, config.phonebookInterval,
TimeUnit.SECONDS);
}
}
@SuppressWarnings("unchecked")
private Collection<Phonebook> processPhonebookList(SOAPMessage soapMessagePhonebookList,
SCPDServiceType scpdService) {
SOAPValueConverter soapValueConverter = new SOAPValueConverter(httpClient);
return (Collection<Phonebook>) soapValueConverter
.getStateFromSOAPValue(soapMessagePhonebookList, "NewPhonebookList", null)
.map(phonebookList -> Arrays.stream(phonebookList.toString().split(","))).orElse(Stream.empty())
.map(index -> {
try {
SOAPMessage soapMessageURL = soapConnector.doSOAPRequest(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());
}
private void retrievePhonebooks() {
String serviceId = "urn:X_AVM-DE_OnTel-com:serviceId:X_AVM-DE_OnTel1";
SCPDUtil scpdUtil = this.scpdUtil;
if (scpdUtil == null) {
logger.warn("Cannot find SCPDUtil. This is most likely a programming error.");
return;
}
Optional<SCPDServiceType> scpdService = scpdUtil.getDevice("").flatMap(deviceType -> deviceType.getServiceList()
.stream().filter(service -> service.getServiceId().equals(serviceId)).findFirst());
phonebooks = scpdService.map(service -> {
try {
return processPhonebookList(
soapConnector.doSOAPRequest(service, "GetPhonebookList", Collections.emptyMap()), service);
} catch (Tr064CommunicationException e) {
return Collections.<Phonebook> emptyList();
}
}).orElse(Collections.emptyList());
if (phonebooks.isEmpty()) {
logger.warn("Could not get phonebooks for thing {}", thing.getUID());
}
}
@Override
public Optional<Phonebook> getPhonebookByName(String name) {
return phonebooks.stream().filter(p -> name.equals(p.getName())).findAny();
}
@Override
public Collection<Phonebook> getPhonebooks() {
return phonebooks;
}
@Override
public ThingUID getUID() {
return thing.getUID();
}
@Override
public String getFriendlyName() {
String friendlyName = thing.getLabel();
return friendlyName != null ? friendlyName : getUID().getId();
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Set.of(Tr064DiscoveryService.class);
}
}

View File

@ -0,0 +1,254 @@
/**
* Copyright (c) 2010-2020 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 static org.openhab.binding.tr064.internal.Tr064BindingConstants.THING_TYPE_SUBDEVICE;
import static org.openhab.binding.tr064.internal.Tr064BindingConstants.THING_TYPE_SUBDEVICE_LAN;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.tr064.internal.config.Tr064ChannelConfig;
import org.openhab.binding.tr064.internal.config.Tr064SubConfiguration;
import org.openhab.binding.tr064.internal.dto.scpd.root.SCPDDeviceType;
import org.openhab.binding.tr064.internal.util.SCPDUtil;
import org.openhab.binding.tr064.internal.util.Util;
import org.openhab.core.cache.ExpiringCacheMap;
import org.openhab.core.thing.*;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.builder.ThingBuilder;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link Tr064SubHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class Tr064SubHandler extends BaseThingHandler {
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_SUBDEVICE,
THING_TYPE_SUBDEVICE_LAN);
private static final int RETRY_INTERVAL = 60;
private final Logger logger = LoggerFactory.getLogger(Tr064SubHandler.class);
private Tr064SubConfiguration config = new Tr064SubConfiguration();
private String deviceType = "";
private boolean isInitialized = false;
private final Map<ChannelUID, Tr064ChannelConfig> channels = new HashMap<>();
// caching is used to prevent excessive calls to the same action
private final ExpiringCacheMap<ChannelUID, State> stateCache = new ExpiringCacheMap<>(2000);
private @Nullable SOAPConnector soapConnector;
private @Nullable ScheduledFuture<?> connectFuture;
private @Nullable ScheduledFuture<?> pollFuture;
Tr064SubHandler(Thing thing) {
super(thing);
}
@Override
@SuppressWarnings("null")
public void handleCommand(ChannelUID channelUID, Command command) {
Tr064ChannelConfig channelConfig = channels.get(channelUID);
if (channelConfig == null) {
logger.trace("Channel {} not supported.", channelUID);
return;
}
if (command instanceof RefreshType) {
State state = stateCache.putIfAbsentAndGet(channelUID, () -> soapConnector == null ? UnDefType.UNDEF
: soapConnector.getChannelStateFromDevice(channelConfig, channels, stateCache));
if (state != null) {
updateState(channelUID, state);
}
return;
}
if (channelConfig.getChannelTypeDescription().getSetAction() == null) {
logger.debug("Discarding command {} to {}, read-only channel", command, channelUID);
return;
}
scheduler.execute(() -> {
if (soapConnector == null) {
logger.warn("Could not send command because connector not available");
} else {
soapConnector.sendChannelCommandToDevice(channelConfig, command);
}
});
}
@Override
public void initialize() {
config = getConfigAs(Tr064SubConfiguration.class);
if (!config.isValid()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"One or more mandatory configuration fields are empty");
return;
}
final Bridge bridge = getBridge();
if (bridge != null && bridge.getStatus().equals(ThingStatus.ONLINE)) {
updateStatus(ThingStatus.UNKNOWN);
connectFuture = scheduler.scheduleWithFixedDelay(this::internalInitialize, 0, 30, TimeUnit.SECONDS);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
}
}
private void internalInitialize() {
final Bridge bridge = getBridge();
if (bridge == null) {
return;
}
final Tr064RootHandler bridgeHandler = (Tr064RootHandler) bridge.getHandler();
if (bridgeHandler == null) {
logger.warn("Bridge-handler is null in thing {}", thing.getUID());
return;
}
final SCPDUtil scpdUtil = bridgeHandler.getSCPDUtil();
if (scpdUtil == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Could not get device definitions");
return;
}
if (checkProperties(scpdUtil)) {
// properties set, check channels
ThingBuilder thingBuilder = editThing();
thingBuilder.withoutChannels(thing.getChannels());
Util.checkAvailableChannels(thing, thingBuilder, scpdUtil, config.uuid, deviceType, channels);
updateThing(thingBuilder.build());
// remove connect scheduler
removeConnectScheduler();
soapConnector = bridgeHandler.getSOAPConnector();
isInitialized = true;
installPolling();
updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
}
}
private void removeConnectScheduler() {
final ScheduledFuture<?> connectFuture = this.connectFuture;
if (connectFuture != null) {
connectFuture.cancel(true);
this.connectFuture = null;
}
}
@Override
public void dispose() {
removeConnectScheduler();
uninstallPolling();
stateCache.clear();
isInitialized = false;
super.dispose();
}
/**
* poll remote device for channel values
*/
private void poll() {
SOAPConnector soapConnector = this.soapConnector;
channels.forEach((channelUID, channelConfig) -> {
if (isLinked(channelUID)) {
State state = stateCache.putIfAbsentAndGet(channelUID, () -> soapConnector == null ? UnDefType.UNDEF
: soapConnector.getChannelStateFromDevice(channelConfig, channels, stateCache));
if (state != null) {
updateState(channelUID, state);
}
}
});
}
/**
* get device properties from remote device
*
* @param scpdUtil the SCPD util of this device
* @return true if successfull
*/
private boolean checkProperties(SCPDUtil scpdUtil) {
try {
SCPDDeviceType device = scpdUtil.getDevice(config.uuid)
.orElseThrow(() -> new SCPDException("Could not find device " + config.uuid));
String deviceType = device.getDeviceType();
if (deviceType == null) {
throw new SCPDException("deviceType can't be null ");
}
this.deviceType = deviceType;
Map<String, String> properties = editProperties();
properties.put("deviceType", deviceType);
updateProperties(properties);
return true;
} catch (SCPDException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Failed to update device properties: " + e.getMessage());
return false;
}
}
@Override
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
if (!bridgeStatusInfo.getStatus().equals(ThingStatus.ONLINE)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
removeConnectScheduler();
} else {
if (isInitialized) {
updateStatus(ThingStatus.ONLINE);
} else {
updateStatus(ThingStatus.UNKNOWN);
connectFuture = scheduler.scheduleWithFixedDelay(this::internalInitialize, 0, RETRY_INTERVAL,
TimeUnit.SECONDS);
}
}
}
/**
* uninstall update polling
*/
private void uninstallPolling() {
final ScheduledFuture<?> pollFuture = this.pollFuture;
if (pollFuture != null) {
pollFuture.cancel(true);
this.pollFuture = null;
}
}
/**
* install update polling
*/
private void installPolling() {
uninstallPolling();
pollFuture = scheduler.scheduleWithFixedDelay(this::poll, 0, config.refresh, TimeUnit.SECONDS);
}
}

View File

@ -0,0 +1,25 @@
/**
* Copyright (c) 2010-2020 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.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link Tr064BaseThingConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class Tr064BaseThingConfiguration {
public int refresh = 60;
}

View File

@ -0,0 +1,86 @@
/**
* Copyright (c) 2010-2020 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.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.tr064.internal.dto.config.ChannelTypeDescription;
import org.openhab.binding.tr064.internal.dto.scpd.root.SCPDServiceType;
import org.openhab.binding.tr064.internal.dto.scpd.service.SCPDActionType;
/**
* The {@link Tr064ChannelConfig} class holds a channel configuration
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class Tr064ChannelConfig {
private ChannelTypeDescription channelTypeDescription;
private SCPDServiceType service;
private @Nullable SCPDActionType getAction;
private String dataType = "";
private @Nullable String parameter;
public Tr064ChannelConfig(ChannelTypeDescription channelTypeDescription, SCPDServiceType service) {
this.channelTypeDescription = channelTypeDescription;
this.service = service;
}
public Tr064ChannelConfig(Tr064ChannelConfig o) {
this.channelTypeDescription = o.channelTypeDescription;
this.service = o.service;
this.getAction = o.getAction;
this.dataType = o.dataType;
this.parameter = o.parameter;
}
public ChannelTypeDescription getChannelTypeDescription() {
return channelTypeDescription;
}
public SCPDServiceType getService() {
return service;
}
public String getDataType() {
return dataType;
}
public void setDataType(String dataType) {
this.dataType = dataType;
}
public @Nullable SCPDActionType getGetAction() {
return getAction;
}
public void setGetAction(SCPDActionType getAction) {
this.getAction = getAction;
}
public @Nullable String getParameter() {
return parameter;
}
public void setParameter(String parameter) {
this.parameter = parameter;
}
@Override
public String toString() {
final SCPDActionType getAction = this.getAction;
return "Tr064ChannelConfig{" + "channelType=" + channelTypeDescription.getName() + ", getAction="
+ ((getAction == null) ? "(null)" : getAction.getName()) + ", dataType='" + dataType + ", parameter='"
+ parameter + "'}";
}
}

View File

@ -0,0 +1,43 @@
/**
* Copyright (c) 2010-2020 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.config;
import java.util.Collections;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link Tr064RootConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class Tr064RootConfiguration extends Tr064BaseThingConfiguration {
public String host = "";
public String user = "dslf-config";
public String password = "";
/* following parameters only available in fritzbox thing */
public List<String> tamIndices = Collections.emptyList();
public List<String> callDeflectionIndices = Collections.emptyList();
public List<String> missedCallDays = Collections.emptyList();
public List<String> rejectedCallDays = Collections.emptyList();
public List<String> inboundCallDays = Collections.emptyList();
public List<String> outboundCallDays = Collections.emptyList();
public int phonebookInterval = 0;
public boolean isValid() {
return !host.isEmpty() && !user.isEmpty() && !password.isEmpty();
}
}

View File

@ -0,0 +1,35 @@
/**
* Copyright (c) 2010-2020 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.config;
import java.util.Collections;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link Tr064SubConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class Tr064SubConfiguration extends Tr064BaseThingConfiguration {
public String uuid = "";
// Lan Device
public List<String> macOnline = Collections.emptyList();
public boolean isValid() {
return !uuid.isEmpty();
}
}

View File

@ -0,0 +1,42 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.tr064.internal.phonebook;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link Phonebook} interface is used by phonebook providers to implement phonebooks
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public interface Phonebook {
/**
* get the name of this phonebook
*
* @return
*/
String getName();
/**
* lookup a number in this phonebook
*
* @param number the number
* @param matchCount the number of matching digits, counting from far right
* @return an Optional containing the name associated with this number (empty of not present)
*/
Optional<String> lookupNumber(String number, int matchCount);
}

View File

@ -0,0 +1,142 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.tr064.internal.phonebook;
import java.util.Map;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.profiles.*;
import org.openhab.core.transform.TransformationService;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.util.UIDUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link PhonebookProfile} class provides a profile for resolving phone number strings to names
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class PhonebookProfile implements StateProfile {
public static final ProfileTypeUID PHONEBOOK_PROFILE_TYPE_UID = new ProfileTypeUID(
TransformationService.TRANSFORM_PROFILE_SCOPE, "PHONEBOOK");
public static final ProfileType PHONEBOOK_PROFILE_TYPE = ProfileTypeBuilder
.newState(PhonebookProfile.PHONEBOOK_PROFILE_TYPE_UID, "Phonebook").build();
public static final String PHONEBOOK_PARAM = "phonebook";
private static final String MATCH_COUNT_PARAM = "matchCount";
private final Logger logger = LoggerFactory.getLogger(PhonebookProfile.class);
private final ProfileCallback callback;
private final @Nullable String phonebookName;
private final @Nullable ThingUID thingUID;
private final Map<ThingUID, PhonebookProvider> phonebookProviders;
private final int matchCount;
public PhonebookProfile(ProfileCallback callback, ProfileContext context,
Map<ThingUID, PhonebookProvider> phonebookProviders) {
this.callback = callback;
this.phonebookProviders = phonebookProviders;
Configuration configuration = context.getConfiguration();
Object phonebookParam = configuration.get(PHONEBOOK_PARAM);
Object matchCountParam = configuration.get(MATCH_COUNT_PARAM);
logger.debug("Profile configured with '{}'='{}', '{}'='{}'", PHONEBOOK_PARAM, phonebookParam, MATCH_COUNT_PARAM,
matchCountParam);
ThingUID thingUID;
String phonebookName = null;
int matchCount = 0;
try {
if (!(phonebookParam instanceof String)
|| ((matchCountParam != null) && !(matchCountParam instanceof String))) {
throw new IllegalArgumentException("Parameters need to be Strings");
}
String[] phonebookParams = ((String) phonebookParam).split(":");
if (phonebookParams.length > 2) {
throw new IllegalArgumentException("Could not split 'phonebook' parameter");
}
thingUID = new ThingUID(UIDUtils.decode(phonebookParams[0]));
if (phonebookParams.length == 2) {
phonebookName = UIDUtils.decode(phonebookParams[1]);
}
if (matchCountParam != null) {
matchCount = Integer.parseInt((String) matchCountParam);
}
} catch (IllegalArgumentException e) {
logger.warn("Could not initialize PHONEBOOK transformation profile: {}. Profile will be inactive.",
e.getMessage());
thingUID = null;
}
this.thingUID = thingUID;
this.phonebookName = phonebookName;
this.matchCount = matchCount;
}
@Override
public void onCommandFromItem(Command command) {
}
@Override
public void onCommandFromHandler(Command command) {
}
@Override
public void onStateUpdateFromHandler(State state) {
if (state instanceof StringType) {
PhonebookProvider provider = phonebookProviders.get(thingUID);
if (provider == null) {
logger.warn("Could not get phonebook provider with thing UID '{}'.", thingUID);
return;
}
final String phonebookName = this.phonebookName;
Optional<String> match;
if (phonebookName != null) {
match = provider.getPhonebookByName(phonebookName).or(() -> {
logger.warn("Could not get phonebook '{}' from provider '{}'", phonebookName, thingUID);
return Optional.empty();
}).flatMap(phonebook -> phonebook.lookupNumber(state.toString(), matchCount));
} else {
match = provider.getPhonebooks().stream().map(p -> p.lookupNumber(state.toString(), matchCount))
.filter(Optional::isPresent).map(Optional::get).findAny();
}
State newState = match.map(name -> (State) new StringType(name)).orElse(state);
if (newState == state) {
logger.debug("Number '{}' not found in phonebook '{}' from provider '{}'", state, phonebookName,
thingUID);
}
callback.sendUpdate(newState);
}
}
@Override
public ProfileTypeUID getProfileTypeUID() {
return PHONEBOOK_PROFILE_TYPE_UID;
}
@Override
public void onStateUpdateFromItem(State state) {
}
}

View File

@ -0,0 +1,118 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.tr064.internal.phonebook;
import java.net.URI;
import java.util.Collection;
import java.util.Collections;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.config.core.ConfigOptionProvider;
import org.openhab.core.config.core.ParameterOption;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.profiles.Profile;
import org.openhab.core.thing.profiles.ProfileCallback;
import org.openhab.core.thing.profiles.ProfileContext;
import org.openhab.core.thing.profiles.ProfileFactory;
import org.openhab.core.thing.profiles.ProfileType;
import org.openhab.core.thing.profiles.ProfileTypeProvider;
import org.openhab.core.thing.profiles.ProfileTypeUID;
import org.openhab.core.util.UIDUtils;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link PhonebookProfileFactory} class is used to create phonebook profiles
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
@Component(service = { ProfileFactory.class, ProfileTypeProvider.class, PhonebookProfileFactory.class,
ConfigOptionProvider.class })
public class PhonebookProfileFactory implements ProfileFactory, ProfileTypeProvider, ConfigOptionProvider {
private final Logger logger = LoggerFactory.getLogger(PhonebookProfileFactory.class);
private final Map<ThingUID, PhonebookProvider> phonebookProviders = new ConcurrentHashMap<>();
@Override
public @Nullable Profile createProfile(ProfileTypeUID profileTypeUID, ProfileCallback callback,
ProfileContext profileContext) {
return new PhonebookProfile(callback, profileContext, phonebookProviders);
}
@Override
public Collection<ProfileTypeUID> getSupportedProfileTypeUIDs() {
return Collections.singleton(PhonebookProfile.PHONEBOOK_PROFILE_TYPE_UID);
}
@Override
public Collection<ProfileType> getProfileTypes(@Nullable Locale locale) {
return Collections.singleton(PhonebookProfile.PHONEBOOK_PROFILE_TYPE);
}
/**
* register a phonebook provider
*
* @param phonebookProvider the provider that shall be added
*/
public void registerPhonebookProvider(PhonebookProvider phonebookProvider) {
if (phonebookProviders.put(phonebookProvider.getUID(), phonebookProvider) != null) {
logger.warn("Tried to register a phonebook provider with UID '{}' for the second time.",
phonebookProvider.getUID());
}
}
/**
* unregister a phonebook provider
*
* @param phonebookProvider the provider that shall be removed
*/
public void unregisterPhonebookProvider(PhonebookProvider phonebookProvider) {
if (phonebookProviders.remove(phonebookProvider.getUID()) == null) {
logger.warn("Tried to unregister a phonebook provider with UID '{}' but it was not found.",
phonebookProvider.getUID());
}
}
private Stream<ParameterOption> createPhonebookList(Map.Entry<ThingUID, PhonebookProvider> entry) {
String thingUid = UIDUtils.encode(entry.getKey().toString());
String thingName = entry.getValue().getFriendlyName();
Stream<ParameterOption> parameterOptions = entry.getValue().getPhonebooks().stream()
.map(phonebook -> new ParameterOption(thingUid + ":" + UIDUtils.encode(phonebook.getName()),
thingName + " " + phonebook.getName()));
if (parameterOptions.count() > 0) {
return Stream.concat(Stream.of(new ParameterOption(thingUid, thingName)), parameterOptions);
}
return parameterOptions;
}
@Override
public @Nullable Collection<ParameterOption> getParameterOptions(URI uri, String s, @Nullable String s1,
@Nullable Locale locale) {
if (uri.getSchemeSpecificPart().equals(PhonebookProfile.PHONEBOOK_PROFILE_TYPE_UID.toString())
&& s.equals(PhonebookProfile.PHONEBOOK_PARAM)) {
return phonebookProviders.entrySet().stream().flatMap(this::createPhonebookList)
.collect(Collectors.toSet());
}
return null;
}
}

View File

@ -0,0 +1,36 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.tr064.internal.phonebook;
import java.util.Collection;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingUID;
/**
* The {@link PhonebookProvider} interface provides methods to lookup a phone number from a phonebook
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public interface PhonebookProvider {
Optional<Phonebook> getPhonebookByName(String name);
Collection<Phonebook> getPhonebooks();
ThingUID getUID();
String getFriendlyName();
}

View File

@ -0,0 +1,100 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.tr064.internal.phonebook;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.transform.stream.StreamSource;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.http.HttpMethod;
import org.openhab.binding.tr064.internal.dto.phonebook.NumberType;
import org.openhab.binding.tr064.internal.dto.phonebook.PhonebooksType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link Tr064PhonebookImpl} class implements a phonebook
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class Tr064PhonebookImpl implements Phonebook {
private final Logger logger = LoggerFactory.getLogger(Tr064PhonebookImpl.class);
private Map<String, String> phonebook = new HashMap<>();
private final HttpClient httpClient;
private final String phonebookUrl;
private String phonebookName = "";
public Tr064PhonebookImpl(HttpClient httpClient, String phonebookUrl) {
this.httpClient = httpClient;
this.phonebookUrl = phonebookUrl;
getPhonebook();
}
private void getPhonebook() {
try {
ContentResponse contentResponse = httpClient.newRequest(phonebookUrl).method(HttpMethod.GET)
.timeout(2, TimeUnit.SECONDS).send();
InputStream xml = new ByteArrayInputStream(contentResponse.getContent());
JAXBContext context = JAXBContext.newInstance(PhonebooksType.class);
Unmarshaller um = context.createUnmarshaller();
PhonebooksType phonebooksType = um.unmarshal(new StreamSource(xml), PhonebooksType.class).getValue();
phonebookName = phonebooksType.getPhonebook().getName();
phonebook = phonebooksType.getPhonebook().getContact().stream().map(contact -> {
String contactName = contact.getPerson().getRealName();
return contact.getTelephony().getNumber().stream()
.collect(Collectors.toMap(NumberType::getValue, number -> contactName));
}).collect(HashMap::new, HashMap::putAll, HashMap::putAll);
logger.debug("Downloaded phonebook {}: {}", phonebookName, phonebook);
} catch (JAXBException | InterruptedException | ExecutionException | TimeoutException e) {
logger.warn("Failed to get phonebook with URL {}:", phonebookUrl, e);
}
}
@Override
public String getName() {
return phonebookName;
}
@Override
public Optional<String> lookupNumber(String number, int matchCount) {
String matchString = matchCount < number.length() ? number.substring(number.length() - matchCount) : number;
logger.trace("matchString for {} is {}", number, matchString);
return phonebook.keySet().stream().filter(n -> n.endsWith(matchString)).findAny().map(phonebook::get);
}
@Override
public String toString() {
return "Phonebook{" + "phonebookName='" + phonebookName + "', phonebook=" + phonebook + '}';
}
}

View File

@ -0,0 +1,150 @@
/**
* Copyright (c) 2010-2020 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.util;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.transform.stream.StreamSource;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.http.HttpMethod;
import org.openhab.binding.tr064.internal.SCPDException;
import org.openhab.binding.tr064.internal.dto.scpd.root.SCPDDeviceType;
import org.openhab.binding.tr064.internal.dto.scpd.root.SCPDRootType;
import org.openhab.binding.tr064.internal.dto.scpd.root.SCPDServiceType;
import org.openhab.binding.tr064.internal.dto.scpd.service.SCPDScpdType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link SCPDUtil} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class SCPDUtil {
private final Logger logger = LoggerFactory.getLogger(SCPDUtil.class);
private final HttpClient httpClient;
private 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 {
this.httpClient = httpClient;
SCPDRootType scpdRoot = getAndUnmarshalSCPD(endpoint + "/tr64desc.xml", SCPDRootType.class);
if (scpdRoot == null) {
throw new SCPDException("could not get SCPD root");
}
this.scpdRoot = scpdRoot;
scpdDevicesList.addAll(flatDeviceList(scpdRoot.getDevice()).collect(Collectors.toList()));
for (SCPDDeviceType device : scpdDevicesList) {
for (SCPDServiceType service : device.getServiceList()) {
SCPDScpdType scpd = serviceMap.computeIfAbsent(service.getServiceId(),
serviceId -> getAndUnmarshalSCPD(endpoint + service.getSCPDURL(), SCPDScpdType.class));
if (scpd == null) {
throw new SCPDException("could not get SCPD service");
}
}
}
}
/**
* generic unmarshaller
*
* @param uri the uri of the XML file
* @param clazz the class describing the XML file
* @return unmarshalling result
*/
private <T> @Nullable T getAndUnmarshalSCPD(String uri, Class<T> clazz) {
try {
ContentResponse contentResponse = httpClient.newRequest(uri).timeout(2, TimeUnit.SECONDS)
.method(HttpMethod.GET).send();
InputStream xml = new ByteArrayInputStream(contentResponse.getContent());
JAXBContext context = JAXBContext.newInstance(clazz);
Unmarshaller um = context.createUnmarshaller();
return um.unmarshal(new StreamSource(xml), clazz).getValue();
} catch (ExecutionException | InterruptedException | TimeoutException e) {
logger.debug("HTTP Failed to GET uri '{}': {}", uri, e.getMessage());
} catch (JAXBException e) {
logger.debug("Unmarshalling failed: {}", e.getMessage());
}
return null;
}
/**
* recursively flatten the device tree to a stream
*
* @param device a device
* @return stream of sub-devices
*/
private Stream<SCPDDeviceType> flatDeviceList(SCPDDeviceType device) {
return Stream.concat(Stream.of(device), device.getDeviceList().stream().flatMap(this::flatDeviceList));
}
/**
* get a list of all sub-devices (root device not included)
*
* @return the device list
*/
public List<SCPDDeviceType> getAllSubDevices() {
return scpdDevicesList.stream().filter(device -> !device.getUDN().equals(scpdRoot.getDevice().getUDN()))
.collect(Collectors.toList());
}
/**
* get a single device by it's UDN
*
* @param udn the device UDN
* @return the device
*/
public Optional<SCPDDeviceType> getDevice(String udn) {
if (udn.isEmpty()) {
return Optional.of(scpdRoot.getDevice());
} else {
return getAllSubDevices().stream().filter(device -> udn.equals(device.getUDN())).findFirst();
}
}
/**
* get a single service by it's serviceId
*
* @param serviceId the service id
* @return the service
*/
public Optional<SCPDScpdType> getService(String serviceId) {
return Optional.ofNullable(serviceMap.get(serviceId));
}
}

View File

@ -0,0 +1,293 @@
/**
* Copyright (c) 2010-2020 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.util;
import static org.openhab.binding.tr064.internal.Tr064BindingConstants.*;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPMessage;
import javax.xml.transform.stream.StreamSource;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.tr064.internal.ChannelConfigException;
import org.openhab.binding.tr064.internal.Tr064RootHandler;
import org.openhab.binding.tr064.internal.config.Tr064BaseThingConfiguration;
import org.openhab.binding.tr064.internal.config.Tr064ChannelConfig;
import org.openhab.binding.tr064.internal.config.Tr064RootConfiguration;
import org.openhab.binding.tr064.internal.config.Tr064SubConfiguration;
import org.openhab.binding.tr064.internal.dto.config.ActionType;
import org.openhab.binding.tr064.internal.dto.config.ChannelTypeDescription;
import org.openhab.binding.tr064.internal.dto.config.ChannelTypeDescriptions;
import org.openhab.binding.tr064.internal.dto.config.ParameterType;
import org.openhab.binding.tr064.internal.dto.scpd.root.SCPDServiceType;
import org.openhab.binding.tr064.internal.dto.scpd.service.*;
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.builder.ThingBuilder;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.util.UIDUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.NodeList;
/**
* The {@link Util} is a set of helper functions
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class Util {
private static final Logger LOGGER = LoggerFactory.getLogger(Util.class);
/**
* read the channel config from the resource file (static initialization)
*
* @return a list of all available channel configurations
*/
public static List<ChannelTypeDescription> readXMLChannelConfig() {
try {
InputStream resource = Thread.currentThread().getContextClassLoader().getResourceAsStream("channels.xml");
JAXBContext context = JAXBContext.newInstance(ChannelTypeDescriptions.class);
Unmarshaller um = context.createUnmarshaller();
JAXBElement<ChannelTypeDescriptions> root = um.unmarshal(new StreamSource(resource),
ChannelTypeDescriptions.class);
return root.getValue().getChannel();
} catch (JAXBException e) {
LOGGER.warn("Failed to read channel definitions", e);
return Collections.emptyList();
}
}
/**
* Extract an argument from an SCPD action definition
*
* @param scpdAction the action object
* @param argumentName the argument's name
* @param direction the direction (in or out)
* @return the requested argument object
* @throws ChannelConfigException if not found
*/
private static SCPDArgumentType getArgument(SCPDActionType scpdAction, String argumentName, SCPDDirection direction)
throws ChannelConfigException {
return scpdAction.getArgumentList().stream()
.filter(argument -> argument.getName().equals(argumentName) && argument.getDirection() == direction)
.findFirst()
.orElseThrow(() -> new ChannelConfigException(
(direction == SCPDDirection.IN ? "Set-Argument '" : "Get-Argument '") + argumentName
+ "' not found"));
}
/**
* Extract the related state variable from the service root for a given argument
*
* @param serviceRoot the service root object
* @param scpdArgument the argument object
* @return the related state variable object for this argument
* @throws ChannelConfigException if not found
*/
private static SCPDStateVariableType getStateVariable(SCPDScpdType serviceRoot, SCPDArgumentType scpdArgument)
throws ChannelConfigException {
return serviceRoot.getServiceStateTable().stream()
.filter(stateVariable -> stateVariable.getName().equals(scpdArgument.getRelatedStateVariable()))
.findFirst().orElseThrow(() -> new ChannelConfigException(
"StateVariable '" + scpdArgument.getRelatedStateVariable() + "' not found"));
}
/**
* Extract an action from the service root
*
* @param serviceRoot the service root object
* @param actionName the action name
* @param actionType "Get-Action" or "Set-Action" (for exception string only)
* @return the requested action object
* @throws ChannelConfigException if not found
*/
private static SCPDActionType getAction(SCPDScpdType serviceRoot, String actionName, String actionType)
throws ChannelConfigException {
return serviceRoot.getActionList().stream().filter(action -> actionName.equals(action.getName())).findFirst()
.orElseThrow(() -> new ChannelConfigException(actionType + " '" + actionName + "' not found"));
}
/**
* check and add available channels on a thing
*
* @param thing the Thing
* @param thingBuilder the ThingBuilder (needs to be passed as editThing is only available in the handler)
* @param scpdUtil the SCPDUtil instance for this thing
* @param deviceId the device id for this thing
* @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) {
Tr064BaseThingConfiguration thingConfig = Tr064RootHandler.SUPPORTED_THING_TYPES
.contains(thing.getThingTypeUID()) ? thing.getConfiguration().as(Tr064RootConfiguration.class)
: thing.getConfiguration().as(Tr064SubConfiguration.class);
channels.clear();
CHANNEL_TYPES.stream().filter(channel -> deviceType.equals(channel.getService().getDeviceType()))
.forEach(channelTypeDescription -> {
String channelId = channelTypeDescription.getName();
String serviceId = channelTypeDescription.getService().getServiceId();
Set<String> parameters = new HashSet<>();
try {
SCPDServiceType deviceService = scpdUtil.getDevice(deviceId)
.flatMap(device -> device.getServiceList().stream()
.filter(service -> service.getServiceId().equals(serviceId)).findFirst())
.orElseThrow(() -> new ChannelConfigException("Service '" + serviceId + "' not found"));
SCPDScpdType serviceRoot = scpdUtil.getService(deviceService.getServiceId())
.orElseThrow(() -> new ChannelConfigException(
"Service definition for '" + serviceId + "' not found"));
Tr064ChannelConfig channelConfig = new Tr064ChannelConfig(channelTypeDescription,
deviceService);
// get
ActionType getAction = channelTypeDescription.getGetAction();
if (getAction != null) {
String actionName = getAction.getName();
String argumentName = getAction.getArgument();
SCPDActionType scpdAction = getAction(serviceRoot, actionName, "Get-Action");
SCPDArgumentType scpdArgument = getArgument(scpdAction, argumentName, SCPDDirection.OUT);
SCPDStateVariableType relatedStateVariable = getStateVariable(serviceRoot, scpdArgument);
parameters.addAll(
getAndCheckParameters(channelId, getAction, scpdAction, serviceRoot, thingConfig));
channelConfig.setGetAction(scpdAction);
channelConfig.setDataType(relatedStateVariable.getDataType());
}
// check set action
ActionType setAction = channelTypeDescription.getSetAction();
if (setAction != null) {
String actionName = setAction.getName();
String argumentName = setAction.getArgument();
SCPDActionType scpdAction = getAction(serviceRoot, actionName, "Set-Action");
if (argumentName != null) {
SCPDArgumentType scpdArgument = getArgument(scpdAction, argumentName, SCPDDirection.IN);
SCPDStateVariableType relatedStateVariable = getStateVariable(serviceRoot,
scpdArgument);
if (channelConfig.getDataType().isEmpty()) {
channelConfig.setDataType(relatedStateVariable.getDataType());
} else if (!channelConfig.getDataType().equals(relatedStateVariable.getDataType())) {
throw new ChannelConfigException("dataType of set and get action are different");
}
}
}
// everything is available, create the channel
ChannelTypeUID channelTypeUID = new ChannelTypeUID(BINDING_ID,
channelTypeDescription.getName());
if (parameters.isEmpty()) {
// 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);
thingBuilder.withChannel(channelBuilder.build());
channels.put(channelUID, channelConfig);
} else {
// create a channel for each parameter
parameters.forEach(parameter -> {
String normalizedParameter = UIDUtils.encode(parameter);
ChannelUID channelUID = new ChannelUID(thing.getUID(),
channelId + "_" + normalizedParameter);
ChannelBuilder channelBuilder = ChannelBuilder
.create(channelUID, channelTypeDescription.getItem().getType())
.withType(channelTypeUID)
.withLabel(channelTypeDescription.getLabel() + " " + parameter);
thingBuilder.withChannel(channelBuilder.build());
Tr064ChannelConfig channelConfig1 = new Tr064ChannelConfig(channelConfig);
channelConfig1.setParameter(parameter);
channels.put(channelUID, channelConfig1);
});
}
} catch (ChannelConfigException e) {
LOGGER.debug("Channel {} not available: {}", channelId, e.getMessage());
}
});
}
private static Set<String> getAndCheckParameters(String channelId, ActionType action, SCPDActionType scpdAction,
SCPDScpdType serviceRoot, Tr064BaseThingConfiguration thingConfig) throws ChannelConfigException {
ParameterType parameter = action.getParameter();
if (parameter == null) {
return Collections.emptySet();
}
try {
Set<String> parameters = new HashSet<>();
// get parameters by reflection from thing config
Field paramField = thingConfig.getClass().getField(parameter.getThingParameter());
Object rawFieldValue = paramField.get(thingConfig);
if ((rawFieldValue instanceof List<?>)) {
((List<?>) rawFieldValue).forEach(obj -> {
if (obj instanceof String) {
parameters.add((String) obj);
}
});
}
// validate parameter against pattern
String parameterPattern = parameter.getPattern();
if (parameterPattern != null) {
parameters.removeIf(param -> !param.matches(parameterPattern));
}
// validate parameter against SCPD (if not internal only)
if (!parameter.isInternalOnly()) {
SCPDArgumentType scpdArgument = getArgument(scpdAction, parameter.getName(), SCPDDirection.IN);
SCPDStateVariableType relatedStateVariable = getStateVariable(serviceRoot, scpdArgument);
if (relatedStateVariable.getAllowedValueRange() != null) {
int paramMin = relatedStateVariable.getAllowedValueRange().getMinimum();
int paramMax = relatedStateVariable.getAllowedValueRange().getMaximum();
int paramStep = relatedStateVariable.getAllowedValueRange().getStep();
Set<String> allowedValues = Stream.iterate(paramMin, i -> i <= paramMax, i -> i + paramStep)
.map(String::valueOf).collect(Collectors.toSet());
parameters.retainAll(allowedValues);
}
}
// check we have at least one valid parameter left
if (parameters.isEmpty()) {
throw new IllegalArgumentException();
}
return parameters;
} catch (NoSuchFieldException | IllegalAccessException | IllegalArgumentException e) {
throw new ChannelConfigException("Could not get required parameter '" + channelId
+ "' from thing config (missing, empty or invalid)");
}
}
public static Optional<String> getSOAPElement(SOAPMessage soapMessage, String elementName) {
try {
NodeList nodeList = soapMessage.getSOAPBody().getElementsByTagName(elementName);
if (nodeList != null && nodeList.getLength() > 0) {
return Optional.of(nodeList.item(0).getTextContent());
}
} catch (SOAPException e) {
// if an error occurs, returning an empty Optional is fine
}
return Optional.empty();
}
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="tr064" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>TR-064 Binding</name>
<description>This is the binding for TR-064 device support.</description>
<author>Jan N. Klug</author>
</binding:binding>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="profile:transform:PHONEBOOK">
<parameter name="phonebook" type="text" required="true">
<label>Phonebook</label>
<description>The name of the the phonebook</description>
</parameter>
<parameter name="matchCount" type="text" required="false">
<label>Match Count</label>
<description>The number of digits matching the incoming value, counted from far right (default is 0 = all matching)</description>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@ -0,0 +1,145 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="tr064"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<bridge-type id="generic">
<label>Generic CPE</label>
<config-description>
<parameter name="host" type="text" required="true">
<label>Host</label>
<description>Host name or IP address.</description>
<context>network-address</context>
</parameter>
<parameter name="user" type="text">
<label>Username</label>
<default>dslf-config</default>
</parameter>
<parameter name="password" type="text" required="true">
<label>Password</label>
<context>password</context>
</parameter>
<parameter name="refresh" type="integer" unit="s">
<label>Refresh Interval</label>
<default>60</default>
<unitLabel>s</unitLabel>
</parameter>
</config-description>
</bridge-type>
<bridge-type id="fritzbox">
<label>FritzBox</label>
<description>A physical FritzBox Device.</description>
<config-description>
<parameter name="host" type="text" required="true">
<label>Host</label>
<description>Host name or IP address.</description>
<context>network-address</context>
</parameter>
<parameter name="user" type="text">
<label>Username</label>
<default>dslf-config</default>
</parameter>
<parameter name="password" type="text" required="true">
<label>Password</label>
<context>password</context>
</parameter>
<parameter name="refresh" type="integer" unit="s">
<label>Refresh Interval</label>
<default>60</default>
<unitLabel>s</unitLabel>
</parameter>
<parameter name="tamIndices" type="text" multiple="true">
<label>TAM</label>
<description>List of answering machines (starting with 0).</description>
<advanced>true</advanced>
</parameter>
<parameter name="callDeflectionIndices" type="text" multiple="true">
<label>Call Deflection</label>
<description>List of call deflection IDs (starting with 0).</description>
<advanced>true</advanced>
</parameter>
<parameter name="missedCallDays" type="text" multiple="true">
<label>Missed Call Days</label>
<description>List of days for which missed calls should be calculated.</description>
<advanced>true</advanced>
</parameter>
<parameter name="rejectedCallDays" type="text" multiple="true">
<label>Rejected Call Days</label>
<description>List of days for which rejected calls should be calculated.</description>
<advanced>true</advanced>
</parameter>
<parameter name="inboundCallDays" type="text" multiple="true">
<label>Inbound Call Days</label>
<description>List of days for which inbound calls should be calculated.</description>
<advanced>true</advanced>
</parameter>
<parameter name="outboundCallDays" type="text" multiple="true">
<label>Outbound Call Days</label>
<description>List of days for which outbound calls should be calculated.</description>
<advanced>true</advanced>
</parameter>
<parameter name="wanBlockIPs" type="text" multiple="true">
<label>WAN Block IPs</label>
<description>List of IPs that can be blocked for WAN access.</description>
<advanced>true</advanced>
</parameter>
<parameter name="phonebookInterval" type="integer" min="0" unit="s">
<label>Phonebook Interval</label>
<description>The interval for refreshing the phonebook (disabled = 0)</description>
<default>600</default>
<advanced>true</advanced>
</parameter>
</config-description>
</bridge-type>
<thing-type id="subdevice">
<supported-bridge-type-refs>
<bridge-type-ref id="generic"/>
<bridge-type-ref id="fritzbox"/>
</supported-bridge-type-refs>
<label>Sub-Device</label>
<description>A virtual sub-device.</description>
<config-description>
<parameter name="uuid" type="text" required="true">
<label>UUID</label>
<description>UUID of the sub-device</description>
</parameter>
<parameter name="refresh" type="integer" unit="s">
<label>Refresh Interval</label>
<default>60</default>
<unitLabel>s</unitLabel>
</parameter>
</config-description>
</thing-type>
<thing-type id="subdeviceLan" listed="false">
<supported-bridge-type-refs>
<bridge-type-ref id="generic"/>
<bridge-type-ref id="fritzbox"/>
</supported-bridge-type-refs>
<label>Sub-Device (LAN)</label>
<description>A virtual Sub-Device (LAN).</description>
<config-description>
<parameter name="uuid" type="text" required="true">
<label>UUID</label>
<description>UUID of the sub-device</description>
</parameter>
<parameter name="refresh" type="integer" unit="s">
<label>Refresh Interval</label>
<default>60</default>
<unitLabel>s</unitLabel>
</parameter>
<parameter name="macOnline" type="text" multiple="true">
<label>MAC Online</label>
<description>List of MACs for "online" status detection (format: 11:11:11:11:11:11).</description>
</parameter>
</config-description>
</thing-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,253 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<channels xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="channelconfig"
xsi:noNamespaceSchemaLocation="xsd/channeltypes.xsd">
<!-- Root Device -->
<channel name="securityPort" label="Security Port"
description="The port for connecting via HTTPS to the TR-064 service." advanced="true">
<item type="Number"></item>
<service deviceType="urn:dslforum-org:device:InternetGatewayDevice:1"
serviceId="urn:DeviceInfo-com:serviceId:DeviceInfo1"></service>
<getAction name="GetSecurityPort" argument="NewSecurityPort"/>
</channel>
<channel name="uptime" label="Uptime">
<item type="Number:Time" unit="s" statePattern="%d s"/>
<service deviceType="urn:dslforum-org:device:InternetGatewayDevice:1"
serviceId="urn:DeviceInfo-com:serviceId:DeviceInfo1"/>
<getAction name="GetInfo" argument="NewUpTime"/>
</channel>
<channel name="deviceLog" label="Device Log" description="A string containing the last log messages.">
<item type="String"/>
<service deviceType="urn:dslforum-org:device:InternetGatewayDevice:1"
serviceId="urn:DeviceInfo-com:serviceId:DeviceInfo1"/>
<getAction name="GetInfo" argument="NewDeviceLog"/>
</channel>
<channel name="reboot" label="Reboot">
<item type="Switch"/>
<service deviceType="urn:dslforum-org:device:InternetGatewayDevice:1"
serviceId="urn:DeviceConfig-com:serviceId:DeviceConfig1"/>
<setAction name="Reboot"/>
</channel>
<channel name="tamEnable" label="TAM" description="Enable/Disable the answering machine with the given index.">
<item type="Switch"/>
<service deviceType="urn:dslforum-org:device:InternetGatewayDevice:1"
serviceId="urn:X_AVM-DE_TAM-com:serviceId:X_AVM-DE_TAM1"/>
<getAction name="GetInfo" argument="NewEnable">
<parameter name="NewIndex" thingParameter="tamIndices"/>
</getAction>
<setAction name="SetEnable" argument="NewEnable">
<parameter name="NewIndex" thingParameter="tamIndices"/>
</setAction>
</channel>
<channel name="tamNewMessages" label="TAM New Messages"
description="The number of new messages of the given answering machine.">
<item type="Number"/>
<service deviceType="urn:dslforum-org:device:InternetGatewayDevice:1"
serviceId="urn:X_AVM-DE_TAM-com:serviceId:X_AVM-DE_TAM1"/>
<getAction name="GetMessageList" argument="NewURL" postProcessor="processTamListURL">
<parameter name="NewIndex" thingParameter="tamIndices"/>
</getAction>
</channel>
<channel name="callDeflectionEnable" label="Call Deflection"
description="Enable/Disable the call deflection setup with the given index.">
<item type="Switch"/>
<service deviceType="urn:dslforum-org:device:InternetGatewayDevice:1"
serviceId="urn:X_AVM-DE_OnTel-com:serviceId:X_AVM-DE_OnTel1"/>
<getAction name="GetDeflection" argument="NewEnable">
<parameter name="NewDeflectionId" thingParameter="callDeflectionIndices"/>
</getAction>
<setAction name="SetDeflectionEnable" argument="NewEnable">
<parameter name="NewDeflectionId" thingParameter="callDeflectionIndices"/>
</setAction>
</channel>
<channel name="wanBlockByIP" label="WAN Block By IP" description="Block/Unblock WAN access with the given IP.">
<item type="Switch"/>
<service deviceType="urn:dslforum-org:device:InternetGatewayDevice:1"
serviceId="urn:X_AVM-DE_HostFilter-com:serviceId:X_AVM-DE_HostFilter1"/>
<getAction name="GetWANAccessByIP" argument="NewDisallow">
<parameter name="NewIPv4Address" thingParameter="wanBlockIPs"/>
</getAction>
<setAction name="DisallowWANAccessByIP" argument="NewDisallow">
<parameter name="NewIPv4Address" thingParameter="wanBlockIPs"/>
</setAction>
</channel>
<channel name="missedCalls" label="Missed Calls"
description="Number of missed calls within the given number of days.">
<item type="Number"/>
<service deviceType="urn:dslforum-org:device:InternetGatewayDevice:1"
serviceId="urn:X_AVM-DE_OnTel-com:serviceId:X_AVM-DE_OnTel1"/>
<getAction name="GetCallList" argument="NewCallListURL" postProcessor="processMissedCalls">
<parameter name="CallDays" thingParameter="missedCallDays" pattern="[0-9]+" internalOnly="true"/>
</getAction>
</channel>
<channel name="rejectedCalls" label="Rejected Calls"
description="Number of rejected calls within the given number of days.">
<item type="Number"/>
<service deviceType="urn:dslforum-org:device:InternetGatewayDevice:1"
serviceId="urn:X_AVM-DE_OnTel-com:serviceId:X_AVM-DE_OnTel1"/>
<getAction name="GetCallList" argument="NewCallListURL" postProcessor="processRejectedCalls">
<parameter name="CallDays" thingParameter="rejectedCallDays" pattern="[0-9]+" internalOnly="true"/>
</getAction>
</channel>
<channel name="inboundCalls" label="Inbound Calls"
description="Number of inbound calls within the given number of days.">
<item type="Number"/>
<service deviceType="urn:dslforum-org:device:InternetGatewayDevice:1"
serviceId="urn:X_AVM-DE_OnTel-com:serviceId:X_AVM-DE_OnTel1"/>
<getAction name="GetCallList" argument="NewCallListURL" postProcessor="processInboundCalls">
<parameter name="CallDays" thingParameter="inboundCallDays" pattern="[0-9]+" internalOnly="true"/>
</getAction>
</channel>
<channel name="outboundCalls" label="Outbound Calls"
description="Number of outbound calls within the given number of days.">
<item type="Number"/>
<service deviceType="urn:dslforum-org:device:InternetGatewayDevice:1"
serviceId="urn:X_AVM-DE_OnTel-com:serviceId:X_AVM-DE_OnTel1"/>
<getAction name="GetCallList" argument="NewCallListURL" postProcessor="processOutboundCalls">
<parameter name="CallDays" thingParameter="outboundCallDays" pattern="[0-9]+" internalOnly="true"/>
</getAction>
</channel>
<!-- LAN Device -->
<channel name="wifi24GHzEnable" label="WiFi 2.4 GHz" description="Enable/Disable the 2.4 GHz WiFi device.">
<item type="Switch"/>
<service deviceType="urn:dslforum-org:device:LANDevice:1"
serviceId="urn:WLANConfiguration-com:serviceId:WLANConfiguration1"/>
<getAction name="GetInfo" argument="NewEnable"/>
<setAction name="SetEnable" argument="NewEnable"/>
</channel>
<channel name="wifi5GHzEnable" label="WiFi 5 GHz" description="Enable/Disable the 5.0 GHz WiFi device.">
<item type="Switch"/>
<service deviceType="urn:dslforum-org:device:LANDevice:1"
serviceId="urn:WLANConfiguration-com:serviceId:WLANConfiguration2"/>
<getAction name="GetInfo" argument="NewEnable"/>
<setAction name="SetEnable" argument="NewEnable"/>
</channel>
<channel name="wifiGuestEnable" label="WiFi Guest" description="Enable/Disable the guest WiFi.">
<item type="Switch"/>
<service deviceType="urn:dslforum-org:device:LANDevice:1"
serviceId="urn:WLANConfiguration-com:serviceId:WLANConfiguration3"/>
<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">
<item type="Switch"/>
<service deviceType="urn:dslforum-org:device:LANDevice:1" serviceId="urn:LanDeviceHosts-com:serviceId:Hosts1"/>
<getAction name="GetSpecificHostEntry" argument="NewActive">
<parameter name="NewMACAddress" thingParameter="macOnline" pattern="([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}"/>
</getAction>
</channel>
<!-- WAN Device -->
<channel name="wanAccessType" label="Access Type">
<item type="String"/>
<service deviceType="urn:dslforum-org:device:WANDevice:1"
serviceId="urn:WANCIfConfig-com:serviceId:WANCommonInterfaceConfig1"/>
<getAction name="GetCommonLinkProperties" argument="NewWANAccessType"/>
</channel>
<channel name="wanPhysicalLinkStatus" label="Link Status">
<item type="String"/>
<service deviceType="urn:dslforum-org:device:WANDevice:1"
serviceId="urn:WANCIfConfig-com:serviceId:WANCommonInterfaceConfig1"/>
<getAction name="GetCommonLinkProperties" argument="NewPhysicalLinkStatus"/>
</channel>
<channel name="wanMaxDownstreamRate" label="Max Downstream Rate">
<item type="Number:DataTransferRate" unit="bit/s" statePattern="%.1f Mbit/s"/>
<service deviceType="urn:dslforum-org:device:WANDevice:1"
serviceId="urn:WANCIfConfig-com:serviceId:WANCommonInterfaceConfig1"/>
<getAction name="GetCommonLinkProperties" argument="NewLayer1DownstreamMaxBitRate"/>
</channel>
<channel name="wanMaxUpstreamRate" label="Max Upstream Rate">
<item type="Number:DataTransferRate" unit="bit/s" statePattern="%.1f Mbit/s"/>
<service deviceType="urn:dslforum-org:device:WANDevice:1"
serviceId="urn:WANCIfConfig-com:serviceId:WANCommonInterfaceConfig1"/>
<getAction name="GetCommonLinkProperties" argument="NewLayer1UpstreamMaxBitRate"/>
</channel>
<channel name="wanTotalBytesReceived" label="Total Bytes Received">
<item type="Number:DataAmount" unit="B" statePattern="%.3f Gio"/>
<service deviceType="urn:dslforum-org:device:WANDevice:1"
serviceId="urn:WANCIfConfig-com:serviceId:WANCommonInterfaceConfig1"/>
<getAction name="GetTotalBytesReceived" argument="NewTotalBytesReceived"/>
</channel>
<channel name="wanTotalBytesSent" label="Total Bytes Send">
<item type="Number:DataAmount" unit="B" statePattern="%.3f Gio"/>
<service deviceType="urn:dslforum-org:device:WANDevice:1"
serviceId="urn:WANCIfConfig-com:serviceId:WANCommonInterfaceConfig1"/>
<getAction name="GetTotalBytesSent" argument="NewTotalBytesSent"/>
</channel>
<channel name="dslEnable" label="DSL Enable">
<item type="Switch"/>
<service deviceType="urn:dslforum-org:service:WANDSLInterfaceConfig:1"
serviceId="urn:WANDSLIfConfig-com:serviceId:WANDSLInterfaceConfig1"/>
<getAction name="GetInfo" argument="NewEnable"/>
</channel>
<channel name="dslStatus" label="DSL Status">
<item type="Switch"/>
<service deviceType="urn:dslforum-org:service:WANDSLInterfaceConfig:1"
serviceId="urn:WANDSLIfConfig-com:serviceId:WANDSLInterfaceConfig1"/>
<getAction name="GetInfo" argument="NewStatus"/>
</channel>
<channel name="dslDownstreamNoiseMargin" label="DSL Downstream Noise Margin">
<item type="Number:Dimensionless" unit="dB" statePattern="%.1f dB"/>
<service deviceType="urn:dslforum-org:service:WANDSLInterfaceConfig:1"
serviceId="urn:WANDSLIfConfig-com:serviceId:WANDSLInterfaceConfig1"/>
<getAction name="GetInfo" argument="NewDownstreamNoiseMargin"/>
</channel>
<channel name="dslUpstreamNoiseMargin" label="DSL Upstream Noise Margin">
<item type="Number:Dimensionless" unit="dB" statePattern="%.1f dB"/>
<service deviceType="urn:dslforum-org:service:WANDSLInterfaceConfig:1"
serviceId="urn:WANDSLIfConfig-com:serviceId:WANDSLInterfaceConfig1"/>
<getAction name="GetInfo" argument="NewUpstreamNoiseMargin"/>
</channel>
<channel name="dslDownstreamNoiseMargin" label="DSL Downstream Attenuation">
<item type="Number:Dimensionless" unit="dB" statePattern="%.1f dB"/>
<service deviceType="urn:dslforum-org:service:WANDSLInterfaceConfig:1"
serviceId="urn:WANDSLIfConfig-com:serviceId:WANDSLInterfaceConfig1"/>
<getAction name="GetInfo" argument="NewDownstreamAttenuation"/>
</channel>
<channel name="dslUpstreamNoiseMargin" label="DSL Upstream Attenuation">
<item type="Number:Dimensionless" unit="dB" statePattern="%.1f dB"/>
<service deviceType="urn:dslforum-org:service:WANDSLInterfaceConfig:1"
serviceId="urn:WANDSLIfConfig-com:serviceId:WANDSLInterfaceConfig1"/>
<getAction name="GetInfo" argument="NewUpstreamAttenuation"/>
</channel>
<channel name="dslFECErrors" label="DSL FEC Errors">
<item type="Number:Dimensionless"/>
<service deviceType="urn:dslforum-org:service:WANDSLInterfaceConfig:1"
serviceId="urn:WANDSLIfConfig-com:serviceId:WANDSLInterfaceConfig1"/>
<getAction name="GetStatisticsTotal" argument="NewFECErrors"/>
</channel>
<channel name="dslHECErrors" label="DSL HEC Errors">
<item type="Number:Dimensionless"/>
<service deviceType="urn:dslforum-org:service:WANDSLInterfaceConfig:1"
serviceId="urn:WANDSLIfConfig-com:serviceId:WANDSLInterfaceConfig1"/>
<getAction name="GetStatisticsTotal" argument="NewHECErrors"/>
</channel>
<channel name="dslCRCErrors" label="DSL CRC Errors">
<item type="Number:Dimensionless"/>
<service deviceType="urn:dslforum-org:service:WANDSLInterfaceConfig:1"
serviceId="urn:WANDSLIfConfig-com:serviceId:WANDSLInterfaceConfig1"/>
<getAction name="GetStatisticsTotal" argument="NeCRCErrors"/>
</channel>
<!-- WAN Connection device -->
<channel name="wanIpAddress" label="WAN IP Address">
<item type="String"/>
<service deviceType="urn:dslforum-org:device:WANConnectionDevice:1"
serviceId="urn:WANIPConnection-com:serviceId:WANIPConnection1"/>
<getAction name="GetInfo" argument="NewExternalIPAddress"/>
</channel>
<channel name="wanConnectionStatus" label="Connection Status">
<item type="String"/>
<service deviceType="urn:dslforum-org:device:WANConnectionDevice:1"
serviceId="urn:WANIPConnection-com:serviceId:WANIPConnection1"/>
<getAction name="GetInfo" argument="NewConnectionStatus"/>
</channel>
<channel name="uptime" label="Uptime">
<item type="Number:Time" unit="s" statePattern="%d s"/>
<service deviceType="urn:dslforum-org:device:WANConnectionDevice:1"
serviceId="urn:WANIPConnection-com:serviceId:WANIPConnection1"/>
<getAction name="GetInfo" argument="NewUptime"/>
</channel>
</channels>

View File

@ -0,0 +1,39 @@
<jaxb:bindings xmlns:jaxb="http://java.sun.com/xml/ns/jaxb" xmlns:xjc="http://java.sun.com/xml/ns/jaxb/xjc"
version="2.0">
<jaxb:globalBindings>
<xjc:serializable uid="1"/>
</jaxb:globalBindings>
<jaxb:bindings schemaLocation="phonebook.xsd">
<jaxb:schemaBindings>
<jaxb:package name="org.openhab.binding.tr064.internal.dto.phonebook"/>
</jaxb:schemaBindings>
</jaxb:bindings>
<jaxb:bindings schemaLocation="channeltypes.xsd">
<jaxb:schemaBindings>
<jaxb:package name="org.openhab.binding.tr064.internal.dto.config"/>
</jaxb:schemaBindings>
</jaxb:bindings>
<jaxb:bindings schemaLocation="scpdservice.xsd">
<jaxb:schemaBindings>
<jaxb:package name="org.openhab.binding.tr064.internal.dto.scpd.service"/>
<jaxb:nameXmlTransform>
<jaxb:typeName prefix="SCPD"/>
<jaxb:anonymousTypeName prefix="SCPD"/>
</jaxb:nameXmlTransform>
</jaxb:schemaBindings>
</jaxb:bindings>
<jaxb:bindings schemaLocation="scpddevice.xsd">
<jaxb:schemaBindings>
<jaxb:package name="org.openhab.binding.tr064.internal.dto.scpd.root"/>
<jaxb:nameXmlTransform>
<jaxb:typeName prefix="SCPD"/>
<jaxb:anonymousTypeName prefix="SCPD"/>
</jaxb:nameXmlTransform>
</jaxb:schemaBindings>
</jaxb:bindings>
</jaxb:bindings>

View File

@ -0,0 +1,56 @@
<xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns="channelconfig" targetNamespace="channelconfig" xmlns:xa="http://www.w3.org/2001/XMLSchema">
<xs:element name="channels" type="channelTypeDescriptions"/>
<xs:complexType name="itemType">
<xs:simpleContent>
<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:string" name="statePattern"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
<xs:complexType name="serviceType">
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:string" name="deviceType" use="required"/>
<xs:attribute type="xs:string" name="serviceId" use="required"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
<xs:complexType name="parameterType">
<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="pattern"/>
<xs:attribute type="xs:boolean" name="internalOnly" default="false"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
<xs:complexType name="actionType">
<xs:sequence>
<xs:element type="parameterType" name="parameter" minOccurs="0"/>
</xs:sequence>
<xs:attribute type="xs:string" name="name" use="required"/>
<xs:attribute type="xs:string" name="argument"/>
<xs:attribute type="xs:string" name="postProcessor"/>
</xs:complexType>
<xs:complexType name="channelTypeDescription">
<xs:sequence>
<xs:element type="itemType" name="item"/>
<xs:element type="serviceType" name="service"/>
<xs:element type="actionType" name="getAction" minOccurs="0"/>
<xs:element type="actionType" name="setAction" minOccurs="0"/>
</xs:sequence>
<xs:attribute type="xs:string" name="name" use="required"/>
<xs:attribute type="xs:string" name="label"/>
<xs:attribute type="xs:string" name="description"/>
<xs:attribute type="xs:boolean" name="advanced" default="false"/>
</xs:complexType>
<xs:complexType name="channelTypeDescriptions">
<xs:sequence>
<xs:element type="channelTypeDescription" name="channel" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:schema>

View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="phonebooks" type="phonebooksType"/>
<xs:complexType name="personType">
<xs:sequence>
<xs:element type="xs:string" name="realName"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="numberType">
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute type="xs:string" name="type" use="optional"/>
<xs:attribute type="xs:string" name="vanity" use="optional"/>
<xs:attribute type="xs:string" name="prio" use="optional"/>
<xs:attribute type="xs:byte" name="quickdial" use="optional"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
<xs:complexType name="telephonyType">
<xs:sequence>
<xs:element type="xs:string" name="services"/>
<xs:element type="numberType" name="number" maxOccurs="unbounded" minOccurs="0"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="contactType">
<xs:sequence>
<xs:element name="category">
<xs:simpleType>
<xs:restriction base="xs:byte">
<xs:enumeration value="0"/>
<xs:enumeration value="1"/>
</xs:restriction>
</xs:simpleType>
</xs:element>
<xs:element type="personType" name="person"/>
<xs:element type="xs:byte" name="uniqueid"/>
<xs:element type="telephonyType" name="telephony"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="phonebookType">
<xs:sequence>
<xs:element type="xs:int" name="timestamp"/>
<xs:element type="contactType" name="contact" maxOccurs="unbounded" minOccurs="0"/>
</xs:sequence>
<xs:attribute type="xs:byte" name="owner"/>
<xs:attribute type="xs:string" name="name"/>
</xs:complexType>
<xs:complexType name="phonebooksType">
<xs:sequence>
<xs:element type="phonebookType" name="phonebook"/>
</xs:sequence>
</xs:complexType>
</xs:schema>

View File

@ -0,0 +1,79 @@
<xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns="urn:dslforum-org:device-1-0" targetNamespace="urn:dslforum-org:device-1-0">
<xs:element name="root" type="rootType"/>
<xs:complexType name="specVersionType">
<xs:sequence>
<xs:element type="xs:byte" name="major"/>
<xs:element type="xs:byte" name="minor"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="systemVersionType">
<xs:sequence>
<xs:element type="xs:short" name="HW"/>
<xs:element type="xs:short" name="Major"/>
<xs:element type="xs:byte" name="Minor"/>
<xs:element type="xs:byte" name="Patch"/>
<xs:element type="xs:int" name="Buildnumber"/>
<xs:element type="xs:string" name="Display"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="iconType">
<xs:sequence>
<xs:element type="xs:string" name="mimetype"/>
<xs:element type="xs:byte" name="width"/>
<xs:element type="xs:byte" name="height"/>
<xs:element type="xs:byte" name="depth"/>
<xs:element type="xs:string" name="url"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="iconListType">
<xs:sequence>
<xs:element type="iconType" name="icon"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="serviceType">
<xs:sequence>
<xs:element type="xs:string" name="serviceType"/>
<xs:element type="xs:string" name="serviceId"/>
<xs:element type="xs:string" name="controlURL"/>
<xs:element type="xs:string" name="eventSubURL"/>
<xs:element type="xs:string" name="SCPDURL"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="serviceListType">
<xs:sequence>
<xs:element type="serviceType" name="service" maxOccurs="unbounded" minOccurs="0"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="deviceType">
<xs:sequence>
<xs:element type="xs:string" name="deviceType"/>
<xs:element type="xs:string" name="friendlyName"/>
<xs:element type="xs:string" name="manufacturer"/>
<xs:element type="xs:anyURI" name="manufacturerURL"/>
<xs:element type="xs:string" name="modelDescription"/>
<xs:element type="xs:string" name="modelName"/>
<xs:element type="xs:string" name="modelNumber"/>
<xs:element type="xs:anyURI" name="modelURL"/>
<xs:element type="xs:string" name="UDN"/>
<xs:element type="xs:string" name="UPC" minOccurs="0"/>
<xs:element type="iconListType" name="iconList" minOccurs="0"/>
<xs:element type="serviceListType" name="serviceList"/>
<xs:element type="deviceListType" name="deviceList" minOccurs="0"/>
<xs:element type="xs:anyURI" name="presentationURL" minOccurs="0"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="deviceListType">
<xs:sequence>
<xs:element type="deviceType" name="device" maxOccurs="unbounded" minOccurs="0"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="rootType">
<xs:sequence>
<xs:element type="specVersionType" name="specVersion"/>
<xs:element type="systemVersionType" name="systemVersion"/>
<xs:element type="deviceType" name="device"/>
</xs:sequence>
</xs:complexType>
</xs:schema>

View File

@ -0,0 +1,73 @@
<xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns="urn:dslforum-org:service-1-0" targetNamespace="urn:dslforum-org:service-1-0">
<xs:element name="scpd" type="scpdType"/>
<xs:complexType name="specVersionType">
<xs:sequence>
<xs:element type="xs:byte" name="major"/>
<xs:element type="xs:byte" name="minor"/>
</xs:sequence>
</xs:complexType>
<xs:simpleType name="direction">
<xs:restriction base="xs:string">
<xs:enumeration value="in"/>
<xs:enumeration value="out"/>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="argumentType">
<xs:sequence>
<xs:element type="xs:string" name="name" minOccurs="0"/>
<xs:element type="direction" name="direction" minOccurs="0"/>
<xs:element type="xs:string" name="relatedStateVariable" minOccurs="0"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="argumentListType">
<xs:sequence>
<xs:element type="argumentType" name="argument" maxOccurs="unbounded" minOccurs="0"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="actionType">
<xs:sequence>
<xs:element type="xs:string" name="name"/>
<xs:element type="argumentListType" name="argumentList"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="actionListType">
<xs:sequence>
<xs:element type="actionType" name="action" maxOccurs="unbounded" minOccurs="0"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="stateVariableType">
<xs:sequence>
<xs:element type="xs:string" name="name"/>
<xs:element type="xs:string" name="dataType"/>
<xs:element type="xs:string" name="defaultValue" minOccurs="0"/>
<xs:element type="allowedValueRangeType" name="allowedValueRange" minOccurs="0"/>
<xs:element type="allowedValueListType" name="allowedValueList" minOccurs="0"/>
</xs:sequence>
<xs:attribute type="xs:string" name="sendEvents"/>
</xs:complexType>
<xs:complexType name="allowedValueRangeType">
<xs:sequence>
<xs:element type="xs:byte" name="minimum"/>
<xs:element type="xs:byte" name="maximum"/>
<xs:element type="xs:byte" name="step"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="allowedValueListType">
<xs:sequence>
<xs:element type="xs:string" name="allowedValue" maxOccurs="unbounded" minOccurs="0"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="serviceStateTableType">
<xs:sequence>
<xs:element type="stateVariableType" name="stateVariable" maxOccurs="unbounded" minOccurs="0"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="scpdType">
<xs:sequence>
<xs:element type="specVersionType" name="specVersion"/>
<xs:element type="actionListType" name="actionList"/>
<xs:element type="serviceStateTableType" name="serviceStateTable"/>
</xs:sequence>
</xs:complexType>
</xs:schema>

View File

@ -0,0 +1,60 @@
/**
* Copyright (c) 2010-2020 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;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.util.Comparator;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.openhab.binding.tr064.internal.Tr064BindingConstants;
import org.openhab.binding.tr064.internal.dto.config.ChannelTypeDescription;
/**
* The {@link ChannelListUtilTest} is a tool for documentation generation
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class ChannelListUtilTest {
@Test
public void createChannelListTest() {
try {
final Writer writer = new OutputStreamWriter(new FileOutputStream("target/channelList.asc"),
StandardCharsets.UTF_8);
Tr064BindingConstants.CHANNEL_TYPES.stream().sorted(Comparator.comparing(ChannelTypeDescription::getName))
.forEach(channel -> {
String description = channel.getDescription() == null ? channel.getLabel()
: channel.getDescription();
String channelString = String.format("| `%s` | `%s`| %s |%c", channel.getName(),
channel.getItem().getType(), description, 13);
try {
writer.write(channelString);
} catch (IOException e) {
Assertions.fail(e.getMessage());
}
});
writer.close();
} catch (IOException e) {
Assertions.fail(e.getMessage());
}
}
}

View File

@ -277,6 +277,7 @@
<module>org.openhab.binding.tibber</module>
<module>org.openhab.binding.touchwand</module>
<module>org.openhab.binding.tplinksmarthome</module>
<module>org.openhab.binding.tr064</module>
<module>org.openhab.binding.tradfri</module>
<module>org.openhab.binding.unifi</module>
<module>org.openhab.binding.unifiedremote</module>