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;
20 import java.util.Collections;
22 import java.util.concurrent.ScheduledFuture;
23 import java.util.concurrent.TimeUnit;
25 import javax.xml.parsers.DocumentBuilder;
26 import javax.xml.parsers.DocumentBuilderFactory;
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
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.Document;
46 import org.w3c.dom.Element;
47 import org.w3c.dom.NodeList;
48 import org.xml.sax.InputSource;
51 * The {@link WemoMakerHandler} is responsible for handling commands, which are
52 * sent to one of the channels and to update their states.
54 * @author Hans-Jörg Merk - Initial contribution
57 public class WemoMakerHandler extends AbstractWemoHandler implements UpnpIOParticipant {
59 private final Logger logger = LoggerFactory.getLogger(WemoMakerHandler.class);
61 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_MAKER);
63 private final Object jobLock = new Object();
65 private @Nullable UpnpIOService service;
67 private WemoHttpCall wemoCall;
69 private String host = "";
71 private @Nullable ScheduledFuture<?> pollingJob;
73 public WemoMakerHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemoHttpcaller) {
74 super(thing, wemoHttpcaller);
76 this.service = upnpIOService;
77 this.wemoCall = wemoHttpcaller;
79 logger.debug("Creating a WemoMakerHandler for thing '{}'", getThing().getUID());
83 public void initialize() {
84 Configuration configuration = getConfig();
86 if (configuration.get(UDN) != null) {
87 logger.debug("Initializing WemoMakerHandler for UDN '{}'", configuration.get(UDN));
88 UpnpIOService localService = service;
89 if (localService != null) {
90 localService.registerParticipant(this);
93 pollingJob = scheduler.scheduleWithFixedDelay(this::poll, 0, DEFAULT_REFRESH_INTERVALL_SECONDS,
95 updateStatus(ThingStatus.ONLINE);
97 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
98 "@text/config-status.error.missing-udn");
99 logger.debug("Cannot initalize WemoMakerHandler. UDN not set.");
104 public void dispose() {
105 logger.debug("WeMoMakerHandler disposed.");
107 ScheduledFuture<?> job = this.pollingJob;
108 if (job != null && !job.isCancelled()) {
111 this.pollingJob = null;
112 UpnpIOService localService = service;
113 if (localService != null) {
114 localService.unregisterParticipant(this);
118 private void poll() {
119 synchronized (jobLock) {
120 if (pollingJob == null) {
124 logger.debug("Polling job");
126 // Check if the Wemo device is set in the UPnP service registry
127 // If not, set the thing state to ONLINE/CONFIG-PENDING and wait for the next poll
128 if (!isUpnpDeviceRegistered()) {
129 logger.debug("UPnP device {} not yet registered", getUDN());
130 updateStatus(ThingStatus.ONLINE, ThingStatusDetail.CONFIGURATION_PENDING,
131 "@text/config-status.pending.device-not-registered [\"" + getUDN() + "\"]");
134 updateStatus(ThingStatus.ONLINE);
136 } catch (Exception e) {
137 logger.debug("Exception during poll: {}", e.getMessage(), e);
143 public void handleCommand(ChannelUID channelUID, Command command) {
144 String localHost = getHost();
145 if (localHost.isEmpty()) {
146 logger.error("Failed to send command '{}' for device '{}': IP address missing", command,
147 getThing().getUID());
148 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
149 "@text/config-status.error.missing-ip");
152 String wemoURL = getWemoURL(localHost, BASICACTION);
153 if (wemoURL == null) {
154 logger.error("Failed to send command '{}' for device '{}': URL cannot be created", command,
155 getThing().getUID());
156 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
157 "@text/config-status.error.missing-url");
160 if (command instanceof RefreshType) {
163 } catch (Exception e) {
164 logger.debug("Exception during poll", e);
166 } else if (channelUID.getId().equals(CHANNEL_RELAY)) {
167 if (command instanceof OnOffType) {
169 boolean binaryState = OnOffType.ON.equals(command) ? true : false;
170 String soapHeader = "\"urn:Belkin:service:basicevent:1#SetBinaryState\"";
171 String content = createBinaryStateContent(binaryState);
172 String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
173 if (wemoCallResponse != null && logger.isTraceEnabled()) {
174 logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
175 logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader, getThing().getUID());
176 logger.trace("wemoCall with content '{}' for device '{}'", content, getThing().getUID());
177 logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse,
178 getThing().getUID());
180 } catch (Exception e) {
181 logger.error("Failed to send command '{}' for device '{}' ", command, getThing().getUID(), e);
187 private boolean isUpnpDeviceRegistered() {
188 UpnpIOService localService = service;
189 if (localService != null) {
190 return localService.isRegistered(this);
196 public String getUDN() {
197 return (String) this.getThing().getConfiguration().get(UDN);
201 * The {@link updateWemoState} polls the actual state of a WeMo Maker.
203 protected void updateWemoState() {
204 String localHost = getHost();
205 if (localHost.isEmpty()) {
206 logger.error("Failed to get actual state for device '{}': IP address missing", getThing().getUID());
207 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
208 "@text/config-status.error.missing-ip");
211 String actionService = DEVICEACTION;
212 String wemoURL = getWemoURL(localHost, actionService);
213 if (wemoURL == null) {
214 logger.error("Failed to get actual state for device '{}': URL cannot be created", getThing().getUID());
215 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
216 "@text/config-status.error.missing-url");
220 String action = "GetAttributes";
221 String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
222 String content = createStateRequestContent(action, actionService);
223 String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
224 if (wemoCallResponse != null) {
225 if (logger.isTraceEnabled()) {
226 logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
227 logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader, getThing().getUID());
228 logger.trace("wemoCall with content '{}' for device '{}'", content, getThing().getUID());
229 logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse, getThing().getUID());
232 String stringParser = substringBetween(wemoCallResponse, "<attributeList>", "</attributeList>");
233 logger.trace("Escaped Maker response for device '{}' :", getThing().getUID());
234 logger.trace("'{}'", stringParser);
236 // Due to Belkins bad response formatting, we need to run this twice.
237 stringParser = unescapeXml(stringParser);
238 stringParser = unescapeXml(stringParser);
239 logger.trace("Maker response '{}' for device '{}' received", stringParser, getThing().getUID());
241 stringParser = "<data>" + stringParser + "</data>";
243 DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
245 // https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html
246 dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
247 dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
248 dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
249 dbf.setXIncludeAware(false);
250 dbf.setExpandEntityReferences(false);
251 DocumentBuilder db = dbf.newDocumentBuilder();
252 InputSource is = new InputSource();
253 is.setCharacterStream(new StringReader(stringParser));
255 Document doc = db.parse(is);
256 NodeList nodes = doc.getElementsByTagName("attribute");
258 // iterate the attributes
259 for (int i = 0; i < nodes.getLength(); i++) {
260 Element element = (Element) nodes.item(i);
262 NodeList deviceIndex = element.getElementsByTagName("name");
263 Element line = (Element) deviceIndex.item(0);
264 String attributeName = getCharacterDataFromElement(line);
265 logger.trace("attributeName: {}", attributeName);
267 NodeList deviceID = element.getElementsByTagName("value");
268 line = (Element) deviceID.item(0);
269 String attributeValue = getCharacterDataFromElement(line);
270 logger.trace("attributeValue: {}", attributeValue);
272 switch (attributeName) {
274 State relayState = "0".equals(attributeValue) ? OnOffType.OFF : OnOffType.ON;
275 logger.debug("New relayState '{}' for device '{}' received", relayState,
276 getThing().getUID());
277 updateState(CHANNEL_RELAY, relayState);
280 State sensorState = "1".equals(attributeValue) ? OnOffType.OFF : OnOffType.ON;
281 logger.debug("New sensorState '{}' for device '{}' received", sensorState,
282 getThing().getUID());
283 updateState(CHANNEL_SENSOR, sensorState);
287 } catch (Exception e) {
288 logger.error("Failed to parse attributeList for WeMo Maker '{}'", this.getThing().getUID(), e);
291 } catch (Exception e) {
292 logger.error("Failed to get attributes for device '{}'", getThing().getUID(), e);
296 public String getHost() {
297 String localHost = host;
298 if (!localHost.isEmpty()) {
301 UpnpIOService localService = service;
302 if (localService != null) {
303 URL descriptorURL = localService.getDescriptorURL(this);
304 if (descriptorURL != null) {
305 return descriptorURL.getHost();
312 public void onStatusChanged(boolean status) {
316 public void onServiceSubscribed(@Nullable String service, boolean succeeded) {
320 public void onValueReceived(@Nullable String variable, @Nullable String value, @Nullable String service) {