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.lang.reflect.InvocationTargetException;
18 import java.lang.reflect.Method;
19 import java.math.BigDecimal;
20 import java.util.Arrays;
21 import java.util.List;
22 import java.util.Optional;
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.SOAPMessage;
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.eclipse.jetty.client.HttpClient;
33 import org.eclipse.jetty.client.api.ContentResponse;
34 import org.openhab.binding.tr064.internal.config.Tr064ChannelConfig;
35 import org.openhab.binding.tr064.internal.dto.additions.Call;
36 import org.openhab.binding.tr064.internal.dto.additions.Root;
37 import org.openhab.binding.tr064.internal.util.Util;
38 import org.openhab.core.library.types.DecimalType;
39 import org.openhab.core.library.types.OnOffType;
40 import org.openhab.core.library.types.QuantityType;
41 import org.openhab.core.library.types.StringType;
42 import org.openhab.core.library.unit.Units;
43 import org.openhab.core.types.Command;
44 import org.openhab.core.types.State;
45 import org.openhab.core.types.UnDefType;
46 import org.slf4j.Logger;
47 import org.slf4j.LoggerFactory;
49 import com.google.gson.Gson;
50 import com.google.gson.GsonBuilder;
53 * The {@link SOAPValueConverter} converts SOAP values and openHAB states
55 * @author Jan N. Klug - Initial contribution
58 public class SOAPValueConverter {
59 private final Logger logger = LoggerFactory.getLogger(SOAPValueConverter.class);
60 private final HttpClient httpClient;
61 private final int timeout;
63 public SOAPValueConverter(HttpClient httpClient, int timeout) {
64 this.httpClient = httpClient;
65 this.timeout = timeout;
69 * convert an openHAB command to a SOAP value
71 * @param command the command to be converted
72 * @param dataType the datatype to send
73 * @param unit if available, the unit of the converted value
74 * @return a string optional containing the converted value
76 public Optional<String> getSOAPValueFromCommand(Command command, String dataType, String unit) {
77 if (dataType.isEmpty()) {
78 // we don't have data to send
79 return Optional.of("");
81 if (command instanceof QuantityType) {
82 QuantityType<?> value = (unit.isEmpty()) ? ((QuantityType<?>) command)
83 : ((QuantityType<?>) command).toUnit(unit);
85 logger.warn("Could not convert {} to unit {}", command, unit);
86 return Optional.empty();
89 case "ui1", "ui2" -> {
90 return Optional.of(String.valueOf(value.shortValue()));
93 return Optional.of(String.valueOf(value.intValue()));
98 } else if (command instanceof DecimalType) {
99 BigDecimal value = ((DecimalType) command).toBigDecimal();
101 case "ui1", "ui2" -> {
102 return Optional.of(String.valueOf(value.shortValue()));
104 case "i4", "ui4" -> {
105 return Optional.of(String.valueOf(value.intValue()));
110 } else if (command instanceof StringType) {
111 if ("string".equals(dataType)) {
112 return Optional.of(command.toString());
114 } else if (command instanceof OnOffType) {
115 if ("boolean".equals(dataType)) {
116 return Optional.of(OnOffType.ON.equals(command) ? "1" : "0");
119 return Optional.empty();
123 * convert the value from a SOAP message to an openHAB value
125 * @param soapMessage the inbound SOAP message
126 * @param element the element that needs to be extracted
127 * @param channelConfig the channel config containing additional information (if null a data-type "string" and
128 * missing unit is assumed)
129 * @return an Optional of State containing the converted value
131 public Optional<State> getStateFromSOAPValue(SOAPMessage soapMessage, String element,
132 @Nullable Tr064ChannelConfig channelConfig) {
133 String dataType = channelConfig != null ? channelConfig.getDataType() : "string";
134 String unit = channelConfig != null ? channelConfig.getChannelTypeDescription().getItem().getUnit() : "";
135 BigDecimal factor = channelConfig != null ? channelConfig.getChannelTypeDescription().getItem().getFactor()
138 return getSOAPElement(soapMessage, element).map(rawValue -> {
139 // map rawValue to State
142 return rawValue.equals("0") ? OnOffType.OFF : OnOffType.ON;
145 return new StringType(rawValue);
147 case "ui1", "ui2", "i4", "ui4" -> {
148 BigDecimal decimalValue = new BigDecimal(rawValue);
149 if (factor != null) {
150 decimalValue = decimalValue.multiply(factor);
152 if (!unit.isEmpty()) {
153 return new QuantityType<>(decimalValue + " " + unit);
155 return new DecimalType(decimalValue);
163 // check if we need post-processing
164 if (channelConfig == null
165 || channelConfig.getChannelTypeDescription().getGetAction().getPostProcessor() == null) {
168 String postProcessor = channelConfig.getChannelTypeDescription().getGetAction().getPostProcessor();
170 Method method = SOAPValueConverter.class.getDeclaredMethod(postProcessor, State.class,
171 Tr064ChannelConfig.class);
172 Object o = method.invoke(this, state, channelConfig);
173 if (o instanceof State) {
176 } catch (NoSuchMethodException | IllegalAccessException e) {
177 logger.warn("Postprocessor {} not found, this most likely is a programming error", postProcessor, e);
178 } catch (InvocationTargetException e) {
179 Throwable cause = e.getCause();
180 logger.info("Postprocessor {} failed: {}", postProcessor,
181 cause != null ? cause.getMessage() : e.getMessage());
184 }).or(Optional::empty);
188 * post processor for current bitrate
190 @SuppressWarnings("unused")
191 private State processCurrentBitrate(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException {
192 Double bps = Arrays.stream(state.toString().split(",")).mapToDouble(s -> {
194 return Double.parseDouble(s);
195 } catch (NumberFormatException e) {
198 }).limit(3).average().orElse(Double.NaN);
200 if (bps.equals(Double.NaN)) {
201 return UnDefType.UNDEF;
203 return new QuantityType<>(bps * 8.0 / 1024.0, Units.KILOBIT_PER_SECOND);
208 * post processor to map mac device signal strength to system.signal-strength 0-4
210 * @param state with signalStrength
211 * @param channelConfig channel config of the mac signal strength
212 * @return the mapped system.signal-strength in range 0-4
214 @SuppressWarnings("unused")
215 private State processMacSignalStrength(State state, Tr064ChannelConfig channelConfig) {
216 State mappedSignalStrength = UnDefType.UNDEF;
217 DecimalType currentStateValue = state.as(DecimalType.class);
219 if (currentStateValue != null) {
220 if (currentStateValue.intValue() > 80) {
221 mappedSignalStrength = new DecimalType(4);
222 } else if (currentStateValue.intValue() > 60) {
223 mappedSignalStrength = new DecimalType(3);
224 } else if (currentStateValue.intValue() > 40) {
225 mappedSignalStrength = new DecimalType(2);
226 } else if (currentStateValue.intValue() > 20) {
227 mappedSignalStrength = new DecimalType(1);
229 mappedSignalStrength = new DecimalType(0);
233 return mappedSignalStrength;
237 * post processor for answering machine new messages channel
239 * @param state the message list URL
240 * @param channelConfig channel config of the TAM new message channel
241 * @return the number of new messages
242 * @throws PostProcessingException if the message list could not be retrieved
244 @SuppressWarnings("unused")
245 private State processTamListURL(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException {
247 ContentResponse response = httpClient.newRequest(state.toString()).timeout(timeout, TimeUnit.SECONDS)
249 String responseContent = response.getContentAsString();
250 int messageCount = responseContent.split("<New>1</New>").length - 1;
252 return new DecimalType(messageCount);
253 } catch (InterruptedException | TimeoutException | ExecutionException e) {
254 throw new PostProcessingException("Failed to get TAM list from URL " + state, e);
259 * post processor for missed calls
261 * @param state the call list URL
262 * @param channelConfig channel config of the missed call channel (contains day number)
263 * @return the number of missed calls
264 * @throws PostProcessingException if call list could not be retrieved
266 @SuppressWarnings("unused")
267 private State processMissedCalls(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException {
268 return processCallList(state, channelConfig.getParameter(), CallListType.MISSED_COUNT);
272 * post processor for inbound calls
274 * @param state the call list URL
275 * @param channelConfig channel config of the inbound call channel (contains day number)
276 * @return the number of inbound calls
277 * @throws PostProcessingException if call list could not be retrieved
279 @SuppressWarnings("unused")
280 private State processInboundCalls(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException {
281 return processCallList(state, channelConfig.getParameter(), CallListType.INBOUND_COUNT);
285 * post processor for rejected calls
287 * @param state the call list URL
288 * @param channelConfig channel config of the rejected call channel (contains day number)
289 * @return the number of rejected calls
290 * @throws PostProcessingException if call list could not be retrieved
292 @SuppressWarnings("unused")
293 private State processRejectedCalls(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException {
294 return processCallList(state, channelConfig.getParameter(), CallListType.REJECTED_COUNT);
298 * post processor for outbound calls
300 * @param state the call list URL
301 * @param channelConfig channel config of the outbound call channel (contains day number)
302 * @return the number of outbound calls
303 * @throws PostProcessingException if call list could not be retrieved
305 @SuppressWarnings("unused")
306 private State processOutboundCalls(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException {
307 return processCallList(state, channelConfig.getParameter(), CallListType.OUTBOUND_COUNT);
311 * post processor for JSON call list
313 * @param state the call list URL
314 * @param channelConfig channel config of the call list channel (contains day number)
315 * @return caller list in JSON format
316 * @throws PostProcessingException if call list could not be retrieved
318 @SuppressWarnings("unused")
319 private State processCallListJSON(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException {
320 return processCallList(state, channelConfig.getParameter(), CallListType.JSON_LIST);
324 * internal helper for call list post processors
326 * @param state the call list URL
327 * @param days number of days to get
328 * @param type type of call (2=missed 1=inbound 4=rejected 3=outbund)
329 * @return the quantity of calls of the given type within the given number of days
330 * @throws PostProcessingException if the call list could not be retrieved
332 private State processCallList(State state, @Nullable String days, CallListType type)
333 throws PostProcessingException {
334 Root callListRoot = Util.getAndUnmarshalXML(httpClient, state + "&days=" + days, Root.class, timeout);
335 if (callListRoot == null) {
336 throw new PostProcessingException("Failed to get call list from URL " + state);
338 List<Call> calls = callListRoot.getCall();
340 case INBOUND_COUNT, MISSED_COUNT, OUTBOUND_COUNT, REJECTED_COUNT -> {
341 long callCount = calls.stream().filter(call -> type.typeString().equals(call.getType())).count();
342 return new DecimalType(callCount);
345 Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ssX").serializeNulls().create();
346 List<CallListEntry> callListEntries = calls.stream().map(CallListEntry::new)
347 .collect(Collectors.toList());
348 return new StringType(gson.toJson(callListEntries));
351 return UnDefType.UNDEF;