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