]> git.basschouten.com Git - openhab-addons.git/blob
15d7de7468bb51adf9041a5adfbbfcbef4614ff9
[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.soap;
14
15 import static org.openhab.binding.tr064.internal.util.Util.getSOAPElement;
16
17 import java.io.ByteArrayInputStream;
18 import java.io.ByteArrayOutputStream;
19 import java.io.IOException;
20 import java.time.Duration;
21 import java.util.HashMap;
22 import java.util.Iterator;
23 import java.util.Map;
24 import java.util.Objects;
25 import java.util.concurrent.ExecutionException;
26 import java.util.concurrent.TimeUnit;
27 import java.util.concurrent.TimeoutException;
28 import java.util.stream.Collectors;
29
30 import javax.xml.soap.*;
31
32 import org.eclipse.jdt.annotation.NonNullByDefault;
33 import org.eclipse.jetty.client.HttpClient;
34 import org.eclipse.jetty.client.api.ContentResponse;
35 import org.eclipse.jetty.client.api.Request;
36 import org.eclipse.jetty.client.util.BytesContentProvider;
37 import org.eclipse.jetty.http.HttpMethod;
38 import org.eclipse.jetty.http.HttpStatus;
39 import org.openhab.binding.tr064.internal.Tr064CommunicationException;
40 import org.openhab.binding.tr064.internal.config.Tr064ChannelConfig;
41 import org.openhab.binding.tr064.internal.dto.config.ActionType;
42 import org.openhab.binding.tr064.internal.dto.config.ChannelTypeDescription;
43 import org.openhab.binding.tr064.internal.dto.scpd.root.SCPDServiceType;
44 import org.openhab.binding.tr064.internal.dto.scpd.service.SCPDActionType;
45 import org.openhab.core.cache.ExpiringCacheMap;
46 import org.openhab.core.library.types.OnOffType;
47 import org.openhab.core.library.types.StringType;
48 import org.openhab.core.thing.ChannelUID;
49 import org.openhab.core.types.Command;
50 import org.openhab.core.types.State;
51 import org.openhab.core.types.UnDefType;
52 import org.slf4j.Logger;
53 import org.slf4j.LoggerFactory;
54
55 /**
56  * The {@link SOAPConnector} provides communication with a remote SOAP device
57  *
58  * @author Jan N. Klug - Initial contribution
59  */
60 @NonNullByDefault
61 public class SOAPConnector {
62     private static final int SOAP_TIMEOUT = 5; // in
63     private final Logger logger = LoggerFactory.getLogger(SOAPConnector.class);
64     private final HttpClient httpClient;
65     private final String endpointBaseURL;
66     private final SOAPValueConverter soapValueConverter;
67
68     private final ExpiringCacheMap<SOAPRequest, SOAPMessage> soapMessageCache = new ExpiringCacheMap<>(
69             Duration.ofMillis(2000));
70
71     public SOAPConnector(HttpClient httpClient, String endpointBaseURL) {
72         this.httpClient = httpClient;
73         this.endpointBaseURL = endpointBaseURL;
74         this.soapValueConverter = new SOAPValueConverter(httpClient);
75     }
76
77     /**
78      * prepare a SOAP request for an action request to a service
79      *
80      * @param soapRequest the request to be generated
81      * @return a jetty Request containing the full SOAP message
82      * @throws IOException if a problem while writing the SOAP message to the Request occurs
83      * @throws SOAPException if a problem with creating the SOAP message occurs
84      */
85     private Request prepareSOAPRequest(SOAPRequest soapRequest) throws IOException, SOAPException {
86         MessageFactory messageFactory = MessageFactory.newInstance();
87         SOAPMessage soapMessage = messageFactory.createMessage();
88         SOAPPart soapPart = soapMessage.getSOAPPart();
89         SOAPEnvelope envelope = soapPart.getEnvelope();
90         envelope.setEncodingStyle("http://schemas.xmlsoap.org/soap/encoding/");
91
92         // SOAP body
93         SOAPBody soapBody = envelope.getBody();
94         SOAPElement soapBodyElem = soapBody.addChildElement(soapRequest.soapAction, "u",
95                 soapRequest.service.getServiceType());
96         soapRequest.arguments.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach(argument -> {
97             try {
98                 soapBodyElem.addChildElement(argument.getKey()).setTextContent(argument.getValue());
99             } catch (SOAPException e) {
100                 logger.warn("Could not add {}:{} to SOAP Request: {}", argument.getKey(), argument.getValue(),
101                         e.getMessage());
102             }
103         });
104
105         // SOAP headers
106         MimeHeaders headers = soapMessage.getMimeHeaders();
107         headers.addHeader("SOAPAction", soapRequest.service.getServiceType() + "#" + soapRequest.soapAction);
108         soapMessage.saveChanges();
109
110         // create Request and add headers and content
111         Request request = httpClient.newRequest(endpointBaseURL + soapRequest.service.getControlURL())
112                 .method(HttpMethod.POST);
113         ((Iterator<MimeHeader>) soapMessage.getMimeHeaders().getAllHeaders())
114                 .forEachRemaining(header -> request.header(header.getName(), header.getValue()));
115         try (final ByteArrayOutputStream os = new ByteArrayOutputStream()) {
116             soapMessage.writeTo(os);
117             byte[] content = os.toByteArray();
118             request.content(new BytesContentProvider(content));
119         }
120
121         return request;
122     }
123
124     /**
125      * execute a SOAP request with cache
126      *
127      * @param soapRequest the request itself
128      * @return the SOAPMessage answer from the remote host
129      * @throws Tr064CommunicationException if an error occurs during the request
130      */
131     public SOAPMessage doSOAPRequest(SOAPRequest soapRequest) throws Tr064CommunicationException {
132         try {
133             SOAPMessage soapMessage = Objects.requireNonNull(soapMessageCache.putIfAbsentAndGet(soapRequest, () -> {
134                 try {
135                     SOAPMessage newValue = doSOAPRequestUncached(soapRequest);
136                     logger.trace("Storing in cache: {}", newValue);
137                     return newValue;
138                 } catch (Tr064CommunicationException e) {
139                     // wrap exception
140                     throw new IllegalArgumentException(e);
141                 }
142             }));
143             logger.trace("Returning from cache: {}", soapMessage);
144             return soapMessage;
145         } catch (IllegalArgumentException e) {
146             Throwable cause = e.getCause();
147             if (cause instanceof Tr064CommunicationException) {
148                 throw (Tr064CommunicationException) cause;
149             } else {
150                 throw e;
151             }
152         }
153     }
154
155     /**
156      * execute a SOAP request without cache
157      *
158      * @param soapRequest the request itself
159      * @return the SOAPMessage answer from the remote host
160      * @throws Tr064CommunicationException if an error occurs during the request
161      */
162     public synchronized SOAPMessage doSOAPRequestUncached(SOAPRequest soapRequest) throws Tr064CommunicationException {
163         try {
164             Request request = prepareSOAPRequest(soapRequest).timeout(SOAP_TIMEOUT, TimeUnit.SECONDS);
165             if (logger.isTraceEnabled()) {
166                 request.getContent().forEach(buffer -> logger.trace("Request: {}", new String(buffer.array())));
167             }
168
169             ContentResponse response = request.send();
170             if (response.getStatus() == HttpStatus.UNAUTHORIZED_401) {
171                 // retry once if authentication expired
172                 logger.trace("Re-Auth needed.");
173                 httpClient.getAuthenticationStore().clearAuthenticationResults();
174                 request = prepareSOAPRequest(soapRequest).timeout(SOAP_TIMEOUT, TimeUnit.SECONDS);
175                 response = request.send();
176             }
177             try (final ByteArrayInputStream is = new ByteArrayInputStream(response.getContent())) {
178                 logger.trace("Received response: {}", response.getContentAsString());
179
180                 SOAPMessage soapMessage = MessageFactory.newInstance().createMessage(null, is);
181                 if (soapMessage.getSOAPBody().hasFault()) {
182                     String soapError = getSOAPElement(soapMessage, "errorCode").orElse("unknown");
183                     String soapReason = getSOAPElement(soapMessage, "errorDescription").orElse("unknown");
184                     String error = String.format("HTTP-Response-Code %d (%s), SOAP-Fault: %s (%s)",
185                             response.getStatus(), response.getReason(), soapError, soapReason);
186                     throw new Tr064CommunicationException(error, response.getStatus(), soapError);
187                 }
188                 return soapMessage;
189             }
190         } catch (IOException | SOAPException | InterruptedException | TimeoutException | ExecutionException e) {
191             throw new Tr064CommunicationException(e);
192         }
193     }
194
195     /**
196      * send a command to the remote device
197      *
198      * @param channelConfig the channel config containing all information
199      * @param command the command to send
200      */
201     public void sendChannelCommandToDevice(Tr064ChannelConfig channelConfig, Command command) {
202         soapValueConverter.getSOAPValueFromCommand(command, channelConfig.getDataType(),
203                 channelConfig.getChannelTypeDescription().getItem().getUnit()).ifPresentOrElse(value -> {
204                     final ChannelTypeDescription channelTypeDescription = channelConfig.getChannelTypeDescription();
205                     final SCPDServiceType service = channelConfig.getService();
206                     logger.debug("Sending {} as {} to {}/{}", command, value, service.getServiceId(),
207                             channelTypeDescription.getSetAction().getName());
208                     try {
209                         Map<String, String> arguments = new HashMap<>();
210                         if (channelTypeDescription.getSetAction().getArgument() != null) {
211                             arguments.put(channelTypeDescription.getSetAction().getArgument(), value);
212                         }
213                         String parameter = channelConfig.getParameter();
214                         if (parameter != null) {
215                             arguments.put(
216                                     channelConfig.getChannelTypeDescription().getGetAction().getParameter().getName(),
217                                     parameter);
218                         }
219                         SOAPRequest soapRequest = new SOAPRequest(service,
220                                 channelTypeDescription.getSetAction().getName(), arguments);
221                         doSOAPRequestUncached(soapRequest);
222                     } catch (Tr064CommunicationException e) {
223                         logger.warn("Could not send command {}: {}", command, e.getMessage());
224                     }
225                 }, () -> logger.warn("Could not convert {} to SOAP value", command));
226     }
227
228     /**
229      * get a value from the remote device - updates state cache for all possible channels
230      *
231      * @param channelConfig the channel config containing all information
232      * @param channelConfigMap map of all channels in the device
233      * @param stateCache the ExpiringCacheMap for states of the device
234      * @return the value for the requested channel
235      */
236     public State getChannelStateFromDevice(final Tr064ChannelConfig channelConfig,
237             Map<ChannelUID, Tr064ChannelConfig> channelConfigMap, ExpiringCacheMap<ChannelUID, State> stateCache) {
238         try {
239             final SCPDActionType getAction = channelConfig.getGetAction();
240             if (getAction == null) {
241                 // channel has no get action, return a default
242                 switch (channelConfig.getDataType()) {
243                     case "boolean":
244                         return OnOffType.OFF;
245                     case "string":
246                         return StringType.EMPTY;
247                     default:
248                         return UnDefType.UNDEF;
249                 }
250             }
251
252             // get value(s) from remote device
253             Map<String, String> arguments = new HashMap<>();
254             String parameter = channelConfig.getParameter();
255             ActionType action = channelConfig.getChannelTypeDescription().getGetAction();
256             if (parameter != null && !action.getParameter().isInternalOnly()) {
257                 arguments.put(action.getParameter().getName(), parameter);
258             }
259             SOAPMessage soapResponse = doSOAPRequest(
260                     new SOAPRequest(channelConfig.getService(), getAction.getName(), arguments));
261             String argumentName = channelConfig.getChannelTypeDescription().getGetAction().getArgument();
262             // find all other channels with the same action that are already in cache, so we can update them
263             Map<ChannelUID, Tr064ChannelConfig> channelsInRequest = channelConfigMap.entrySet().stream()
264                     .filter(map -> getAction.equals(map.getValue().getGetAction())
265                             && stateCache.containsKey(map.getKey())
266                             && !argumentName
267                                     .equals(map.getValue().getChannelTypeDescription().getGetAction().getArgument()))
268                     .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
269             channelsInRequest
270                     .forEach(
271                             (channelUID,
272                                     channelConfig1) -> soapValueConverter
273                                             .getStateFromSOAPValue(soapResponse,
274                                                     channelConfig1.getChannelTypeDescription().getGetAction()
275                                                             .getArgument(),
276                                                     channelConfig1)
277                                             .ifPresent(state -> stateCache.putValue(channelUID, state)));
278
279             return soapValueConverter.getStateFromSOAPValue(soapResponse, argumentName, channelConfig)
280                     .orElseThrow(() -> new Tr064CommunicationException("failed to transform '"
281                             + channelConfig.getChannelTypeDescription().getGetAction().getArgument() + "'"));
282         } catch (Tr064CommunicationException e) {
283             if (e.getHttpError() == 500) {
284                 switch (e.getSoapError()) {
285                     case "714":
286                         // NoSuchEntryInArray usually is an unknown entry in the MAC list
287                         logger.debug("Failed to get {}: {}", channelConfig, e.getMessage());
288                         return UnDefType.UNDEF;
289                     default:
290                 }
291             }
292             // all other cases are an error
293             logger.warn("Failed to get {}: {}", channelConfig, e.getMessage());
294             return UnDefType.UNDEF;
295         }
296     }
297 }