]> git.basschouten.com Git - openhab-addons.git/blob
97ecfc36209b12293dd0edd9478b7e62cb280a9e
[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                         DocumentBuilder db = dbf.newDocumentBuilder();
226                         InputSource is = new InputSource();
227                         is.setCharacterStream(new StringReader(stringParser));
228
229                         Document doc = db.parse(is);
230                         NodeList nodes = doc.getElementsByTagName("attribute");
231
232                         // iterate the attributes
233                         for (int i = 0; i < nodes.getLength(); i++) {
234                             Element element = (Element) nodes.item(i);
235
236                             NodeList deviceIndex = element.getElementsByTagName("name");
237                             Element line = (Element) deviceIndex.item(0);
238                             String attributeName = getCharacterDataFromElement(line);
239                             logger.trace("attributeName: {}", attributeName);
240
241                             NodeList deviceID = element.getElementsByTagName("value");
242                             line = (Element) deviceID.item(0);
243                             String attributeValue = getCharacterDataFromElement(line);
244                             logger.trace("attributeValue: {}", attributeValue);
245
246                             switch (attributeName) {
247                                 case "Switch":
248                                     State relayState = attributeValue.equals("0") ? OnOffType.OFF : OnOffType.ON;
249                                     if (relayState != null) {
250                                         logger.debug("New relayState '{}' for device '{}' received", relayState,
251                                                 getThing().getUID());
252                                         updateState(CHANNEL_RELAY, relayState);
253                                     }
254                                     break;
255                                 case "Sensor":
256                                     State sensorState = attributeValue.equals("1") ? OnOffType.OFF : OnOffType.ON;
257                                     if (sensorState != null) {
258                                         logger.debug("New sensorState '{}' for device '{}' received", sensorState,
259                                                 getThing().getUID());
260                                         updateState(CHANNEL_SENSOR, sensorState);
261                                     }
262                                     break;
263                             }
264                         }
265                     } catch (Exception e) {
266                         logger.error("Failed to parse attributeList for WeMo Maker '{}'", this.getThing().getUID(), e);
267                     }
268                 }
269             }
270         } catch (Exception e) {
271             logger.error("Failed to get attributes for device '{}'", getThing().getUID(), e);
272         }
273     }
274
275     public String getWemoURL(String actionService) {
276         URL descriptorURL = service.getDescriptorURL(this);
277         String wemoURL = null;
278         if (descriptorURL != null) {
279             String deviceURL = StringUtils.substringBefore(descriptorURL.toString(), "/setup.xml");
280             wemoURL = deviceURL + "/upnp/control/" + actionService + "1";
281             return wemoURL;
282         }
283         return null;
284     }
285
286     public static String getCharacterDataFromElement(Element e) {
287         Node child = e.getFirstChild();
288         if (child instanceof CharacterData) {
289             CharacterData cd = (CharacterData) child;
290             return cd.getData();
291         }
292         return "?";
293     }
294
295     @Override
296     public void onStatusChanged(boolean status) {
297     }
298
299     @Override
300     public void onServiceSubscribed(String service, boolean succeeded) {
301     }
302
303     @Override
304     public void onValueReceived(String variable, String value, String service) {
305     }
306 }