]> git.basschouten.com Git - openhab-addons.git/commitdiff
[tr064] fix certificate problems and add call list channel (#9149)
authorJ-N-K <J-N-K@users.noreply.github.com>
Sat, 28 Nov 2020 18:14:07 +0000 (19:14 +0100)
committerGitHub <noreply@github.com>
Sat, 28 Nov 2020 18:14:07 +0000 (10:14 -0800)
* improvements

- use insecure client and remove TrustManager
- add call list channel

* address review comments

Signed-off-by: Jan N. Klug <jan.n.klug@rub.de>
22 files changed:
bundles/org.openhab.binding.tr064/README.md
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/AVMFritzTlsTrustManagerProvider.java [deleted file]
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/PostProcessingException.java [deleted file]
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/SOAPConnector.java [deleted file]
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/SOAPValueConverter.java [deleted file]
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064HandlerFactory.java
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064RootHandler.java
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064SubHandler.java
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/config/Tr064RootConfiguration.java
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/Tr064PhonebookImpl.java
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/soap/CallListEntry.java [new file with mode: 0644]
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/soap/CallListType.java [new file with mode: 0644]
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/soap/PostProcessingException.java [new file with mode: 0644]
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/soap/SOAPConnector.java [new file with mode: 0644]
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/soap/SOAPValueConverter.java [new file with mode: 0644]
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/util/SCPDUtil.java
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/util/Util.java
bundles/org.openhab.binding.tr064/src/main/resources/OH-INF/thing/thing-types.xml
bundles/org.openhab.binding.tr064/src/main/resources/channels.xml
bundles/org.openhab.binding.tr064/src/main/resources/xsd/additions.xsd [new file with mode: 0644]
bundles/org.openhab.binding.tr064/src/main/resources/xsd/bindings.xjb
bundles/org.openhab.binding.tr064/src/main/resources/xsd/phonebook.xsd [deleted file]

index 5fedf7b8e10bebc7ebe2a7b6a80fff062f542380..d49730bead3e50fe8ebcd7ab5fd10680de8a6aa0 100644 (file)
@@ -49,7 +49,7 @@ 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`
+The days for which this analysis takes place can be controlled with the `missedCallDays`, `rejectedCallDays`, `inboundCallDays`, `outboundCallDays` and `callListDays`.
 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.
@@ -75,7 +75,8 @@ This is an optional parameter and multiple values are allowed.
 | 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.                     |
+| `callList`                 | `String`                  |     x    | A string containing the call list as JSON (see below)          |    
+| `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                                     |
@@ -107,6 +108,12 @@ This is an optional parameter and multiple values are allowed.
 | `wifi5GHzEnable`           | `Switch`                  |          | Enable/Disable the 5.0 GHz WiFi device.                        |
 | `wifiGuestEnable`          | `Switch`                  |          | Enable/Disable the guest WiFi.                                 |
 
+### Channel `callList`
+
+Call lists are provided for one or more days (as configured) as JSON.
+The JSON consists of an array of individual calls with the fields `date`, `type`, `localNumber`, `remoteNumber`, `duration`.
+The call-types are the same as provided by the FritzBox, i.e. `1` (inbound), `2` (missed), `3` (outbound), `10` (rejected).
 ## `PHONEBOOK` Profile
 
 The binding provides a profile for using the FritzBox phonebooks for resolving numbers to names.
@@ -117,3 +124,4 @@ If only a specific phonebook from the device should be used, this can be specifi
 The default is to use all available phonebooks from the specified thing.
 In case the format of the number in the phonebook and the format of the number from the channel are different (e.g. regarding country prefixes), the `matchCount` parameter can be used.
 The configured `matchCount` is counted from the right end and denotes the number of matching characters needed to consider this number as matching.
+
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
deleted file mode 100644 (file)
index 1217333..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-/**
- * 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/PostProcessingException.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/PostProcessingException.java
deleted file mode 100644 (file)
index 9b2c0dd..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-/**
- * 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/SOAPConnector.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/SOAPConnector.java
deleted file mode 100644 (file)
index 9abf59e..0000000
+++ /dev/null
@@ -1,254 +0,0 @@
-/**
- * Copyright (c) 2010-2020 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.tr064.internal;
-
-import static org.openhab.binding.tr064.internal.util.Util.getSOAPElement;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.Map;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-import java.util.stream.Collectors;
-
-import javax.xml.soap.*;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jetty.client.HttpClient;
-import org.eclipse.jetty.client.api.ContentResponse;
-import org.eclipse.jetty.client.api.Request;
-import org.eclipse.jetty.client.util.BytesContentProvider;
-import org.eclipse.jetty.http.HttpMethod;
-import org.eclipse.jetty.http.HttpStatus;
-import org.openhab.binding.tr064.internal.config.Tr064ChannelConfig;
-import org.openhab.binding.tr064.internal.dto.config.ActionType;
-import org.openhab.binding.tr064.internal.dto.config.ChannelTypeDescription;
-import org.openhab.binding.tr064.internal.dto.scpd.root.SCPDServiceType;
-import org.openhab.binding.tr064.internal.dto.scpd.service.SCPDActionType;
-import org.openhab.core.cache.ExpiringCacheMap;
-import org.openhab.core.library.types.OnOffType;
-import org.openhab.core.library.types.StringType;
-import org.openhab.core.thing.ChannelUID;
-import org.openhab.core.types.Command;
-import org.openhab.core.types.State;
-import org.openhab.core.types.UnDefType;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * The {@link SOAPConnector} provides communication with a remote SOAP device
- *
- * @author Jan N. Klug - Initial contribution
- */
-@NonNullByDefault
-public class SOAPConnector {
-    private static final int SOAP_TIMEOUT = 2000; // in ms
-    private final Logger logger = LoggerFactory.getLogger(SOAPConnector.class);
-    private final HttpClient httpClient;
-    private final String endpointBaseURL;
-    private final SOAPValueConverter soapValueConverter;
-
-    public SOAPConnector(HttpClient httpClient, String endpointBaseURL) {
-        this.httpClient = httpClient;
-        this.endpointBaseURL = endpointBaseURL;
-        this.soapValueConverter = new SOAPValueConverter(httpClient);
-    }
-
-    /**
-     * prepare a SOAP request for an action request to a service
-     *
-     * @param service the service
-     * @param soapAction the action to send
-     * @param arguments arguments to send along with the request
-     * @return a jetty Request containing the full SOAP message
-     * @throws IOException if a problem while writing the SOAP message to the Request occurs
-     * @throws SOAPException if a problem with creating the SOAP message occurs
-     */
-    private Request prepareSOAPRequest(SCPDServiceType service, String soapAction, Map<String, String> arguments)
-            throws IOException, SOAPException {
-        MessageFactory messageFactory = MessageFactory.newInstance();
-        SOAPMessage soapMessage = messageFactory.createMessage();
-        SOAPPart soapPart = soapMessage.getSOAPPart();
-        SOAPEnvelope envelope = soapPart.getEnvelope();
-        envelope.setEncodingStyle("http://schemas.xmlsoap.org/soap/encoding/");
-
-        // SOAP body
-        SOAPBody soapBody = envelope.getBody();
-        SOAPElement soapBodyElem = soapBody.addChildElement(soapAction, "u", service.getServiceType());
-        arguments.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach(argument -> {
-            try {
-                soapBodyElem.addChildElement(argument.getKey()).setTextContent(argument.getValue());
-            } catch (SOAPException e) {
-                logger.warn("Could not add {}:{} to SOAP Request: {}", argument.getKey(), argument.getValue(),
-                        e.getMessage());
-            }
-        });
-
-        // SOAP headers
-        MimeHeaders headers = soapMessage.getMimeHeaders();
-        headers.addHeader("SOAPAction", service.getServiceType() + "#" + soapAction);
-        soapMessage.saveChanges();
-
-        // create Request and add headers and content
-        Request request = httpClient.newRequest(endpointBaseURL + service.getControlURL()).method(HttpMethod.POST);
-        ((Iterator<MimeHeader>) soapMessage.getMimeHeaders().getAllHeaders())
-                .forEachRemaining(header -> request.header(header.getName(), header.getValue()));
-        try (final ByteArrayOutputStream os = new ByteArrayOutputStream()) {
-            soapMessage.writeTo(os);
-            byte[] content = os.toByteArray();
-            request.content(new BytesContentProvider(content));
-        }
-
-        return request;
-    }
-
-    /**
-     * execute a SOAP request
-     *
-     * @param service the service to send the action to
-     * @param soapAction the action itself
-     * @param arguments arguments to send along with the request
-     * @return the SOAPMessage answer from the remote host
-     * @throws Tr064CommunicationException if an error occurs during the request
-     */
-    public synchronized SOAPMessage doSOAPRequest(SCPDServiceType service, String soapAction,
-            Map<String, String> arguments) throws Tr064CommunicationException {
-        try {
-            Request request = prepareSOAPRequest(service, soapAction, arguments).timeout(SOAP_TIMEOUT,
-                    TimeUnit.MILLISECONDS);
-            if (logger.isTraceEnabled()) {
-                request.getContent().forEach(buffer -> logger.trace("Request: {}", new String(buffer.array())));
-            }
-
-            ContentResponse response = request.send();
-            if (response.getStatus() == HttpStatus.UNAUTHORIZED_401) {
-                // retry once if authentication expired
-                logger.trace("Re-Auth needed.");
-                httpClient.getAuthenticationStore().clearAuthenticationResults();
-                request = prepareSOAPRequest(service, soapAction, arguments).timeout(SOAP_TIMEOUT,
-                        TimeUnit.MILLISECONDS);
-                response = request.send();
-            }
-            try (final ByteArrayInputStream is = new ByteArrayInputStream(response.getContent())) {
-                logger.trace("Received response: {}", response.getContentAsString());
-
-                SOAPMessage soapMessage = MessageFactory.newInstance().createMessage(null, is);
-                if (soapMessage.getSOAPBody().hasFault()) {
-                    String soapError = getSOAPElement(soapMessage, "errorCode").orElse("unknown");
-                    String soapReason = getSOAPElement(soapMessage, "errorDescription").orElse("unknown");
-                    String error = String.format("HTTP-Response-Code %d (%s), SOAP-Fault: %s (%s)",
-                            response.getStatus(), response.getReason(), soapError, soapReason);
-                    throw new Tr064CommunicationException(error);
-                }
-                return soapMessage;
-            }
-        } catch (IOException | SOAPException | InterruptedException | TimeoutException | ExecutionException e) {
-            throw new Tr064CommunicationException(e);
-        }
-    }
-
-    /**
-     * send a command to the remote device
-     *
-     * @param channelConfig the channel config containing all information
-     * @param command the command to send
-     */
-    public void sendChannelCommandToDevice(Tr064ChannelConfig channelConfig, Command command) {
-        soapValueConverter.getSOAPValueFromCommand(command, channelConfig.getDataType(),
-                channelConfig.getChannelTypeDescription().getItem().getUnit()).ifPresentOrElse(value -> {
-                    final ChannelTypeDescription channelTypeDescription = channelConfig.getChannelTypeDescription();
-                    final SCPDServiceType service = channelConfig.getService();
-                    logger.debug("Sending {} as {} to {}/{}", command, value, service.getServiceId(),
-                            channelTypeDescription.getSetAction().getName());
-                    try {
-                        Map<String, String> arguments = new HashMap<>();
-                        if (channelTypeDescription.getSetAction().getArgument() != null) {
-                            arguments.put(channelTypeDescription.getSetAction().getArgument(), value);
-                        }
-                        String parameter = channelConfig.getParameter();
-                        if (parameter != null) {
-                            arguments.put(
-                                    channelConfig.getChannelTypeDescription().getGetAction().getParameter().getName(),
-                                    parameter);
-                        }
-                        doSOAPRequest(service, channelTypeDescription.getSetAction().getName(), arguments);
-                    } catch (Tr064CommunicationException e) {
-                        logger.warn("Could not send command {}: {}", command, e.getMessage());
-                    }
-                }, () -> logger.warn("Could not convert {} to SOAP value", command));
-    }
-
-    /**
-     * get a value from the remote device - updates state cache for all possible channels
-     *
-     * @param channelConfig the channel config containing all information
-     * @param channelConfigMap map of all channels in the device
-     * @param stateCache the ExpiringCacheMap for states of the device
-     * @return the value for the requested channel
-     */
-    public State getChannelStateFromDevice(final Tr064ChannelConfig channelConfig,
-            Map<ChannelUID, Tr064ChannelConfig> channelConfigMap, ExpiringCacheMap<ChannelUID, State> stateCache) {
-        try {
-            final SCPDActionType getAction = channelConfig.getGetAction();
-            if (getAction == null) {
-                // channel has no get action, return a default
-                switch (channelConfig.getDataType()) {
-                    case "boolean":
-                        return OnOffType.OFF;
-                    case "string":
-                        return StringType.EMPTY;
-                    default:
-                        return UnDefType.UNDEF;
-                }
-            }
-
-            // get value(s) from remote device
-            Map<String, String> arguments = new HashMap<>();
-            String parameter = channelConfig.getParameter();
-            ActionType action = channelConfig.getChannelTypeDescription().getGetAction();
-            if (parameter != null && !action.getParameter().isInternalOnly()) {
-                arguments.put(action.getParameter().getName(), parameter);
-            }
-            SOAPMessage soapResponse = doSOAPRequest(channelConfig.getService(), getAction.getName(), arguments);
-
-            String argumentName = channelConfig.getChannelTypeDescription().getGetAction().getArgument();
-            // find all other channels with the same action that are already in cache, so we can update them
-            Map<ChannelUID, Tr064ChannelConfig> channelsInRequest = channelConfigMap.entrySet().stream()
-                    .filter(map -> getAction.equals(map.getValue().getGetAction())
-                            && stateCache.containsKey(map.getKey())
-                            && !argumentName
-                                    .equals(map.getValue().getChannelTypeDescription().getGetAction().getArgument()))
-                    .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
-            channelsInRequest
-                    .forEach(
-                            (channelUID,
-                                    channelConfig1) -> soapValueConverter
-                                            .getStateFromSOAPValue(soapResponse,
-                                                    channelConfig1.getChannelTypeDescription().getGetAction()
-                                                            .getArgument(),
-                                                    channelConfig1)
-                                            .ifPresent(state -> stateCache.putValue(channelUID, state)));
-
-            return soapValueConverter.getStateFromSOAPValue(soapResponse, argumentName, channelConfig)
-                    .orElseThrow(() -> new Tr064CommunicationException("failed to transform '"
-                            + channelConfig.getChannelTypeDescription().getGetAction().getArgument() + "'"));
-        } catch (Tr064CommunicationException e) {
-            logger.info("Failed to get {}: {}", channelConfig, e.getMessage());
-            return UnDefType.UNDEF;
-        }
-    }
-}
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
deleted file mode 100644 (file)
index 214a49b..0000000
+++ /dev/null
@@ -1,255 +0,0 @@
-/**
- * Copyright (c) 2010-2020 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.tr064.internal;
-
-import static org.openhab.binding.tr064.internal.util.Util.getSOAPElement;
-
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.math.BigDecimal;
-import java.util.Optional;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-import javax.xml.soap.SOAPMessage;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
-import org.eclipse.jetty.client.HttpClient;
-import org.eclipse.jetty.client.api.ContentResponse;
-import org.openhab.binding.tr064.internal.config.Tr064ChannelConfig;
-import org.openhab.core.library.types.DecimalType;
-import org.openhab.core.library.types.OnOffType;
-import org.openhab.core.library.types.QuantityType;
-import org.openhab.core.library.types.StringType;
-import org.openhab.core.types.Command;
-import org.openhab.core.types.State;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * The {@link SOAPValueConverter} converts SOAP values and openHAB states
- *
- * @author Jan N. Klug - Initial contribution
- */
-@NonNullByDefault
-public class SOAPValueConverter {
-    private static final int REQUEST_TIMEOUT = 5000; // in ms
-    private final Logger logger = LoggerFactory.getLogger(SOAPValueConverter.class);
-    private final HttpClient httpClient;
-
-    public SOAPValueConverter(HttpClient httpClient) {
-        this.httpClient = httpClient;
-    }
-
-    /**
-     * convert an openHAB command to a SOAP value
-     *
-     * @param command the command to be converted
-     * @param dataType the datatype to send
-     * @param unit if available, the unit of the converted value
-     * @return a string optional containing the converted value
-     */
-    public Optional<String> getSOAPValueFromCommand(Command command, String dataType, String unit) {
-        if (dataType.isEmpty()) {
-            // we don't have data to send
-            return Optional.of("");
-        }
-        if (command instanceof QuantityType) {
-            QuantityType<?> value = (unit.isEmpty()) ? ((QuantityType<?>) command)
-                    : ((QuantityType<?>) command).toUnit(unit);
-            if (value == null) {
-                logger.warn("Could not convert {} to unit {}", command, unit);
-                return Optional.empty();
-            }
-            switch (dataType) {
-                case "ui2":
-                    return Optional.of(String.valueOf(value.shortValue()));
-                case "ui4":
-                    return Optional.of(String.valueOf(value.intValue()));
-                default:
-            }
-        } else if (command instanceof DecimalType) {
-            BigDecimal value = ((DecimalType) command).toBigDecimal();
-            switch (dataType) {
-                case "ui2":
-                    return Optional.of(String.valueOf(value.shortValue()));
-                case "ui4":
-                    return Optional.of(String.valueOf(value.intValue()));
-                default:
-            }
-        } else if (command instanceof StringType) {
-            if (dataType.equals("string")) {
-                return Optional.of(command.toString());
-            }
-        } else if (command instanceof OnOffType) {
-            if (dataType.equals("boolean")) {
-                return Optional.of(OnOffType.ON.equals(command) ? "1" : "0");
-            }
-        }
-        return Optional.empty();
-    }
-
-    /**
-     * convert the value from a SOAP message to an openHAB value
-     *
-     * @param soapMessage the inbound SOAP message
-     * @param element the element that needs to be extracted
-     * @param channelConfig the channel config containing additional information (if null a data-type "string" and
-     *            missing unit is assumed)
-     * @return an Optional of State containing the converted value
-     */
-    public Optional<State> getStateFromSOAPValue(SOAPMessage soapMessage, String element,
-            @Nullable Tr064ChannelConfig channelConfig) {
-        String dataType = channelConfig != null ? channelConfig.getDataType() : "string";
-        String unit = channelConfig != null ? channelConfig.getChannelTypeDescription().getItem().getUnit() : "";
-
-        return getSOAPElement(soapMessage, element).map(rawValue -> {
-            // map rawValue to State
-            switch (dataType) {
-                case "boolean":
-                    return rawValue.equals("0") ? OnOffType.OFF : OnOffType.ON;
-                case "string":
-                    return new StringType(rawValue);
-                case "ui2":
-                case "ui4":
-                    if (!unit.isEmpty()) {
-                        return new QuantityType<>(rawValue + " " + unit);
-                    } else {
-                        return new DecimalType(rawValue);
-                    }
-                default:
-                    return null;
-            }
-        }).map(state -> {
-            // check if we need post processing
-            if (channelConfig == null
-                    || channelConfig.getChannelTypeDescription().getGetAction().getPostProcessor() == null) {
-                return state;
-            }
-            String postProcessor = channelConfig.getChannelTypeDescription().getGetAction().getPostProcessor();
-            try {
-                Method method = SOAPValueConverter.class.getDeclaredMethod(postProcessor, State.class,
-                        Tr064ChannelConfig.class);
-                Object o = method.invoke(this, state, channelConfig);
-                if (o instanceof State) {
-                    return (State) o;
-                }
-            } catch (NoSuchMethodException | IllegalAccessException e) {
-                logger.warn("Postprocessor {} not found, this most likely is a programming error", postProcessor, e);
-            } catch (InvocationTargetException e) {
-                Throwable cause = e.getCause();
-                logger.info("Postprocessor {} failed: {}", postProcessor,
-                        cause != null ? cause.getMessage() : e.getMessage());
-            }
-            return null;
-        }).or(Optional::empty);
-    }
-
-    /**
-     * post processor for answering machine new messages channel
-     *
-     * @param state the message list URL
-     * @param channelConfig channel config of the TAM new message channel
-     * @return the number of new messages
-     * @throws PostProcessingException if the message list could not be retrieved
-     */
-    @SuppressWarnings("unused")
-    private State processTamListURL(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException {
-        try {
-            ContentResponse response = httpClient.newRequest(state.toString()).timeout(1000, TimeUnit.MILLISECONDS)
-                    .send();
-            String responseContent = response.getContentAsString();
-            int messageCount = responseContent.split("<New>1</New>").length - 1;
-
-            return new DecimalType(messageCount);
-        } catch (InterruptedException | TimeoutException | ExecutionException e) {
-            throw new PostProcessingException("Failed to get TAM list from URL " + state.toString(), e);
-        }
-    }
-
-    /**
-     * post processor for missed calls
-     *
-     * @param state the call list URL
-     * @param channelConfig channel config of the missed call channel (contains day number)
-     * @return the number of missed calls
-     * @throws PostProcessingException if call list could not be retrieved
-     */
-    @SuppressWarnings("unused")
-    private State processMissedCalls(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException {
-        return processCallList(state, channelConfig.getParameter(), "2");
-    }
-
-    /**
-     * post processor for inbound calls
-     *
-     * @param state the call list URL
-     * @param channelConfig channel config of the inbound call channel (contains day number)
-     * @return the number of inbound calls
-     * @throws PostProcessingException if call list could not be retrieved
-     */
-    @SuppressWarnings("unused")
-    private State processInboundCalls(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException {
-        return processCallList(state, channelConfig.getParameter(), "1");
-    }
-
-    /**
-     * post processor for rejected calls
-     *
-     * @param state the call list URL
-     * @param channelConfig channel config of the rejected call channel (contains day number)
-     * @return the number of rejected calls
-     * @throws PostProcessingException if call list could not be retrieved
-     */
-    @SuppressWarnings("unused")
-    private State processRejectedCalls(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException {
-        return processCallList(state, channelConfig.getParameter(), "3");
-    }
-
-    /**
-     * post processor for outbound calls
-     *
-     * @param state the call list URL
-     * @param channelConfig channel config of the outbound call channel (contains day number)
-     * @return the number of outbound calls
-     * @throws PostProcessingException if call list could not be retrieved
-     */
-    @SuppressWarnings("unused")
-    private State processOutboundCalls(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException {
-        return processCallList(state, channelConfig.getParameter(), "4");
-    }
-
-    /**
-     * internal helper for call list post processors
-     *
-     * @param state the call list URL
-     * @param days number of days to get
-     * @param type type of call (1=missed 2=inbound 3=rejected 4=outbund)
-     * @return the quantity of calls of the given type within the given number of days
-     * @throws PostProcessingException if the call list could not be retrieved
-     */
-    private State processCallList(State state, @Nullable String days, String type) throws PostProcessingException {
-        try {
-            ContentResponse response = httpClient.newRequest(state.toString() + "&days=" + days)
-                    .timeout(REQUEST_TIMEOUT, TimeUnit.MILLISECONDS).send();
-            String responseContent = response.getContentAsString();
-            int callCount = responseContent.split("<Type>" + type + "</Type>").length - 1;
-
-            return new DecimalType(callCount);
-        } catch (InterruptedException | TimeoutException | ExecutionException e) {
-            throw new PostProcessingException("Failed to get call list from URL " + state.toString(), e);
-        }
-    }
-}
index c40dd503de6c19e76900a2f97f386239e1424c41..d62acf3c8eec3e364c8cba7eee40c9530429e5cc 100644 (file)
@@ -21,8 +21,8 @@ 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.eclipse.jetty.util.ssl.SslContextFactory;
 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;
@@ -31,7 +31,10 @@ 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.Deactivate;
 import org.osgi.service.component.annotations.Reference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * The {@link Tr064HandlerFactory} is responsible for creating things and thing
@@ -46,6 +49,7 @@ public class Tr064HandlerFactory extends BaseThingHandlerFactory {
             .of(Tr064RootHandler.SUPPORTED_THING_TYPES, Tr064SubHandler.SUPPORTED_THING_TYPES).flatMap(Set::stream)
             .collect(Collectors.toSet());
 
+    private final Logger logger = LoggerFactory.getLogger(Tr064HandlerFactory.class);
     private final HttpClient httpClient;
     private final PhonebookProfileFactory phonebookProfileFactory;
 
@@ -56,12 +60,29 @@ public class Tr064HandlerFactory extends BaseThingHandlerFactory {
     private final Tr064ChannelTypeProvider channelTypeProvider;
 
     @Activate
-    public Tr064HandlerFactory(@Reference HttpClientFactory httpClientFactory,
-            @Reference Tr064ChannelTypeProvider channelTypeProvider,
+    public Tr064HandlerFactory(@Reference Tr064ChannelTypeProvider channelTypeProvider,
             @Reference PhonebookProfileFactory phonebookProfileFactory) {
-        httpClient = httpClientFactory.getCommonHttpClient();
         this.channelTypeProvider = channelTypeProvider;
         this.phonebookProfileFactory = phonebookProfileFactory;
+        // use an insecure client (i.e. without verifying the certificate)
+        this.httpClient = new HttpClient(new SslContextFactory(true));
+        try {
+            this.httpClient.start();
+        } catch (Exception e) {
+            // catching exception is necessary due to the signature of HttpClient.start()
+            logger.warn("Failed to start http client: {}", e.getMessage());
+            throw new IllegalStateException("Could not create HttpClient instance.", e);
+        }
+    }
+
+    @Deactivate
+    public void deactivate() {
+        try {
+            httpClient.stop();
+        } catch (Exception e) {
+            // catching exception is necessary due to the signature of HttpClient.stop()
+            logger.warn("Failed to stop http client: {}", e.getMessage());
+        }
     }
 
     @Override
index af03c1dcf90196426e73600e327637efa5976c7c..5889f770d35af2b459329268943565506e64bd20 100644 (file)
@@ -40,6 +40,8 @@ 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.soap.SOAPConnector;
+import org.openhab.binding.tr064.internal.soap.SOAPValueConverter;
 import org.openhab.binding.tr064.internal.util.SCPDUtil;
 import org.openhab.binding.tr064.internal.util.Util;
 import org.openhab.core.cache.ExpiringCacheMap;
index ab70fdd45622aa57825800e129b567526115f3d3..1cd6dff42b465d502d31d609e8cb8a996fe474f3 100644 (file)
@@ -26,6 +26,7 @@ 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.soap.SOAPConnector;
 import org.openhab.binding.tr064.internal.util.SCPDUtil;
 import org.openhab.binding.tr064.internal.util.Util;
 import org.openhab.core.cache.ExpiringCacheMap;
index 51b391edd28b19940aba8a03e240e85967f805e4..5800a54cc1fd15f98237b45d21439ed19ec3a2ed 100644 (file)
@@ -35,6 +35,7 @@ public class Tr064RootConfiguration extends Tr064BaseThingConfiguration {
     public List<String> rejectedCallDays = Collections.emptyList();
     public List<String> inboundCallDays = Collections.emptyList();
     public List<String> outboundCallDays = Collections.emptyList();
+    public List<String> callListDays = Collections.emptyList();
     public int phonebookInterval = 0;
 
     public boolean isValid() {
index a020c72dce5c91a0b01f7e7c6cb4fc7cd17eb123..bb2131fe749b8ebbdb881c7ea10ef2b7acc1a4b9 100644 (file)
@@ -31,8 +31,8 @@ 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.openhab.binding.tr064.internal.dto.additions.NumberType;
+import org.openhab.binding.tr064.internal.dto.additions.PhonebooksType;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/soap/CallListEntry.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/soap/CallListEntry.java
new file mode 100644 (file)
index 0000000..2ffa433
--- /dev/null
@@ -0,0 +1,56 @@
+/**
+ * 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.soap;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.tr064.internal.dto.additions.Call;
+
+/**
+ * The {@link CallListEntry} is used for post processing the retrieved call
+ * lists
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class CallListEntry {
+    private static final SimpleDateFormat DATE_FORMAT_PARSER = new SimpleDateFormat("dd.MM.yy hh:mm");
+    public @Nullable String localNumber;
+    public @Nullable String remoteNumber;
+    public @Nullable Date date;
+    public @Nullable Integer type;
+    public @Nullable Integer duration;
+
+    public CallListEntry(Call call) {
+        try {
+            date = DATE_FORMAT_PARSER.parse(call.getDate());
+        } catch (ParseException e) {
+            // ignore parsing error
+            date = null;
+        }
+        String[] durationParts = call.getDuration().split(":");
+        duration = Integer.parseInt(durationParts[0]) * 60 + Integer.parseInt(durationParts[1]);
+        type = Integer.parseInt(call.getType());
+        if (CallListType.OUTBOUND_COUNT.typeString().equals(call.getType())) {
+            localNumber = call.getCallerNumber();
+            remoteNumber = call.getCalled();
+        } else {
+            localNumber = call.getCalledNumber();
+            remoteNumber = call.getCaller();
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/soap/CallListType.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/soap/CallListType.java
new file mode 100644 (file)
index 0000000..f9aaca6
--- /dev/null
@@ -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.soap;
+
+/**
+ * The {@link CallListType} is used for post processing the retrieved call list
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+public enum CallListType {
+    MISSED_COUNT("2"),
+    INBOUND_COUNT("1"),
+    REJECTED_COUNT("10"),
+    OUTBOUND_COUNT("3"),
+    JSON_LIST("");
+
+    private final String value;
+
+    CallListType(String value) {
+        this.value = value;
+    }
+
+    public String typeString() {
+        return value;
+    }
+}
diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/soap/PostProcessingException.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/soap/PostProcessingException.java
new file mode 100644 (file)
index 0000000..5be14fb
--- /dev/null
@@ -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.soap;
+
+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) {
+        super(message);
+    }
+
+    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/soap/SOAPConnector.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/soap/SOAPConnector.java
new file mode 100644 (file)
index 0000000..35da04b
--- /dev/null
@@ -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.soap;
+
+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.Tr064CommunicationException;
+import org.openhab.binding.tr064.internal.config.Tr064ChannelConfig;
+import org.openhab.binding.tr064.internal.dto.config.ActionType;
+import org.openhab.binding.tr064.internal.dto.config.ChannelTypeDescription;
+import org.openhab.binding.tr064.internal.dto.scpd.root.SCPDServiceType;
+import org.openhab.binding.tr064.internal.dto.scpd.service.SCPDActionType;
+import org.openhab.core.cache.ExpiringCacheMap;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link SOAPConnector} provides communication with a remote SOAP device
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class SOAPConnector {
+    private static final int SOAP_TIMEOUT = 2000; // in ms
+    private final Logger logger = LoggerFactory.getLogger(SOAPConnector.class);
+    private final HttpClient httpClient;
+    private final String endpointBaseURL;
+    private final SOAPValueConverter soapValueConverter;
+
+    public SOAPConnector(HttpClient httpClient, String endpointBaseURL) {
+        this.httpClient = httpClient;
+        this.endpointBaseURL = endpointBaseURL;
+        this.soapValueConverter = new SOAPValueConverter(httpClient);
+    }
+
+    /**
+     * prepare a SOAP request for an action request to a service
+     *
+     * @param service the service
+     * @param soapAction the action to send
+     * @param arguments arguments to send along with the request
+     * @return a jetty Request containing the full SOAP message
+     * @throws IOException if a problem while writing the SOAP message to the Request occurs
+     * @throws SOAPException if a problem with creating the SOAP message occurs
+     */
+    private Request prepareSOAPRequest(SCPDServiceType service, String soapAction, Map<String, String> arguments)
+            throws IOException, SOAPException {
+        MessageFactory messageFactory = MessageFactory.newInstance();
+        SOAPMessage soapMessage = messageFactory.createMessage();
+        SOAPPart soapPart = soapMessage.getSOAPPart();
+        SOAPEnvelope envelope = soapPart.getEnvelope();
+        envelope.setEncodingStyle("http://schemas.xmlsoap.org/soap/encoding/");
+
+        // SOAP body
+        SOAPBody soapBody = envelope.getBody();
+        SOAPElement soapBodyElem = soapBody.addChildElement(soapAction, "u", service.getServiceType());
+        arguments.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach(argument -> {
+            try {
+                soapBodyElem.addChildElement(argument.getKey()).setTextContent(argument.getValue());
+            } catch (SOAPException e) {
+                logger.warn("Could not add {}:{} to SOAP Request: {}", argument.getKey(), argument.getValue(),
+                        e.getMessage());
+            }
+        });
+
+        // SOAP headers
+        MimeHeaders headers = soapMessage.getMimeHeaders();
+        headers.addHeader("SOAPAction", service.getServiceType() + "#" + soapAction);
+        soapMessage.saveChanges();
+
+        // create Request and add headers and content
+        Request request = httpClient.newRequest(endpointBaseURL + service.getControlURL()).method(HttpMethod.POST);
+        ((Iterator<MimeHeader>) soapMessage.getMimeHeaders().getAllHeaders())
+                .forEachRemaining(header -> request.header(header.getName(), header.getValue()));
+        try (final ByteArrayOutputStream os = new ByteArrayOutputStream()) {
+            soapMessage.writeTo(os);
+            byte[] content = os.toByteArray();
+            request.content(new BytesContentProvider(content));
+        }
+
+        return request;
+    }
+
+    /**
+     * execute a SOAP request
+     *
+     * @param service the service to send the action to
+     * @param soapAction the action itself
+     * @param arguments arguments to send along with the request
+     * @return the SOAPMessage answer from the remote host
+     * @throws Tr064CommunicationException if an error occurs during the request
+     */
+    public synchronized SOAPMessage doSOAPRequest(SCPDServiceType service, String soapAction,
+            Map<String, String> arguments) throws Tr064CommunicationException {
+        try {
+            Request request = prepareSOAPRequest(service, soapAction, arguments).timeout(SOAP_TIMEOUT,
+                    TimeUnit.MILLISECONDS);
+            if (logger.isTraceEnabled()) {
+                request.getContent().forEach(buffer -> logger.trace("Request: {}", new String(buffer.array())));
+            }
+
+            ContentResponse response = request.send();
+            if (response.getStatus() == HttpStatus.UNAUTHORIZED_401) {
+                // retry once if authentication expired
+                logger.trace("Re-Auth needed.");
+                httpClient.getAuthenticationStore().clearAuthenticationResults();
+                request = prepareSOAPRequest(service, soapAction, arguments).timeout(SOAP_TIMEOUT,
+                        TimeUnit.MILLISECONDS);
+                response = request.send();
+            }
+            try (final ByteArrayInputStream is = new ByteArrayInputStream(response.getContent())) {
+                logger.trace("Received response: {}", response.getContentAsString());
+
+                SOAPMessage soapMessage = MessageFactory.newInstance().createMessage(null, is);
+                if (soapMessage.getSOAPBody().hasFault()) {
+                    String soapError = getSOAPElement(soapMessage, "errorCode").orElse("unknown");
+                    String soapReason = getSOAPElement(soapMessage, "errorDescription").orElse("unknown");
+                    String error = String.format("HTTP-Response-Code %d (%s), SOAP-Fault: %s (%s)",
+                            response.getStatus(), response.getReason(), soapError, soapReason);
+                    throw new Tr064CommunicationException(error);
+                }
+                return soapMessage;
+            }
+        } catch (IOException | SOAPException | InterruptedException | TimeoutException | ExecutionException e) {
+            throw new Tr064CommunicationException(e);
+        }
+    }
+
+    /**
+     * send a command to the remote device
+     *
+     * @param channelConfig the channel config containing all information
+     * @param command the command to send
+     */
+    public void sendChannelCommandToDevice(Tr064ChannelConfig channelConfig, Command command) {
+        soapValueConverter.getSOAPValueFromCommand(command, channelConfig.getDataType(),
+                channelConfig.getChannelTypeDescription().getItem().getUnit()).ifPresentOrElse(value -> {
+                    final ChannelTypeDescription channelTypeDescription = channelConfig.getChannelTypeDescription();
+                    final SCPDServiceType service = channelConfig.getService();
+                    logger.debug("Sending {} as {} to {}/{}", command, value, service.getServiceId(),
+                            channelTypeDescription.getSetAction().getName());
+                    try {
+                        Map<String, String> arguments = new HashMap<>();
+                        if (channelTypeDescription.getSetAction().getArgument() != null) {
+                            arguments.put(channelTypeDescription.getSetAction().getArgument(), value);
+                        }
+                        String parameter = channelConfig.getParameter();
+                        if (parameter != null) {
+                            arguments.put(
+                                    channelConfig.getChannelTypeDescription().getGetAction().getParameter().getName(),
+                                    parameter);
+                        }
+                        doSOAPRequest(service, channelTypeDescription.getSetAction().getName(), arguments);
+                    } catch (Tr064CommunicationException e) {
+                        logger.warn("Could not send command {}: {}", command, e.getMessage());
+                    }
+                }, () -> logger.warn("Could not convert {} to SOAP value", command));
+    }
+
+    /**
+     * get a value from the remote device - updates state cache for all possible channels
+     *
+     * @param channelConfig the channel config containing all information
+     * @param channelConfigMap map of all channels in the device
+     * @param stateCache the ExpiringCacheMap for states of the device
+     * @return the value for the requested channel
+     */
+    public State getChannelStateFromDevice(final Tr064ChannelConfig channelConfig,
+            Map<ChannelUID, Tr064ChannelConfig> channelConfigMap, ExpiringCacheMap<ChannelUID, State> stateCache) {
+        try {
+            final SCPDActionType getAction = channelConfig.getGetAction();
+            if (getAction == null) {
+                // channel has no get action, return a default
+                switch (channelConfig.getDataType()) {
+                    case "boolean":
+                        return OnOffType.OFF;
+                    case "string":
+                        return StringType.EMPTY;
+                    default:
+                        return UnDefType.UNDEF;
+                }
+            }
+
+            // get value(s) from remote device
+            Map<String, String> arguments = new HashMap<>();
+            String parameter = channelConfig.getParameter();
+            ActionType action = channelConfig.getChannelTypeDescription().getGetAction();
+            if (parameter != null && !action.getParameter().isInternalOnly()) {
+                arguments.put(action.getParameter().getName(), parameter);
+            }
+            SOAPMessage soapResponse = doSOAPRequest(channelConfig.getService(), getAction.getName(), arguments);
+
+            String argumentName = channelConfig.getChannelTypeDescription().getGetAction().getArgument();
+            // find all other channels with the same action that are already in cache, so we can update them
+            Map<ChannelUID, Tr064ChannelConfig> channelsInRequest = channelConfigMap.entrySet().stream()
+                    .filter(map -> getAction.equals(map.getValue().getGetAction())
+                            && stateCache.containsKey(map.getKey())
+                            && !argumentName
+                                    .equals(map.getValue().getChannelTypeDescription().getGetAction().getArgument()))
+                    .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+            channelsInRequest
+                    .forEach(
+                            (channelUID,
+                                    channelConfig1) -> soapValueConverter
+                                            .getStateFromSOAPValue(soapResponse,
+                                                    channelConfig1.getChannelTypeDescription().getGetAction()
+                                                            .getArgument(),
+                                                    channelConfig1)
+                                            .ifPresent(state -> stateCache.putValue(channelUID, state)));
+
+            return soapValueConverter.getStateFromSOAPValue(soapResponse, argumentName, channelConfig)
+                    .orElseThrow(() -> new Tr064CommunicationException("failed to transform '"
+                            + channelConfig.getChannelTypeDescription().getGetAction().getArgument() + "'"));
+        } catch (Tr064CommunicationException e) {
+            logger.info("Failed to get {}: {}", channelConfig, e.getMessage());
+            return UnDefType.UNDEF;
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/soap/SOAPValueConverter.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/soap/SOAPValueConverter.java
new file mode 100644 (file)
index 0000000..f653d23
--- /dev/null
@@ -0,0 +1,287 @@
+/**
+ * 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.soap;
+
+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.List;
+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.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.binding.tr064.internal.dto.additions.Call;
+import org.openhab.binding.tr064.internal.dto.additions.Root;
+import org.openhab.binding.tr064.internal.util.Util;
+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.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+/**
+ * The {@link SOAPValueConverter} converts SOAP values and openHAB states
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class SOAPValueConverter {
+    private static final int REQUEST_TIMEOUT = 5000; // in ms
+    private final Logger logger = LoggerFactory.getLogger(SOAPValueConverter.class);
+    private final HttpClient httpClient;
+
+    public SOAPValueConverter(HttpClient httpClient) {
+        this.httpClient = httpClient;
+    }
+
+    /**
+     * convert an openHAB command to a SOAP value
+     *
+     * @param command the command to be converted
+     * @param dataType the datatype to send
+     * @param unit if available, the unit of the converted value
+     * @return a string optional containing the converted value
+     */
+    public Optional<String> getSOAPValueFromCommand(Command command, String dataType, String unit) {
+        if (dataType.isEmpty()) {
+            // we don't have data to send
+            return Optional.of("");
+        }
+        if (command instanceof QuantityType) {
+            QuantityType<?> value = (unit.isEmpty()) ? ((QuantityType<?>) command)
+                    : ((QuantityType<?>) command).toUnit(unit);
+            if (value == null) {
+                logger.warn("Could not convert {} to unit {}", command, unit);
+                return Optional.empty();
+            }
+            switch (dataType) {
+                case "ui2":
+                    return Optional.of(String.valueOf(value.shortValue()));
+                case "ui4":
+                    return Optional.of(String.valueOf(value.intValue()));
+                default:
+            }
+        } else if (command instanceof DecimalType) {
+            BigDecimal value = ((DecimalType) command).toBigDecimal();
+            switch (dataType) {
+                case "ui2":
+                    return Optional.of(String.valueOf(value.shortValue()));
+                case "ui4":
+                    return Optional.of(String.valueOf(value.intValue()));
+                default:
+            }
+        } else if (command instanceof StringType) {
+            if (dataType.equals("string")) {
+                return Optional.of(command.toString());
+            }
+        } else if (command instanceof OnOffType) {
+            if (dataType.equals("boolean")) {
+                return Optional.of(OnOffType.ON.equals(command) ? "1" : "0");
+            }
+        }
+        return Optional.empty();
+    }
+
+    /**
+     * convert the value from a SOAP message to an openHAB value
+     *
+     * @param soapMessage the inbound SOAP message
+     * @param element the element that needs to be extracted
+     * @param channelConfig the channel config containing additional information (if null a data-type "string" and
+     *            missing unit is assumed)
+     * @return an Optional of State containing the converted value
+     */
+    public Optional<State> getStateFromSOAPValue(SOAPMessage soapMessage, String element,
+            @Nullable Tr064ChannelConfig channelConfig) {
+        String dataType = channelConfig != null ? channelConfig.getDataType() : "string";
+        String unit = channelConfig != null ? channelConfig.getChannelTypeDescription().getItem().getUnit() : "";
+
+        return getSOAPElement(soapMessage, element).map(rawValue -> {
+            // map rawValue to State
+            switch (dataType) {
+                case "boolean":
+                    return rawValue.equals("0") ? OnOffType.OFF : OnOffType.ON;
+                case "string":
+                    return new StringType(rawValue);
+                case "ui2":
+                case "ui4":
+                    if (!unit.isEmpty()) {
+                        return new QuantityType<>(rawValue + " " + unit);
+                    } else {
+                        return new DecimalType(rawValue);
+                    }
+                default:
+                    return null;
+            }
+        }).map(state -> {
+            // check if we need post processing
+            if (channelConfig == null
+                    || channelConfig.getChannelTypeDescription().getGetAction().getPostProcessor() == null) {
+                return state;
+            }
+            String postProcessor = channelConfig.getChannelTypeDescription().getGetAction().getPostProcessor();
+            try {
+                Method method = SOAPValueConverter.class.getDeclaredMethod(postProcessor, State.class,
+                        Tr064ChannelConfig.class);
+                Object o = method.invoke(this, state, channelConfig);
+                if (o instanceof State) {
+                    return (State) o;
+                }
+            } catch (NoSuchMethodException | IllegalAccessException e) {
+                logger.warn("Postprocessor {} not found, this most likely is a programming error", postProcessor, e);
+            } catch (InvocationTargetException e) {
+                Throwable cause = e.getCause();
+                logger.info("Postprocessor {} failed: {}", postProcessor,
+                        cause != null ? cause.getMessage() : e.getMessage());
+            }
+            return null;
+        }).or(Optional::empty);
+    }
+
+    /**
+     * post processor for answering machine new messages channel
+     *
+     * @param state the message list URL
+     * @param channelConfig channel config of the TAM new message channel
+     * @return the number of new messages
+     * @throws PostProcessingException if the message list could not be retrieved
+     */
+    @SuppressWarnings("unused")
+    private State processTamListURL(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException {
+        try {
+            ContentResponse response = httpClient.newRequest(state.toString()).timeout(1000, TimeUnit.MILLISECONDS)
+                    .send();
+            String responseContent = response.getContentAsString();
+            int messageCount = responseContent.split("<New>1</New>").length - 1;
+
+            return new DecimalType(messageCount);
+        } catch (InterruptedException | TimeoutException | ExecutionException e) {
+            throw new PostProcessingException("Failed to get TAM list from URL " + state.toString(), e);
+        }
+    }
+
+    /**
+     * post processor for missed calls
+     *
+     * @param state the call list URL
+     * @param channelConfig channel config of the missed call channel (contains day number)
+     * @return the number of missed calls
+     * @throws PostProcessingException if call list could not be retrieved
+     */
+    @SuppressWarnings("unused")
+    private State processMissedCalls(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException {
+        return processCallList(state, channelConfig.getParameter(), CallListType.MISSED_COUNT);
+    }
+
+    /**
+     * 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(), CallListType.INBOUND_COUNT);
+    }
+
+    /**
+     * 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(), CallListType.REJECTED_COUNT);
+    }
+
+    /**
+     * 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(), CallListType.OUTBOUND_COUNT);
+    }
+
+    /**
+     * post processor for JSON call list
+     *
+     * @param state the call list URL
+     * @param channelConfig channel config of the call list channel (contains day number)
+     * @return caller list in JSON format
+     * @throws PostProcessingException if call list could not be retrieved
+     */
+    @SuppressWarnings("unused")
+    private State processCallListJSON(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException {
+        return processCallList(state, channelConfig.getParameter(), CallListType.JSON_LIST);
+    }
+
+    /**
+     * 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 (2=missed 1=inbound 4=rejected 3=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, CallListType type)
+            throws PostProcessingException {
+        Root callListRoot = Util.getAndUnmarshalXML(httpClient, state.toString() + "&days=" + days, Root.class);
+        if (callListRoot == null) {
+            throw new PostProcessingException("Failed to get call list from URL " + state.toString());
+        }
+        List<Call> calls = callListRoot.getCall();
+        switch (type) {
+            case INBOUND_COUNT:
+            case MISSED_COUNT:
+            case OUTBOUND_COUNT:
+            case REJECTED_COUNT:
+                long callCount = calls.stream().filter(call -> type.typeString().equals(call.getType())).count();
+                return new DecimalType(callCount);
+            case JSON_LIST:
+                Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ssX").serializeNulls().create();
+                List<CallListEntry> callListEntries = calls.stream().map(CallListEntry::new)
+                        .collect(Collectors.toList());
+                return new StringType(gson.toJson(callListEntries));
+        }
+        return UnDefType.UNDEF;
+    }
+}
index 3de7469785349064788aa0416e5ce046257db34c..5269c31f13d04d5fe01493a4dd5e8a213c49523c 100644 (file)
  */
 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
@@ -51,18 +36,12 @@ import org.slf4j.LoggerFactory;
  */
 @NonNullByDefault
 public class SCPDUtil {
-    private final Logger logger = LoggerFactory.getLogger(SCPDUtil.class);
-
-    private final HttpClient httpClient;
-
     private SCPDRootType scpdRoot;
     private final List<SCPDDeviceType> scpdDevicesList = new ArrayList<>();
     private final Map<String, SCPDScpdType> serviceMap = new HashMap<>();
 
     public SCPDUtil(HttpClient httpClient, String endpoint) throws SCPDException {
-        this.httpClient = httpClient;
-
-        SCPDRootType scpdRoot = getAndUnmarshalSCPD(endpoint + "/tr64desc.xml", SCPDRootType.class);
+        SCPDRootType scpdRoot = Util.getAndUnmarshalXML(httpClient, endpoint + "/tr64desc.xml", SCPDRootType.class);
         if (scpdRoot == null) {
             throw new SCPDException("could not get SCPD root");
         }
@@ -71,8 +50,8 @@ public class SCPDUtil {
         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));
+                SCPDScpdType scpd = serviceMap.computeIfAbsent(service.getServiceId(), serviceId -> Util
+                        .getAndUnmarshalXML(httpClient, endpoint + service.getSCPDURL(), SCPDScpdType.class));
                 if (scpd == null) {
                     throw new SCPDException("could not get SCPD service");
                 }
@@ -80,30 +59,6 @@ public class SCPDUtil {
         }
     }
 
-    /**
-     * generic unmarshaller
-     *
-     * @param uri the uri of the XML file
-     * @param clazz the class describing the XML file
-     * @return unmarshalling result
-     */
-    private <T> @Nullable T getAndUnmarshalSCPD(String uri, Class<T> clazz) {
-        try {
-            ContentResponse contentResponse = httpClient.newRequest(uri).timeout(2, TimeUnit.SECONDS)
-                    .method(HttpMethod.GET).send();
-            InputStream xml = new ByteArrayInputStream(contentResponse.getContent());
-
-            JAXBContext context = JAXBContext.newInstance(clazz);
-            Unmarshaller um = context.createUnmarshaller();
-            return um.unmarshal(new StreamSource(xml), clazz).getValue();
-        } catch (ExecutionException | InterruptedException | TimeoutException e) {
-            logger.debug("HTTP Failed to GET uri '{}': {}", uri, e.getMessage());
-        } catch (JAXBException e) {
-            logger.debug("Unmarshalling failed: {}", e.getMessage());
-        }
-        return null;
-    }
-
     /**
      * recursively flatten the device tree to a stream
      *
index a3146bc810b015069fc13f972351f743deab93a9..7b5b81ba6d35e2dc9bddee81d577899743b10456 100644 (file)
@@ -14,9 +14,13 @@ package org.openhab.binding.tr064.internal.util;
 
 import static org.openhab.binding.tr064.internal.Tr064BindingConstants.*;
 
+import java.io.ByteArrayInputStream;
 import java.io.InputStream;
 import java.lang.reflect.Field;
 import java.util.*;
+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;
 
@@ -29,6 +33,10 @@ import javax.xml.soap.SOAPMessage;
 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.ChannelConfigException;
 import org.openhab.binding.tr064.internal.Tr064RootHandler;
 import org.openhab.binding.tr064.internal.config.Tr064BaseThingConfiguration;
@@ -75,7 +83,7 @@ public class Util {
             return root.getValue().getChannel();
         } catch (JAXBException e) {
             LOGGER.warn("Failed to read channel definitions", e);
-            return Collections.emptyList();
+            return List.of();
         }
     }
 
@@ -274,7 +282,7 @@ public class Util {
             }
             return parameters;
         } catch (NoSuchFieldException | IllegalAccessException | IllegalArgumentException e) {
-            throw new ChannelConfigException("Could not get required parameter '" + channelId
+            throw new ChannelConfigException("Could not get required parameter for channel '" + channelId
                     + "' from thing config (missing, empty or invalid)");
         }
     }
@@ -290,4 +298,32 @@ public class Util {
         }
         return Optional.empty();
     }
+
+    /**
+     * generic unmarshaller
+     *
+     * @param uri the uri of the XML file
+     * @param clazz the class describing the XML file
+     * @return unmarshalling result
+     */
+    public static <T> @Nullable T getAndUnmarshalXML(HttpClient httpClient, String uri, Class<T> clazz) {
+        try {
+            ContentResponse contentResponse = httpClient.newRequest(uri).timeout(2, TimeUnit.SECONDS)
+                    .method(HttpMethod.GET).send();
+            byte[] response = contentResponse.getContent();
+            if (LOGGER.isTraceEnabled()) {
+                LOGGER.trace("XML = {}", new String(response));
+            }
+            InputStream xml = new ByteArrayInputStream(response);
+
+            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;
+    }
 }
index e419d713939656418c5704f917fae2154ee9f371..cf24acdb93c08d6d40f4d613a855b0b27cc22139 100644 (file)
                                <description>List of days for which outbound calls should be calculated.</description>
                                <advanced>true</advanced>
                        </parameter>
+                       <parameter name="callListDays" type="text" multiple="true">
+                               <label>Call List Days</label>
+                               <description>List of days for which JSON call list should be generated.</description>
+                               <advanced>true</advanced>
+                       </parameter>
                        <parameter name="wanBlockIPs" type="text" multiple="true">
                                <label>WAN Block IPs</label>
                                <description>List of IPs that can be blocked for WAN access.</description>
index 186e9b6b6cac643c3b921b68b625b01632c57e5d..dfa3a7ca1d6872828211203f9a8a84f1f6723270 100644 (file)
                        <parameter name="CallDays" thingParameter="outboundCallDays" pattern="[0-9]+" internalOnly="true"/>
                </getAction>
        </channel>
+       <channel name="callList" label="Call List" description="Call list in JSON format for the given number of days.">
+               <item type="String"/>
+               <service deviceType="urn:dslforum-org:device:InternetGatewayDevice:1"
+                       serviceId="urn:X_AVM-DE_OnTel-com:serviceId:X_AVM-DE_OnTel1"/>
+               <getAction name="GetCallList" argument="NewCallListURL" postProcessor="processCallListJSON">
+                       <parameter name="CallDays" thingParameter="callListDays" pattern="[0-9]+" internalOnly="true"/>
+               </getAction>
+       </channel>
 
        <!-- LAN Device -->
        <channel name="wifi24GHzEnable" label="WiFi 2.4 GHz" description="Enable/Disable the 2.4 GHz WiFi device.">
diff --git a/bundles/org.openhab.binding.tr064/src/main/resources/xsd/additions.xsd b/bundles/org.openhab.binding.tr064/src/main/resources/xsd/additions.xsd
new file mode 100644 (file)
index 0000000..6ca4ee6
--- /dev/null
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified"
+           xmlns:xs="http://www.w3.org/2001/XMLSchema">
+    <xs:element name="phonebooks" type="phonebooksType"/>
+    <xs:complexType name="personType">
+        <xs:sequence>
+            <xs:element type="xs:string" name="realName"/>
+        </xs:sequence>
+    </xs:complexType>
+    <xs:complexType name="numberType">
+        <xs:simpleContent>
+            <xs:extension base="xs:string">
+                <xs:attribute type="xs:string" name="type" use="optional"/>
+                <xs:attribute type="xs:string" name="vanity" use="optional"/>
+                <xs:attribute type="xs:string" name="prio" use="optional"/>
+                <xs:attribute type="xs:byte" name="quickdial" use="optional"/>
+            </xs:extension>
+        </xs:simpleContent>
+    </xs:complexType>
+    <xs:complexType name="telephonyType">
+        <xs:sequence>
+            <xs:element type="xs:string" name="services"/>
+            <xs:element type="numberType" name="number" maxOccurs="unbounded" minOccurs="0"/>
+        </xs:sequence>
+    </xs:complexType>
+    <xs:complexType name="contactType">
+        <xs:sequence>
+            <xs:element name="category">
+                <xs:simpleType>
+                    <xs:restriction base="xs:byte">
+                        <xs:enumeration value="0"/>
+                        <xs:enumeration value="1"/>
+                    </xs:restriction>
+                </xs:simpleType>
+            </xs:element>
+            <xs:element type="personType" name="person"/>
+            <xs:element type="xs:byte" name="uniqueid"/>
+            <xs:element type="telephonyType" name="telephony"/>
+        </xs:sequence>
+    </xs:complexType>
+    <xs:complexType name="phonebookType">
+        <xs:sequence>
+            <xs:element type="xs:int" name="timestamp"/>
+            <xs:element type="contactType" name="contact" maxOccurs="unbounded" minOccurs="0"/>
+        </xs:sequence>
+        <xs:attribute type="xs:byte" name="owner"/>
+        <xs:attribute type="xs:string" name="name"/>
+    </xs:complexType>
+    <xs:complexType name="phonebooksType">
+        <xs:sequence>
+            <xs:element type="phonebookType" name="phonebook"/>
+        </xs:sequence>
+    </xs:complexType>
+    <xs:element name="root">
+        <xs:complexType>
+            <xs:sequence>
+                <xs:element ref="timestamp"/>
+                <xs:element maxOccurs="unbounded" ref="Call"/>
+            </xs:sequence>
+        </xs:complexType>
+    </xs:element>
+    <xs:element name="timestamp" type="xs:integer"/>
+    <xs:element name="Call">
+        <xs:complexType>
+            <xs:sequence>
+                <xs:element ref="Id"/>
+                <xs:element ref="Type"/>
+                <xs:element ref="Called"/>
+                <xs:element ref="Caller"/>
+                <xs:element ref="CalledNumber"/>
+                <xs:element ref="CallerNumber"/>
+                <xs:element ref="Name"/>
+                <xs:element ref="Numbertype"/>
+                <xs:element ref="Device"/>
+                <xs:element ref="Port"/>
+                <xs:element ref="Date"/>
+                <xs:element ref="Duration"/>
+                <xs:element ref="Count"/>
+                <xs:element ref="Path"/>
+            </xs:sequence>
+        </xs:complexType>
+    </xs:element>
+    <xs:element name="Id" type="xs:integer"/>
+    <xs:element name="Type" type="xs:string"/>
+    <xs:element name="Called" type="xs:string"/>
+    <xs:element name="Caller" type="xs:string"/>
+    <xs:element name="CalledNumber" type="xs:string"/>
+    <xs:element name="CallerNumber" type="xs:string"/>
+    <xs:element name="Name" type="xs:string" />
+    <xs:element name="Numbertype" type="xs:string"/>
+    <xs:element name="Device" type="xs:string"/>
+    <xs:element name="Port" type="xs:string"/>
+    <xs:element name="Date" type="xs:string"/>
+    <xs:element name="Duration" type="xs:string"/>
+    <xs:element name="Count" type="xs:string"/>
+    <xs:element name="Path" type = "xs:string"/>
+</xs:schema>
\ No newline at end of file
index 7cd2e8db71ecab981ae85cb5b74595e6dcefa31f..eeef5213955260b9bdb18a28a54c8de868022fe6 100644 (file)
@@ -4,9 +4,10 @@
         <xjc:serializable uid="1"/>
     </jaxb:globalBindings>
 
-    <jaxb:bindings schemaLocation="phonebook.xsd">
+    <!-- additions (without namespace): phonebook, calllist -->
+    <jaxb:bindings schemaLocation="additions.xsd">
         <jaxb:schemaBindings>
-            <jaxb:package name="org.openhab.binding.tr064.internal.dto.phonebook"/>
+            <jaxb:package name="org.openhab.binding.tr064.internal.dto.additions"/>
         </jaxb:schemaBindings>
     </jaxb:bindings>
 
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
deleted file mode 100644 (file)
index f5eaf5a..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified"
-           xmlns:xs="http://www.w3.org/2001/XMLSchema">
-    <xs:element name="phonebooks" type="phonebooksType"/>
-    <xs:complexType name="personType">
-        <xs:sequence>
-            <xs:element type="xs:string" name="realName"/>
-        </xs:sequence>
-    </xs:complexType>
-    <xs:complexType name="numberType">
-        <xs:simpleContent>
-            <xs:extension base="xs:string">
-                <xs:attribute type="xs:string" name="type" use="optional"/>
-                <xs:attribute type="xs:string" name="vanity" use="optional"/>
-                <xs:attribute type="xs:string" name="prio" use="optional"/>
-                <xs:attribute type="xs:byte" name="quickdial" use="optional"/>
-            </xs:extension>
-        </xs:simpleContent>
-    </xs:complexType>
-    <xs:complexType name="telephonyType">
-        <xs:sequence>
-            <xs:element type="xs:string" name="services"/>
-            <xs:element type="numberType" name="number" maxOccurs="unbounded" minOccurs="0"/>
-        </xs:sequence>
-    </xs:complexType>
-    <xs:complexType name="contactType">
-        <xs:sequence>
-            <xs:element name="category">
-                <xs:simpleType>
-                    <xs:restriction base="xs:byte">
-                        <xs:enumeration value="0"/>
-                        <xs:enumeration value="1"/>
-                    </xs:restriction>
-                </xs:simpleType>
-            </xs:element>
-            <xs:element type="personType" name="person"/>
-            <xs:element type="xs:byte" name="uniqueid"/>
-            <xs:element type="telephonyType" name="telephony"/>
-        </xs:sequence>
-    </xs:complexType>
-    <xs:complexType name="phonebookType">
-        <xs:sequence>
-            <xs:element type="xs:int" name="timestamp"/>
-            <xs:element type="contactType" name="contact" maxOccurs="unbounded" minOccurs="0"/>
-        </xs:sequence>
-        <xs:attribute type="xs:byte" name="owner"/>
-        <xs:attribute type="xs:string" name="name"/>
-    </xs:complexType>
-    <xs:complexType name="phonebooksType">
-        <xs:sequence>
-            <xs:element type="phonebookType" name="phonebook"/>
-        </xs:sequence>
-    </xs:complexType>
-</xs:schema>
\ No newline at end of file