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