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