]> git.basschouten.com Git - openhab-addons.git/blob
ff9df9b23bc375ffc36fedb43810081bd57fcf15
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.time.Duration;
21 import java.util.*;
22 import java.util.concurrent.ScheduledFuture;
23 import java.util.concurrent.TimeUnit;
24 import java.util.stream.Collectors;
25 import java.util.stream.Stream;
26
27 import javax.xml.soap.SOAPException;
28 import javax.xml.soap.SOAPMessage;
29
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;
60
61 /**
62  * The {@link Tr064RootHandler} is responsible for handling commands, which are
63  * sent to one of the channels and update channel values
64  *
65  * @author Jan N. Klug - Initial contribution
66  */
67 @NonNullByDefault
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",
72             "NewModelName");
73
74     private final Logger logger = LoggerFactory.getLogger(Tr064RootHandler.class);
75     private final HttpClient httpClient;
76
77     private Tr064RootConfiguration config = new Tr064RootConfiguration();
78     private String deviceType = "";
79
80     private @Nullable SCPDUtil scpdUtil;
81     private SOAPConnector soapConnector;
82     private String endpointBaseURL = "";
83
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();
88
89     private @Nullable ScheduledFuture<?> connectFuture;
90     private @Nullable ScheduledFuture<?> pollFuture;
91     private @Nullable ScheduledFuture<?> phonebookFuture;
92
93     private boolean communicationEstablished = false;
94
95     Tr064RootHandler(Bridge bridge, HttpClient httpClient) {
96         super(bridge);
97         this.httpClient = httpClient;
98         this.soapConnector = new SOAPConnector(httpClient, endpointBaseURL);
99     }
100
101     @Override
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);
105         }
106         Tr064ChannelConfig channelConfig = channels.get(channelUID);
107         if (channelConfig == null) {
108             logger.trace("Channel {} not supported.", channelUID);
109             return;
110         }
111
112         if (command instanceof RefreshType) {
113             SOAPConnector soapConnector = this.soapConnector;
114             State state = stateCache.putIfAbsentAndGet(channelUID,
115                     () -> soapConnector.getChannelStateFromDevice(channelConfig, channels, stateCache));
116             if (state != null) {
117                 updateState(channelUID, state);
118             }
119             return;
120         }
121
122         if (channelConfig.getChannelTypeDescription().getSetAction() == null) {
123             logger.debug("Discarding command {} to {}, read-only channel", command, channelUID);
124             return;
125         }
126         scheduler.execute(() -> soapConnector.sendChannelCommandToDevice(channelConfig, command));
127     }
128
129     @Override
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");
135             return;
136         }
137
138         endpointBaseURL = "http://" + config.host + ":49000";
139         soapConnector = new SOAPConnector(httpClient, endpointBaseURL);
140         updateStatus(ThingStatus.UNKNOWN);
141
142         connectFuture = scheduler.scheduleWithFixedDelay(this::internalInitialize, 0, RETRY_INTERVAL, TimeUnit.SECONDS);
143     }
144
145     /**
146      * internal thing initializer (sets SCPDUtil and connects to remote device)
147      */
148     private void internalInitialize() {
149         try {
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);
154             return;
155         }
156
157         if (establishSecureConnectionAndUpdateProperties()) {
158             removeConnectScheduler();
159
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());
167             }
168
169             communicationEstablished = true;
170             installPolling();
171             updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
172         }
173     }
174
175     private void removeConnectScheduler() {
176         final ScheduledFuture<?> connectFuture = this.connectFuture;
177         if (connectFuture != null) {
178             connectFuture.cancel(true);
179             this.connectFuture = null;
180         }
181     }
182
183     @Override
184     public void dispose() {
185         communicationEstablished = false;
186         removeConnectScheduler();
187         uninstallPolling();
188         stateCache.clear();
189
190         super.dispose();
191     }
192
193     /**
194      * poll remote device for channel values
195      */
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));
201                 if (state != null) {
202                     updateState(channelUID, state);
203                 }
204             }
205         });
206     }
207
208     /**
209      * establish the connection - get secure port (if avallable), install authentication, get device properties
210      *
211      * @return true if successful
212      */
213     private boolean establishSecureConnectionAndUpdateProperties() {
214         final SCPDUtil scpdUtil = this.scpdUtil;
215         if (scpdUtil != null) {
216             try {
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"));
223
224                 this.deviceType = device.getDeviceType();
225
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"));
237                 } else {
238                     logger.warn("Could not determine secure port, disabling https");
239                 }
240
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));
246
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);
264
265                 return true;
266             } catch (SCPDException | SOAPException | Tr064CommunicationException | URISyntaxException e) {
267                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
268                 return false;
269             }
270         }
271         return false;
272     }
273
274     /**
275      * get all sub devices of this root device (used for discovery)
276      *
277      * @return the list
278      */
279     public List<SCPDDeviceType> getAllSubDevices() {
280         final SCPDUtil scpdUtil = this.scpdUtil;
281         return (scpdUtil == null) ? List.of() : scpdUtil.getAllSubDevices();
282     }
283
284     /**
285      * get the SOAP connector (used by sub devices for communication with the remote device)
286      *
287      * @return the SOAP connector
288      */
289     public SOAPConnector getSOAPConnector() {
290         return soapConnector;
291     }
292
293     /**
294      * get the SCPD processing utility
295      *
296      * @return the SCPD utility (or null if not available)
297      */
298     public @Nullable SCPDUtil getSCPDUtil() {
299         return scpdUtil;
300     }
301
302     /**
303      * uninstall the polling
304      */
305     private void uninstallPolling() {
306         final ScheduledFuture<?> pollFuture = this.pollFuture;
307         if (pollFuture != null) {
308             pollFuture.cancel(true);
309             this.pollFuture = null;
310         }
311         final ScheduledFuture<?> phonebookFuture = this.phonebookFuture;
312         if (phonebookFuture != null) {
313             phonebookFuture.cancel(true);
314             this.phonebookFuture = null;
315         }
316     }
317
318     /**
319      * install the polling
320      */
321     private void installPolling() {
322         uninstallPolling();
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,
326                     TimeUnit.SECONDS);
327         }
328     }
329
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())
337                 .map(index -> {
338                     try {
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);
345                     }
346                     return Optional.empty();
347                 }).filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList());
348     }
349
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.");
355             return;
356         }
357         Optional<SCPDServiceType> scpdService = scpdUtil.getDevice("").flatMap(deviceType -> deviceType.getServiceList()
358                 .stream().filter(service -> service.getServiceId().equals(serviceId)).findFirst());
359
360         phonebooks = scpdService.map(service -> {
361             try {
362                 return processPhonebookList(soapConnector.doSOAPRequest(new SOAPRequest(service, "GetPhonebookList")),
363                         service);
364             } catch (Tr064CommunicationException e) {
365                 return Collections.<Phonebook> emptyList();
366             }
367         }).orElse(List.of());
368
369         if (phonebooks.isEmpty()) {
370             logger.warn("Could not get phonebooks for thing {}", thing.getUID());
371         }
372     }
373
374     @Override
375     public Optional<Phonebook> getPhonebookByName(String name) {
376         return phonebooks.stream().filter(p -> name.equals(p.getName())).findAny();
377     }
378
379     @Override
380     public Collection<Phonebook> getPhonebooks() {
381         return phonebooks;
382     }
383
384     @Override
385     public ThingUID getUID() {
386         return thing.getUID();
387     }
388
389     @Override
390     public String getFriendlyName() {
391         String friendlyName = thing.getLabel();
392         return friendlyName != null ? friendlyName : getUID().getId();
393     }
394
395     @Override
396     public Collection<Class<? extends ThingHandlerService>> getServices() {
397         return Set.of(Tr064DiscoveryService.class, PhonebookActions.class);
398     }
399 }