2 * Copyright (c) 2010-2022 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.lang.reflect.InvocationTargetException;
18 import java.lang.reflect.Method;
19 import java.math.BigDecimal;
20 import java.util.List;
21 import java.util.Optional;
22 import java.util.concurrent.ExecutionException;
23 import java.util.concurrent.TimeUnit;
24 import java.util.concurrent.TimeoutException;
25 import java.util.stream.Collectors;
27 import javax.xml.soap.SOAPMessage;
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.eclipse.jetty.client.HttpClient;
32 import org.eclipse.jetty.client.api.ContentResponse;
33 import org.openhab.binding.tr064.internal.config.Tr064ChannelConfig;
34 import org.openhab.binding.tr064.internal.dto.additions.Call;
35 import org.openhab.binding.tr064.internal.dto.additions.Root;
36 import org.openhab.binding.tr064.internal.util.Util;
37 import org.openhab.core.library.types.DecimalType;
38 import org.openhab.core.library.types.OnOffType;
39 import org.openhab.core.library.types.QuantityType;
40 import org.openhab.core.library.types.StringType;
41 import org.openhab.core.library.unit.Units;
42 import org.openhab.core.types.Command;
43 import org.openhab.core.types.State;
44 import org.openhab.core.types.UnDefType;
45 import org.slf4j.Logger;
46 import org.slf4j.LoggerFactory;
48 import com.google.gson.Gson;
49 import com.google.gson.GsonBuilder;
52 * The {@link SOAPValueConverter} converts SOAP values and openHAB states
54 * @author Jan N. Klug - Initial contribution
57 public class SOAPValueConverter {
58 private final Logger logger = LoggerFactory.getLogger(SOAPValueConverter.class);
59 private final HttpClient httpClient;
61 public SOAPValueConverter(HttpClient httpClient) {
62 this.httpClient = httpClient;
66 * convert an openHAB command to a SOAP value
68 * @param command the command to be converted
69 * @param dataType the datatype to send
70 * @param unit if available, the unit of the converted value
71 * @return a string optional containing the converted value
73 public Optional<String> getSOAPValueFromCommand(Command command, String dataType, String unit) {
74 if (dataType.isEmpty()) {
75 // we don't have data to send
76 return Optional.of("");
78 if (command instanceof QuantityType) {
79 QuantityType<?> value = (unit.isEmpty()) ? ((QuantityType<?>) command)
80 : ((QuantityType<?>) command).toUnit(unit);
82 logger.warn("Could not convert {} to unit {}", command, unit);
83 return Optional.empty();
88 return Optional.of(String.valueOf(value.shortValue()));
91 return Optional.of(String.valueOf(value.intValue()));
94 } else if (command instanceof DecimalType) {
95 BigDecimal value = ((DecimalType) command).toBigDecimal();
99 return Optional.of(String.valueOf(value.shortValue()));
102 return Optional.of(String.valueOf(value.intValue()));
105 } else if (command instanceof StringType) {
106 if ("string".equals(dataType)) {
107 return Optional.of(command.toString());
109 } else if (command instanceof OnOffType) {
110 if ("boolean".equals(dataType)) {
111 return Optional.of(OnOffType.ON.equals(command) ? "1" : "0");
114 return Optional.empty();
118 * convert the value from a SOAP message to an openHAB value
120 * @param soapMessage the inbound SOAP message
121 * @param element the element that needs to be extracted
122 * @param channelConfig the channel config containing additional information (if null a data-type "string" and
123 * missing unit is assumed)
124 * @return an Optional of State containing the converted value
126 public Optional<State> getStateFromSOAPValue(SOAPMessage soapMessage, String element,
127 @Nullable Tr064ChannelConfig channelConfig) {
128 String dataType = channelConfig != null ? channelConfig.getDataType() : "string";
129 String unit = channelConfig != null ? channelConfig.getChannelTypeDescription().getItem().getUnit() : "";
131 return getSOAPElement(soapMessage, element).map(rawValue -> {
132 // map rawValue to State
135 return rawValue.equals("0") ? OnOffType.OFF : OnOffType.ON;
137 return new StringType(rawValue);
142 if (!unit.isEmpty()) {
143 return new QuantityType<>(rawValue + " " + unit);
145 return new DecimalType(rawValue);
151 // check if we need post processing
152 if (channelConfig == null
153 || channelConfig.getChannelTypeDescription().getGetAction().getPostProcessor() == null) {
156 String postProcessor = channelConfig.getChannelTypeDescription().getGetAction().getPostProcessor();
158 Method method = SOAPValueConverter.class.getDeclaredMethod(postProcessor, State.class,
159 Tr064ChannelConfig.class);
160 Object o = method.invoke(this, state, channelConfig);
161 if (o instanceof State) {
164 } catch (NoSuchMethodException | IllegalAccessException e) {
165 logger.warn("Postprocessor {} not found, this most likely is a programming error", postProcessor, e);
166 } catch (InvocationTargetException e) {
167 Throwable cause = e.getCause();
168 logger.info("Postprocessor {} failed: {}", postProcessor,
169 cause != null ? cause.getMessage() : e.getMessage());
172 }).or(Optional::empty);
176 * post processor to map mac device signal strength to system.signal-strength 0-4
178 * @param state with signalStrength
179 * @param channelConfig channel config of the mac signal strength
180 * @return the mapped system.signal-strength in range 0-4
182 @SuppressWarnings("unused")
183 private State processMacSignalStrength(State state, Tr064ChannelConfig channelConfig) {
184 State mappedSignalStrength = UnDefType.UNDEF;
185 DecimalType currentStateValue = state.as(DecimalType.class);
187 if (currentStateValue != null) {
188 if (currentStateValue.intValue() > 80) {
189 mappedSignalStrength = new DecimalType(4);
190 } else if (currentStateValue.intValue() > 60) {
191 mappedSignalStrength = new DecimalType(3);
192 } else if (currentStateValue.intValue() > 40) {
193 mappedSignalStrength = new DecimalType(2);
194 } else if (currentStateValue.intValue() > 20) {
195 mappedSignalStrength = new DecimalType(1);
197 mappedSignalStrength = new DecimalType(0);
201 return mappedSignalStrength;
205 * post processor for decibel values (which are served as deca decibel)
207 * @param state the channel value in deca decibel
208 * @param channelConfig channel config of the channel
209 * @return the state converted to decibel
211 @SuppressWarnings("unused")
212 private State processDecaDecibel(State state, Tr064ChannelConfig channelConfig) {
213 Float value = state.as(DecimalType.class).floatValue() / 10;
215 return new QuantityType<>(value, Units.DECIBEL);
219 * post processor for answering machine new messages channel
221 * @param state the message list URL
222 * @param channelConfig channel config of the TAM new message channel
223 * @return the number of new messages
224 * @throws PostProcessingException if the message list could not be retrieved
226 @SuppressWarnings("unused")
227 private State processTamListURL(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException {
229 ContentResponse response = httpClient.newRequest(state.toString()).timeout(1500, TimeUnit.MILLISECONDS)
231 String responseContent = response.getContentAsString();
232 int messageCount = responseContent.split("<New>1</New>").length - 1;
234 return new DecimalType(messageCount);
235 } catch (TimeoutException e) {
236 throw new PostProcessingException("Failed to get TAM list due to time out from URL " + state.toString(), e);
237 } catch (InterruptedException | ExecutionException e) {
238 throw new PostProcessingException("Failed to get TAM list from URL " + state.toString(), e);
243 * post processor for missed calls
245 * @param state the call list URL
246 * @param channelConfig channel config of the missed call channel (contains day number)
247 * @return the number of missed calls
248 * @throws PostProcessingException if call list could not be retrieved
250 @SuppressWarnings("unused")
251 private State processMissedCalls(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException {
252 return processCallList(state, channelConfig.getParameter(), CallListType.MISSED_COUNT);
256 * post processor for inbound calls
258 * @param state the call list URL
259 * @param channelConfig channel config of the inbound call channel (contains day number)
260 * @return the number of inbound calls
261 * @throws PostProcessingException if call list could not be retrieved
263 @SuppressWarnings("unused")
264 private State processInboundCalls(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException {
265 return processCallList(state, channelConfig.getParameter(), CallListType.INBOUND_COUNT);
269 * post processor for rejected calls
271 * @param state the call list URL
272 * @param channelConfig channel config of the rejected call channel (contains day number)
273 * @return the number of rejected calls
274 * @throws PostProcessingException if call list could not be retrieved
276 @SuppressWarnings("unused")
277 private State processRejectedCalls(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException {
278 return processCallList(state, channelConfig.getParameter(), CallListType.REJECTED_COUNT);
282 * post processor for outbound calls
284 * @param state the call list URL
285 * @param channelConfig channel config of the outbound call channel (contains day number)
286 * @return the number of outbound calls
287 * @throws PostProcessingException if call list could not be retrieved
289 @SuppressWarnings("unused")
290 private State processOutboundCalls(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException {
291 return processCallList(state, channelConfig.getParameter(), CallListType.OUTBOUND_COUNT);
295 * post processor for JSON call list
297 * @param state the call list URL
298 * @param channelConfig channel config of the call list channel (contains day number)
299 * @return caller list in JSON format
300 * @throws PostProcessingException if call list could not be retrieved
302 @SuppressWarnings("unused")
303 private State processCallListJSON(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException {
304 return processCallList(state, channelConfig.getParameter(), CallListType.JSON_LIST);
308 * internal helper for call list post processors
310 * @param state the call list URL
311 * @param days number of days to get
312 * @param type type of call (2=missed 1=inbound 4=rejected 3=outbund)
313 * @return the quantity of calls of the given type within the given number of days
314 * @throws PostProcessingException if the call list could not be retrieved
316 private State processCallList(State state, @Nullable String days, CallListType type)
317 throws PostProcessingException {
318 Root callListRoot = Util.getAndUnmarshalXML(httpClient, state.toString() + "&days=" + days, Root.class);
319 if (callListRoot == null) {
320 throw new PostProcessingException("Failed to get call list from URL " + state.toString());
322 List<Call> calls = callListRoot.getCall();
328 long callCount = calls.stream().filter(call -> type.typeString().equals(call.getType())).count();
329 return new DecimalType(callCount);
331 Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ssX").serializeNulls().create();
332 List<CallListEntry> callListEntries = calls.stream().map(CallListEntry::new)
333 .collect(Collectors.toList());
334 return new StringType(gson.toJson(callListEntries));
336 return UnDefType.UNDEF;