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