2 * Copyright (c) 2010-2022 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.wemo.internal.handler;
15 import static org.openhab.binding.wemo.internal.WemoBindingConstants.*;
16 import static org.openhab.binding.wemo.internal.WemoUtil.*;
18 import java.io.StringReader;
19 import java.math.BigDecimal;
21 import java.util.Collections;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
26 import javax.xml.parsers.DocumentBuilder;
27 import javax.xml.parsers.DocumentBuilderFactory;
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;
54 * The {@link WemoMakerHandler} is responsible for handling commands, which are
55 * sent to one of the channels and to update their states.
57 * @author Hans-Jörg Merk - Initial contribution
60 public class WemoMakerHandler extends AbstractWemoHandler implements UpnpIOParticipant {
62 private final Logger logger = LoggerFactory.getLogger(WemoMakerHandler.class);
64 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_MAKER);
66 private UpnpIOService service;
67 private WemoHttpCall wemoCall;
69 private @Nullable ScheduledFuture<?> refreshJob;
71 private final Runnable refreshRunnable = new Runnable() {
77 } catch (Exception e) {
78 logger.debug("Exception during poll", e);
79 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
84 public WemoMakerHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemoHttpcaller) {
85 super(thing, wemoHttpcaller);
87 this.service = upnpIOService;
88 this.wemoCall = wemoHttpcaller;
90 logger.debug("Creating a WemoMakerHandler for thing '{}'", getThing().getUID());
94 public void initialize() {
95 Configuration configuration = getConfig();
97 if (configuration.get("udn") != null) {
98 logger.debug("Initializing WemoMakerHandler for UDN '{}'", configuration.get("udn"));
100 updateStatus(ThingStatus.ONLINE);
102 logger.debug("Cannot initalize WemoMakerHandler. UDN not set.");
107 public void dispose() {
108 logger.debug("WeMoMakerHandler disposed.");
110 ScheduledFuture<?> job = refreshJob;
111 if (job != null && !job.isCancelled()) {
118 public void handleCommand(ChannelUID channelUID, Command command) {
119 logger.trace("Command '{}' received for channel '{}'", command, channelUID);
121 if (command instanceof RefreshType) {
124 } catch (Exception e) {
125 logger.debug("Exception during poll", e);
127 } else if (channelUID.getId().equals(CHANNEL_RELAY)) {
128 if (command instanceof OnOffType) {
130 String binaryState = null;
132 if (command.equals(OnOffType.ON)) {
134 } else if (command.equals(OnOffType.OFF)) {
138 String soapHeader = "\"urn:Belkin:service:basicevent:1#SetBinaryState\"";
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>"
146 URL descriptorURL = service.getDescriptorURL(this);
147 String wemoURL = getWemoURL(descriptorURL, "basicevent");
149 if (wemoURL != null) {
150 wemoCall.executeCall(wemoURL, soapHeader, content);
152 } catch (Exception e) {
153 logger.error("Failed to send command '{}' for device '{}' ", command, getThing().getUID(), e);
159 @SuppressWarnings("unused")
160 private synchronized void onSubscription() {
163 @SuppressWarnings("unused")
164 private synchronized void removeSubscription() {
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();
176 refreshJob = scheduler.scheduleWithFixedDelay(refreshRunnable, 0, refreshInterval, TimeUnit.SECONDS);
181 public String getUDN() {
182 return (String) this.getThing().getConfiguration().get(UDN);
186 * The {@link updateWemoState} polls the actual state of a WeMo Maker.
188 protected void updateWemoState() {
189 String action = "GetAttributes";
190 String actionService = "deviceevent";
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>";
199 URL descriptorURL = service.getDescriptorURL(this);
200 String wemoURL = getWemoURL(descriptorURL, actionService);
202 if (wemoURL != null) {
203 String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
204 if (wemoCallResponse != null) {
206 String stringParser = substringBetween(wemoCallResponse, "<attributeList>", "</attributeList>");
207 logger.trace("Escaped Maker response for device '{}' :", getThing().getUID());
208 logger.trace("'{}'", stringParser);
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());
215 stringParser = "<data>" + stringParser + "</data>";
217 DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
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));
229 Document doc = db.parse(is);
230 NodeList nodes = doc.getElementsByTagName("attribute");
232 // iterate the attributes
233 for (int i = 0; i < nodes.getLength(); i++) {
234 Element element = (Element) nodes.item(i);
236 NodeList deviceIndex = element.getElementsByTagName("name");
237 Element line = (Element) deviceIndex.item(0);
238 String attributeName = getCharacterDataFromElement(line);
239 logger.trace("attributeName: {}", attributeName);
241 NodeList deviceID = element.getElementsByTagName("value");
242 line = (Element) deviceID.item(0);
243 String attributeValue = getCharacterDataFromElement(line);
244 logger.trace("attributeValue: {}", attributeValue);
246 switch (attributeName) {
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);
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);
261 } catch (Exception e) {
262 logger.error("Failed to parse attributeList for WeMo Maker '{}'", this.getThing().getUID(), e);
266 } catch (Exception e) {
267 logger.error("Failed to get attributes for device '{}'", getThing().getUID(), e);
271 public static String getCharacterDataFromElement(Element e) {
272 Node child = e.getFirstChild();
273 if (child instanceof CharacterData) {
274 CharacterData cd = (CharacterData) child;
281 public void onStatusChanged(boolean status) {
285 public void onServiceSubscribed(@Nullable String service, boolean succeeded) {
289 public void onValueReceived(@Nullable String variable, @Nullable String value, @Nullable String service) {