]> git.basschouten.com Git - openhab-addons.git/blob
03cdbcd95600533e79c0f5b6a12c86ca6d78b092
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.wemo.internal.handler;
14
15 import static org.openhab.binding.wemo.internal.WemoBindingConstants.*;
16
17 import java.io.StringReader;
18 import java.math.BigDecimal;
19 import java.net.URL;
20 import java.util.Collections;
21 import java.util.Set;
22 import java.util.concurrent.ScheduledFuture;
23 import java.util.concurrent.TimeUnit;
24
25 import javax.xml.parsers.DocumentBuilder;
26 import javax.xml.parsers.DocumentBuilderFactory;
27
28 import org.apache.commons.lang.StringEscapeUtils;
29 import org.apache.commons.lang.StringUtils;
30 import org.openhab.binding.wemo.internal.http.WemoHttpCall;
31 import org.openhab.core.config.core.Configuration;
32 import org.openhab.core.io.transport.upnp.UpnpIOParticipant;
33 import org.openhab.core.io.transport.upnp.UpnpIOService;
34 import org.openhab.core.library.types.OnOffType;
35 import org.openhab.core.thing.ChannelUID;
36 import org.openhab.core.thing.Thing;
37 import org.openhab.core.thing.ThingStatus;
38 import org.openhab.core.thing.ThingStatusDetail;
39 import org.openhab.core.thing.ThingTypeUID;
40 import org.openhab.core.types.Command;
41 import org.openhab.core.types.RefreshType;
42 import org.openhab.core.types.State;
43 import org.slf4j.Logger;
44 import org.slf4j.LoggerFactory;
45 import org.w3c.dom.CharacterData;
46 import org.w3c.dom.Document;
47 import org.w3c.dom.Element;
48 import org.w3c.dom.Node;
49 import org.w3c.dom.NodeList;
50 import org.xml.sax.InputSource;
51
52 /**
53  * The {@link WemoMakerHandler} is responsible for handling commands, which are
54  * sent to one of the channels and to update their states.
55  *
56  * @author Hans-Jörg Merk - Initial contribution
57  */
58
59 public class WemoMakerHandler extends AbstractWemoHandler implements UpnpIOParticipant {
60
61     private final Logger logger = LoggerFactory.getLogger(WemoMakerHandler.class);
62
63     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_MAKER);
64
65     private UpnpIOService service;
66
67     /**
68      * The default refresh interval in Seconds.
69      */
70     private final int DEFAULT_REFRESH_INTERVAL = 15;
71
72     private ScheduledFuture<?> refreshJob;
73
74     private final Runnable refreshRunnable = new Runnable() {
75
76         @Override
77         public void run() {
78             try {
79                 updateWemoState();
80             } catch (Exception e) {
81                 logger.debug("Exception during poll", e);
82                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
83             }
84         }
85     };
86
87     public WemoMakerHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemoHttpcaller) {
88         super(thing);
89
90         this.wemoHttpCaller = wemoHttpcaller;
91
92         logger.debug("Creating a WemoMakerHandler for thing '{}'", getThing().getUID());
93
94         if (upnpIOService != null) {
95             this.service = upnpIOService;
96         } else {
97             logger.debug("upnpIOService not set.");
98         }
99     }
100
101     @Override
102     public void initialize() {
103         Configuration configuration = getConfig();
104
105         if (configuration.get("udn") != null) {
106             logger.debug("Initializing WemoMakerHandler for UDN '{}'", configuration.get("udn"));
107             onUpdate();
108             updateStatus(ThingStatus.ONLINE);
109         } else {
110             logger.debug("Cannot initalize WemoMakerHandler. UDN not set.");
111         }
112     }
113
114     @Override
115     public void dispose() {
116         logger.debug("WeMoMakerHandler disposed.");
117
118         if (refreshJob != null && !refreshJob.isCancelled()) {
119             refreshJob.cancel(true);
120             refreshJob = null;
121         }
122     }
123
124     @Override
125     public void handleCommand(ChannelUID channelUID, Command command) {
126         logger.trace("Command '{}' received for channel '{}'", command, channelUID);
127
128         if (command instanceof RefreshType) {
129             try {
130                 updateWemoState();
131             } catch (Exception e) {
132                 logger.debug("Exception during poll", e);
133             }
134         } else if (channelUID.getId().equals(CHANNEL_RELAY)) {
135             if (command instanceof OnOffType) {
136                 try {
137                     String binaryState = null;
138
139                     if (command.equals(OnOffType.ON)) {
140                         binaryState = "1";
141                     } else if (command.equals(OnOffType.OFF)) {
142                         binaryState = "0";
143                     }
144
145                     String soapHeader = "\"urn:Belkin:service:basicevent:1#SetBinaryState\"";
146
147                     String content = "<?xml version=\"1.0\"?>"
148                             + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
149                             + "<s:Body>" + "<u:SetBinaryState xmlns:u=\"urn:Belkin:service:basicevent:1\">"
150                             + "<BinaryState>" + binaryState + "</BinaryState>" + "</u:SetBinaryState>" + "</s:Body>"
151                             + "</s:Envelope>";
152
153                     String wemoURL = getWemoURL("basicevent");
154
155                     if (wemoURL != null) {
156                         @SuppressWarnings("unused")
157                         String wemoCallResponse = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
158                     }
159                 } catch (Exception e) {
160                     logger.error("Failed to send command '{}' for device '{}' ", command, getThing().getUID(), e);
161                 }
162             }
163         }
164     }
165
166     @SuppressWarnings("unused")
167     private synchronized void onSubscription() {
168     }
169
170     @SuppressWarnings("unused")
171     private synchronized void removeSubscription() {
172     }
173
174     private synchronized void onUpdate() {
175         if (service.isRegistered(this)) {
176             if (refreshJob == null || refreshJob.isCancelled()) {
177                 Configuration config = getThing().getConfiguration();
178                 int refreshInterval = DEFAULT_REFRESH_INTERVAL;
179                 Object refreshConfig = config.get("refresh");
180                 if (refreshConfig != null) {
181                     refreshInterval = ((BigDecimal) refreshConfig).intValue();
182                 }
183                 refreshJob = scheduler.scheduleWithFixedDelay(refreshRunnable, 0, refreshInterval, TimeUnit.SECONDS);
184             }
185         }
186     }
187
188     @Override
189     public String getUDN() {
190         return (String) this.getThing().getConfiguration().get(UDN);
191     }
192
193     /**
194      * The {@link updateWemoState} polls the actual state of a WeMo Maker.
195      */
196     @SuppressWarnings("null")
197     protected void updateWemoState() {
198         String action = "GetAttributes";
199         String actionService = "deviceevent";
200
201         String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
202         String content = "<?xml version=\"1.0\"?>"
203                 + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
204                 + "<s:Body>" + "<u:" + action + " xmlns:u=\"urn:Belkin:service:" + actionService + ":1\">" + "</u:"
205                 + action + ">" + "</s:Body>" + "</s:Envelope>";
206
207         try {
208             String wemoURL = getWemoURL(actionService);
209             if (wemoURL != null) {
210                 String wemoCallResponse = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
211                 if (wemoCallResponse != null) {
212                     try {
213                         String stringParser = StringUtils.substringBetween(wemoCallResponse, "<attributeList>",
214                                 "</attributeList>");
215
216                         // Due to Belkins bad response formatting, we need to run this twice.
217                         stringParser = StringEscapeUtils.unescapeXml(stringParser);
218                         stringParser = StringEscapeUtils.unescapeXml(stringParser);
219
220                         logger.trace("Maker response '{}' for device '{}' received", stringParser, getThing().getUID());
221
222                         stringParser = "<data>" + stringParser + "</data>";
223
224                         DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
225                         // see
226                         // https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html
227                         dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
228                         dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
229                         dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
230                         dbf.setXIncludeAware(false);
231                         dbf.setExpandEntityReferences(false);
232                         DocumentBuilder db = dbf.newDocumentBuilder();
233                         InputSource is = new InputSource();
234                         is.setCharacterStream(new StringReader(stringParser));
235
236                         Document doc = db.parse(is);
237                         NodeList nodes = doc.getElementsByTagName("attribute");
238
239                         // iterate the attributes
240                         for (int i = 0; i < nodes.getLength(); i++) {
241                             Element element = (Element) nodes.item(i);
242
243                             NodeList deviceIndex = element.getElementsByTagName("name");
244                             Element line = (Element) deviceIndex.item(0);
245                             String attributeName = getCharacterDataFromElement(line);
246                             logger.trace("attributeName: {}", attributeName);
247
248                             NodeList deviceID = element.getElementsByTagName("value");
249                             line = (Element) deviceID.item(0);
250                             String attributeValue = getCharacterDataFromElement(line);
251                             logger.trace("attributeValue: {}", attributeValue);
252
253                             switch (attributeName) {
254                                 case "Switch":
255                                     State relayState = attributeValue.equals("0") ? OnOffType.OFF : OnOffType.ON;
256                                     if (relayState != null) {
257                                         logger.debug("New relayState '{}' for device '{}' received", relayState,
258                                                 getThing().getUID());
259                                         updateState(CHANNEL_RELAY, relayState);
260                                     }
261                                     break;
262                                 case "Sensor":
263                                     State sensorState = attributeValue.equals("1") ? OnOffType.OFF : OnOffType.ON;
264                                     if (sensorState != null) {
265                                         logger.debug("New sensorState '{}' for device '{}' received", sensorState,
266                                                 getThing().getUID());
267                                         updateState(CHANNEL_SENSOR, sensorState);
268                                     }
269                                     break;
270                             }
271                         }
272                     } catch (Exception e) {
273                         logger.error("Failed to parse attributeList for WeMo Maker '{}'", this.getThing().getUID(), e);
274                     }
275                 }
276             }
277         } catch (Exception e) {
278             logger.error("Failed to get attributes for device '{}'", getThing().getUID(), e);
279         }
280     }
281
282     public String getWemoURL(String actionService) {
283         URL descriptorURL = service.getDescriptorURL(this);
284         String wemoURL = null;
285         if (descriptorURL != null) {
286             String deviceURL = StringUtils.substringBefore(descriptorURL.toString(), "/setup.xml");
287             wemoURL = deviceURL + "/upnp/control/" + actionService + "1";
288             return wemoURL;
289         }
290         return null;
291     }
292
293     public static String getCharacterDataFromElement(Element e) {
294         Node child = e.getFirstChild();
295         if (child instanceof CharacterData) {
296             CharacterData cd = (CharacterData) child;
297             return cd.getData();
298         }
299         return "?";
300     }
301
302     @Override
303     public void onStatusChanged(boolean status) {
304     }
305
306     @Override
307     public void onServiceSubscribed(String service, boolean succeeded) {
308     }
309
310     @Override
311     public void onValueReceived(String variable, String value, String service) {
312     }
313 }