]> git.basschouten.com Git - openhab-addons.git/blob
0017e9395a4b994bae0aa07a76ed5705d4390602
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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
17 import java.io.IOException;
18 import java.io.StringReader;
19 import java.math.BigDecimal;
20 import java.net.URL;
21 import java.util.Collections;
22 import java.util.HashMap;
23 import java.util.Map;
24 import java.util.Set;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
27
28 import javax.xml.parsers.DocumentBuilder;
29 import javax.xml.parsers.DocumentBuilderFactory;
30 import javax.xml.parsers.ParserConfigurationException;
31
32 import org.apache.commons.lang3.StringEscapeUtils;
33 import org.apache.commons.lang3.StringUtils;
34 import org.openhab.binding.wemo.internal.http.WemoHttpCall;
35 import org.openhab.core.config.core.Configuration;
36 import org.openhab.core.io.transport.upnp.UpnpIOParticipant;
37 import org.openhab.core.io.transport.upnp.UpnpIOService;
38 import org.openhab.core.library.types.OnOffType;
39 import org.openhab.core.library.types.PercentType;
40 import org.openhab.core.library.types.StringType;
41 import org.openhab.core.thing.ChannelUID;
42 import org.openhab.core.thing.Thing;
43 import org.openhab.core.thing.ThingStatus;
44 import org.openhab.core.thing.ThingStatusDetail;
45 import org.openhab.core.thing.ThingTypeUID;
46 import org.openhab.core.types.Command;
47 import org.openhab.core.types.RefreshType;
48 import org.openhab.core.types.State;
49 import org.slf4j.Logger;
50 import org.slf4j.LoggerFactory;
51 import org.w3c.dom.CharacterData;
52 import org.w3c.dom.Document;
53 import org.w3c.dom.Element;
54 import org.w3c.dom.Node;
55 import org.w3c.dom.NodeList;
56 import org.xml.sax.InputSource;
57 import org.xml.sax.SAXException;
58
59 /**
60  * The {@link WemoHolmesHandler} is responsible for handling commands, which are
61  * sent to one of the channels and to update their states.
62  *
63  * @author Hans-Jörg Merk - Initial contribution;
64  */
65
66 public class WemoHolmesHandler extends AbstractWemoHandler implements UpnpIOParticipant {
67
68     private final Logger logger = LoggerFactory.getLogger(WemoHolmesHandler.class);
69
70     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_PURIFIER);
71
72     /**
73      * The default refresh interval in Seconds.
74      */
75     private static final int DEFAULT_REFRESH_INTERVAL_SECONDS = 120;
76     private static final int FILTER_LIFE_DAYS = 330;
77     private static final int FILTER_LIFE_MINS = FILTER_LIFE_DAYS * 24 * 60;
78     private final Map<String, Boolean> subscriptionState = new HashMap<>();
79     private final Map<String, String> stateMap = Collections.synchronizedMap(new HashMap<>());
80
81     private UpnpIOService service;
82
83     private ScheduledFuture<?> refreshJob;
84
85     private final Runnable refreshRunnable = () -> {
86         if (!isUpnpDeviceRegistered()) {
87             logger.debug("WeMo UPnP device {} not yet registered", getUDN());
88         } else {
89             updateWemoState();
90             onSubscription();
91         }
92     };
93
94     public WemoHolmesHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemohttpCaller) {
95         super(thing);
96
97         this.wemoHttpCaller = wemohttpCaller;
98
99         logger.debug("Creating a WemoHolmesHandler for thing '{}'", getThing().getUID());
100
101         if (upnpIOService != null) {
102             this.service = upnpIOService;
103         } else {
104             logger.debug("upnpIOService not set.");
105         }
106     }
107
108     @Override
109     public void initialize() {
110         Configuration configuration = getConfig();
111
112         if (configuration.get("udn") != null) {
113             logger.debug("Initializing WemoHolmesHandler for UDN '{}'", configuration.get("udn"));
114             service.registerParticipant(this);
115             onSubscription();
116             onUpdate();
117             updateStatus(ThingStatus.ONLINE);
118         } else {
119             logger.debug("Cannot initalize WemoHolmesHandler. UDN not set.");
120         }
121     }
122
123     @Override
124     public void dispose() {
125         logger.debug("WemoHolmesHandler disposed.");
126
127         removeSubscription();
128
129         if (refreshJob != null && !refreshJob.isCancelled()) {
130             refreshJob.cancel(true);
131             refreshJob = null;
132         }
133     }
134
135     @Override
136     public void handleCommand(ChannelUID channelUID, Command command) {
137         logger.trace("Command '{}' received for channel '{}'", command, channelUID);
138
139         String attribute = null;
140         String value = null;
141
142         if (command instanceof RefreshType) {
143             updateWemoState();
144         } else if (CHANNEL_PURIFIERMODE.equals(channelUID.getId())) {
145             attribute = "Mode";
146             String commandString = command.toString();
147             switch (commandString) {
148                 case "OFF":
149                     value = "0";
150                     break;
151                 case "LOW":
152                     value = "1";
153                     break;
154                 case "MED":
155                     value = "2";
156                     break;
157                 case "HIGH":
158                     value = "3";
159                     break;
160                 case "AUTO":
161                     value = "4";
162                     break;
163             }
164         } else if (CHANNEL_IONIZER.equals(channelUID.getId())) {
165             attribute = "Ionizer";
166             if (OnOffType.ON.equals(command)) {
167                 value = "1";
168             } else if (OnOffType.OFF.equals(command)) {
169                 value = "0";
170             }
171         } else if (CHANNEL_HUMIDIFIERMODE.equals(channelUID.getId())) {
172             attribute = "FanMode";
173             String commandString = command.toString();
174             switch (commandString) {
175                 case "OFF":
176                     value = "0";
177                     break;
178                 case "MIN":
179                     value = "1";
180                     break;
181                 case "LOW":
182                     value = "2";
183                     break;
184                 case "MED":
185                     value = "3";
186                     break;
187                 case "HIGH":
188                     value = "4";
189                     break;
190                 case "MAX":
191                     value = "5";
192                     break;
193             }
194         } else if (CHANNEL_DESIREDHUMIDITY.equals(channelUID.getId())) {
195             attribute = "DesiredHumidity";
196             String commandString = command.toString();
197             switch (commandString) {
198                 case "45":
199                     value = "0";
200                     break;
201                 case "50":
202                     value = "1";
203                     break;
204                 case "55":
205                     value = "2";
206                     break;
207                 case "60":
208                     value = "3";
209                     break;
210                 case "100":
211                     value = "4";
212                     break;
213             }
214         } else if (CHANNEL_HEATERMODE.equals(channelUID.getId())) {
215             attribute = "Mode";
216             String commandString = command.toString();
217             switch (commandString) {
218                 case "OFF":
219                     value = "0";
220                     break;
221                 case "FROSTPROTECT":
222                     value = "1";
223                     break;
224                 case "HIGH":
225                     value = "2";
226                     break;
227                 case "LOW":
228                     value = "3";
229                     break;
230                 case "ECO":
231                     value = "4";
232                     break;
233             }
234         } else if (CHANNEL_TARGETTEMP.equals(channelUID.getId())) {
235             attribute = "SetTemperature";
236             value = command.toString();
237         }
238         try {
239             String soapHeader = "\"urn:Belkin:service:deviceevent:1#SetAttributes\"";
240             String content = "<?xml version=\"1.0\"?>"
241                     + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
242                     + "<s:Body>" + "<u:SetAttributes xmlns:u=\"urn:Belkin:service:deviceevent:1\">"
243                     + "<attributeList>&lt;attribute&gt;&lt;name&gt;" + attribute + "&lt;/name&gt;&lt;value&gt;" + value
244                     + "&lt;/value&gt;&lt;/attribute&gt;</attributeList>" + "</u:SetAttributes>" + "</s:Body>"
245                     + "</s:Envelope>";
246             String wemoURL = getWemoURL("deviceevent");
247
248             if (wemoURL != null) {
249                 wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
250             }
251         } catch (RuntimeException e) {
252             logger.debug("Failed to send command '{}' for device '{}':", command, getThing().getUID(), e);
253             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
254         }
255         updateStatus(ThingStatus.ONLINE);
256     }
257
258     @Override
259     public void onServiceSubscribed(String service, boolean succeeded) {
260         logger.debug("WeMo {}: Subscription to service {} {}", getUDN(), service, succeeded ? "succeeded" : "failed");
261         subscriptionState.put(service, succeeded);
262     }
263
264     @Override
265     public void onValueReceived(String variable, String value, String service) {
266         logger.debug("Received pair '{}':'{}' (service '{}') for thing '{}'", variable, value, service,
267                 this.getThing().getUID());
268
269         updateStatus(ThingStatus.ONLINE);
270         this.stateMap.put(variable, value);
271     }
272
273     private synchronized void onSubscription() {
274         if (service.isRegistered(this)) {
275             logger.debug("Checking WeMo GENA subscription for '{}'", this);
276
277             String subscription = "basicevent1";
278
279             if ((subscriptionState.get(subscription) == null) || !subscriptionState.get(subscription).booleanValue()) {
280                 logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(), subscription);
281                 service.addSubscription(this, subscription, SUBSCRIPTION_DURATION);
282                 subscriptionState.put(subscription, true);
283             }
284
285         } else {
286             logger.debug("Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
287                     this);
288         }
289     }
290
291     private synchronized void removeSubscription() {
292         logger.debug("Removing WeMo GENA subscription for '{}'", this);
293
294         if (service.isRegistered(this)) {
295             String subscription = "basicevent1";
296
297             if ((subscriptionState.get(subscription) != null) && subscriptionState.get(subscription).booleanValue()) {
298                 logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription);
299                 service.removeSubscription(this, subscription);
300             }
301
302             subscriptionState.remove(subscription);
303             service.unregisterParticipant(this);
304         }
305     }
306
307     private synchronized void onUpdate() {
308         if (refreshJob == null || refreshJob.isCancelled()) {
309             Configuration config = getThing().getConfiguration();
310             int refreshInterval = DEFAULT_REFRESH_INTERVAL_SECONDS;
311             Object refreshConfig = config.get("refresh");
312             refreshInterval = refreshConfig == null ? DEFAULT_REFRESH_INTERVAL_SECONDS
313                     : ((BigDecimal) refreshConfig).intValue();
314             refreshJob = scheduler.scheduleWithFixedDelay(refreshRunnable, 0, refreshInterval, TimeUnit.SECONDS);
315         }
316     }
317
318     private boolean isUpnpDeviceRegistered() {
319         return service.isRegistered(this);
320     }
321
322     @Override
323     public String getUDN() {
324         return (String) this.getThing().getConfiguration().get(UDN);
325     }
326
327     /**
328      * The {@link updateWemoState} polls the actual state of a WeMo device and
329      * calls {@link onValueReceived} to update the statemap and channels..
330      *
331      */
332     protected void updateWemoState() {
333         String action = "GetAttributes";
334         String actionService = "deviceevent";
335
336         String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
337         String content = "<?xml version=\"1.0\"?>"
338                 + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
339                 + "<s:Body>" + "<u:" + action + " xmlns:u=\"urn:Belkin:service:" + actionService + ":1\">" + "</u:"
340                 + action + ">" + "</s:Body>" + "</s:Envelope>";
341
342         try {
343             String wemoURL = getWemoURL(actionService);
344             if (wemoURL != null) {
345                 String wemoCallResponse = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
346                 if (wemoCallResponse != null) {
347                     logger.trace("State response '{}' for device '{}' received", wemoCallResponse, getThing().getUID());
348
349                     String stringParser = StringUtils.substringBetween(wemoCallResponse, "<attributeList>",
350                             "</attributeList>");
351
352                     // Due to Belkins bad response formatting, we need to run this twice.
353                     stringParser = StringEscapeUtils.unescapeXml(stringParser);
354                     stringParser = StringEscapeUtils.unescapeXml(stringParser);
355
356                     logger.trace("AirPurifier response '{}' for device '{}' received", stringParser,
357                             getThing().getUID());
358
359                     stringParser = "<data>" + stringParser + "</data>";
360
361                     DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
362                     // see
363                     // https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html
364                     dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
365                     dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
366                     dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
367                     dbf.setXIncludeAware(false);
368                     dbf.setExpandEntityReferences(false);
369                     DocumentBuilder db = dbf.newDocumentBuilder();
370                     InputSource is = new InputSource();
371                     is.setCharacterStream(new StringReader(stringParser));
372
373                     Document doc = db.parse(is);
374                     NodeList nodes = doc.getElementsByTagName("attribute");
375
376                     // iterate the attributes
377                     for (int i = 0; i < nodes.getLength(); i++) {
378                         Element element = (Element) nodes.item(i);
379
380                         NodeList deviceIndex = element.getElementsByTagName("name");
381                         Element line = (Element) deviceIndex.item(0);
382                         String attributeName = getCharacterDataFromElement(line);
383                         logger.trace("attributeName: {}", attributeName);
384
385                         NodeList deviceID = element.getElementsByTagName("value");
386                         line = (Element) deviceID.item(0);
387                         String attributeValue = getCharacterDataFromElement(line);
388                         logger.trace("attributeValue: {}", attributeValue);
389
390                         State newMode = new StringType();
391                         switch (attributeName) {
392                             case "Mode":
393                                 if ("purifier".equals(getThing().getThingTypeUID().getId())) {
394                                     switch (attributeValue) {
395                                         case "0":
396                                             newMode = new StringType("OFF");
397                                             break;
398                                         case "1":
399                                             newMode = new StringType("LOW");
400                                             break;
401                                         case "2":
402                                             newMode = new StringType("MED");
403                                             break;
404                                         case "3":
405                                             newMode = new StringType("HIGH");
406                                             break;
407                                         case "4":
408                                             newMode = new StringType("AUTO");
409                                             break;
410                                     }
411                                     updateState(CHANNEL_PURIFIERMODE, newMode);
412                                 } else {
413                                     switch (attributeValue) {
414                                         case "0":
415                                             newMode = new StringType("OFF");
416                                             break;
417                                         case "1":
418                                             newMode = new StringType("FROSTPROTECT");
419                                             break;
420                                         case "2":
421                                             newMode = new StringType("HIGH");
422                                             break;
423                                         case "3":
424                                             newMode = new StringType("LOW");
425                                             break;
426                                         case "4":
427                                             newMode = new StringType("ECO");
428                                             break;
429                                     }
430                                     updateState(CHANNEL_HEATERMODE, newMode);
431                                 }
432                                 break;
433                             case "Ionizer":
434                                 switch (attributeValue) {
435                                     case "0":
436                                         newMode = OnOffType.OFF;
437                                         break;
438                                     case "1":
439                                         newMode = OnOffType.ON;
440                                         break;
441                                 }
442                                 updateState(CHANNEL_IONIZER, newMode);
443                                 break;
444                             case "AirQuality":
445                                 switch (attributeValue) {
446                                     case "0":
447                                         newMode = new StringType("POOR");
448                                         break;
449                                     case "1":
450                                         newMode = new StringType("MODERATE");
451                                         break;
452                                     case "2":
453                                         newMode = new StringType("GOOD");
454                                         break;
455                                 }
456                                 updateState(CHANNEL_AIRQUALITY, newMode);
457                                 break;
458                             case "FilterLife":
459                                 int filterLife = Integer.valueOf(attributeValue);
460                                 if ("purifier".equals(getThing().getThingTypeUID().getId())) {
461                                     filterLife = Math.round((filterLife / FILTER_LIFE_MINS) * 100);
462                                 } else {
463                                     filterLife = Math.round((filterLife / 60480) * 100);
464                                 }
465                                 updateState(CHANNEL_FILTERLIFE, new PercentType(String.valueOf(filterLife)));
466                                 break;
467                             case "ExpiredFilterTime":
468                                 switch (attributeValue) {
469                                     case "0":
470                                         newMode = OnOffType.OFF;
471                                         break;
472                                     case "1":
473                                         newMode = OnOffType.ON;
474                                         break;
475                                 }
476                                 updateState(CHANNEL_EXPIREDFILTERTIME, newMode);
477                                 break;
478                             case "FilterPresent":
479                                 switch (attributeValue) {
480                                     case "0":
481                                         newMode = OnOffType.OFF;
482                                         break;
483                                     case "1":
484                                         newMode = OnOffType.ON;
485                                         break;
486                                 }
487                                 updateState(CHANNEL_FILTERPRESENT, newMode);
488                                 break;
489                             case "FANMode":
490                                 switch (attributeValue) {
491                                     case "0":
492                                         newMode = new StringType("OFF");
493                                         break;
494                                     case "1":
495                                         newMode = new StringType("LOW");
496                                         break;
497                                     case "2":
498                                         newMode = new StringType("MED");
499                                         break;
500                                     case "3":
501                                         newMode = new StringType("HIGH");
502                                         break;
503                                     case "4":
504                                         newMode = new StringType("AUTO");
505                                         break;
506                                 }
507                                 updateState(CHANNEL_PURIFIERMODE, newMode);
508                                 break;
509                             case "DesiredHumidity":
510                                 switch (attributeValue) {
511                                     case "0":
512                                         newMode = new PercentType("45");
513                                         break;
514                                     case "1":
515                                         newMode = new PercentType("50");
516                                         break;
517                                     case "2":
518                                         newMode = new PercentType("55");
519                                         break;
520                                     case "3":
521                                         newMode = new PercentType("60");
522                                         break;
523                                     case "4":
524                                         newMode = new PercentType("100");
525                                         break;
526                                 }
527                                 updateState(CHANNEL_DESIREDHUMIDITY, newMode);
528                                 break;
529                             case "CurrentHumidity":
530                                 newMode = new StringType(attributeValue);
531                                 updateState(CHANNEL_CURRENTHUMIDITY, newMode);
532                                 break;
533                             case "Temperature":
534                                 newMode = new StringType(attributeValue);
535                                 updateState(CHANNEL_CURRENTTEMP, newMode);
536                                 break;
537                             case "SetTemperature":
538                                 newMode = new StringType(attributeValue);
539                                 updateState(CHANNEL_TARGETTEMP, newMode);
540                                 break;
541                             case "AutoOffTime":
542                                 newMode = new StringType(attributeValue);
543                                 updateState(CHANNEL_AUTOOFFTIME, newMode);
544                                 break;
545                             case "TimeRemaining":
546                                 newMode = new StringType(attributeValue);
547                                 updateState(CHANNEL_HEATINGREMAINING, newMode);
548                                 break;
549                         }
550                     }
551                 }
552             }
553         } catch (RuntimeException | ParserConfigurationException | SAXException | IOException e) {
554             logger.debug("Failed to get actual state for device '{}':", getThing().getUID(), e);
555             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
556         }
557         updateStatus(ThingStatus.ONLINE);
558     }
559
560     public String getWemoURL(String actionService) {
561         URL descriptorURL = service.getDescriptorURL(this);
562         String wemoURL = null;
563         if (descriptorURL != null) {
564             String deviceURL = StringUtils.substringBefore(descriptorURL.toString(), "/setup.xml");
565             wemoURL = deviceURL + "/upnp/control/" + actionService + "1";
566             return wemoURL;
567         }
568         return null;
569     }
570
571     public static String getCharacterDataFromElement(Element e) {
572         Node child = e.getFirstChild();
573         if (child instanceof CharacterData) {
574             CharacterData cd = (CharacterData) child;
575             return cd.getData();
576         }
577         return "?";
578     }
579
580     @Override
581     public void onStatusChanged(boolean status) {
582     }
583 }