]> git.basschouten.com Git - openhab-addons.git/blob
2bfb66deda81448d9335817b5d9f6908da5b7a45
[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.util.Collections;
20 import java.util.Set;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
23
24 import javax.xml.parsers.DocumentBuilder;
25 import javax.xml.parsers.DocumentBuilderFactory;
26
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;
47
48 /**
49  * The {@link WemoMakerHandler} is responsible for handling commands, which are
50  * sent to one of the channels and to update their states.
51  *
52  * @author Hans-Jörg Merk - Initial contribution
53  */
54 @NonNullByDefault
55 public class WemoMakerHandler extends WemoBaseThingHandler {
56
57     private final Logger logger = LoggerFactory.getLogger(WemoMakerHandler.class);
58
59     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_MAKER);
60
61     private final Object jobLock = new Object();
62
63     private @Nullable ScheduledFuture<?> pollingJob;
64
65     public WemoMakerHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemoHttpcaller) {
66         super(thing, upnpIOService, wemoHttpcaller);
67
68         logger.debug("Creating a WemoMakerHandler for thing '{}'", getThing().getUID());
69     }
70
71     @Override
72     public void initialize() {
73         Configuration configuration = getConfig();
74
75         if (configuration.get(UDN) != null) {
76             logger.debug("Initializing WemoMakerHandler for UDN '{}'", configuration.get(UDN));
77             UpnpIOService localService = service;
78             if (localService != null) {
79                 localService.registerParticipant(this);
80             }
81             host = getHost();
82             pollingJob = scheduler.scheduleWithFixedDelay(this::poll, 0, DEFAULT_REFRESH_INTERVAL_SECONDS,
83                     TimeUnit.SECONDS);
84             updateStatus(ThingStatus.ONLINE);
85         } else {
86             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
87                     "@text/config-status.error.missing-udn");
88             logger.debug("Cannot initalize WemoMakerHandler. UDN not set.");
89         }
90     }
91
92     @Override
93     public void dispose() {
94         logger.debug("WeMoMakerHandler disposed.");
95
96         ScheduledFuture<?> job = this.pollingJob;
97         if (job != null && !job.isCancelled()) {
98             job.cancel(true);
99         }
100         this.pollingJob = null;
101         UpnpIOService localService = service;
102         if (localService != null) {
103             localService.unregisterParticipant(this);
104         }
105     }
106
107     private void poll() {
108         synchronized (jobLock) {
109             if (pollingJob == null) {
110                 return;
111             }
112             try {
113                 logger.debug("Polling job");
114                 host = getHost();
115                 // Check if the Wemo device is set in the UPnP service registry
116                 // If not, set the thing state to ONLINE/CONFIG-PENDING and wait for the next poll
117                 if (!isUpnpDeviceRegistered()) {
118                     logger.debug("UPnP device {} not yet registered", getUDN());
119                     updateStatus(ThingStatus.ONLINE, ThingStatusDetail.CONFIGURATION_PENDING,
120                             "@text/config-status.pending.device-not-registered [\"" + getUDN() + "\"]");
121                     return;
122                 }
123                 updateStatus(ThingStatus.ONLINE);
124                 updateWemoState();
125             } catch (Exception e) {
126                 logger.debug("Exception during poll: {}", e.getMessage(), e);
127             }
128         }
129     }
130
131     @Override
132     public void handleCommand(ChannelUID channelUID, Command command) {
133         String localHost = getHost();
134         if (localHost.isEmpty()) {
135             logger.error("Failed to send command '{}' for device '{}': IP address missing", command,
136                     getThing().getUID());
137             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
138                     "@text/config-status.error.missing-ip");
139             return;
140         }
141         String wemoURL = getWemoURL(localHost, BASICACTION);
142         if (wemoURL == null) {
143             logger.error("Failed to send command '{}' for device '{}': URL cannot be created", command,
144                     getThing().getUID());
145             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
146                     "@text/config-status.error.missing-url");
147             return;
148         }
149         if (command instanceof RefreshType) {
150             try {
151                 updateWemoState();
152             } catch (Exception e) {
153                 logger.debug("Exception during poll", e);
154             }
155         } else if (channelUID.getId().equals(CHANNEL_RELAY)) {
156             if (command instanceof OnOffType) {
157                 try {
158                     boolean binaryState = OnOffType.ON.equals(command) ? true : false;
159                     String soapHeader = "\"urn:Belkin:service:basicevent:1#SetBinaryState\"";
160                     String content = createBinaryStateContent(binaryState);
161                     String wemoCallResponse = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
162                     if (wemoCallResponse != null && logger.isTraceEnabled()) {
163                         logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
164                         logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader, getThing().getUID());
165                         logger.trace("wemoCall with content '{}' for device '{}'", content, getThing().getUID());
166                         logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse,
167                                 getThing().getUID());
168                     }
169                 } catch (Exception e) {
170                     logger.error("Failed to send command '{}' for device '{}' ", command, getThing().getUID(), e);
171                 }
172             }
173         }
174     }
175
176     /**
177      * The {@link updateWemoState} polls the actual state of a WeMo Maker.
178      */
179     protected void updateWemoState() {
180         String localHost = getHost();
181         if (localHost.isEmpty()) {
182             logger.error("Failed to get actual state for device '{}': IP address missing", getThing().getUID());
183             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
184                     "@text/config-status.error.missing-ip");
185             return;
186         }
187         String actionService = DEVICEACTION;
188         String wemoURL = getWemoURL(localHost, actionService);
189         if (wemoURL == null) {
190             logger.error("Failed to get actual state for device '{}': URL cannot be created", getThing().getUID());
191             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
192                     "@text/config-status.error.missing-url");
193             return;
194         }
195         try {
196             String action = "GetAttributes";
197             String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
198             String content = createStateRequestContent(action, actionService);
199             String wemoCallResponse = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
200             if (wemoCallResponse != null) {
201                 if (logger.isTraceEnabled()) {
202                     logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
203                     logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader, getThing().getUID());
204                     logger.trace("wemoCall with content '{}' for device '{}'", content, getThing().getUID());
205                     logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse, getThing().getUID());
206                 }
207                 try {
208                     String stringParser = substringBetween(wemoCallResponse, "<attributeList>", "</attributeList>");
209                     logger.trace("Escaped Maker response for device '{}' :", getThing().getUID());
210                     logger.trace("'{}'", stringParser);
211
212                     // Due to Belkins bad response formatting, we need to run this twice.
213                     stringParser = unescapeXml(stringParser);
214                     stringParser = unescapeXml(stringParser);
215                     logger.trace("Maker response '{}' for device '{}' received", stringParser, getThing().getUID());
216
217                     stringParser = "<data>" + stringParser + "</data>";
218
219                     DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
220                     // see
221                     // https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html
222                     dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
223                     dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
224                     dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
225                     dbf.setXIncludeAware(false);
226                     dbf.setExpandEntityReferences(false);
227                     DocumentBuilder db = dbf.newDocumentBuilder();
228                     InputSource is = new InputSource();
229                     is.setCharacterStream(new StringReader(stringParser));
230
231                     Document doc = db.parse(is);
232                     NodeList nodes = doc.getElementsByTagName("attribute");
233
234                     // iterate the attributes
235                     for (int i = 0; i < nodes.getLength(); i++) {
236                         Element element = (Element) nodes.item(i);
237
238                         NodeList deviceIndex = element.getElementsByTagName("name");
239                         Element line = (Element) deviceIndex.item(0);
240                         String attributeName = getCharacterDataFromElement(line);
241                         logger.trace("attributeName: {}", attributeName);
242
243                         NodeList deviceID = element.getElementsByTagName("value");
244                         line = (Element) deviceID.item(0);
245                         String attributeValue = getCharacterDataFromElement(line);
246                         logger.trace("attributeValue: {}", attributeValue);
247
248                         switch (attributeName) {
249                             case "Switch":
250                                 State relayState = "0".equals(attributeValue) ? OnOffType.OFF : OnOffType.ON;
251                                 logger.debug("New relayState '{}' for device '{}' received", relayState,
252                                         getThing().getUID());
253                                 updateState(CHANNEL_RELAY, relayState);
254                                 break;
255                             case "Sensor":
256                                 State sensorState = "1".equals(attributeValue) ? OnOffType.OFF : OnOffType.ON;
257                                 logger.debug("New sensorState '{}' for device '{}' received", sensorState,
258                                         getThing().getUID());
259                                 updateState(CHANNEL_SENSOR, sensorState);
260                                 break;
261                         }
262                     }
263                 } catch (Exception e) {
264                     logger.error("Failed to parse attributeList for WeMo Maker '{}'", this.getThing().getUID(), e);
265                 }
266             }
267         } catch (Exception e) {
268             logger.error("Failed to get attributes for device '{}'", getThing().getUID(), e);
269         }
270     }
271 }