diff --git a/CODEOWNERS b/CODEOWNERS index 7222bf6c3..1a4e6e29f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -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 diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index 169663f18..c6579d85a 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -1211,6 +1211,11 @@ org.openhab.binding.tplinksmarthome ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.tr064 + ${project.version} + org.openhab.addons.bundles org.openhab.binding.tradfri diff --git a/bundles/org.openhab.binding.tr064/NOTICE b/bundles/org.openhab.binding.tr064/NOTICE new file mode 100644 index 000000000..4c20ef446 --- /dev/null +++ b/bundles/org.openhab.binding.tr064/NOTICE @@ -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 diff --git a/bundles/org.openhab.binding.tr064/README.md b/bundles/org.openhab.binding.tr064/README.md new file mode 100644 index 000000000..429699552 --- /dev/null +++ b/bundles/org.openhab.binding.tr064/README.md @@ -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. diff --git a/bundles/org.openhab.binding.tr064/pom.xml b/bundles/org.openhab.binding.tr064/pom.xml new file mode 100644 index 000000000..8c71c147f --- /dev/null +++ b/bundles/org.openhab.binding.tr064/pom.xml @@ -0,0 +1,52 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.0.0-SNAPSHOT + + + org.openhab.binding.tr064 + + openHAB Add-ons :: Bundles :: TR-064 Binding + + + + + org.jvnet.jaxb2.maven2 + maven-jaxb2-plugin + 0.14.0 + + + generate-jaxb-sources + + generate + + + + + src/main/resources/xsd + true + en + false + true + + -Xxew + -Xxew:instantiate early + + + + com.github.jaxb-xew-plugin + jaxb-xew-plugin + 1.10 + + + + + + + diff --git a/bundles/org.openhab.binding.tr064/src/main/feature/feature.xml b/bundles/org.openhab.binding.tr064/src/main/feature/feature.xml new file mode 100644 index 000000000..9f9ca83fb --- /dev/null +++ b/bundles/org.openhab.binding.tr064/src/main/feature/feature.xml @@ -0,0 +1,13 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + openhab.tp;filter:="(feature=jaxb)" + openhab.tp-jaxb + openhab.tp;filter:="(feature=jax-ws)" + openhab.tp-jaxws + mvn:org.openhab.addons.bundles/org.openhab.binding.tr064/${project.version} + + diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/AvmFritzTlsTrustManagerProvider.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/AvmFritzTlsTrustManagerProvider.java new file mode 100644 index 000000000..1f8f24f0c --- /dev/null +++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/AvmFritzTlsTrustManagerProvider.java @@ -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(); + } +} diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/ChannelConfigException.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/ChannelConfigException.java new file mode 100644 index 000000000..362927966 --- /dev/null +++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/ChannelConfigException.java @@ -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); + } +} diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/PostProcessingException.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/PostProcessingException.java new file mode 100644 index 000000000..9b2c0dd9a --- /dev/null +++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/PostProcessingException.java @@ -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); + } +} diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/SCPDException.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/SCPDException.java new file mode 100644 index 000000000..f9af40cef --- /dev/null +++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/SCPDException.java @@ -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); + } +} diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/SOAPConnector.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/SOAPConnector.java new file mode 100644 index 000000000..9abf59e80 --- /dev/null +++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/SOAPConnector.java @@ -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 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) 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 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 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 channelConfigMap, ExpiringCacheMap 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 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 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; + } + } +} diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/SOAPValueConverter.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/SOAPValueConverter.java new file mode 100644 index 000000000..214a49b4e --- /dev/null +++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/SOAPValueConverter.java @@ -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 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 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("1").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 + "").length - 1; + + return new DecimalType(callCount); + } catch (InterruptedException | TimeoutException | ExecutionException e) { + throw new PostProcessingException("Failed to get call list from URL " + state.toString(), e); + } + } +} diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064BindingConstants.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064BindingConstants.java new file mode 100644 index 000000000..c8cbaaab7 --- /dev/null +++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064BindingConstants.java @@ -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 CHANNEL_TYPES = Util.readXMLChannelConfig(); +} diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064ChannelTypeProvider.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064ChannelTypeProvider.java new file mode 100644 index 000000000..d9c6e38af --- /dev/null +++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064ChannelTypeProvider.java @@ -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 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 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 getChannelTypes(@Nullable Locale locale) { + return channelTypeMap.values(); + } + + @Override + public @Nullable ChannelType getChannelType(ChannelTypeUID channelTypeUID, @Nullable Locale locale) { + return channelTypeMap.get(channelTypeUID); + } +} diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064CommunicationException.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064CommunicationException.java new file mode 100644 index 000000000..8e1bee6c6 --- /dev/null +++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064CommunicationException.java @@ -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); + } +} diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064DiscoveryService.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064DiscoveryService.java new file mode 100644 index 000000000..f48c3f6a3 --- /dev/null +++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064DiscoveryService.java @@ -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 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 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 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 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()); + } +} diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064DynamicStateDescriptionProvider.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064DynamicStateDescriptionProvider.java new file mode 100644 index 000000000..4cd4fa6cd --- /dev/null +++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064DynamicStateDescriptionProvider.java @@ -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 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; + } + } +} diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064HandlerFactory.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064HandlerFactory.java new file mode 100644 index 000000000..c40dd503d --- /dev/null +++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064HandlerFactory.java @@ -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 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); + } + } +} diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064RootHandler.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064RootHandler.java new file mode 100644 index 000000000..af03c1dcf --- /dev/null +++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064RootHandler.java @@ -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 SUPPORTED_THING_TYPES = Set.of(THING_TYPE_GENERIC, THING_TYPE_FRITZBOX); + private static final int RETRY_INTERVAL = 60; + private static final Set 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 channels = new HashMap<>(); + // caching is used to prevent excessive calls to the same action + private final ExpiringCacheMap stateCache = new ExpiringCacheMap<>(2000); + private Collection 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 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 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 processPhonebookList(SOAPMessage soapMessagePhonebookList, + SCPDServiceType scpdService) { + SOAPValueConverter soapValueConverter = new SOAPValueConverter(httpClient); + return (Collection) 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 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. emptyList(); + } + }).orElse(Collections.emptyList()); + + if (phonebooks.isEmpty()) { + logger.warn("Could not get phonebooks for thing {}", thing.getUID()); + } + } + + @Override + public Optional getPhonebookByName(String name) { + return phonebooks.stream().filter(p -> name.equals(p.getName())).findAny(); + } + + @Override + public Collection 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> getServices() { + return Set.of(Tr064DiscoveryService.class); + } +} diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064SubHandler.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064SubHandler.java new file mode 100644 index 000000000..ab70fdd45 --- /dev/null +++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064SubHandler.java @@ -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 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 channels = new HashMap<>(); + // caching is used to prevent excessive calls to the same action + private final ExpiringCacheMap 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 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); + } +} diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/config/Tr064BaseThingConfiguration.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/config/Tr064BaseThingConfiguration.java new file mode 100644 index 000000000..cc63ed767 --- /dev/null +++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/config/Tr064BaseThingConfiguration.java @@ -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; +} diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/config/Tr064ChannelConfig.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/config/Tr064ChannelConfig.java new file mode 100644 index 000000000..d0f77c856 --- /dev/null +++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/config/Tr064ChannelConfig.java @@ -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 + "'}"; + } +} diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/config/Tr064RootConfiguration.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/config/Tr064RootConfiguration.java new file mode 100644 index 000000000..51b391edd --- /dev/null +++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/config/Tr064RootConfiguration.java @@ -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 tamIndices = Collections.emptyList(); + public List callDeflectionIndices = Collections.emptyList(); + public List missedCallDays = Collections.emptyList(); + public List rejectedCallDays = Collections.emptyList(); + public List inboundCallDays = Collections.emptyList(); + public List outboundCallDays = Collections.emptyList(); + public int phonebookInterval = 0; + + public boolean isValid() { + return !host.isEmpty() && !user.isEmpty() && !password.isEmpty(); + } +} diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/config/Tr064SubConfiguration.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/config/Tr064SubConfiguration.java new file mode 100644 index 000000000..41e40b728 --- /dev/null +++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/config/Tr064SubConfiguration.java @@ -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 macOnline = Collections.emptyList(); + + public boolean isValid() { + return !uuid.isEmpty(); + } +} diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/Phonebook.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/Phonebook.java new file mode 100644 index 000000000..fb7e11708 --- /dev/null +++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/Phonebook.java @@ -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 lookupNumber(String number, int matchCount); +} diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/PhonebookProfile.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/PhonebookProfile.java new file mode 100644 index 000000000..33bc639f2 --- /dev/null +++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/PhonebookProfile.java @@ -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 phonebookProviders; + private final int matchCount; + + public PhonebookProfile(ProfileCallback callback, ProfileContext context, + Map 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 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) { + } +} diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/PhonebookProfileFactory.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/PhonebookProfileFactory.java new file mode 100644 index 000000000..054e09c0c --- /dev/null +++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/PhonebookProfileFactory.java @@ -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 phonebookProviders = new ConcurrentHashMap<>(); + + @Override + public @Nullable Profile createProfile(ProfileTypeUID profileTypeUID, ProfileCallback callback, + ProfileContext profileContext) { + return new PhonebookProfile(callback, profileContext, phonebookProviders); + } + + @Override + public Collection getSupportedProfileTypeUIDs() { + return Collections.singleton(PhonebookProfile.PHONEBOOK_PROFILE_TYPE_UID); + } + + @Override + public Collection 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 createPhonebookList(Map.Entry entry) { + String thingUid = UIDUtils.encode(entry.getKey().toString()); + String thingName = entry.getValue().getFriendlyName(); + + Stream 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 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; + } +} diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/PhonebookProvider.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/PhonebookProvider.java new file mode 100644 index 000000000..5a41c0139 --- /dev/null +++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/PhonebookProvider.java @@ -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 getPhonebookByName(String name); + + Collection getPhonebooks(); + + ThingUID getUID(); + + String getFriendlyName(); +} diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/Tr064PhonebookImpl.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/Tr064PhonebookImpl.java new file mode 100644 index 000000000..bf105c313 --- /dev/null +++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/Tr064PhonebookImpl.java @@ -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 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 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 + '}'; + } +} diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/util/SCPDUtil.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/util/SCPDUtil.java new file mode 100644 index 000000000..3de746978 --- /dev/null +++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/util/SCPDUtil.java @@ -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 scpdDevicesList = new ArrayList<>(); + private final Map 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 @Nullable T getAndUnmarshalSCPD(String uri, Class 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 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 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 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 getService(String serviceId) { + return Optional.ofNullable(serviceMap.get(serviceId)); + } +} diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/util/Util.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/util/Util.java new file mode 100644 index 000000000..a3146bc81 --- /dev/null +++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/util/Util.java @@ -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 readXMLChannelConfig() { + try { + InputStream resource = Thread.currentThread().getContextClassLoader().getResourceAsStream("channels.xml"); + JAXBContext context = JAXBContext.newInstance(ChannelTypeDescriptions.class); + Unmarshaller um = context.createUnmarshaller(); + JAXBElement 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 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 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 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 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 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 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(); + } +} diff --git a/bundles/org.openhab.binding.tr064/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.tr064/src/main/resources/OH-INF/binding/binding.xml new file mode 100644 index 000000000..c50a9f7fc --- /dev/null +++ b/bundles/org.openhab.binding.tr064/src/main/resources/OH-INF/binding/binding.xml @@ -0,0 +1,10 @@ + + + + TR-064 Binding + This is the binding for TR-064 device support. + Jan N. Klug + + diff --git a/bundles/org.openhab.binding.tr064/src/main/resources/OH-INF/config/phonebookProfile.xml b/bundles/org.openhab.binding.tr064/src/main/resources/OH-INF/config/phonebookProfile.xml new file mode 100644 index 000000000..f603d7d21 --- /dev/null +++ b/bundles/org.openhab.binding.tr064/src/main/resources/OH-INF/config/phonebookProfile.xml @@ -0,0 +1,16 @@ + + + + + + The name of the the phonebook + + + + The number of digits matching the incoming value, counted from far right (default is 0 = all matching) + + + diff --git a/bundles/org.openhab.binding.tr064/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.tr064/src/main/resources/OH-INF/thing/thing-types.xml new file mode 100644 index 000000000..e419d7139 --- /dev/null +++ b/bundles/org.openhab.binding.tr064/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,145 @@ + + + + + + + + + + Host name or IP address. + network-address + + + + dslf-config + + + + password + + + + 60 + s + + + + + + + A physical FritzBox Device. + + + + + Host name or IP address. + network-address + + + + dslf-config + + + + password + + + + 60 + s + + + + List of answering machines (starting with 0). + true + + + + List of call deflection IDs (starting with 0). + true + + + + List of days for which missed calls should be calculated. + true + + + + List of days for which rejected calls should be calculated. + true + + + + List of days for which inbound calls should be calculated. + true + + + + List of days for which outbound calls should be calculated. + true + + + + List of IPs that can be blocked for WAN access. + true + + + + The interval for refreshing the phonebook (disabled = 0) + 600 + true + + + + + + + + + + + A virtual sub-device. + + + + + UUID of the sub-device + + + + 60 + s + + + + + + + + + + + A virtual Sub-Device (LAN). + + + + + UUID of the sub-device + + + + 60 + s + + + + List of MACs for "online" status detection (format: 11:11:11:11:11:11). + + + + + diff --git a/bundles/org.openhab.binding.tr064/src/main/resources/channels.xml b/bundles/org.openhab.binding.tr064/src/main/resources/channels.xml new file mode 100644 index 000000000..c723baf3c --- /dev/null +++ b/bundles/org.openhab.binding.tr064/src/main/resources/channels.xml @@ -0,0 +1,253 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.tr064/src/main/resources/xsd/bindings.xjb b/bundles/org.openhab.binding.tr064/src/main/resources/xsd/bindings.xjb new file mode 100644 index 000000000..7cd2e8db7 --- /dev/null +++ b/bundles/org.openhab.binding.tr064/src/main/resources/xsd/bindings.xjb @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.tr064/src/main/resources/xsd/channeltypes.xsd b/bundles/org.openhab.binding.tr064/src/main/resources/xsd/channeltypes.xsd new file mode 100644 index 000000000..87a769195 --- /dev/null +++ b/bundles/org.openhab.binding.tr064/src/main/resources/xsd/channeltypes.xsd @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.tr064/src/main/resources/xsd/phonebook.xsd b/bundles/org.openhab.binding.tr064/src/main/resources/xsd/phonebook.xsd new file mode 100644 index 000000000..f5eaf5a7c --- /dev/null +++ b/bundles/org.openhab.binding.tr064/src/main/resources/xsd/phonebook.xsd @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.tr064/src/main/resources/xsd/scpddevice.xsd b/bundles/org.openhab.binding.tr064/src/main/resources/xsd/scpddevice.xsd new file mode 100644 index 000000000..894e7d4b0 --- /dev/null +++ b/bundles/org.openhab.binding.tr064/src/main/resources/xsd/scpddevice.xsd @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.tr064/src/main/resources/xsd/scpdservice.xsd b/bundles/org.openhab.binding.tr064/src/main/resources/xsd/scpdservice.xsd new file mode 100644 index 000000000..bcf37a2f3 --- /dev/null +++ b/bundles/org.openhab.binding.tr064/src/main/resources/xsd/scpdservice.xsd @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.tr064/src/test/java/org/openhab/binding/tr064/ChannelListUtilTest.java b/bundles/org.openhab.binding.tr064/src/test/java/org/openhab/binding/tr064/ChannelListUtilTest.java new file mode 100644 index 000000000..d3267cad1 --- /dev/null +++ b/bundles/org.openhab.binding.tr064/src/test/java/org/openhab/binding/tr064/ChannelListUtilTest.java @@ -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()); + } + } +} diff --git a/bundles/pom.xml b/bundles/pom.xml index afb37fa9f..73be0d30a 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -277,6 +277,7 @@ org.openhab.binding.tibber org.openhab.binding.touchwand org.openhab.binding.tplinksmarthome + org.openhab.binding.tr064 org.openhab.binding.tradfri org.openhab.binding.unifi org.openhab.binding.unifiedremote