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