]> git.basschouten.com Git - openhab-addons.git/blob
499231f1b55aa29b9d1f53e27df6b597b5b1aefb
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.tr064.internal;
14
15 import static org.openhab.binding.tr064.internal.Tr064BindingConstants.THING_TYPE_FRITZBOX;
16 import static org.openhab.binding.tr064.internal.Tr064BindingConstants.THING_TYPE_GENERIC;
17
18 import java.net.URI;
19 import java.net.URISyntaxException;
20 import java.util.*;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
23 import java.util.stream.Collectors;
24 import java.util.stream.Stream;
25
26 import javax.xml.soap.SOAPException;
27 import javax.xml.soap.SOAPMessage;
28
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.eclipse.jetty.client.HttpClient;
32 import org.eclipse.jetty.client.api.Authentication;
33 import org.eclipse.jetty.client.api.AuthenticationStore;
34 import org.eclipse.jetty.client.util.DigestAuthentication;
35 import org.openhab.binding.tr064.internal.config.Tr064ChannelConfig;
36 import org.openhab.binding.tr064.internal.config.Tr064RootConfiguration;
37 import org.openhab.binding.tr064.internal.dto.scpd.root.SCPDDeviceType;
38 import org.openhab.binding.tr064.internal.dto.scpd.root.SCPDServiceType;
39 import org.openhab.binding.tr064.internal.dto.scpd.service.SCPDActionType;
40 import org.openhab.binding.tr064.internal.phonebook.Phonebook;
41 import org.openhab.binding.tr064.internal.phonebook.PhonebookProvider;
42 import org.openhab.binding.tr064.internal.phonebook.Tr064PhonebookImpl;
43 import org.openhab.binding.tr064.internal.soap.SOAPConnector;
44 import org.openhab.binding.tr064.internal.soap.SOAPValueConverter;
45 import org.openhab.binding.tr064.internal.util.SCPDUtil;
46 import org.openhab.binding.tr064.internal.util.Util;
47 import org.openhab.core.cache.ExpiringCacheMap;
48 import org.openhab.core.thing.*;
49 import org.openhab.core.thing.binding.BaseBridgeHandler;
50 import org.openhab.core.thing.binding.ThingHandlerService;
51 import org.openhab.core.thing.binding.builder.ThingBuilder;
52 import org.openhab.core.types.Command;
53 import org.openhab.core.types.RefreshType;
54 import org.openhab.core.types.State;
55 import org.slf4j.Logger;
56 import org.slf4j.LoggerFactory;
57
58 /**
59  * The {@link Tr064RootHandler} is responsible for handling commands, which are
60  * sent to one of the channels and update channel values
61  *
62  * @author Jan N. Klug - Initial contribution
63  */
64 @NonNullByDefault
65 public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProvider {
66     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_GENERIC, THING_TYPE_FRITZBOX);
67     private static final int RETRY_INTERVAL = 60;
68     private static final Set<String> PROPERTY_ARGUMENTS = Set.of("NewSerialNumber", "NewSoftwareVersion",
69             "NewModelName");
70
71     private final Logger logger = LoggerFactory.getLogger(Tr064RootHandler.class);
72     private final HttpClient httpClient;
73
74     private Tr064RootConfiguration config = new Tr064RootConfiguration();
75     private String deviceType = "";
76
77     private @Nullable SCPDUtil scpdUtil;
78     private SOAPConnector soapConnector;
79     private String endpointBaseURL = "";
80
81     private final Map<ChannelUID, Tr064ChannelConfig> channels = new HashMap<>();
82     // caching is used to prevent excessive calls to the same action
83     private final ExpiringCacheMap<ChannelUID, State> stateCache = new ExpiringCacheMap<>(2000);
84     private Collection<Phonebook> phonebooks = Collections.emptyList();
85
86     private @Nullable ScheduledFuture<?> connectFuture;
87     private @Nullable ScheduledFuture<?> pollFuture;
88     private @Nullable ScheduledFuture<?> phonebookFuture;
89
90     private boolean communicationEstablished = false;
91
92     Tr064RootHandler(Bridge bridge, HttpClient httpClient) {
93         super(bridge);
94         this.httpClient = httpClient;
95         this.soapConnector = new SOAPConnector(httpClient, endpointBaseURL);
96     }
97
98     @Override
99     public void handleCommand(ChannelUID channelUID, Command command) {
100         if (!communicationEstablished) {
101             logger.debug("Tried to process command, but thing is not yet ready: {} to {}", channelUID, command);
102         }
103         Tr064ChannelConfig channelConfig = channels.get(channelUID);
104         if (channelConfig == null) {
105             logger.trace("Channel {} not supported.", channelUID);
106             return;
107         }
108
109         if (command instanceof RefreshType) {
110             SOAPConnector soapConnector = this.soapConnector;
111             State state = stateCache.putIfAbsentAndGet(channelUID,
112                     () -> soapConnector.getChannelStateFromDevice(channelConfig, channels, stateCache));
113             if (state != null) {
114                 updateState(channelUID, state);
115             }
116             return;
117         }
118
119         if (channelConfig.getChannelTypeDescription().getSetAction() == null) {
120             logger.debug("Discarding command {} to {}, read-only channel", command, channelUID);
121             return;
122         }
123         scheduler.execute(() -> soapConnector.sendChannelCommandToDevice(channelConfig, command));
124     }
125
126     @Override
127     public void initialize() {
128         config = getConfigAs(Tr064RootConfiguration.class);
129         if (!config.isValid()) {
130             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
131                     "At least one mandatory configuration field is empty");
132             return;
133         }
134
135         endpointBaseURL = "http://" + config.host + ":49000";
136         soapConnector = new SOAPConnector(httpClient, endpointBaseURL);
137         updateStatus(ThingStatus.UNKNOWN);
138
139         connectFuture = scheduler.scheduleWithFixedDelay(this::internalInitialize, 0, RETRY_INTERVAL, TimeUnit.SECONDS);
140     }
141
142     /**
143      * internal thing initializer (sets SCPDUtil and connects to remote device)
144      */
145     private void internalInitialize() {
146         try {
147             scpdUtil = new SCPDUtil(httpClient, endpointBaseURL);
148         } catch (SCPDException e) {
149             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
150                     "could not get device definitions from " + config.host);
151             return;
152         }
153
154         if (establishSecureConnectionAndUpdateProperties()) {
155             removeConnectScheduler();
156
157             // connection successful, check channels
158             ThingBuilder thingBuilder = editThing();
159             thingBuilder.withoutChannels(thing.getChannels());
160             final SCPDUtil scpdUtil = this.scpdUtil;
161             if (scpdUtil != null) {
162                 Util.checkAvailableChannels(thing, thingBuilder, scpdUtil, "", deviceType, channels);
163                 updateThing(thingBuilder.build());
164             }
165
166             communicationEstablished = true;
167             installPolling();
168             updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
169         }
170     }
171
172     private void removeConnectScheduler() {
173         final ScheduledFuture<?> connectFuture = this.connectFuture;
174         if (connectFuture != null) {
175             connectFuture.cancel(true);
176             this.connectFuture = null;
177         }
178     }
179
180     @Override
181     public void dispose() {
182         communicationEstablished = false;
183         removeConnectScheduler();
184         uninstallPolling();
185         stateCache.clear();
186
187         super.dispose();
188     }
189
190     /**
191      * poll remote device for channel values
192      */
193     private void poll() {
194         channels.forEach((channelUID, channelConfig) -> {
195             if (isLinked(channelUID)) {
196                 State state = stateCache.putIfAbsentAndGet(channelUID,
197                         () -> soapConnector.getChannelStateFromDevice(channelConfig, channels, stateCache));
198                 if (state != null) {
199                     updateState(channelUID, state);
200                 }
201             }
202         });
203     }
204
205     /**
206      * establish the connection - get secure port (if avallable), install authentication, get device properties
207      *
208      * @return true if successful
209      */
210     private boolean establishSecureConnectionAndUpdateProperties() {
211         final SCPDUtil scpdUtil = this.scpdUtil;
212         if (scpdUtil != null) {
213             try {
214                 SCPDDeviceType device = scpdUtil.getDevice("")
215                         .orElseThrow(() -> new SCPDException("Root device not found"));
216                 SCPDServiceType deviceService = device.getServiceList().stream()
217                         .filter(service -> service.getServiceId().equals("urn:DeviceInfo-com:serviceId:DeviceInfo1"))
218                         .findFirst().orElseThrow(() -> new SCPDException(
219                                 "service 'urn:DeviceInfo-com:serviceId:DeviceInfo1' not found"));
220
221                 this.deviceType = device.getDeviceType();
222
223                 // try to get security (https) port
224                 SOAPMessage soapResponse = soapConnector.doSOAPRequest(deviceService, "GetSecurityPort",
225                         Collections.emptyMap());
226                 if (!soapResponse.getSOAPBody().hasFault()) {
227                     SOAPValueConverter soapValueConverter = new SOAPValueConverter(httpClient);
228                     soapValueConverter.getStateFromSOAPValue(soapResponse, "NewSecurityPort", null)
229                             .ifPresentOrElse(port -> {
230                                 endpointBaseURL = "https://" + config.host + ":" + port.toString();
231                                 soapConnector = new SOAPConnector(httpClient, endpointBaseURL);
232                                 logger.debug("endpointBaseURL is now '{}'", endpointBaseURL);
233                             }, () -> logger.warn("Could not determine secure port, disabling https"));
234                 } else {
235                     logger.warn("Could not determine secure port, disabling https");
236                 }
237
238                 // clear auth cache and force re-auth
239                 httpClient.getAuthenticationStore().clearAuthenticationResults();
240                 AuthenticationStore auth = httpClient.getAuthenticationStore();
241                 auth.addAuthentication(new DigestAuthentication(new URI(endpointBaseURL), Authentication.ANY_REALM,
242                         config.user, config.password));
243
244                 // check & update properties
245                 SCPDActionType getInfoAction = scpdUtil.getService(deviceService.getServiceId())
246                         .orElseThrow(() -> new SCPDException(
247                                 "Could not get service definition for 'urn:DeviceInfo-com:serviceId:DeviceInfo1'"))
248                         .getActionList().stream().filter(action -> action.getName().equals("GetInfo")).findFirst()
249                         .orElseThrow(() -> new SCPDException("Action 'GetInfo' not found"));
250                 SOAPMessage soapResponse1 = soapConnector.doSOAPRequest(deviceService, getInfoAction.getName(),
251                         Collections.emptyMap());
252                 SOAPValueConverter soapValueConverter = new SOAPValueConverter(httpClient);
253                 Map<String, String> properties = editProperties();
254                 PROPERTY_ARGUMENTS.forEach(argumentName -> getInfoAction.getArgumentList().stream()
255                         .filter(argument -> argument.getName().equals(argumentName)).findFirst()
256                         .ifPresent(argument -> soapValueConverter
257                                 .getStateFromSOAPValue(soapResponse1, argumentName, null).ifPresent(value -> properties
258                                         .put(argument.getRelatedStateVariable(), value.toString()))));
259                 properties.put("deviceType", device.getDeviceType());
260                 updateProperties(properties);
261
262                 return true;
263             } catch (SCPDException | SOAPException | Tr064CommunicationException | URISyntaxException e) {
264                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
265                 return false;
266             }
267         }
268         return false;
269     }
270
271     /**
272      * get all sub devices of this root device (used for discovery)
273      *
274      * @return the list
275      */
276     public List<SCPDDeviceType> getAllSubDevices() {
277         final SCPDUtil scpdUtil = this.scpdUtil;
278         return (scpdUtil == null) ? Collections.emptyList() : scpdUtil.getAllSubDevices();
279     }
280
281     /**
282      * get the SOAP connector (used by sub devices for communication with the remote device)
283      *
284      * @return the SOAP connector
285      */
286     public SOAPConnector getSOAPConnector() {
287         return soapConnector;
288     }
289
290     /**
291      * get the SCPD processing utility
292      *
293      * @return the SCPD utility (or null if not available)
294      */
295     public @Nullable SCPDUtil getSCPDUtil() {
296         return scpdUtil;
297     }
298
299     /**
300      * uninstall the polling
301      */
302     private void uninstallPolling() {
303         final ScheduledFuture<?> pollFuture = this.pollFuture;
304         if (pollFuture != null) {
305             pollFuture.cancel(true);
306             this.pollFuture = null;
307         }
308         final ScheduledFuture<?> phonebookFuture = this.phonebookFuture;
309         if (phonebookFuture != null) {
310             phonebookFuture.cancel(true);
311             this.phonebookFuture = null;
312         }
313     }
314
315     /**
316      * install the polling
317      */
318     private void installPolling() {
319         uninstallPolling();
320         pollFuture = scheduler.scheduleWithFixedDelay(this::poll, 0, config.refresh, TimeUnit.SECONDS);
321         if (config.phonebookInterval > 0) {
322             phonebookFuture = scheduler.scheduleWithFixedDelay(this::retrievePhonebooks, 0, config.phonebookInterval,
323                     TimeUnit.SECONDS);
324         }
325     }
326
327     @SuppressWarnings("unchecked")
328     private Collection<Phonebook> processPhonebookList(SOAPMessage soapMessagePhonebookList,
329             SCPDServiceType scpdService) {
330         SOAPValueConverter soapValueConverter = new SOAPValueConverter(httpClient);
331         return (Collection<Phonebook>) soapValueConverter
332                 .getStateFromSOAPValue(soapMessagePhonebookList, "NewPhonebookList", null)
333                 .map(phonebookList -> Arrays.stream(phonebookList.toString().split(","))).orElse(Stream.empty())
334                 .map(index -> {
335                     try {
336                         SOAPMessage soapMessageURL = soapConnector.doSOAPRequest(scpdService, "GetPhonebook",
337                                 Map.of("NewPhonebookID", index));
338                         return soapValueConverter.getStateFromSOAPValue(soapMessageURL, "NewPhonebookURL", null)
339                                 .map(url -> (Phonebook) new Tr064PhonebookImpl(httpClient, url.toString()));
340                     } catch (Tr064CommunicationException e) {
341                         logger.warn("Failed to get phonebook with index {}:", index, e);
342                     }
343                     return Optional.empty();
344                 }).filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList());
345     }
346
347     private void retrievePhonebooks() {
348         String serviceId = "urn:X_AVM-DE_OnTel-com:serviceId:X_AVM-DE_OnTel1";
349         SCPDUtil scpdUtil = this.scpdUtil;
350         if (scpdUtil == null) {
351             logger.warn("Cannot find SCPDUtil. This is most likely a programming error.");
352             return;
353         }
354         Optional<SCPDServiceType> scpdService = scpdUtil.getDevice("").flatMap(deviceType -> deviceType.getServiceList()
355                 .stream().filter(service -> service.getServiceId().equals(serviceId)).findFirst());
356
357         phonebooks = scpdService.map(service -> {
358             try {
359                 return processPhonebookList(
360                         soapConnector.doSOAPRequest(service, "GetPhonebookList", Collections.emptyMap()), service);
361             } catch (Tr064CommunicationException e) {
362                 return Collections.<Phonebook> emptyList();
363             }
364         }).orElse(Collections.emptyList());
365
366         if (phonebooks.isEmpty()) {
367             logger.warn("Could not get phonebooks for thing {}", thing.getUID());
368         }
369     }
370
371     @Override
372     public Optional<Phonebook> getPhonebookByName(String name) {
373         return phonebooks.stream().filter(p -> name.equals(p.getName())).findAny();
374     }
375
376     @Override
377     public Collection<Phonebook> getPhonebooks() {
378         return phonebooks;
379     }
380
381     @Override
382     public ThingUID getUID() {
383         return thing.getUID();
384     }
385
386     @Override
387     public String getFriendlyName() {
388         String friendlyName = thing.getLabel();
389         return friendlyName != null ? friendlyName : getUID().getId();
390     }
391
392     @Override
393     public Collection<Class<? extends ThingHandlerService>> getServices() {
394         return Set.of(Tr064DiscoveryService.class);
395     }
396 }