]> git.basschouten.com Git - openhab-addons.git/blob
de174836c645a84893632d13d123343d5d36d533
[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 = Set.of(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 = """
243                     <?xml version="1.0"?>\
244                     <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">\
245                     <s:Body>\
246                     <u:SetAttributes xmlns:u="urn:Belkin:service:deviceevent:1">\
247                     <attributeList>&lt;attribute&gt;&lt;name&gt;\
248                     """
249                     + attribute + "&lt;/name&gt;&lt;value&gt;" + value
250                     + "&lt;/value&gt;&lt;/attribute&gt;</attributeList>" + "</u:SetAttributes>" + "</s:Body>"
251                     + "</s:Envelope>";
252             wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
253             updateStatus(ThingStatus.ONLINE);
254         } catch (IOException e) {
255             logger.debug("Failed to send command '{}' for device '{}':", command, getThing().getUID(), e);
256             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
257         }
258     }
259
260     @Override
261     public void onValueReceived(@Nullable String variable, @Nullable String value, @Nullable String service) {
262         logger.debug("Received pair '{}':'{}' (service '{}') for thing '{}'", variable, value, service,
263                 this.getThing().getUID());
264
265         updateStatus(ThingStatus.ONLINE);
266         if (variable != null && value != null) {
267             this.stateMap.put(variable, value);
268         }
269     }
270
271     /**
272      * The {@link updateWemoState} polls the actual state of a WeMo device and
273      * calls {@link onValueReceived} to update the statemap and channels..
274      *
275      */
276     protected void updateWemoState() {
277         String actionService = DEVICEACTION;
278         String wemoURL = getWemoURL(actionService);
279         if (wemoURL == null) {
280             logger.debug("Failed to get actual state for device '{}': URL cannot be created", getThing().getUID());
281             return;
282         }
283         try {
284             String action = "GetAttributes";
285             String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
286             String content = createStateRequestContent(action, actionService);
287             String wemoCallResponse = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
288             String stringParser = substringBetween(wemoCallResponse, "<attributeList>", "</attributeList>");
289
290             // Due to Belkins bad response formatting, we need to run this twice.
291             stringParser = unescapeXml(stringParser);
292             stringParser = unescapeXml(stringParser);
293
294             logger.trace("AirPurifier response '{}' for device '{}' received", stringParser, getThing().getUID());
295
296             stringParser = "<data>" + stringParser + "</data>";
297
298             DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
299             // see
300             // https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html
301             dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
302             dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
303             dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
304             dbf.setXIncludeAware(false);
305             dbf.setExpandEntityReferences(false);
306             DocumentBuilder db = dbf.newDocumentBuilder();
307             InputSource is = new InputSource();
308             is.setCharacterStream(new StringReader(stringParser));
309
310             Document doc = db.parse(is);
311             NodeList nodes = doc.getElementsByTagName("attribute");
312
313             // iterate the attributes
314             for (int i = 0; i < nodes.getLength(); i++) {
315                 Element element = (Element) nodes.item(i);
316
317                 NodeList deviceIndex = element.getElementsByTagName("name");
318                 Element line = (Element) deviceIndex.item(0);
319                 String attributeName = getCharacterDataFromElement(line);
320                 logger.trace("attributeName: {}", attributeName);
321
322                 NodeList deviceID = element.getElementsByTagName("value");
323                 line = (Element) deviceID.item(0);
324                 String attributeValue = getCharacterDataFromElement(line);
325                 logger.trace("attributeValue: {}", attributeValue);
326
327                 State newMode = new StringType();
328                 switch (attributeName) {
329                     case "Mode":
330                         if ("purifier".equals(getThing().getThingTypeUID().getId())) {
331                             switch (attributeValue) {
332                                 case "0":
333                                     newMode = new StringType("OFF");
334                                     break;
335                                 case "1":
336                                     newMode = new StringType("LOW");
337                                     break;
338                                 case "2":
339                                     newMode = new StringType("MED");
340                                     break;
341                                 case "3":
342                                     newMode = new StringType("HIGH");
343                                     break;
344                                 case "4":
345                                     newMode = new StringType("AUTO");
346                                     break;
347                             }
348                             updateState(CHANNEL_PURIFIER_MODE, newMode);
349                         } else {
350                             switch (attributeValue) {
351                                 case "0":
352                                     newMode = new StringType("OFF");
353                                     break;
354                                 case "1":
355                                     newMode = new StringType("FROSTPROTECT");
356                                     break;
357                                 case "2":
358                                     newMode = new StringType("HIGH");
359                                     break;
360                                 case "3":
361                                     newMode = new StringType("LOW");
362                                     break;
363                                 case "4":
364                                     newMode = new StringType("ECO");
365                                     break;
366                             }
367                             updateState(CHANNEL_HEATER_MODE, newMode);
368                         }
369                         break;
370                     case "Ionizer":
371                         switch (attributeValue) {
372                             case "0":
373                                 newMode = OnOffType.OFF;
374                                 break;
375                             case "1":
376                                 newMode = OnOffType.ON;
377                                 break;
378                         }
379                         updateState(CHANNEL_IONIZER, newMode);
380                         break;
381                     case "AirQuality":
382                         switch (attributeValue) {
383                             case "0":
384                                 newMode = new StringType("POOR");
385                                 break;
386                             case "1":
387                                 newMode = new StringType("MODERATE");
388                                 break;
389                             case "2":
390                                 newMode = new StringType("GOOD");
391                                 break;
392                         }
393                         updateState(CHANNEL_AIR_QUALITY, newMode);
394                         break;
395                     case "FilterLife":
396                         int filterLife = Integer.valueOf(attributeValue);
397                         if ("purifier".equals(getThing().getThingTypeUID().getId())) {
398                             filterLife = Math.round((filterLife / FILTER_LIFE_MINS) * 100);
399                         } else {
400                             filterLife = Math.round((filterLife / 60480) * 100);
401                         }
402                         updateState(CHANNEL_FILTER_LIFE, new PercentType(String.valueOf(filterLife)));
403                         break;
404                     case "ExpiredFilterTime":
405                         switch (attributeValue) {
406                             case "0":
407                                 newMode = OnOffType.OFF;
408                                 break;
409                             case "1":
410                                 newMode = OnOffType.ON;
411                                 break;
412                         }
413                         updateState(CHANNEL_EXPIRED_FILTER_TIME, newMode);
414                         break;
415                     case "FilterPresent":
416                         switch (attributeValue) {
417                             case "0":
418                                 newMode = OnOffType.OFF;
419                                 break;
420                             case "1":
421                                 newMode = OnOffType.ON;
422                                 break;
423                         }
424                         updateState(CHANNEL_FILTER_PRESENT, newMode);
425                         break;
426                     case "FANMode":
427                         switch (attributeValue) {
428                             case "0":
429                                 newMode = new StringType("OFF");
430                                 break;
431                             case "1":
432                                 newMode = new StringType("LOW");
433                                 break;
434                             case "2":
435                                 newMode = new StringType("MED");
436                                 break;
437                             case "3":
438                                 newMode = new StringType("HIGH");
439                                 break;
440                             case "4":
441                                 newMode = new StringType("AUTO");
442                                 break;
443                         }
444                         updateState(CHANNEL_PURIFIER_MODE, newMode);
445                         break;
446                     case "DesiredHumidity":
447                         switch (attributeValue) {
448                             case "0":
449                                 newMode = new PercentType("45");
450                                 break;
451                             case "1":
452                                 newMode = new PercentType("50");
453                                 break;
454                             case "2":
455                                 newMode = new PercentType("55");
456                                 break;
457                             case "3":
458                                 newMode = new PercentType("60");
459                                 break;
460                             case "4":
461                                 newMode = new PercentType("100");
462                                 break;
463                         }
464                         updateState(CHANNEL_DESIRED_HUMIDITY, newMode);
465                         break;
466                     case "CurrentHumidity":
467                         newMode = new StringType(attributeValue);
468                         updateState(CHANNEL_CURRENT_HUMIDITY, newMode);
469                         break;
470                     case "Temperature":
471                         newMode = new StringType(attributeValue);
472                         updateState(CHANNEL_CURRENT_TEMPERATURE, newMode);
473                         break;
474                     case "SetTemperature":
475                         newMode = new StringType(attributeValue);
476                         updateState(CHANNEL_TARGET_TEMPERATURE, newMode);
477                         break;
478                     case "AutoOffTime":
479                         newMode = new StringType(attributeValue);
480                         updateState(CHANNEL_AUTO_OFF_TIME, newMode);
481                         break;
482                     case "TimeRemaining":
483                         newMode = new StringType(attributeValue);
484                         updateState(CHANNEL_HEATING_REMAINING, newMode);
485                         break;
486                 }
487             }
488             updateStatus(ThingStatus.ONLINE);
489         } catch (RuntimeException | ParserConfigurationException | SAXException | IOException e) {
490             logger.debug("Failed to get actual state for device '{}':", getThing().getUID(), e);
491             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
492         }
493     }
494 }