]> git.basschouten.com Git - openhab-addons.git/blob
1f29f9d9d34b9488b410befac73ad89ac77b9f1f
[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.jupnp.UpnpService;
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.UpnpIOService;
33 import org.openhab.core.library.types.OnOffType;
34 import org.openhab.core.thing.ChannelUID;
35 import org.openhab.core.thing.Thing;
36 import org.openhab.core.thing.ThingStatus;
37 import org.openhab.core.thing.ThingStatusDetail;
38 import org.openhab.core.thing.ThingTypeUID;
39 import org.openhab.core.types.Command;
40 import org.openhab.core.types.RefreshType;
41 import org.openhab.core.types.State;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
44 import org.w3c.dom.Document;
45 import org.w3c.dom.Element;
46 import org.w3c.dom.NodeList;
47 import org.xml.sax.InputSource;
48
49 /**
50  * The {@link WemoMakerHandler} is responsible for handling commands, which are
51  * sent to one of the channels and to update their states.
52  *
53  * @author Hans-Jörg Merk - Initial contribution
54  */
55 @NonNullByDefault
56 public class WemoMakerHandler extends WemoBaseThingHandler {
57
58     private final Logger logger = LoggerFactory.getLogger(WemoMakerHandler.class);
59
60     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_MAKER);
61
62     private final Object jobLock = new Object();
63
64     private @Nullable ScheduledFuture<?> pollingJob;
65
66     public WemoMakerHandler(Thing thing, UpnpIOService upnpIOService, UpnpService upnpService,
67             WemoHttpCall wemoHttpcaller) {
68         super(thing, upnpIOService, upnpService, wemoHttpcaller);
69
70         logger.debug("Creating a WemoMakerHandler for thing '{}'", getThing().getUID());
71     }
72
73     @Override
74     public void initialize() {
75         super.initialize();
76         Configuration configuration = getConfig();
77
78         if (configuration.get(UDN) != null) {
79             logger.debug("Initializing WemoMakerHandler for UDN '{}'", configuration.get(UDN));
80             pollingJob = scheduler.scheduleWithFixedDelay(this::poll, 0, DEFAULT_REFRESH_INTERVAL_SECONDS,
81                     TimeUnit.SECONDS);
82             updateStatus(ThingStatus.UNKNOWN);
83         } else {
84             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
85                     "@text/config-status.error.missing-udn");
86         }
87     }
88
89     @Override
90     public void dispose() {
91         logger.debug("WemoMakerHandler disposed.");
92
93         ScheduledFuture<?> job = this.pollingJob;
94         if (job != null && !job.isCancelled()) {
95             job.cancel(true);
96         }
97         this.pollingJob = null;
98         super.dispose();
99     }
100
101     private void poll() {
102         synchronized (jobLock) {
103             if (pollingJob == null) {
104                 return;
105             }
106             try {
107                 logger.debug("Polling job");
108                 // Check if the Wemo device is set in the UPnP service registry
109                 if (!isUpnpDeviceRegistered()) {
110                     logger.debug("UPnP device {} not yet registered", getUDN());
111                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
112                             "@text/config-status.pending.device-not-registered [\"" + getUDN() + "\"]");
113                     return;
114                 }
115                 updateWemoState();
116             } catch (Exception e) {
117                 logger.debug("Exception during poll: {}", e.getMessage(), e);
118             }
119         }
120     }
121
122     @Override
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());
128             return;
129         }
130         if (command instanceof RefreshType) {
131             try {
132                 updateWemoState();
133             } catch (Exception e) {
134                 logger.debug("Exception during poll", e);
135             }
136         } else if (channelUID.getId().equals(CHANNEL_RELAY)) {
137             if (command instanceof OnOffType) {
138                 try {
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);
147                 }
148             }
149         }
150     }
151
152     /**
153      * The {@link updateWemoState} polls the actual state of a WeMo Maker.
154      */
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());
160             return;
161         }
162         try {
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);
167             try {
168                 String stringParser = substringBetween(wemoCallResponse, "<attributeList>", "</attributeList>");
169                 logger.trace("Escaped Maker response for device '{}' :", getThing().getUID());
170                 logger.trace("'{}'", stringParser);
171
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());
176
177                 stringParser = "<data>" + stringParser + "</data>";
178
179                 DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
180                 // see
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));
190
191                 Document doc = db.parse(is);
192                 NodeList nodes = doc.getElementsByTagName("attribute");
193
194                 // iterate the attributes
195                 for (int i = 0; i < nodes.getLength(); i++) {
196                     Element element = (Element) nodes.item(i);
197
198                     NodeList deviceIndex = element.getElementsByTagName("name");
199                     Element line = (Element) deviceIndex.item(0);
200                     String attributeName = getCharacterDataFromElement(line);
201                     logger.trace("attributeName: {}", attributeName);
202
203                     NodeList deviceID = element.getElementsByTagName("value");
204                     line = (Element) deviceID.item(0);
205                     String attributeValue = getCharacterDataFromElement(line);
206                     logger.trace("attributeValue: {}", attributeValue);
207
208                     switch (attributeName) {
209                         case "Switch":
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);
214                             break;
215                         case "Sensor":
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);
220                             break;
221                     }
222                 }
223                 updateStatus(ThingStatus.ONLINE);
224             } catch (Exception e) {
225                 logger.warn("Failed to parse attributeList for WeMo Maker '{}'", this.getThing().getUID(), e);
226             }
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());
230         }
231     }
232 }