2 * Copyright (c) 2010-2020 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;
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.util.HashMap;
21 import java.util.Iterator;
23 import java.util.concurrent.ExecutionException;
24 import java.util.concurrent.TimeUnit;
25 import java.util.concurrent.TimeoutException;
26 import java.util.stream.Collectors;
28 import javax.xml.soap.*;
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.config.Tr064ChannelConfig;
38 import org.openhab.binding.tr064.internal.dto.config.ActionType;
39 import org.openhab.binding.tr064.internal.dto.config.ChannelTypeDescription;
40 import org.openhab.binding.tr064.internal.dto.scpd.root.SCPDServiceType;
41 import org.openhab.binding.tr064.internal.dto.scpd.service.SCPDActionType;
42 import org.openhab.core.cache.ExpiringCacheMap;
43 import org.openhab.core.library.types.OnOffType;
44 import org.openhab.core.library.types.StringType;
45 import org.openhab.core.thing.ChannelUID;
46 import org.openhab.core.types.Command;
47 import org.openhab.core.types.State;
48 import org.openhab.core.types.UnDefType;
49 import org.slf4j.Logger;
50 import org.slf4j.LoggerFactory;
53 * The {@link SOAPConnector} provides communication with a remote SOAP device
55 * @author Jan N. Klug - Initial contribution
58 public class SOAPConnector {
59 private static final int SOAP_TIMEOUT = 2000; // in ms
60 private final Logger logger = LoggerFactory.getLogger(SOAPConnector.class);
61 private final HttpClient httpClient;
62 private final String endpointBaseURL;
63 private final SOAPValueConverter soapValueConverter;
65 public SOAPConnector(HttpClient httpClient, String endpointBaseURL) {
66 this.httpClient = httpClient;
67 this.endpointBaseURL = endpointBaseURL;
68 this.soapValueConverter = new SOAPValueConverter(httpClient);
72 * prepare a SOAP request for an action request to a service
74 * @param service the service
75 * @param soapAction the action to send
76 * @param arguments arguments to send along with the request
77 * @return a jetty Request containing the full SOAP message
78 * @throws IOException if a problem while writing the SOAP message to the Request occurs
79 * @throws SOAPException if a problem with creating the SOAP message occurs
81 private Request prepareSOAPRequest(SCPDServiceType service, String soapAction, Map<String, String> arguments)
82 throws IOException, SOAPException {
83 MessageFactory messageFactory = MessageFactory.newInstance();
84 SOAPMessage soapMessage = messageFactory.createMessage();
85 SOAPPart soapPart = soapMessage.getSOAPPart();
86 SOAPEnvelope envelope = soapPart.getEnvelope();
87 envelope.setEncodingStyle("http://schemas.xmlsoap.org/soap/encoding/");
90 SOAPBody soapBody = envelope.getBody();
91 SOAPElement soapBodyElem = soapBody.addChildElement(soapAction, "u", service.getServiceType());
92 arguments.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach(argument -> {
94 soapBodyElem.addChildElement(argument.getKey()).setTextContent(argument.getValue());
95 } catch (SOAPException e) {
96 logger.warn("Could not add {}:{} to SOAP Request: {}", argument.getKey(), argument.getValue(),
102 MimeHeaders headers = soapMessage.getMimeHeaders();
103 headers.addHeader("SOAPAction", service.getServiceType() + "#" + soapAction);
104 soapMessage.saveChanges();
106 // create Request and add headers and content
107 Request request = httpClient.newRequest(endpointBaseURL + service.getControlURL()).method(HttpMethod.POST);
108 ((Iterator<MimeHeader>) soapMessage.getMimeHeaders().getAllHeaders())
109 .forEachRemaining(header -> request.header(header.getName(), header.getValue()));
110 try (final ByteArrayOutputStream os = new ByteArrayOutputStream()) {
111 soapMessage.writeTo(os);
112 byte[] content = os.toByteArray();
113 request.content(new BytesContentProvider(content));
120 * execute a SOAP request
122 * @param service the service to send the action to
123 * @param soapAction the action itself
124 * @param arguments arguments to send along with the request
125 * @return the SOAPMessage answer from the remote host
126 * @throws Tr064CommunicationException if an error occurs during the request
128 public synchronized SOAPMessage doSOAPRequest(SCPDServiceType service, String soapAction,
129 Map<String, String> arguments) throws Tr064CommunicationException {
131 Request request = prepareSOAPRequest(service, soapAction, arguments).timeout(SOAP_TIMEOUT,
132 TimeUnit.MILLISECONDS);
133 if (logger.isTraceEnabled()) {
134 request.getContent().forEach(buffer -> logger.trace("Request: {}", new String(buffer.array())));
137 ContentResponse response = request.send();
138 if (response.getStatus() == HttpStatus.UNAUTHORIZED_401) {
139 // retry once if authentication expired
140 logger.trace("Re-Auth needed.");
141 httpClient.getAuthenticationStore().clearAuthenticationResults();
142 request = prepareSOAPRequest(service, soapAction, arguments).timeout(SOAP_TIMEOUT,
143 TimeUnit.MILLISECONDS);
144 response = request.send();
146 try (final ByteArrayInputStream is = new ByteArrayInputStream(response.getContent())) {
147 logger.trace("Received response: {}", response.getContentAsString());
149 SOAPMessage soapMessage = MessageFactory.newInstance().createMessage(null, is);
150 if (soapMessage.getSOAPBody().hasFault()) {
151 String soapError = getSOAPElement(soapMessage, "errorCode").orElse("unknown");
152 String soapReason = getSOAPElement(soapMessage, "errorDescription").orElse("unknown");
153 String error = String.format("HTTP-Response-Code %d (%s), SOAP-Fault: %s (%s)",
154 response.getStatus(), response.getReason(), soapError, soapReason);
155 throw new Tr064CommunicationException(error);
159 } catch (IOException | SOAPException | InterruptedException | TimeoutException | ExecutionException e) {
160 throw new Tr064CommunicationException(e);
165 * send a command to the remote device
167 * @param channelConfig the channel config containing all information
168 * @param command the command to send
170 public void sendChannelCommandToDevice(Tr064ChannelConfig channelConfig, Command command) {
171 soapValueConverter.getSOAPValueFromCommand(command, channelConfig.getDataType(),
172 channelConfig.getChannelTypeDescription().getItem().getUnit()).ifPresentOrElse(value -> {
173 final ChannelTypeDescription channelTypeDescription = channelConfig.getChannelTypeDescription();
174 final SCPDServiceType service = channelConfig.getService();
175 logger.debug("Sending {} as {} to {}/{}", command, value, service.getServiceId(),
176 channelTypeDescription.getSetAction().getName());
178 Map<String, String> arguments = new HashMap<>();
179 if (channelTypeDescription.getSetAction().getArgument() != null) {
180 arguments.put(channelTypeDescription.getSetAction().getArgument(), value);
182 String parameter = channelConfig.getParameter();
183 if (parameter != null) {
185 channelConfig.getChannelTypeDescription().getGetAction().getParameter().getName(),
188 doSOAPRequest(service, channelTypeDescription.getSetAction().getName(), arguments);
189 } catch (Tr064CommunicationException e) {
190 logger.warn("Could not send command {}: {}", command, e.getMessage());
192 }, () -> logger.warn("Could not convert {} to SOAP value", command));
196 * get a value from the remote device - updates state cache for all possible channels
198 * @param channelConfig the channel config containing all information
199 * @param channelConfigMap map of all channels in the device
200 * @param stateCache the ExpiringCacheMap for states of the device
201 * @return the value for the requested channel
203 public State getChannelStateFromDevice(final Tr064ChannelConfig channelConfig,
204 Map<ChannelUID, Tr064ChannelConfig> channelConfigMap, ExpiringCacheMap<ChannelUID, State> stateCache) {
206 final SCPDActionType getAction = channelConfig.getGetAction();
207 if (getAction == null) {
208 // channel has no get action, return a default
209 switch (channelConfig.getDataType()) {
211 return OnOffType.OFF;
213 return StringType.EMPTY;
215 return UnDefType.UNDEF;
219 // get value(s) from remote device
220 Map<String, String> arguments = new HashMap<>();
221 String parameter = channelConfig.getParameter();
222 ActionType action = channelConfig.getChannelTypeDescription().getGetAction();
223 if (parameter != null && !action.getParameter().isInternalOnly()) {
224 arguments.put(action.getParameter().getName(), parameter);
226 SOAPMessage soapResponse = doSOAPRequest(channelConfig.getService(), getAction.getName(), arguments);
228 String argumentName = channelConfig.getChannelTypeDescription().getGetAction().getArgument();
229 // find all other channels with the same action that are already in cache, so we can update them
230 Map<ChannelUID, Tr064ChannelConfig> channelsInRequest = channelConfigMap.entrySet().stream()
231 .filter(map -> getAction.equals(map.getValue().getGetAction())
232 && stateCache.containsKey(map.getKey())
234 .equals(map.getValue().getChannelTypeDescription().getGetAction().getArgument()))
235 .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
239 channelConfig1) -> soapValueConverter
240 .getStateFromSOAPValue(soapResponse,
241 channelConfig1.getChannelTypeDescription().getGetAction()
244 .ifPresent(state -> stateCache.putValue(channelUID, state)));
246 return soapValueConverter.getStateFromSOAPValue(soapResponse, argumentName, channelConfig)
247 .orElseThrow(() -> new Tr064CommunicationException("failed to transform '"
248 + channelConfig.getChannelTypeDescription().getGetAction().getArgument() + "'"));
249 } catch (Tr064CommunicationException e) {
250 logger.info("Failed to get {}: {}", channelConfig, e.getMessage());
251 return UnDefType.UNDEF;