2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.tr064.internal.soap;
15 import static org.openhab.binding.tr064.internal.util.Util.getSOAPElement;
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;
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;
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;
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;
62 * The {@link SOAPConnector} provides communication with a remote SOAP device
64 * @author Jan N. Klug - Initial contribution
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;
74 private final ExpiringCacheMap<SOAPRequest, SOAPMessage> soapMessageCache = new ExpiringCacheMap<>(
75 Duration.ofMillis(2000));
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);
85 * prepare a SOAP request for an action request to a service
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
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/");
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 -> {
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(),
113 MimeHeaders headers = soapMessage.getMimeHeaders();
114 headers.addHeader("SOAPAction", soapRequest.service.getServiceType() + "#" + soapRequest.soapAction);
115 soapMessage.saveChanges();
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));
132 * execute a SOAP request with cache
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
138 public SOAPMessage doSOAPRequest(SOAPRequest soapRequest) throws Tr064CommunicationException {
140 SOAPMessage soapMessage = Objects.requireNonNull(soapMessageCache.putIfAbsentAndGet(soapRequest, () -> {
142 SOAPMessage newValue = doSOAPRequestUncached(soapRequest);
143 logger.trace("Storing in cache: {}", newValue);
145 } catch (Tr064CommunicationException e) {
147 throw new IllegalArgumentException(e);
150 logger.trace("Returning from cache: {}", soapMessage);
152 } catch (IllegalArgumentException e) {
153 Throwable cause = e.getCause();
154 if (cause instanceof Tr064CommunicationException) {
155 throw (Tr064CommunicationException) cause;
163 * execute a SOAP request without cache
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
169 public synchronized SOAPMessage doSOAPRequestUncached(SOAPRequest soapRequest) throws Tr064CommunicationException {
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())));
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();
184 try (final ByteArrayInputStream is = new ByteArrayInputStream(response.getContent())) {
185 logger.trace("Received response: {}", response.getContentAsString());
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);
197 } catch (IOException | SOAPException | InterruptedException | TimeoutException | ExecutionException e) {
198 throw new Tr064CommunicationException(e);
203 * send a command to the remote device
205 * @param channelConfig the channel config containing all information
206 * @param command the command to send
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());
216 Map<String, String> arguments = new HashMap<>();
217 if (channelTypeDescription.getSetAction().getArgument() != null) {
218 arguments.put(channelTypeDescription.getSetAction().getArgument(), value);
220 String parameter = channelConfig.getParameter();
221 if (parameter != null) {
223 channelConfig.getChannelTypeDescription().getGetAction().getParameter().getName(),
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());
232 }, () -> logger.warn("Could not convert {} to SOAP value", command));
236 * get a value from the remote device - updates state cache for all possible channels
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
243 public State getChannelStateFromDevice(final Tr064ChannelConfig channelConfig,
244 Map<ChannelUID, Tr064ChannelConfig> channelConfigMap, ExpiringCacheMap<ChannelUID, State> stateCache) {
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;
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);
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())
271 .equals(map.getValue().getChannelTypeDescription().getGetAction().getArgument()))
272 .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
276 channelConfig1) -> soapValueConverter
277 .getStateFromSOAPValue(soapResponse,
278 channelConfig1.getChannelTypeDescription().getGetAction()
281 .ifPresent(state -> stateCache.putValue(channelUID, state)));
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()) {
290 // NoSuchEntryInArray usually is an unknown entry in the MAC list
291 logger.debug("Failed to get {}: {}", channelConfig, e.getMessage());
292 return UnDefType.UNDEF;
298 // all other cases are an error
299 logger.warn("Failed to get {}: {}", channelConfig, e.getMessage());
300 return UnDefType.UNDEF;