]> git.basschouten.com Git - openhab-addons.git/blob
5586bfe43f4c5554a6127777f8f3ed9872980e82
[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.net.URL;
20 import java.util.Collections;
21 import java.util.Set;
22 import java.util.concurrent.ScheduledFuture;
23 import java.util.concurrent.TimeUnit;
24
25 import javax.xml.parsers.DocumentBuilder;
26 import javax.xml.parsers.DocumentBuilderFactory;
27
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;
49
50 /**
51  * The {@link WemoMakerHandler} is responsible for handling commands, which are
52  * sent to one of the channels and to update their states.
53  *
54  * @author Hans-Jörg Merk - Initial contribution
55  */
56 @NonNullByDefault
57 public class WemoMakerHandler extends AbstractWemoHandler implements UpnpIOParticipant {
58
59     private final Logger logger = LoggerFactory.getLogger(WemoMakerHandler.class);
60
61     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_MAKER);
62
63     private final Object jobLock = new Object();
64
65     private @Nullable UpnpIOService service;
66
67     private WemoHttpCall wemoCall;
68
69     private String host = "";
70
71     private @Nullable ScheduledFuture<?> pollingJob;
72
73     public WemoMakerHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemoHttpcaller) {
74         super(thing, wemoHttpcaller);
75
76         this.service = upnpIOService;
77         this.wemoCall = wemoHttpcaller;
78
79         logger.debug("Creating a WemoMakerHandler for thing '{}'", getThing().getUID());
80     }
81
82     @Override
83     public void initialize() {
84         Configuration configuration = getConfig();
85
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);
91             }
92             host = getHost();
93             pollingJob = scheduler.scheduleWithFixedDelay(this::poll, 0, DEFAULT_REFRESH_INTERVALL_SECONDS,
94                     TimeUnit.SECONDS);
95             updateStatus(ThingStatus.ONLINE);
96         } else {
97             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
98                     "@text/config-status.error.missing-udn");
99             logger.debug("Cannot initalize WemoMakerHandler. UDN not set.");
100         }
101     }
102
103     @Override
104     public void dispose() {
105         logger.debug("WeMoMakerHandler disposed.");
106
107         ScheduledFuture<?> job = this.pollingJob;
108         if (job != null && !job.isCancelled()) {
109             job.cancel(true);
110         }
111         this.pollingJob = null;
112         UpnpIOService localService = service;
113         if (localService != null) {
114             localService.unregisterParticipant(this);
115         }
116     }
117
118     private void poll() {
119         synchronized (jobLock) {
120             if (pollingJob == null) {
121                 return;
122             }
123             try {
124                 logger.debug("Polling job");
125                 host = getHost();
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() + "\"]");
132                     return;
133                 }
134                 updateStatus(ThingStatus.ONLINE);
135                 updateWemoState();
136             } catch (Exception e) {
137                 logger.debug("Exception during poll: {}", e.getMessage(), e);
138             }
139         }
140     }
141
142     @Override
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");
150             return;
151         }
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");
158             return;
159         }
160         if (command instanceof RefreshType) {
161             try {
162                 updateWemoState();
163             } catch (Exception e) {
164                 logger.debug("Exception during poll", e);
165             }
166         } else if (channelUID.getId().equals(CHANNEL_RELAY)) {
167             if (command instanceof OnOffType) {
168                 try {
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());
179                     }
180                 } catch (Exception e) {
181                     logger.error("Failed to send command '{}' for device '{}' ", command, getThing().getUID(), e);
182                 }
183             }
184         }
185     }
186
187     private boolean isUpnpDeviceRegistered() {
188         UpnpIOService localService = service;
189         if (localService != null) {
190             return localService.isRegistered(this);
191         }
192         return false;
193     }
194
195     @Override
196     public String getUDN() {
197         return (String) this.getThing().getConfiguration().get(UDN);
198     }
199
200     /**
201      * The {@link updateWemoState} polls the actual state of a WeMo Maker.
202      */
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");
209             return;
210         }
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");
217             return;
218         }
219         try {
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());
230                 }
231                 try {
232                     String stringParser = substringBetween(wemoCallResponse, "<attributeList>", "</attributeList>");
233                     logger.trace("Escaped Maker response for device '{}' :", getThing().getUID());
234                     logger.trace("'{}'", stringParser);
235
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());
240
241                     stringParser = "<data>" + stringParser + "</data>";
242
243                     DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
244                     // see
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));
254
255                     Document doc = db.parse(is);
256                     NodeList nodes = doc.getElementsByTagName("attribute");
257
258                     // iterate the attributes
259                     for (int i = 0; i < nodes.getLength(); i++) {
260                         Element element = (Element) nodes.item(i);
261
262                         NodeList deviceIndex = element.getElementsByTagName("name");
263                         Element line = (Element) deviceIndex.item(0);
264                         String attributeName = getCharacterDataFromElement(line);
265                         logger.trace("attributeName: {}", attributeName);
266
267                         NodeList deviceID = element.getElementsByTagName("value");
268                         line = (Element) deviceID.item(0);
269                         String attributeValue = getCharacterDataFromElement(line);
270                         logger.trace("attributeValue: {}", attributeValue);
271
272                         switch (attributeName) {
273                             case "Switch":
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);
278                                 break;
279                             case "Sensor":
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);
284                                 break;
285                         }
286                     }
287                 } catch (Exception e) {
288                     logger.error("Failed to parse attributeList for WeMo Maker '{}'", this.getThing().getUID(), e);
289                 }
290             }
291         } catch (Exception e) {
292             logger.error("Failed to get attributes for device '{}'", getThing().getUID(), e);
293         }
294     }
295
296     public String getHost() {
297         String localHost = host;
298         if (!localHost.isEmpty()) {
299             return localHost;
300         }
301         UpnpIOService localService = service;
302         if (localService != null) {
303             URL descriptorURL = localService.getDescriptorURL(this);
304             if (descriptorURL != null) {
305                 return descriptorURL.getHost();
306             }
307         }
308         return "";
309     }
310
311     @Override
312     public void onStatusChanged(boolean status) {
313     }
314
315     @Override
316     public void onServiceSubscribed(@Nullable String service, boolean succeeded) {
317     }
318
319     @Override
320     public void onValueReceived(@Nullable String variable, @Nullable String value, @Nullable String service) {
321     }
322 }