[tr064] fix certificate problems and add call list channel (#9149)

* 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>
This commit is contained in:
J-N-K 2020-11-28 19:14:07 +01:00 committed by GitHub
parent 66421c6939
commit 936a4dc8d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 288 additions and 118 deletions

View File

@ -49,7 +49,7 @@ This is an optional parameter and multiple values are allowed.
Most devices support call lists. 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 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. 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. 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 | | channel | item-type | advanced | description |
|----------------------------|---------------------------|:--------:|----------------------------------------------------------------| |----------------------------|---------------------------|:--------:|----------------------------------------------------------------|
| `callDeflectionEnable` | `Switch` | | Enable/Disable the call deflection setup with the given index. | | `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 | | `dslCRCErrors` | `Number:Dimensionless` | x | DSL CRC Errors |
| `dslDownstreamNoiseMargin` | `Number:Dimensionless` | x | DSL Downstream Noise Margin | | `dslDownstreamNoiseMargin` | `Number:Dimensionless` | x | DSL Downstream Noise Margin |
| `dslDownstreamNoiseMargin` | `Number:Dimensionless` | x | DSL Downstream Attenuation | | `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. | | `wifi5GHzEnable` | `Switch` | | Enable/Disable the 5.0 GHz WiFi device. |
| `wifiGuestEnable` | `Switch` | | Enable/Disable the guest WiFi. | | `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 ## `PHONEBOOK` Profile
The binding provides a profile for using the FritzBox phonebooks for resolving numbers to names. 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. The default is to use all available phonebooks from the specified thing.
In case the format of the number in the phonebook and the format of the number from the channel are different (e.g. regarding country prefixes), the `matchCount` parameter can be used. In case the format of the number in the phonebook and the format of the number from the channel are different (e.g. regarding country prefixes), the `matchCount` parameter can be used.
The configured `matchCount` is counted from the right end and denotes the number of matching characters needed to consider this number as matching. The configured `matchCount` is counted from the right end and denotes the number of matching characters needed to consider this number as matching.

View File

@ -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();
}
}

View File

@ -21,8 +21,8 @@ import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.openhab.binding.tr064.internal.phonebook.PhonebookProfileFactory; 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.Bridge;
import org.openhab.core.thing.Thing; import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID; 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.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference; 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 * 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) .of(Tr064RootHandler.SUPPORTED_THING_TYPES, Tr064SubHandler.SUPPORTED_THING_TYPES).flatMap(Set::stream)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
private final Logger logger = LoggerFactory.getLogger(Tr064HandlerFactory.class);
private final HttpClient httpClient; private final HttpClient httpClient;
private final PhonebookProfileFactory phonebookProfileFactory; private final PhonebookProfileFactory phonebookProfileFactory;
@ -56,12 +60,29 @@ public class Tr064HandlerFactory extends BaseThingHandlerFactory {
private final Tr064ChannelTypeProvider channelTypeProvider; private final Tr064ChannelTypeProvider channelTypeProvider;
@Activate @Activate
public Tr064HandlerFactory(@Reference HttpClientFactory httpClientFactory, public Tr064HandlerFactory(@Reference Tr064ChannelTypeProvider channelTypeProvider,
@Reference Tr064ChannelTypeProvider channelTypeProvider,
@Reference PhonebookProfileFactory phonebookProfileFactory) { @Reference PhonebookProfileFactory phonebookProfileFactory) {
httpClient = httpClientFactory.getCommonHttpClient();
this.channelTypeProvider = channelTypeProvider; this.channelTypeProvider = channelTypeProvider;
this.phonebookProfileFactory = phonebookProfileFactory; 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 @Override

View 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.Phonebook;
import org.openhab.binding.tr064.internal.phonebook.PhonebookProvider; import org.openhab.binding.tr064.internal.phonebook.PhonebookProvider;
import org.openhab.binding.tr064.internal.phonebook.Tr064PhonebookImpl; import org.openhab.binding.tr064.internal.phonebook.Tr064PhonebookImpl;
import org.openhab.binding.tr064.internal.soap.SOAPConnector;
import org.openhab.binding.tr064.internal.soap.SOAPValueConverter;
import org.openhab.binding.tr064.internal.util.SCPDUtil; import org.openhab.binding.tr064.internal.util.SCPDUtil;
import org.openhab.binding.tr064.internal.util.Util; import org.openhab.binding.tr064.internal.util.Util;
import org.openhab.core.cache.ExpiringCacheMap; import org.openhab.core.cache.ExpiringCacheMap;

View 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.Tr064ChannelConfig;
import org.openhab.binding.tr064.internal.config.Tr064SubConfiguration; import org.openhab.binding.tr064.internal.config.Tr064SubConfiguration;
import org.openhab.binding.tr064.internal.dto.scpd.root.SCPDDeviceType; 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.SCPDUtil;
import org.openhab.binding.tr064.internal.util.Util; import org.openhab.binding.tr064.internal.util.Util;
import org.openhab.core.cache.ExpiringCacheMap; import org.openhab.core.cache.ExpiringCacheMap;

View File

@ -35,6 +35,7 @@ public class Tr064RootConfiguration extends Tr064BaseThingConfiguration {
public List<String> rejectedCallDays = Collections.emptyList(); public List<String> rejectedCallDays = Collections.emptyList();
public List<String> inboundCallDays = Collections.emptyList(); public List<String> inboundCallDays = Collections.emptyList();
public List<String> outboundCallDays = Collections.emptyList(); public List<String> outboundCallDays = Collections.emptyList();
public List<String> callListDays = Collections.emptyList();
public int phonebookInterval = 0; public int phonebookInterval = 0;
public boolean isValid() { public boolean isValid() {

View File

@ -31,8 +31,8 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpMethod;
import org.openhab.binding.tr064.internal.dto.phonebook.NumberType; import org.openhab.binding.tr064.internal.dto.additions.NumberType;
import org.openhab.binding.tr064.internal.dto.phonebook.PhonebooksType; import org.openhab.binding.tr064.internal.dto.additions.PhonebooksType;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;

View File

@ -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();
}
}
}

View File

@ -0,0 +1,36 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.tr064.internal.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;
}
}

View File

@ -10,7 +10,7 @@
* *
* SPDX-License-Identifier: EPL-2.0 * SPDX-License-Identifier: EPL-2.0
*/ */
package org.openhab.binding.tr064.internal; package org.openhab.binding.tr064.internal.soap;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
@ -25,6 +25,10 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
public class PostProcessingException extends Exception { public class PostProcessingException extends Exception {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
public PostProcessingException(String message) {
super(message);
}
public PostProcessingException(String message, Throwable t) { public PostProcessingException(String message, Throwable t) {
super(message, t); super(message, t);
} }

View File

@ -10,7 +10,7 @@
* *
* SPDX-License-Identifier: EPL-2.0 * SPDX-License-Identifier: EPL-2.0
*/ */
package org.openhab.binding.tr064.internal; package org.openhab.binding.tr064.internal.soap;
import static org.openhab.binding.tr064.internal.util.Util.getSOAPElement; import static org.openhab.binding.tr064.internal.util.Util.getSOAPElement;
@ -34,6 +34,7 @@ import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.BytesContentProvider; import org.eclipse.jetty.client.util.BytesContentProvider;
import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus; 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.config.Tr064ChannelConfig;
import org.openhab.binding.tr064.internal.dto.config.ActionType; 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.ChannelTypeDescription;

View File

@ -10,17 +10,19 @@
* *
* SPDX-License-Identifier: EPL-2.0 * SPDX-License-Identifier: EPL-2.0
*/ */
package org.openhab.binding.tr064.internal; package org.openhab.binding.tr064.internal.soap;
import static org.openhab.binding.tr064.internal.util.Util.getSOAPElement; import static org.openhab.binding.tr064.internal.util.Util.getSOAPElement;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import javax.xml.soap.SOAPMessage; import javax.xml.soap.SOAPMessage;
@ -29,15 +31,22 @@ import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.ContentResponse;
import org.openhab.binding.tr064.internal.config.Tr064ChannelConfig; 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.DecimalType;
import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType; import org.openhab.core.library.types.StringType;
import org.openhab.core.types.Command; import org.openhab.core.types.Command;
import org.openhab.core.types.State; import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/** /**
* The {@link SOAPValueConverter} converts SOAP values and openHAB states * The {@link SOAPValueConverter} converts SOAP values and openHAB states
* *
@ -189,7 +198,7 @@ public class SOAPValueConverter {
*/ */
@SuppressWarnings("unused") @SuppressWarnings("unused")
private State processMissedCalls(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException { private State processMissedCalls(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException {
return processCallList(state, channelConfig.getParameter(), "2"); return processCallList(state, channelConfig.getParameter(), CallListType.MISSED_COUNT);
} }
/** /**
@ -202,7 +211,7 @@ public class SOAPValueConverter {
*/ */
@SuppressWarnings("unused") @SuppressWarnings("unused")
private State processInboundCalls(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException { private State processInboundCalls(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException {
return processCallList(state, channelConfig.getParameter(), "1"); return processCallList(state, channelConfig.getParameter(), CallListType.INBOUND_COUNT);
} }
/** /**
@ -215,7 +224,7 @@ public class SOAPValueConverter {
*/ */
@SuppressWarnings("unused") @SuppressWarnings("unused")
private State processRejectedCalls(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException { private State processRejectedCalls(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException {
return processCallList(state, channelConfig.getParameter(), "3"); return processCallList(state, channelConfig.getParameter(), CallListType.REJECTED_COUNT);
} }
/** /**
@ -228,7 +237,20 @@ public class SOAPValueConverter {
*/ */
@SuppressWarnings("unused") @SuppressWarnings("unused")
private State processOutboundCalls(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException { private State processOutboundCalls(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException {
return processCallList(state, channelConfig.getParameter(), "4"); 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);
} }
/** /**
@ -236,20 +258,30 @@ public class SOAPValueConverter {
* *
* @param state the call list URL * @param state the call list URL
* @param days number of days to get * @param days number of days to get
* @param type type of call (1=missed 2=inbound 3=rejected 4=outbund) * @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 * @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 * @throws PostProcessingException if the call list could not be retrieved
*/ */
private State processCallList(State state, @Nullable String days, String type) throws PostProcessingException { private State processCallList(State state, @Nullable String days, CallListType type)
try { throws PostProcessingException {
ContentResponse response = httpClient.newRequest(state.toString() + "&days=" + days) Root callListRoot = Util.getAndUnmarshalXML(httpClient, state.toString() + "&days=" + days, Root.class);
.timeout(REQUEST_TIMEOUT, TimeUnit.MILLISECONDS).send(); if (callListRoot == null) {
String responseContent = response.getContentAsString(); throw new PostProcessingException("Failed to get call list from URL " + state.toString());
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);
} }
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;
} }
} }

View File

@ -12,36 +12,21 @@
*/ */
package org.openhab.binding.tr064.internal.util; package org.openhab.binding.tr064.internal.util;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; 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.Collectors;
import java.util.stream.Stream; 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.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient; 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.SCPDException;
import org.openhab.binding.tr064.internal.dto.scpd.root.SCPDDeviceType; 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.SCPDRootType;
import org.openhab.binding.tr064.internal.dto.scpd.root.SCPDServiceType; import org.openhab.binding.tr064.internal.dto.scpd.root.SCPDServiceType;
import org.openhab.binding.tr064.internal.dto.scpd.service.SCPDScpdType; 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 * The {@link SCPDUtil} is responsible for handling commands, which are
@ -51,18 +36,12 @@ import org.slf4j.LoggerFactory;
*/ */
@NonNullByDefault @NonNullByDefault
public class SCPDUtil { public class SCPDUtil {
private final Logger logger = LoggerFactory.getLogger(SCPDUtil.class);
private final HttpClient httpClient;
private SCPDRootType scpdRoot; private SCPDRootType scpdRoot;
private final List<SCPDDeviceType> scpdDevicesList = new ArrayList<>(); private final List<SCPDDeviceType> scpdDevicesList = new ArrayList<>();
private final Map<String, SCPDScpdType> serviceMap = new HashMap<>(); private final Map<String, SCPDScpdType> serviceMap = new HashMap<>();
public SCPDUtil(HttpClient httpClient, String endpoint) throws SCPDException { public SCPDUtil(HttpClient httpClient, String endpoint) throws SCPDException {
this.httpClient = httpClient; SCPDRootType scpdRoot = Util.getAndUnmarshalXML(httpClient, endpoint + "/tr64desc.xml", SCPDRootType.class);
SCPDRootType scpdRoot = getAndUnmarshalSCPD(endpoint + "/tr64desc.xml", SCPDRootType.class);
if (scpdRoot == null) { if (scpdRoot == null) {
throw new SCPDException("could not get SCPD root"); throw new SCPDException("could not get SCPD root");
} }
@ -71,8 +50,8 @@ public class SCPDUtil {
scpdDevicesList.addAll(flatDeviceList(scpdRoot.getDevice()).collect(Collectors.toList())); scpdDevicesList.addAll(flatDeviceList(scpdRoot.getDevice()).collect(Collectors.toList()));
for (SCPDDeviceType device : scpdDevicesList) { for (SCPDDeviceType device : scpdDevicesList) {
for (SCPDServiceType service : device.getServiceList()) { for (SCPDServiceType service : device.getServiceList()) {
SCPDScpdType scpd = serviceMap.computeIfAbsent(service.getServiceId(), SCPDScpdType scpd = serviceMap.computeIfAbsent(service.getServiceId(), serviceId -> Util
serviceId -> getAndUnmarshalSCPD(endpoint + service.getSCPDURL(), SCPDScpdType.class)); .getAndUnmarshalXML(httpClient, endpoint + service.getSCPDURL(), SCPDScpdType.class));
if (scpd == null) { if (scpd == null) {
throw new SCPDException("could not get SCPD service"); 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 * recursively flatten the device tree to a stream
* *

View File

@ -14,9 +14,13 @@ package org.openhab.binding.tr064.internal.util;
import static org.openhab.binding.tr064.internal.Tr064BindingConstants.*; import static org.openhab.binding.tr064.internal.Tr064BindingConstants.*;
import java.io.ByteArrayInputStream;
import java.io.InputStream; import java.io.InputStream;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.util.*; 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.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -29,6 +33,10 @@ import javax.xml.soap.SOAPMessage;
import javax.xml.transform.stream.StreamSource; import javax.xml.transform.stream.StreamSource;
import org.eclipse.jdt.annotation.NonNullByDefault; 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.ChannelConfigException;
import org.openhab.binding.tr064.internal.Tr064RootHandler; import org.openhab.binding.tr064.internal.Tr064RootHandler;
import org.openhab.binding.tr064.internal.config.Tr064BaseThingConfiguration; import org.openhab.binding.tr064.internal.config.Tr064BaseThingConfiguration;
@ -75,7 +83,7 @@ public class Util {
return root.getValue().getChannel(); return root.getValue().getChannel();
} catch (JAXBException e) { } catch (JAXBException e) {
LOGGER.warn("Failed to read channel definitions", e); LOGGER.warn("Failed to read channel definitions", e);
return Collections.emptyList(); return List.of();
} }
} }
@ -274,7 +282,7 @@ public class Util {
} }
return parameters; return parameters;
} catch (NoSuchFieldException | IllegalAccessException | IllegalArgumentException e) { } 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)"); + "' from thing config (missing, empty or invalid)");
} }
} }
@ -290,4 +298,32 @@ public class Util {
} }
return Optional.empty(); 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;
}
} }

View File

@ -82,6 +82,11 @@
<description>List of days for which outbound calls should be calculated.</description> <description>List of days for which outbound calls should be calculated.</description>
<advanced>true</advanced> <advanced>true</advanced>
</parameter> </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"> <parameter name="wanBlockIPs" type="text" multiple="true">
<label>WAN Block IPs</label> <label>WAN Block IPs</label>
<description>List of IPs that can be blocked for WAN access.</description> <description>List of IPs that can be blocked for WAN access.</description>

View File

@ -106,6 +106,14 @@
<parameter name="CallDays" thingParameter="outboundCallDays" pattern="[0-9]+" internalOnly="true"/> <parameter name="CallDays" thingParameter="outboundCallDays" pattern="[0-9]+" internalOnly="true"/>
</getAction> </getAction>
</channel> </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 --> <!-- LAN Device -->
<channel name="wifi24GHzEnable" label="WiFi 2.4 GHz" description="Enable/Disable the 2.4 GHz WiFi device."> <channel name="wifi24GHzEnable" label="WiFi 2.4 GHz" description="Enable/Disable the 2.4 GHz WiFi device.">

View File

@ -51,4 +51,47 @@
<xs:element type="phonebookType" name="phonebook"/> <xs:element type="phonebookType" name="phonebook"/>
</xs:sequence> </xs:sequence>
</xs:complexType> </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> </xs:schema>

View File

@ -4,9 +4,10 @@
<xjc:serializable uid="1"/> <xjc:serializable uid="1"/>
</jaxb:globalBindings> </jaxb:globalBindings>
<jaxb:bindings schemaLocation="phonebook.xsd"> <!-- additions (without namespace): phonebook, calllist -->
<jaxb:bindings schemaLocation="additions.xsd">
<jaxb:schemaBindings> <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:schemaBindings>
</jaxb:bindings> </jaxb:bindings>