2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.tr064.internal;
15 import static org.openhab.binding.tr064.internal.Tr064BindingConstants.THING_TYPE_FRITZBOX;
16 import static org.openhab.binding.tr064.internal.Tr064BindingConstants.THING_TYPE_GENERIC;
19 import java.net.URISyntaxException;
20 import java.time.Duration;
22 import java.util.concurrent.ScheduledFuture;
23 import java.util.concurrent.TimeUnit;
24 import java.util.stream.Collectors;
25 import java.util.stream.Stream;
27 import javax.xml.soap.SOAPException;
28 import javax.xml.soap.SOAPMessage;
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.eclipse.jetty.client.HttpClient;
33 import org.eclipse.jetty.client.api.Authentication;
34 import org.eclipse.jetty.client.api.AuthenticationStore;
35 import org.eclipse.jetty.client.util.DigestAuthentication;
36 import org.openhab.binding.tr064.internal.config.Tr064ChannelConfig;
37 import org.openhab.binding.tr064.internal.config.Tr064RootConfiguration;
38 import org.openhab.binding.tr064.internal.dto.scpd.root.SCPDDeviceType;
39 import org.openhab.binding.tr064.internal.dto.scpd.root.SCPDServiceType;
40 import org.openhab.binding.tr064.internal.dto.scpd.service.SCPDActionType;
41 import org.openhab.binding.tr064.internal.phonebook.Phonebook;
42 import org.openhab.binding.tr064.internal.phonebook.PhonebookActions;
43 import org.openhab.binding.tr064.internal.phonebook.PhonebookProvider;
44 import org.openhab.binding.tr064.internal.phonebook.Tr064PhonebookImpl;
45 import org.openhab.binding.tr064.internal.soap.SOAPConnector;
46 import org.openhab.binding.tr064.internal.soap.SOAPRequest;
47 import org.openhab.binding.tr064.internal.soap.SOAPValueConverter;
48 import org.openhab.binding.tr064.internal.util.SCPDUtil;
49 import org.openhab.binding.tr064.internal.util.Util;
50 import org.openhab.core.cache.ExpiringCacheMap;
51 import org.openhab.core.thing.*;
52 import org.openhab.core.thing.binding.BaseBridgeHandler;
53 import org.openhab.core.thing.binding.ThingHandlerService;
54 import org.openhab.core.thing.binding.builder.ThingBuilder;
55 import org.openhab.core.types.Command;
56 import org.openhab.core.types.RefreshType;
57 import org.openhab.core.types.State;
58 import org.slf4j.Logger;
59 import org.slf4j.LoggerFactory;
62 * The {@link Tr064RootHandler} is responsible for handling commands, which are
63 * sent to one of the channels and update channel values
65 * @author Jan N. Klug - Initial contribution
68 public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProvider {
69 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_GENERIC, THING_TYPE_FRITZBOX);
70 private static final int RETRY_INTERVAL = 60;
71 private static final Set<String> PROPERTY_ARGUMENTS = Set.of("NewSerialNumber", "NewSoftwareVersion",
74 private final Logger logger = LoggerFactory.getLogger(Tr064RootHandler.class);
75 private final HttpClient httpClient;
77 private Tr064RootConfiguration config = new Tr064RootConfiguration();
78 private String deviceType = "";
80 private @Nullable SCPDUtil scpdUtil;
81 private SOAPConnector soapConnector;
82 private String endpointBaseURL = "";
84 private final Map<ChannelUID, Tr064ChannelConfig> channels = new HashMap<>();
85 // caching is used to prevent excessive calls to the same action
86 private final ExpiringCacheMap<ChannelUID, State> stateCache = new ExpiringCacheMap<>(Duration.ofMillis(2000));
87 private Collection<Phonebook> phonebooks = List.of();
89 private @Nullable ScheduledFuture<?> connectFuture;
90 private @Nullable ScheduledFuture<?> pollFuture;
91 private @Nullable ScheduledFuture<?> phonebookFuture;
93 private boolean communicationEstablished = false;
95 Tr064RootHandler(Bridge bridge, HttpClient httpClient) {
97 this.httpClient = httpClient;
98 this.soapConnector = new SOAPConnector(httpClient, endpointBaseURL);
102 public void handleCommand(ChannelUID channelUID, Command command) {
103 if (!communicationEstablished) {
104 logger.debug("Tried to process command, but thing is not yet ready: {} to {}", channelUID, command);
106 Tr064ChannelConfig channelConfig = channels.get(channelUID);
107 if (channelConfig == null) {
108 logger.trace("Channel {} not supported.", channelUID);
112 if (command instanceof RefreshType) {
113 SOAPConnector soapConnector = this.soapConnector;
114 State state = stateCache.putIfAbsentAndGet(channelUID,
115 () -> soapConnector.getChannelStateFromDevice(channelConfig, channels, stateCache));
117 updateState(channelUID, state);
122 if (channelConfig.getChannelTypeDescription().getSetAction() == null) {
123 logger.debug("Discarding command {} to {}, read-only channel", command, channelUID);
126 scheduler.execute(() -> soapConnector.sendChannelCommandToDevice(channelConfig, command));
130 public void initialize() {
131 config = getConfigAs(Tr064RootConfiguration.class);
132 if (!config.isValid()) {
133 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
134 "At least one mandatory configuration field is empty");
138 endpointBaseURL = "http://" + config.host + ":49000";
139 soapConnector = new SOAPConnector(httpClient, endpointBaseURL);
140 updateStatus(ThingStatus.UNKNOWN);
142 connectFuture = scheduler.scheduleWithFixedDelay(this::internalInitialize, 0, RETRY_INTERVAL, TimeUnit.SECONDS);
146 * internal thing initializer (sets SCPDUtil and connects to remote device)
148 private void internalInitialize() {
150 scpdUtil = new SCPDUtil(httpClient, endpointBaseURL);
151 } catch (SCPDException e) {
152 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
153 "could not get device definitions from " + config.host);
157 if (establishSecureConnectionAndUpdateProperties()) {
158 removeConnectScheduler();
160 // connection successful, check channels
161 ThingBuilder thingBuilder = editThing();
162 thingBuilder.withoutChannels(thing.getChannels());
163 final SCPDUtil scpdUtil = this.scpdUtil;
164 if (scpdUtil != null) {
165 Util.checkAvailableChannels(thing, thingBuilder, scpdUtil, "", deviceType, channels);
166 updateThing(thingBuilder.build());
169 communicationEstablished = true;
171 updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
175 private void removeConnectScheduler() {
176 final ScheduledFuture<?> connectFuture = this.connectFuture;
177 if (connectFuture != null) {
178 connectFuture.cancel(true);
179 this.connectFuture = null;
184 public void dispose() {
185 communicationEstablished = false;
186 removeConnectScheduler();
194 * poll remote device for channel values
196 private void poll() {
197 channels.forEach((channelUID, channelConfig) -> {
198 if (isLinked(channelUID)) {
199 State state = stateCache.putIfAbsentAndGet(channelUID,
200 () -> soapConnector.getChannelStateFromDevice(channelConfig, channels, stateCache));
202 updateState(channelUID, state);
209 * establish the connection - get secure port (if avallable), install authentication, get device properties
211 * @return true if successful
213 private boolean establishSecureConnectionAndUpdateProperties() {
214 final SCPDUtil scpdUtil = this.scpdUtil;
215 if (scpdUtil != null) {
217 SCPDDeviceType device = scpdUtil.getDevice("")
218 .orElseThrow(() -> new SCPDException("Root device not found"));
219 SCPDServiceType deviceService = device.getServiceList().stream()
220 .filter(service -> service.getServiceId().equals("urn:DeviceInfo-com:serviceId:DeviceInfo1"))
221 .findFirst().orElseThrow(() -> new SCPDException(
222 "service 'urn:DeviceInfo-com:serviceId:DeviceInfo1' not found"));
224 this.deviceType = device.getDeviceType();
226 // try to get security (https) port
227 SOAPMessage soapResponse = soapConnector
228 .doSOAPRequest(new SOAPRequest(deviceService, "GetSecurityPort"));
229 if (!soapResponse.getSOAPBody().hasFault()) {
230 SOAPValueConverter soapValueConverter = new SOAPValueConverter(httpClient);
231 soapValueConverter.getStateFromSOAPValue(soapResponse, "NewSecurityPort", null)
232 .ifPresentOrElse(port -> {
233 endpointBaseURL = "https://" + config.host + ":" + port.toString();
234 soapConnector = new SOAPConnector(httpClient, endpointBaseURL);
235 logger.debug("endpointBaseURL is now '{}'", endpointBaseURL);
236 }, () -> logger.warn("Could not determine secure port, disabling https"));
238 logger.warn("Could not determine secure port, disabling https");
241 // clear auth cache and force re-auth
242 httpClient.getAuthenticationStore().clearAuthenticationResults();
243 AuthenticationStore auth = httpClient.getAuthenticationStore();
244 auth.addAuthentication(new DigestAuthentication(new URI(endpointBaseURL), Authentication.ANY_REALM,
245 config.user, config.password));
247 // check & update properties
248 SCPDActionType getInfoAction = scpdUtil.getService(deviceService.getServiceId())
249 .orElseThrow(() -> new SCPDException(
250 "Could not get service definition for 'urn:DeviceInfo-com:serviceId:DeviceInfo1'"))
251 .getActionList().stream().filter(action -> action.getName().equals("GetInfo")).findFirst()
252 .orElseThrow(() -> new SCPDException("Action 'GetInfo' not found"));
253 SOAPMessage soapResponse1 = soapConnector
254 .doSOAPRequest(new SOAPRequest(deviceService, getInfoAction.getName()));
255 SOAPValueConverter soapValueConverter = new SOAPValueConverter(httpClient);
256 Map<String, String> properties = editProperties();
257 PROPERTY_ARGUMENTS.forEach(argumentName -> getInfoAction.getArgumentList().stream()
258 .filter(argument -> argument.getName().equals(argumentName)).findFirst()
259 .ifPresent(argument -> soapValueConverter
260 .getStateFromSOAPValue(soapResponse1, argumentName, null).ifPresent(value -> properties
261 .put(argument.getRelatedStateVariable(), value.toString()))));
262 properties.put("deviceType", device.getDeviceType());
263 updateProperties(properties);
266 } catch (SCPDException | SOAPException | Tr064CommunicationException | URISyntaxException e) {
267 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
275 * get all sub devices of this root device (used for discovery)
279 public List<SCPDDeviceType> getAllSubDevices() {
280 final SCPDUtil scpdUtil = this.scpdUtil;
281 return (scpdUtil == null) ? List.of() : scpdUtil.getAllSubDevices();
285 * get the SOAP connector (used by sub devices for communication with the remote device)
287 * @return the SOAP connector
289 public SOAPConnector getSOAPConnector() {
290 return soapConnector;
294 * get the SCPD processing utility
296 * @return the SCPD utility (or null if not available)
298 public @Nullable SCPDUtil getSCPDUtil() {
303 * uninstall the polling
305 private void uninstallPolling() {
306 final ScheduledFuture<?> pollFuture = this.pollFuture;
307 if (pollFuture != null) {
308 pollFuture.cancel(true);
309 this.pollFuture = null;
311 final ScheduledFuture<?> phonebookFuture = this.phonebookFuture;
312 if (phonebookFuture != null) {
313 phonebookFuture.cancel(true);
314 this.phonebookFuture = null;
319 * install the polling
321 private void installPolling() {
323 pollFuture = scheduler.scheduleWithFixedDelay(this::poll, 0, config.refresh, TimeUnit.SECONDS);
324 if (config.phonebookInterval > 0) {
325 phonebookFuture = scheduler.scheduleWithFixedDelay(this::retrievePhonebooks, 0, config.phonebookInterval,
330 @SuppressWarnings("unchecked")
331 private Collection<Phonebook> processPhonebookList(SOAPMessage soapMessagePhonebookList,
332 SCPDServiceType scpdService) {
333 SOAPValueConverter soapValueConverter = new SOAPValueConverter(httpClient);
334 return (Collection<Phonebook>) soapValueConverter
335 .getStateFromSOAPValue(soapMessagePhonebookList, "NewPhonebookList", null)
336 .map(phonebookList -> Arrays.stream(phonebookList.toString().split(","))).orElse(Stream.empty())
339 SOAPMessage soapMessageURL = soapConnector.doSOAPRequest(
340 new SOAPRequest(scpdService, "GetPhonebook", Map.of("NewPhonebookID", index)));
341 return soapValueConverter.getStateFromSOAPValue(soapMessageURL, "NewPhonebookURL", null)
342 .map(url -> (Phonebook) new Tr064PhonebookImpl(httpClient, url.toString()));
343 } catch (Tr064CommunicationException e) {
344 logger.warn("Failed to get phonebook with index {}:", index, e);
346 return Optional.empty();
347 }).filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList());
350 private void retrievePhonebooks() {
351 String serviceId = "urn:X_AVM-DE_OnTel-com:serviceId:X_AVM-DE_OnTel1";
352 SCPDUtil scpdUtil = this.scpdUtil;
353 if (scpdUtil == null) {
354 logger.warn("Cannot find SCPDUtil. This is most likely a programming error.");
357 Optional<SCPDServiceType> scpdService = scpdUtil.getDevice("").flatMap(deviceType -> deviceType.getServiceList()
358 .stream().filter(service -> service.getServiceId().equals(serviceId)).findFirst());
360 phonebooks = scpdService.map(service -> {
362 return processPhonebookList(soapConnector.doSOAPRequest(new SOAPRequest(service, "GetPhonebookList")),
364 } catch (Tr064CommunicationException e) {
365 return Collections.<Phonebook> emptyList();
367 }).orElse(List.of());
369 if (phonebooks.isEmpty()) {
370 logger.warn("Could not get phonebooks for thing {}", thing.getUID());
375 public Optional<Phonebook> getPhonebookByName(String name) {
376 return phonebooks.stream().filter(p -> name.equals(p.getName())).findAny();
380 public Collection<Phonebook> getPhonebooks() {
385 public ThingUID getUID() {
386 return thing.getUID();
390 public String getFriendlyName() {
391 String friendlyName = thing.getLabel();
392 return friendlyName != null ? friendlyName : getUID().getId();
396 public Collection<Class<? extends ThingHandlerService>> getServices() {
397 return Set.of(Tr064DiscoveryService.class, PhonebookActions.class);