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