]> git.basschouten.com Git - openhab-addons.git/blob
22f93ae9f531f8ee188a2cdda40ea44bd2231bf3
[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.StringReader;
19 import java.time.Instant;
20 import java.time.ZonedDateTime;
21 import java.util.Collections;
22 import java.util.Set;
23 import java.util.TimeZone;
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
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.openhab.binding.wemo.internal.http.WemoHttpCall;
33 import org.openhab.core.config.core.Configuration;
34 import org.openhab.core.io.transport.upnp.UpnpIOService;
35 import org.openhab.core.library.types.DateTimeType;
36 import org.openhab.core.library.types.DecimalType;
37 import org.openhab.core.library.types.OnOffType;
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
54 /**
55  * The {@link WemoCoffeeHandler} is responsible for handling commands, which are
56  * sent to one of the channels and to update their states.
57  *
58  * @author Hans-Jörg Merk - Initial contribution
59  * @author Erdoan Hadzhiyusein - Adapted the class to work with the new DateTimeType
60  */
61 @NonNullByDefault
62 public class WemoCoffeeHandler extends WemoBaseThingHandler {
63
64     private final Logger logger = LoggerFactory.getLogger(WemoCoffeeHandler.class);
65
66     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_COFFEE);
67
68     private final Object jobLock = new Object();
69
70     private @Nullable ScheduledFuture<?> pollingJob;
71
72     public WemoCoffeeHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemoHttpCaller) {
73         super(thing, upnpIOService, wemoHttpCaller);
74
75         logger.debug("Creating a WemoCoffeeHandler for thing '{}'", getThing().getUID());
76     }
77
78     @Override
79     public void initialize() {
80         super.initialize();
81         Configuration configuration = getConfig();
82
83         if (configuration.get(UDN) != null) {
84             logger.debug("Initializing WemoCoffeeHandler for UDN '{}'", configuration.get(UDN));
85             addSubscription(DEVICEEVENT);
86             pollingJob = scheduler.scheduleWithFixedDelay(this::poll, 0, DEFAULT_REFRESH_INTERVAL_SECONDS,
87                     TimeUnit.SECONDS);
88             updateStatus(ThingStatus.ONLINE);
89         } else {
90             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
91                     "@text/config-status.error.missing-udn");
92             logger.debug("Cannot initalize WemoCoffeeHandler. UDN not set.");
93         }
94     }
95
96     @Override
97     public void dispose() {
98         logger.debug("WemoCoffeeHandler disposed.");
99         ScheduledFuture<?> job = this.pollingJob;
100         if (job != null && !job.isCancelled()) {
101             job.cancel(true);
102         }
103         this.pollingJob = null;
104         super.dispose();
105     }
106
107     private void poll() {
108         synchronized (jobLock) {
109             if (pollingJob == null) {
110                 return;
111             }
112             try {
113                 logger.debug("Polling job");
114
115                 // Check if the Wemo device is set in the UPnP service registry
116                 // If not, set the thing state to ONLINE/CONFIG-PENDING and wait for the next poll
117                 if (!isUpnpDeviceRegistered()) {
118                     logger.debug("UPnP device {} not yet registered", getUDN());
119                     updateStatus(ThingStatus.ONLINE, ThingStatusDetail.CONFIGURATION_PENDING,
120                             "@text/config-status.pending.device-not-registered [\"" + getUDN() + "\"]");
121                     return;
122                 }
123                 updateWemoState();
124             } catch (Exception e) {
125                 logger.debug("Exception during poll: {}", e.getMessage(), e);
126             }
127         }
128     }
129
130     @Override
131     public void handleCommand(ChannelUID channelUID, Command command) {
132         String wemoURL = getWemoURL(BASICACTION);
133         if (wemoURL == null) {
134             logger.debug("Failed to send command '{}' for device '{}': URL cannot be created", command,
135                     getThing().getUID());
136             return;
137         }
138         if (command instanceof RefreshType) {
139             try {
140                 updateWemoState();
141             } catch (Exception e) {
142                 logger.debug("Exception during poll", e);
143             }
144         } else if (channelUID.getId().equals(CHANNEL_STATE)) {
145             if (command instanceof OnOffType) {
146                 if (command.equals(OnOffType.ON)) {
147                     try {
148                         String soapHeader = "\"urn:Belkin:service:deviceevent:1#SetAttributes\"";
149
150                         String content = "<?xml version=\"1.0\"?>"
151                                 + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
152                                 + "<s:Body>" + "<u:SetAttributes xmlns:u=\"urn:Belkin:service:deviceevent:1\">"
153                                 + "<attributeList>&lt;attribute&gt;&lt;name&gt;Brewed&lt;/name&gt;&lt;value&gt;NULL&lt;/value&gt;&lt;/attribute&gt;"
154                                 + "&lt;attribute&gt;&lt;name&gt;LastCleaned&lt;/name&gt;&lt;value&gt;NULL&lt;/value&gt;&lt;/attribute&gt;&lt;attribute&gt;"
155                                 + "&lt;name&gt;ModeTime&lt;/name&gt;&lt;value&gt;NULL&lt;/value&gt;&lt;/attribute&gt;&lt;attribute&gt;&lt;name&gt;Brewing&lt;/name&gt;"
156                                 + "&lt;value&gt;NULL&lt;/value&gt;&lt;/attribute&gt;&lt;attribute&gt;&lt;name&gt;TimeRemaining&lt;/name&gt;&lt;value&gt;NULL&lt;/value&gt;"
157                                 + "&lt;/attribute&gt;&lt;attribute&gt;&lt;name&gt;WaterLevelReached&lt;/name&gt;&lt;value&gt;NULL&lt;/value&gt;&lt;/attribute&gt;&lt;"
158                                 + "attribute&gt;&lt;name&gt;Mode&lt;/name&gt;&lt;value&gt;4&lt;/value&gt;&lt;/attribute&gt;&lt;attribute&gt;&lt;name&gt;CleanAdvise&lt;/name&gt;"
159                                 + "&lt;value&gt;NULL&lt;/value&gt;&lt;/attribute&gt;&lt;attribute&gt;&lt;name&gt;FilterAdvise&lt;/name&gt;&lt;value&gt;NULL&lt;/value&gt;&lt;/attribute&gt;"
160                                 + "&lt;attribute&gt;&lt;name&gt;Cleaning&lt;/name&gt;&lt;value&gt;NULL&lt;/value&gt;&lt;/attribute&gt;</attributeList>"
161                                 + "</u:SetAttributes>" + "</s:Body>" + "</s:Envelope>";
162
163                         wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
164                         updateState(CHANNEL_STATE, OnOffType.ON);
165                         State newMode = new StringType("Brewing");
166                         updateState(CHANNEL_COFFEEMODE, newMode);
167                         updateStatus(ThingStatus.ONLINE);
168                     } catch (Exception e) {
169                         logger.warn("Failed to send command '{}' for device '{}': {}", command, getThing().getUID(),
170                                 e.getMessage());
171                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
172                     }
173                 }
174                 // if command.equals(OnOffType.OFF) we do nothing because WeMo Coffee Maker cannot be switched
175                 // off remotely
176             }
177         }
178     }
179
180     @Override
181     public void onValueReceived(@Nullable String variable, @Nullable String value, @Nullable String service) {
182         // We can subscribe to GENA events, but there is no usefull response right now.
183     }
184
185     /**
186      * The {@link updateWemoState} polls the actual state of a WeMo CoffeeMaker.
187      */
188     protected void updateWemoState() {
189         String actionService = DEVICEACTION;
190         String wemoURL = getWemoURL(actionService);
191         if (wemoURL == null) {
192             logger.debug("Failed to get actual state for device '{}': URL cannot be created", getThing().getUID());
193             return;
194         }
195         try {
196             String action = "GetAttributes";
197             String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
198             String content = createStateRequestContent(action, actionService);
199             String wemoCallResponse = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
200             try {
201                 String stringParser = substringBetween(wemoCallResponse, "<attributeList>", "</attributeList>");
202
203                 // Due to Belkins bad response formatting, we need to run this twice.
204                 stringParser = unescapeXml(stringParser);
205                 stringParser = unescapeXml(stringParser);
206
207                 logger.trace("CoffeeMaker response '{}' for device '{}' received", stringParser, getThing().getUID());
208
209                 stringParser = "<data>" + stringParser + "</data>";
210
211                 DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
212                 // see
213                 // https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html
214                 dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
215                 dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
216                 dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
217                 dbf.setXIncludeAware(false);
218                 dbf.setExpandEntityReferences(false);
219                 DocumentBuilder db = dbf.newDocumentBuilder();
220                 InputSource is = new InputSource();
221                 is.setCharacterStream(new StringReader(stringParser));
222
223                 Document doc = db.parse(is);
224                 NodeList nodes = doc.getElementsByTagName("attribute");
225
226                 // iterate the attributes
227                 for (int i = 0; i < nodes.getLength(); i++) {
228                     Element element = (Element) nodes.item(i);
229
230                     NodeList deviceIndex = element.getElementsByTagName("name");
231                     Element line = (Element) deviceIndex.item(0);
232                     String attributeName = getCharacterDataFromElement(line);
233                     logger.trace("attributeName: {}", attributeName);
234
235                     NodeList deviceID = element.getElementsByTagName("value");
236                     line = (Element) deviceID.item(0);
237                     String attributeValue = getCharacterDataFromElement(line);
238                     logger.trace("attributeValue: {}", attributeValue);
239
240                     switch (attributeName) {
241                         case "Mode":
242                             State newMode = new StringType("Brewing");
243                             State newAttributeValue;
244
245                             switch (attributeValue) {
246                                 case "0":
247                                     updateState(CHANNEL_STATE, OnOffType.ON);
248                                     newMode = new StringType("Refill");
249                                     updateState(CHANNEL_COFFEEMODE, newMode);
250                                     break;
251                                 case "1":
252                                     updateState(CHANNEL_STATE, OnOffType.OFF);
253                                     newMode = new StringType("PlaceCarafe");
254                                     updateState(CHANNEL_COFFEEMODE, newMode);
255                                     break;
256                                 case "2":
257                                     updateState(CHANNEL_STATE, OnOffType.OFF);
258                                     newMode = new StringType("RefillWater");
259                                     updateState(CHANNEL_COFFEEMODE, newMode);
260                                     break;
261                                 case "3":
262                                     updateState(CHANNEL_STATE, OnOffType.OFF);
263                                     newMode = new StringType("Ready");
264                                     updateState(CHANNEL_COFFEEMODE, newMode);
265                                     break;
266                                 case "4":
267                                     updateState(CHANNEL_STATE, OnOffType.ON);
268                                     newMode = new StringType("Brewing");
269                                     updateState(CHANNEL_COFFEEMODE, newMode);
270                                     break;
271                                 case "5":
272                                     updateState(CHANNEL_STATE, OnOffType.OFF);
273                                     newMode = new StringType("Brewed");
274                                     updateState(CHANNEL_COFFEEMODE, newMode);
275                                     break;
276                                 case "6":
277                                     updateState(CHANNEL_STATE, OnOffType.OFF);
278                                     newMode = new StringType("CleaningBrewing");
279                                     updateState(CHANNEL_COFFEEMODE, newMode);
280                                     break;
281                                 case "7":
282                                     updateState(CHANNEL_STATE, OnOffType.OFF);
283                                     newMode = new StringType("CleaningSoaking");
284                                     updateState(CHANNEL_COFFEEMODE, newMode);
285                                     break;
286                                 case "8":
287                                     updateState(CHANNEL_STATE, OnOffType.OFF);
288                                     newMode = new StringType("BrewFailCarafeRemoved");
289                                     updateState(CHANNEL_COFFEEMODE, newMode);
290                                     break;
291                             }
292                             break;
293                         case "ModeTime":
294                             newAttributeValue = new DecimalType(attributeValue);
295                             updateState(CHANNEL_MODETIME, newAttributeValue);
296                             break;
297                         case "TimeRemaining":
298                             newAttributeValue = new DecimalType(attributeValue);
299                             updateState(CHANNEL_TIMEREMAINING, newAttributeValue);
300                             break;
301                         case "WaterLevelReached":
302                             newAttributeValue = new DecimalType(attributeValue);
303                             updateState(CHANNEL_WATERLEVELREACHED, newAttributeValue);
304                             break;
305                         case "CleanAdvise":
306                             newAttributeValue = "0".equals(attributeValue) ? OnOffType.OFF : OnOffType.ON;
307                             updateState(CHANNEL_CLEANADVISE, newAttributeValue);
308                             break;
309                         case "FilterAdvise":
310                             newAttributeValue = "0".equals(attributeValue) ? OnOffType.OFF : OnOffType.ON;
311                             updateState(CHANNEL_FILTERADVISE, newAttributeValue);
312                             break;
313                         case "Brewed":
314                             newAttributeValue = getDateTimeState(attributeValue);
315                             if (newAttributeValue != null) {
316                                 updateState(CHANNEL_BREWED, newAttributeValue);
317                             }
318                             break;
319                         case "LastCleaned":
320                             newAttributeValue = getDateTimeState(attributeValue);
321                             if (newAttributeValue != null) {
322                                 updateState(CHANNEL_LASTCLEANED, newAttributeValue);
323                             }
324                             break;
325                     }
326                 }
327                 updateStatus(ThingStatus.ONLINE);
328             } catch (Exception e) {
329                 logger.warn("Failed to parse attributeList for WeMo CoffeMaker '{}'", this.getThing().getUID(), e);
330             }
331         } catch (Exception e) {
332             logger.warn("Failed to get attributes for device '{}'", getThing().getUID(), e);
333             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
334         }
335     }
336
337     public @Nullable State getDateTimeState(String attributeValue) {
338         long value = 0;
339         try {
340             value = Long.parseLong(attributeValue);
341         } catch (NumberFormatException e) {
342             logger.warn("Unable to parse attributeValue '{}' for device '{}'; expected long", attributeValue,
343                     getThing().getUID());
344             return null;
345         }
346         ZonedDateTime zoned = ZonedDateTime.ofInstant(Instant.ofEpochSecond(value), TimeZone.getDefault().toZoneId());
347         State dateTimeState = new DateTimeType(zoned);
348         logger.trace("New attribute brewed '{}' received", dateTimeState);
349         return dateTimeState;
350     }
351 }