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