]> git.basschouten.com Git - openhab-addons.git/blob
7324ccf838ce7f73273afa73aac577a4bdd3b4fc
[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.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;
27
28 import javax.xml.soap.SOAPMessage;
29
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;
48
49 import com.google.gson.Gson;
50 import com.google.gson.GsonBuilder;
51
52 /**
53  * The {@link SOAPValueConverter} converts SOAP values and openHAB states
54  *
55  * @author Jan N. Klug - Initial contribution
56  */
57 @NonNullByDefault
58 public class SOAPValueConverter {
59     private final Logger logger = LoggerFactory.getLogger(SOAPValueConverter.class);
60     private final HttpClient httpClient;
61     private final int timeout;
62
63     public SOAPValueConverter(HttpClient httpClient, int timeout) {
64         this.httpClient = httpClient;
65         this.timeout = timeout;
66     }
67
68     /**
69      * convert an openHAB command to a SOAP value
70      *
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
75      */
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("");
80         }
81         if (command instanceof QuantityType) {
82             QuantityType<?> value = (unit.isEmpty()) ? ((QuantityType<?>) command)
83                     : ((QuantityType<?>) command).toUnit(unit);
84             if (value == null) {
85                 logger.warn("Could not convert {} to unit {}", command, unit);
86                 return Optional.empty();
87             }
88             switch (dataType) {
89                 case "ui1", "ui2" -> {
90                     return Optional.of(String.valueOf(value.shortValue()));
91                 }
92                 case "i4", "ui4" -> {
93                     return Optional.of(String.valueOf(value.intValue()));
94                 }
95                 default -> {
96                 }
97             }
98         } else if (command instanceof DecimalType) {
99             BigDecimal value = ((DecimalType) command).toBigDecimal();
100             switch (dataType) {
101                 case "ui1", "ui2" -> {
102                     return Optional.of(String.valueOf(value.shortValue()));
103                 }
104                 case "i4", "ui4" -> {
105                     return Optional.of(String.valueOf(value.intValue()));
106                 }
107                 default -> {
108                 }
109             }
110         } else if (command instanceof StringType) {
111             if ("string".equals(dataType)) {
112                 return Optional.of(command.toString());
113             }
114         } else if (command instanceof OnOffType) {
115             if ("boolean".equals(dataType)) {
116                 return Optional.of(OnOffType.ON.equals(command) ? "1" : "0");
117             }
118         }
119         return Optional.empty();
120     }
121
122     /**
123      * convert the value from a SOAP message to an openHAB value
124      *
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
130      */
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()
136                 : null;
137
138         return getSOAPElement(soapMessage, element).map(rawValue -> {
139             // map rawValue to State
140             switch (dataType) {
141                 case "boolean" -> {
142                     return rawValue.equals("0") ? OnOffType.OFF : OnOffType.ON;
143                 }
144                 case "string" -> {
145                     return new StringType(rawValue);
146                 }
147                 case "ui1", "ui2", "i4", "ui4" -> {
148                     BigDecimal decimalValue = new BigDecimal(rawValue);
149                     if (factor != null) {
150                         decimalValue = decimalValue.multiply(factor);
151                     }
152                     if (!unit.isEmpty()) {
153                         return new QuantityType<>(decimalValue + " " + unit);
154                     } else {
155                         return new DecimalType(decimalValue);
156                     }
157                 }
158                 default -> {
159                     return null;
160                 }
161             }
162         }).map(state -> {
163             // check if we need post-processing
164             if (channelConfig == null
165                     || channelConfig.getChannelTypeDescription().getGetAction().getPostProcessor() == null) {
166                 return state;
167             }
168             String postProcessor = channelConfig.getChannelTypeDescription().getGetAction().getPostProcessor();
169             try {
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) {
174                     return (State) o;
175                 }
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());
182             }
183             return null;
184         }).or(Optional::empty);
185     }
186
187     /**
188      * post processor for current bitrate
189      */
190     @SuppressWarnings("unused")
191     private State processCurrentBitrate(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException {
192         Double bps = Arrays.stream(state.toString().split(",")).mapToDouble(s -> {
193             try {
194                 return Double.parseDouble(s);
195             } catch (NumberFormatException e) {
196                 return 0.0;
197             }
198         }).limit(3).average().orElse(Double.NaN);
199
200         if (bps.equals(Double.NaN)) {
201             return UnDefType.UNDEF;
202         } else {
203             return new QuantityType<>(bps * 8.0 / 1024.0, Units.KILOBIT_PER_SECOND);
204         }
205     }
206
207     /**
208      * post processor to map mac device signal strength to system.signal-strength 0-4
209      *
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
213      */
214     @SuppressWarnings("unused")
215     private State processMacSignalStrength(State state, Tr064ChannelConfig channelConfig) {
216         State mappedSignalStrength = UnDefType.UNDEF;
217         DecimalType currentStateValue = state.as(DecimalType.class);
218
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);
228             } else {
229                 mappedSignalStrength = new DecimalType(0);
230             }
231         }
232
233         return mappedSignalStrength;
234     }
235
236     /**
237      * post processor for answering machine new messages channel
238      *
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
243      */
244     @SuppressWarnings("unused")
245     private State processTamListURL(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException {
246         try {
247             ContentResponse response = httpClient.newRequest(state.toString()).timeout(timeout, TimeUnit.MILLISECONDS)
248                     .send();
249             String responseContent = response.getContentAsString();
250             int messageCount = responseContent.split("<New>1</New>").length - 1;
251
252             return new DecimalType(messageCount);
253         } catch (InterruptedException | TimeoutException | ExecutionException e) {
254             throw new PostProcessingException("Failed to get TAM list from URL " + state, e);
255         }
256     }
257
258     /**
259      * post processor for missed calls
260      *
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
265      */
266     @SuppressWarnings("unused")
267     private State processMissedCalls(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException {
268         return processCallList(state, channelConfig.getParameter(), CallListType.MISSED_COUNT);
269     }
270
271     /**
272      * post processor for inbound calls
273      *
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
278      */
279     @SuppressWarnings("unused")
280     private State processInboundCalls(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException {
281         return processCallList(state, channelConfig.getParameter(), CallListType.INBOUND_COUNT);
282     }
283
284     /**
285      * post processor for rejected calls
286      *
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
291      */
292     @SuppressWarnings("unused")
293     private State processRejectedCalls(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException {
294         return processCallList(state, channelConfig.getParameter(), CallListType.REJECTED_COUNT);
295     }
296
297     /**
298      * post processor for outbound calls
299      *
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
304      */
305     @SuppressWarnings("unused")
306     private State processOutboundCalls(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException {
307         return processCallList(state, channelConfig.getParameter(), CallListType.OUTBOUND_COUNT);
308     }
309
310     /**
311      * post processor for JSON call list
312      *
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
317      */
318     @SuppressWarnings("unused")
319     private State processCallListJSON(State state, Tr064ChannelConfig channelConfig) throws PostProcessingException {
320         return processCallList(state, channelConfig.getParameter(), CallListType.JSON_LIST);
321     }
322
323     /**
324      * internal helper for call list post processors
325      *
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
331      */
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);
337         }
338         List<Call> calls = callListRoot.getCall();
339         switch (type) {
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);
343             }
344             case JSON_LIST -> {
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));
349             }
350         }
351         return UnDefType.UNDEF;
352     }
353 }