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.util.Collections;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
24 import javax.xml.parsers.DocumentBuilder;
25 import javax.xml.parsers.DocumentBuilderFactory;
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.openhab.binding.wemo.internal.http.WemoHttpCall;
30 import org.openhab.core.config.core.Configuration;
31 import org.openhab.core.io.transport.upnp.UpnpIOService;
32 import org.openhab.core.library.types.OnOffType;
33 import org.openhab.core.thing.ChannelUID;
34 import org.openhab.core.thing.Thing;
35 import org.openhab.core.thing.ThingStatus;
36 import org.openhab.core.thing.ThingStatusDetail;
37 import org.openhab.core.thing.ThingTypeUID;
38 import org.openhab.core.types.Command;
39 import org.openhab.core.types.RefreshType;
40 import org.openhab.core.types.State;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
43 import org.w3c.dom.Document;
44 import org.w3c.dom.Element;
45 import org.w3c.dom.NodeList;
46 import org.xml.sax.InputSource;
49 * The {@link WemoMakerHandler} is responsible for handling commands, which are
50 * sent to one of the channels and to update their states.
52 * @author Hans-Jörg Merk - Initial contribution
55 public class WemoMakerHandler extends WemoBaseThingHandler {
57 private final Logger logger = LoggerFactory.getLogger(WemoMakerHandler.class);
59 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_MAKER);
61 private final Object jobLock = new Object();
63 private @Nullable ScheduledFuture<?> pollingJob;
65 public WemoMakerHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemoHttpcaller) {
66 super(thing, upnpIOService, wemoHttpcaller);
68 logger.debug("Creating a WemoMakerHandler for thing '{}'", getThing().getUID());
72 public void initialize() {
74 Configuration configuration = getConfig();
76 if (configuration.get(UDN) != null) {
77 logger.debug("Initializing WemoMakerHandler for UDN '{}'", configuration.get(UDN));
78 pollingJob = scheduler.scheduleWithFixedDelay(this::poll, 0, DEFAULT_REFRESH_INTERVAL_SECONDS,
80 updateStatus(ThingStatus.ONLINE);
82 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
83 "@text/config-status.error.missing-udn");
84 logger.debug("Cannot initalize WemoMakerHandler. UDN not set.");
89 public void dispose() {
90 logger.debug("WemoMakerHandler disposed.");
92 ScheduledFuture<?> job = this.pollingJob;
93 if (job != null && !job.isCancelled()) {
96 this.pollingJob = null;
100 private void poll() {
101 synchronized (jobLock) {
102 if (pollingJob == null) {
106 logger.debug("Polling job");
107 // Check if the Wemo device is set in the UPnP service registry
108 // If not, set the thing state to ONLINE/CONFIG-PENDING and wait for the next poll
109 if (!isUpnpDeviceRegistered()) {
110 logger.debug("UPnP device {} not yet registered", getUDN());
111 updateStatus(ThingStatus.ONLINE, ThingStatusDetail.CONFIGURATION_PENDING,
112 "@text/config-status.pending.device-not-registered [\"" + getUDN() + "\"]");
116 } catch (Exception e) {
117 logger.debug("Exception during poll: {}", e.getMessage(), e);
123 public void handleCommand(ChannelUID channelUID, Command command) {
124 String wemoURL = getWemoURL(BASICACTION);
125 if (wemoURL == null) {
126 logger.debug("Failed to send command '{}' for device '{}': URL cannot be created", command,
127 getThing().getUID());
130 if (command instanceof RefreshType) {
133 } catch (Exception e) {
134 logger.debug("Exception during poll", e);
136 } else if (channelUID.getId().equals(CHANNEL_RELAY)) {
137 if (command instanceof OnOffType) {
139 boolean binaryState = OnOffType.ON.equals(command) ? true : false;
140 String soapHeader = "\"urn:Belkin:service:basicevent:1#SetBinaryState\"";
141 String content = createBinaryStateContent(binaryState);
142 wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
143 updateStatus(ThingStatus.ONLINE);
144 } catch (Exception e) {
145 logger.warn("Failed to send command '{}' for device '{}' ", command, getThing().getUID(), e);
146 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
153 * The {@link updateWemoState} polls the actual state of a WeMo Maker.
155 protected void updateWemoState() {
156 String actionService = DEVICEACTION;
157 String wemoURL = getWemoURL(actionService);
158 if (wemoURL == null) {
159 logger.debug("Failed to get actual state for device '{}': URL cannot be created", getThing().getUID());
163 String action = "GetAttributes";
164 String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
165 String content = createStateRequestContent(action, actionService);
166 String wemoCallResponse = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
168 String stringParser = substringBetween(wemoCallResponse, "<attributeList>", "</attributeList>");
169 logger.trace("Escaped Maker response for device '{}' :", getThing().getUID());
170 logger.trace("'{}'", stringParser);
172 // Due to Belkins bad response formatting, we need to run this twice.
173 stringParser = unescapeXml(stringParser);
174 stringParser = unescapeXml(stringParser);
175 logger.trace("Maker response '{}' for device '{}' received", stringParser, getThing().getUID());
177 stringParser = "<data>" + stringParser + "</data>";
179 DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
181 // https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html
182 dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
183 dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
184 dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
185 dbf.setXIncludeAware(false);
186 dbf.setExpandEntityReferences(false);
187 DocumentBuilder db = dbf.newDocumentBuilder();
188 InputSource is = new InputSource();
189 is.setCharacterStream(new StringReader(stringParser));
191 Document doc = db.parse(is);
192 NodeList nodes = doc.getElementsByTagName("attribute");
194 // iterate the attributes
195 for (int i = 0; i < nodes.getLength(); i++) {
196 Element element = (Element) nodes.item(i);
198 NodeList deviceIndex = element.getElementsByTagName("name");
199 Element line = (Element) deviceIndex.item(0);
200 String attributeName = getCharacterDataFromElement(line);
201 logger.trace("attributeName: {}", attributeName);
203 NodeList deviceID = element.getElementsByTagName("value");
204 line = (Element) deviceID.item(0);
205 String attributeValue = getCharacterDataFromElement(line);
206 logger.trace("attributeValue: {}", attributeValue);
208 switch (attributeName) {
210 State relayState = "0".equals(attributeValue) ? OnOffType.OFF : OnOffType.ON;
211 logger.debug("New relayState '{}' for device '{}' received", relayState,
212 getThing().getUID());
213 updateState(CHANNEL_RELAY, relayState);
216 State sensorState = "1".equals(attributeValue) ? OnOffType.OFF : OnOffType.ON;
217 logger.debug("New sensorState '{}' for device '{}' received", sensorState,
218 getThing().getUID());
219 updateState(CHANNEL_SENSOR, sensorState);
223 updateStatus(ThingStatus.ONLINE);
224 } catch (Exception e) {
225 logger.warn("Failed to parse attributeList for WeMo Maker '{}'", this.getThing().getUID(), e);
227 } catch (Exception e) {
228 logger.warn("Failed to get attributes for device '{}'", getThing().getUID(), e);
229 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());