]> git.basschouten.com Git - openhab-addons.git/blob
828069edab8d9e43d07e4415623975b6848ce813
[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.IOException;
19 import java.io.StringReader;
20 import java.util.Collections;
21 import java.util.HashMap;
22 import java.util.Map;
23 import java.util.Set;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
26
27 import javax.xml.parsers.DocumentBuilder;
28 import javax.xml.parsers.DocumentBuilderFactory;
29 import javax.xml.parsers.ParserConfigurationException;
30
31 import org.eclipse.jdt.annotation.NonNullByDefault;
32 import org.eclipse.jdt.annotation.Nullable;
33 import org.jupnp.UpnpService;
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.UpnpIOService;
37 import org.openhab.core.library.types.OnOffType;
38 import org.openhab.core.library.types.PercentType;
39 import org.openhab.core.library.types.StringType;
40 import org.openhab.core.thing.ChannelUID;
41 import org.openhab.core.thing.Thing;
42 import org.openhab.core.thing.ThingStatus;
43 import org.openhab.core.thing.ThingStatusDetail;
44 import org.openhab.core.thing.ThingTypeUID;
45 import org.openhab.core.types.Command;
46 import org.openhab.core.types.RefreshType;
47 import org.openhab.core.types.State;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
50 import org.w3c.dom.Document;
51 import org.w3c.dom.Element;
52 import org.w3c.dom.NodeList;
53 import org.xml.sax.InputSource;
54 import org.xml.sax.SAXException;
55
56 /**
57  * The {@link WemoHolmesHandler} is responsible for handling commands, which are
58  * sent to one of the channels and to update their states.
59  *
60  * @author Hans-Jörg Merk - Initial contribution;
61  */
62 @NonNullByDefault
63 public class WemoHolmesHandler extends WemoBaseThingHandler {
64
65     private final Logger logger = LoggerFactory.getLogger(WemoHolmesHandler.class);
66
67     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_PURIFIER);
68
69     private static final int FILTER_LIFE_DAYS = 330;
70     private static final int FILTER_LIFE_MINS = FILTER_LIFE_DAYS * 24 * 60;
71
72     private final Object jobLock = new Object();
73
74     private final Map<String, String> stateMap = Collections.synchronizedMap(new HashMap<>());
75
76     private @Nullable ScheduledFuture<?> pollingJob;
77
78     public WemoHolmesHandler(Thing thing, UpnpIOService upnpIOService, UpnpService upnpService,
79             WemoHttpCall wemoHttpCaller) {
80         super(thing, upnpIOService, upnpService, wemoHttpCaller);
81
82         logger.debug("Creating a WemoHolmesHandler for thing '{}'", getThing().getUID());
83     }
84
85     @Override
86     public void initialize() {
87         super.initialize();
88         Configuration configuration = getConfig();
89
90         if (configuration.get(UDN) != null) {
91             logger.debug("Initializing WemoHolmesHandler for UDN '{}'", configuration.get(UDN));
92             addSubscription(BASICEVENT);
93             pollingJob = scheduler.scheduleWithFixedDelay(this::poll, 0, DEFAULT_REFRESH_INTERVAL_SECONDS,
94                     TimeUnit.SECONDS);
95             updateStatus(ThingStatus.UNKNOWN);
96         } else {
97             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
98                     "@text/config-status.error.missing-udn");
99         }
100     }
101
102     @Override
103     public void dispose() {
104         logger.debug("WemoHolmesHandler disposed.");
105
106         ScheduledFuture<?> job = this.pollingJob;
107         if (job != null && !job.isCancelled()) {
108             job.cancel(true);
109         }
110         this.pollingJob = null;
111         super.dispose();
112     }
113
114     private void poll() {
115         synchronized (jobLock) {
116             if (pollingJob == null) {
117                 return;
118             }
119             try {
120                 logger.debug("Polling job");
121                 // Check if the Wemo device is set in the UPnP service registry
122                 if (!isUpnpDeviceRegistered()) {
123                     logger.debug("UPnP device {} not yet registered", getUDN());
124                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
125                             "@text/config-status.pending.device-not-registered [\"" + getUDN() + "\"]");
126                     return;
127                 }
128                 updateWemoState();
129             } catch (Exception e) {
130                 logger.debug("Exception during poll: {}", e.getMessage(), e);
131             }
132         }
133     }
134
135     @Override
136     public void handleCommand(ChannelUID channelUID, Command command) {
137         String wemoURL = getWemoURL(DEVICEACTION);
138         if (wemoURL == null) {
139             logger.debug("Failed to send command '{}' for device '{}': URL cannot be created", command,
140                     getThing().getUID());
141             return;
142         }
143         String attribute = null;
144         String value = null;
145
146         if (command instanceof RefreshType) {
147             updateWemoState();
148         } else if (CHANNEL_PURIFIER_MODE.equals(channelUID.getId())) {
149             attribute = "Mode";
150             String commandString = command.toString();
151             switch (commandString) {
152                 case "OFF":
153                     value = "0";
154                     break;
155                 case "LOW":
156                     value = "1";
157                     break;
158                 case "MED":
159                     value = "2";
160                     break;
161                 case "HIGH":
162                     value = "3";
163                     break;
164                 case "AUTO":
165                     value = "4";
166                     break;
167             }
168         } else if (CHANNEL_IONIZER.equals(channelUID.getId())) {
169             attribute = "Ionizer";
170             if (OnOffType.ON.equals(command)) {
171                 value = "1";
172             } else if (OnOffType.OFF.equals(command)) {
173                 value = "0";
174             }
175         } else if (CHANNEL_HUMIDIFIER_MODE.equals(channelUID.getId())) {
176             attribute = "FanMode";
177             String commandString = command.toString();
178             switch (commandString) {
179                 case "OFF":
180                     value = "0";
181                     break;
182                 case "MIN":
183                     value = "1";
184                     break;
185                 case "LOW":
186                     value = "2";
187                     break;
188                 case "MED":
189                     value = "3";
190                     break;
191                 case "HIGH":
192                     value = "4";
193                     break;
194                 case "MAX":
195                     value = "5";
196                     break;
197             }
198         } else if (CHANNEL_DESIRED_HUMIDITY.equals(channelUID.getId())) {
199             attribute = "DesiredHumidity";
200             String commandString = command.toString();
201             switch (commandString) {
202                 case "45":
203                     value = "0";
204                     break;
205                 case "50":
206                     value = "1";
207                     break;
208                 case "55":
209                     value = "2";
210                     break;
211                 case "60":
212                     value = "3";
213                     break;
214                 case "100":
215                     value = "4";
216                     break;
217             }
218         } else if (CHANNEL_HEATER_MODE.equals(channelUID.getId())) {
219             attribute = "Mode";
220             String commandString = command.toString();
221             switch (commandString) {
222                 case "OFF":
223                     value = "0";
224                     break;
225                 case "FROSTPROTECT":
226                     value = "1";
227                     break;
228                 case "HIGH":
229                     value = "2";
230                     break;
231                 case "LOW":
232                     value = "3";
233                     break;
234                 case "ECO":
235                     value = "4";
236                     break;
237             }
238         } else if (CHANNEL_TARGET_TEMPERATURE.equals(channelUID.getId())) {
239             attribute = "SetTemperature";
240             value = command.toString();
241         }
242         try {
243             String soapHeader = "\"urn:Belkin:service:deviceevent:1#SetAttributes\"";
244             String content = "<?xml version=\"1.0\"?>"
245                     + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
246                     + "<s:Body>" + "<u:SetAttributes xmlns:u=\"urn:Belkin:service:deviceevent:1\">"
247                     + "<attributeList>&lt;attribute&gt;&lt;name&gt;" + attribute + "&lt;/name&gt;&lt;value&gt;" + value
248                     + "&lt;/value&gt;&lt;/attribute&gt;</attributeList>" + "</u:SetAttributes>" + "</s:Body>"
249                     + "</s:Envelope>";
250             wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
251             updateStatus(ThingStatus.ONLINE);
252         } catch (IOException e) {
253             logger.debug("Failed to send command '{}' for device '{}':", command, getThing().getUID(), e);
254             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
255         }
256     }
257
258     @Override
259     public void onValueReceived(@Nullable String variable, @Nullable String value, @Nullable String service) {
260         logger.debug("Received pair '{}':'{}' (service '{}') for thing '{}'", variable, value, service,
261                 this.getThing().getUID());
262
263         updateStatus(ThingStatus.ONLINE);
264         if (variable != null && value != null) {
265             this.stateMap.put(variable, value);
266         }
267     }
268
269     /**
270      * The {@link updateWemoState} polls the actual state of a WeMo device and
271      * calls {@link onValueReceived} to update the statemap and channels..
272      *
273      */
274     protected void updateWemoState() {
275         String actionService = DEVICEACTION;
276         String wemoURL = getWemoURL(actionService);
277         if (wemoURL == null) {
278             logger.debug("Failed to get actual state for device '{}': URL cannot be created", getThing().getUID());
279             return;
280         }
281         try {
282             String action = "GetAttributes";
283             String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
284             String content = createStateRequestContent(action, actionService);
285             String wemoCallResponse = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
286             String stringParser = substringBetween(wemoCallResponse, "<attributeList>", "</attributeList>");
287
288             // Due to Belkins bad response formatting, we need to run this twice.
289             stringParser = unescapeXml(stringParser);
290             stringParser = unescapeXml(stringParser);
291
292             logger.trace("AirPurifier response '{}' for device '{}' received", stringParser, getThing().getUID());
293
294             stringParser = "<data>" + stringParser + "</data>";
295
296             DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
297             // see
298             // https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html
299             dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
300             dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
301             dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
302             dbf.setXIncludeAware(false);
303             dbf.setExpandEntityReferences(false);
304             DocumentBuilder db = dbf.newDocumentBuilder();
305             InputSource is = new InputSource();
306             is.setCharacterStream(new StringReader(stringParser));
307
308             Document doc = db.parse(is);
309             NodeList nodes = doc.getElementsByTagName("attribute");
310
311             // iterate the attributes
312             for (int i = 0; i < nodes.getLength(); i++) {
313                 Element element = (Element) nodes.item(i);
314
315                 NodeList deviceIndex = element.getElementsByTagName("name");
316                 Element line = (Element) deviceIndex.item(0);
317                 String attributeName = getCharacterDataFromElement(line);
318                 logger.trace("attributeName: {}", attributeName);
319
320                 NodeList deviceID = element.getElementsByTagName("value");
321                 line = (Element) deviceID.item(0);
322                 String attributeValue = getCharacterDataFromElement(line);
323                 logger.trace("attributeValue: {}", attributeValue);
324
325                 State newMode = new StringType();
326                 switch (attributeName) {
327                     case "Mode":
328                         if ("purifier".equals(getThing().getThingTypeUID().getId())) {
329                             switch (attributeValue) {
330                                 case "0":
331                                     newMode = new StringType("OFF");
332                                     break;
333                                 case "1":
334                                     newMode = new StringType("LOW");
335                                     break;
336                                 case "2":
337                                     newMode = new StringType("MED");
338                                     break;
339                                 case "3":
340                                     newMode = new StringType("HIGH");
341                                     break;
342                                 case "4":
343                                     newMode = new StringType("AUTO");
344                                     break;
345                             }
346                             updateState(CHANNEL_PURIFIER_MODE, newMode);
347                         } else {
348                             switch (attributeValue) {
349                                 case "0":
350                                     newMode = new StringType("OFF");
351                                     break;
352                                 case "1":
353                                     newMode = new StringType("FROSTPROTECT");
354                                     break;
355                                 case "2":
356                                     newMode = new StringType("HIGH");
357                                     break;
358                                 case "3":
359                                     newMode = new StringType("LOW");
360                                     break;
361                                 case "4":
362                                     newMode = new StringType("ECO");
363                                     break;
364                             }
365                             updateState(CHANNEL_HEATER_MODE, newMode);
366                         }
367                         break;
368                     case "Ionizer":
369                         switch (attributeValue) {
370                             case "0":
371                                 newMode = OnOffType.OFF;
372                                 break;
373                             case "1":
374                                 newMode = OnOffType.ON;
375                                 break;
376                         }
377                         updateState(CHANNEL_IONIZER, newMode);
378                         break;
379                     case "AirQuality":
380                         switch (attributeValue) {
381                             case "0":
382                                 newMode = new StringType("POOR");
383                                 break;
384                             case "1":
385                                 newMode = new StringType("MODERATE");
386                                 break;
387                             case "2":
388                                 newMode = new StringType("GOOD");
389                                 break;
390                         }
391                         updateState(CHANNEL_AIR_QUALITY, newMode);
392                         break;
393                     case "FilterLife":
394                         int filterLife = Integer.valueOf(attributeValue);
395                         if ("purifier".equals(getThing().getThingTypeUID().getId())) {
396                             filterLife = Math.round((filterLife / FILTER_LIFE_MINS) * 100);
397                         } else {
398                             filterLife = Math.round((filterLife / 60480) * 100);
399                         }
400                         updateState(CHANNEL_FILTER_LIFE, new PercentType(String.valueOf(filterLife)));
401                         break;
402                     case "ExpiredFilterTime":
403                         switch (attributeValue) {
404                             case "0":
405                                 newMode = OnOffType.OFF;
406                                 break;
407                             case "1":
408                                 newMode = OnOffType.ON;
409                                 break;
410                         }
411                         updateState(CHANNEL_EXPIRED_FILTER_TIME, newMode);
412                         break;
413                     case "FilterPresent":
414                         switch (attributeValue) {
415                             case "0":
416                                 newMode = OnOffType.OFF;
417                                 break;
418                             case "1":
419                                 newMode = OnOffType.ON;
420                                 break;
421                         }
422                         updateState(CHANNEL_FILTER_PRESENT, newMode);
423                         break;
424                     case "FANMode":
425                         switch (attributeValue) {
426                             case "0":
427                                 newMode = new StringType("OFF");
428                                 break;
429                             case "1":
430                                 newMode = new StringType("LOW");
431                                 break;
432                             case "2":
433                                 newMode = new StringType("MED");
434                                 break;
435                             case "3":
436                                 newMode = new StringType("HIGH");
437                                 break;
438                             case "4":
439                                 newMode = new StringType("AUTO");
440                                 break;
441                         }
442                         updateState(CHANNEL_PURIFIER_MODE, newMode);
443                         break;
444                     case "DesiredHumidity":
445                         switch (attributeValue) {
446                             case "0":
447                                 newMode = new PercentType("45");
448                                 break;
449                             case "1":
450                                 newMode = new PercentType("50");
451                                 break;
452                             case "2":
453                                 newMode = new PercentType("55");
454                                 break;
455                             case "3":
456                                 newMode = new PercentType("60");
457                                 break;
458                             case "4":
459                                 newMode = new PercentType("100");
460                                 break;
461                         }
462                         updateState(CHANNEL_DESIRED_HUMIDITY, newMode);
463                         break;
464                     case "CurrentHumidity":
465                         newMode = new StringType(attributeValue);
466                         updateState(CHANNEL_CURRENT_HUMIDITY, newMode);
467                         break;
468                     case "Temperature":
469                         newMode = new StringType(attributeValue);
470                         updateState(CHANNEL_CURRENT_TEMPERATURE, newMode);
471                         break;
472                     case "SetTemperature":
473                         newMode = new StringType(attributeValue);
474                         updateState(CHANNEL_TARGET_TEMPERATURE, newMode);
475                         break;
476                     case "AutoOffTime":
477                         newMode = new StringType(attributeValue);
478                         updateState(CHANNEL_AUTO_OFF_TIME, newMode);
479                         break;
480                     case "TimeRemaining":
481                         newMode = new StringType(attributeValue);
482                         updateState(CHANNEL_HEATING_REMAINING, newMode);
483                         break;
484                 }
485             }
486             updateStatus(ThingStatus.ONLINE);
487         } catch (RuntimeException | ParserConfigurationException | SAXException | IOException e) {
488             logger.debug("Failed to get actual state for device '{}':", getThing().getUID(), e);
489             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
490         }
491     }
492 }