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