]> git.basschouten.com Git - openhab-addons.git/blob
c3ca9ac3337e065191296c187a6d1720d0e1c2c3
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.lang.StringEscapeUtils;
33 import org.apache.commons.lang.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                     DocumentBuilder db = dbf.newDocumentBuilder();
363                     InputSource is = new InputSource();
364                     is.setCharacterStream(new StringReader(stringParser));
365
366                     Document doc = db.parse(is);
367                     NodeList nodes = doc.getElementsByTagName("attribute");
368
369                     // iterate the attributes
370                     for (int i = 0; i < nodes.getLength(); i++) {
371                         Element element = (Element) nodes.item(i);
372
373                         NodeList deviceIndex = element.getElementsByTagName("name");
374                         Element line = (Element) deviceIndex.item(0);
375                         String attributeName = getCharacterDataFromElement(line);
376                         logger.trace("attributeName: {}", attributeName);
377
378                         NodeList deviceID = element.getElementsByTagName("value");
379                         line = (Element) deviceID.item(0);
380                         String attributeValue = getCharacterDataFromElement(line);
381                         logger.trace("attributeValue: {}", attributeValue);
382
383                         State newMode = new StringType();
384                         switch (attributeName) {
385                             case "Mode":
386                                 if ("purifier".equals(getThing().getThingTypeUID().getId())) {
387                                     switch (attributeValue) {
388                                         case "0":
389                                             newMode = new StringType("OFF");
390                                             break;
391                                         case "1":
392                                             newMode = new StringType("LOW");
393                                             break;
394                                         case "2":
395                                             newMode = new StringType("MED");
396                                             break;
397                                         case "3":
398                                             newMode = new StringType("HIGH");
399                                             break;
400                                         case "4":
401                                             newMode = new StringType("AUTO");
402                                             break;
403                                     }
404                                     updateState(CHANNEL_PURIFIERMODE, newMode);
405                                 } else {
406                                     switch (attributeValue) {
407                                         case "0":
408                                             newMode = new StringType("OFF");
409                                             break;
410                                         case "1":
411                                             newMode = new StringType("FROSTPROTECT");
412                                             break;
413                                         case "2":
414                                             newMode = new StringType("HIGH");
415                                             break;
416                                         case "3":
417                                             newMode = new StringType("LOW");
418                                             break;
419                                         case "4":
420                                             newMode = new StringType("ECO");
421                                             break;
422                                     }
423                                     updateState(CHANNEL_HEATERMODE, newMode);
424                                 }
425                                 break;
426                             case "Ionizer":
427                                 switch (attributeValue) {
428                                     case "0":
429                                         newMode = OnOffType.OFF;
430                                         break;
431                                     case "1":
432                                         newMode = OnOffType.ON;
433                                         break;
434                                 }
435                                 updateState(CHANNEL_IONIZER, newMode);
436                                 break;
437                             case "AirQuality":
438                                 switch (attributeValue) {
439                                     case "0":
440                                         newMode = new StringType("POOR");
441                                         break;
442                                     case "1":
443                                         newMode = new StringType("MODERATE");
444                                         break;
445                                     case "2":
446                                         newMode = new StringType("GOOD");
447                                         break;
448                                 }
449                                 updateState(CHANNEL_AIRQUALITY, newMode);
450                                 break;
451                             case "FilterLife":
452                                 int filterLife = Integer.valueOf(attributeValue);
453                                 if ("purifier".equals(getThing().getThingTypeUID().getId())) {
454                                     filterLife = Math.round((filterLife / FILTER_LIFE_MINS) * 100);
455                                 } else {
456                                     filterLife = Math.round((filterLife / 60480) * 100);
457                                 }
458                                 updateState(CHANNEL_FILTERLIFE, new PercentType(String.valueOf(filterLife)));
459                                 break;
460                             case "ExpiredFilterTime":
461                                 switch (attributeValue) {
462                                     case "0":
463                                         newMode = OnOffType.OFF;
464                                         break;
465                                     case "1":
466                                         newMode = OnOffType.ON;
467                                         break;
468                                 }
469                                 updateState(CHANNEL_EXPIREDFILTERTIME, newMode);
470                                 break;
471                             case "FilterPresent":
472                                 switch (attributeValue) {
473                                     case "0":
474                                         newMode = OnOffType.OFF;
475                                         break;
476                                     case "1":
477                                         newMode = OnOffType.ON;
478                                         break;
479                                 }
480                                 updateState(CHANNEL_FILTERPRESENT, newMode);
481                                 break;
482                             case "FANMode":
483                                 switch (attributeValue) {
484                                     case "0":
485                                         newMode = new StringType("OFF");
486                                         break;
487                                     case "1":
488                                         newMode = new StringType("LOW");
489                                         break;
490                                     case "2":
491                                         newMode = new StringType("MED");
492                                         break;
493                                     case "3":
494                                         newMode = new StringType("HIGH");
495                                         break;
496                                     case "4":
497                                         newMode = new StringType("AUTO");
498                                         break;
499                                 }
500                                 updateState(CHANNEL_PURIFIERMODE, newMode);
501                                 break;
502                             case "DesiredHumidity":
503                                 switch (attributeValue) {
504                                     case "0":
505                                         newMode = new PercentType("45");
506                                         break;
507                                     case "1":
508                                         newMode = new PercentType("50");
509                                         break;
510                                     case "2":
511                                         newMode = new PercentType("55");
512                                         break;
513                                     case "3":
514                                         newMode = new PercentType("60");
515                                         break;
516                                     case "4":
517                                         newMode = new PercentType("100");
518                                         break;
519                                 }
520                                 updateState(CHANNEL_DESIREDHUMIDITY, newMode);
521                                 break;
522                             case "CurrentHumidity":
523                                 newMode = new StringType(attributeValue);
524                                 updateState(CHANNEL_CURRENTHUMIDITY, newMode);
525                                 break;
526                             case "Temperature":
527                                 newMode = new StringType(attributeValue);
528                                 updateState(CHANNEL_CURRENTTEMP, newMode);
529                                 break;
530                             case "SetTemperature":
531                                 newMode = new StringType(attributeValue);
532                                 updateState(CHANNEL_TARGETTEMP, newMode);
533                                 break;
534                             case "AutoOffTime":
535                                 newMode = new StringType(attributeValue);
536                                 updateState(CHANNEL_AUTOOFFTIME, newMode);
537                                 break;
538                             case "TimeRemaining":
539                                 newMode = new StringType(attributeValue);
540                                 updateState(CHANNEL_HEATINGREMAINING, newMode);
541                                 break;
542                         }
543                     }
544                 }
545             }
546         } catch (RuntimeException | ParserConfigurationException | SAXException | IOException e) {
547             logger.debug("Failed to get actual state for device '{}':", getThing().getUID(), e);
548             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
549         }
550         updateStatus(ThingStatus.ONLINE);
551     }
552
553     public String getWemoURL(String actionService) {
554         URL descriptorURL = service.getDescriptorURL(this);
555         String wemoURL = null;
556         if (descriptorURL != null) {
557             String deviceURL = StringUtils.substringBefore(descriptorURL.toString(), "/setup.xml");
558             wemoURL = deviceURL + "/upnp/control/" + actionService + "1";
559             return wemoURL;
560         }
561         return null;
562     }
563
564     public static String getCharacterDataFromElement(Element e) {
565         Node child = e.getFirstChild();
566         if (child instanceof CharacterData) {
567             CharacterData cd = (CharacterData) child;
568             return cd.getData();
569         }
570         return "?";
571     }
572
573     @Override
574     public void onStatusChanged(boolean status) {
575     }
576 }