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