]> git.basschouten.com Git - openhab-addons.git/blob
e531e3fa82aa5cf320d1d72ecf602ca4ce2ad39f
[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.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.UNKNOWN);
89         } else {
90             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
91                     "@text/config-status.error.missing-udn");
92         }
93     }
94
95     @Override
96     public void dispose() {
97         logger.debug("WemoCoffeeHandler disposed.");
98         ScheduledFuture<?> job = this.pollingJob;
99         if (job != null && !job.isCancelled()) {
100             job.cancel(true);
101         }
102         this.pollingJob = null;
103         super.dispose();
104     }
105
106     private void poll() {
107         synchronized (jobLock) {
108             if (pollingJob == null) {
109                 return;
110             }
111             try {
112                 logger.debug("Polling job");
113
114                 // Check if the Wemo device is set in the UPnP service registry
115                 if (!isUpnpDeviceRegistered()) {
116                     logger.debug("UPnP device {} not yet registered", getUDN());
117                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
118                             "@text/config-status.pending.device-not-registered [\"" + getUDN() + "\"]");
119                     return;
120                 }
121                 updateWemoState();
122             } catch (Exception e) {
123                 logger.debug("Exception during poll: {}", e.getMessage(), e);
124             }
125         }
126     }
127
128     @Override
129     public void handleCommand(ChannelUID channelUID, Command command) {
130         String wemoURL = getWemoURL(BASICACTION);
131         if (wemoURL == null) {
132             logger.debug("Failed to send command '{}' for device '{}': URL cannot be created", command,
133                     getThing().getUID());
134             return;
135         }
136         if (command instanceof RefreshType) {
137             try {
138                 updateWemoState();
139             } catch (Exception e) {
140                 logger.debug("Exception during poll", e);
141             }
142         } else if (channelUID.getId().equals(CHANNEL_STATE)) {
143             if (command instanceof OnOffType) {
144                 if (command.equals(OnOffType.ON)) {
145                     try {
146                         String soapHeader = "\"urn:Belkin:service:deviceevent:1#SetAttributes\"";
147
148                         String content = "<?xml version=\"1.0\"?>"
149                                 + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
150                                 + "<s:Body>" + "<u:SetAttributes xmlns:u=\"urn:Belkin:service:deviceevent:1\">"
151                                 + "<attributeList>&lt;attribute&gt;&lt;name&gt;Brewed&lt;/name&gt;&lt;value&gt;NULL&lt;/value&gt;&lt;/attribute&gt;"
152                                 + "&lt;attribute&gt;&lt;name&gt;LastCleaned&lt;/name&gt;&lt;value&gt;NULL&lt;/value&gt;&lt;/attribute&gt;&lt;attribute&gt;"
153                                 + "&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;"
154                                 + "&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;"
155                                 + "&lt;/attribute&gt;&lt;attribute&gt;&lt;name&gt;WaterLevelReached&lt;/name&gt;&lt;value&gt;NULL&lt;/value&gt;&lt;/attribute&gt;&lt;"
156                                 + "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;"
157                                 + "&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;"
158                                 + "&lt;attribute&gt;&lt;name&gt;Cleaning&lt;/name&gt;&lt;value&gt;NULL&lt;/value&gt;&lt;/attribute&gt;</attributeList>"
159                                 + "</u:SetAttributes>" + "</s:Body>" + "</s:Envelope>";
160
161                         wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
162                         updateState(CHANNEL_STATE, OnOffType.ON);
163                         State newMode = new StringType("Brewing");
164                         updateState(CHANNEL_COFFEE_MODE, newMode);
165                         updateStatus(ThingStatus.ONLINE);
166                     } catch (Exception e) {
167                         logger.warn("Failed to send command '{}' for device '{}': {}", command, getThing().getUID(),
168                                 e.getMessage());
169                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
170                     }
171                 }
172                 // if command.equals(OnOffType.OFF) we do nothing because WeMo Coffee Maker cannot be switched
173                 // off remotely
174             }
175         }
176     }
177
178     @Override
179     public void onValueReceived(@Nullable String variable, @Nullable String value, @Nullable String service) {
180         // We can subscribe to GENA events, but there is no usefull response right now.
181     }
182
183     /**
184      * The {@link updateWemoState} polls the actual state of a WeMo CoffeeMaker.
185      */
186     protected void updateWemoState() {
187         String actionService = DEVICEACTION;
188         String wemoURL = getWemoURL(actionService);
189         if (wemoURL == null) {
190             logger.debug("Failed to get actual state for device '{}': URL cannot be created", getThing().getUID());
191             return;
192         }
193         try {
194             String action = "GetAttributes";
195             String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
196             String content = createStateRequestContent(action, actionService);
197             String wemoCallResponse = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
198             try {
199                 String stringParser = substringBetween(wemoCallResponse, "<attributeList>", "</attributeList>");
200
201                 // Due to Belkins bad response formatting, we need to run this twice.
202                 stringParser = unescapeXml(stringParser);
203                 stringParser = unescapeXml(stringParser);
204
205                 logger.trace("CoffeeMaker response '{}' for device '{}' received", stringParser, getThing().getUID());
206
207                 stringParser = "<data>" + stringParser + "</data>";
208
209                 DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
210                 // see
211                 // https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html
212                 dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
213                 dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
214                 dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
215                 dbf.setXIncludeAware(false);
216                 dbf.setExpandEntityReferences(false);
217                 DocumentBuilder db = dbf.newDocumentBuilder();
218                 InputSource is = new InputSource();
219                 is.setCharacterStream(new StringReader(stringParser));
220
221                 Document doc = db.parse(is);
222                 NodeList nodes = doc.getElementsByTagName("attribute");
223
224                 // iterate the attributes
225                 for (int i = 0; i < nodes.getLength(); i++) {
226                     Element element = (Element) nodes.item(i);
227
228                     NodeList deviceIndex = element.getElementsByTagName("name");
229                     Element line = (Element) deviceIndex.item(0);
230                     String attributeName = getCharacterDataFromElement(line);
231                     logger.trace("attributeName: {}", attributeName);
232
233                     NodeList deviceID = element.getElementsByTagName("value");
234                     line = (Element) deviceID.item(0);
235                     String attributeValue = getCharacterDataFromElement(line);
236                     logger.trace("attributeValue: {}", attributeValue);
237
238                     switch (attributeName) {
239                         case "Mode":
240                             State newMode = new StringType("Brewing");
241                             State newAttributeValue;
242
243                             switch (attributeValue) {
244                                 case "0":
245                                     updateState(CHANNEL_STATE, OnOffType.ON);
246                                     newMode = new StringType("Refill");
247                                     updateState(CHANNEL_COFFEE_MODE, newMode);
248                                     break;
249                                 case "1":
250                                     updateState(CHANNEL_STATE, OnOffType.OFF);
251                                     newMode = new StringType("PlaceCarafe");
252                                     updateState(CHANNEL_COFFEE_MODE, newMode);
253                                     break;
254                                 case "2":
255                                     updateState(CHANNEL_STATE, OnOffType.OFF);
256                                     newMode = new StringType("RefillWater");
257                                     updateState(CHANNEL_COFFEE_MODE, newMode);
258                                     break;
259                                 case "3":
260                                     updateState(CHANNEL_STATE, OnOffType.OFF);
261                                     newMode = new StringType("Ready");
262                                     updateState(CHANNEL_COFFEE_MODE, newMode);
263                                     break;
264                                 case "4":
265                                     updateState(CHANNEL_STATE, OnOffType.ON);
266                                     newMode = new StringType("Brewing");
267                                     updateState(CHANNEL_COFFEE_MODE, newMode);
268                                     break;
269                                 case "5":
270                                     updateState(CHANNEL_STATE, OnOffType.OFF);
271                                     newMode = new StringType("Brewed");
272                                     updateState(CHANNEL_COFFEE_MODE, newMode);
273                                     break;
274                                 case "6":
275                                     updateState(CHANNEL_STATE, OnOffType.OFF);
276                                     newMode = new StringType("CleaningBrewing");
277                                     updateState(CHANNEL_COFFEE_MODE, newMode);
278                                     break;
279                                 case "7":
280                                     updateState(CHANNEL_STATE, OnOffType.OFF);
281                                     newMode = new StringType("CleaningSoaking");
282                                     updateState(CHANNEL_COFFEE_MODE, newMode);
283                                     break;
284                                 case "8":
285                                     updateState(CHANNEL_STATE, OnOffType.OFF);
286                                     newMode = new StringType("BrewFailCarafeRemoved");
287                                     updateState(CHANNEL_COFFEE_MODE, newMode);
288                                     break;
289                             }
290                             break;
291                         case "ModeTime":
292                             newAttributeValue = new DecimalType(attributeValue);
293                             updateState(CHANNEL_MODE_TIME, newAttributeValue);
294                             break;
295                         case "TimeRemaining":
296                             newAttributeValue = new DecimalType(attributeValue);
297                             updateState(CHANNEL_TIME_REMAINING, newAttributeValue);
298                             break;
299                         case "WaterLevelReached":
300                             newAttributeValue = new DecimalType(attributeValue);
301                             updateState(CHANNEL_WATER_LEVEL_REACHED, newAttributeValue);
302                             break;
303                         case "CleanAdvise":
304                             newAttributeValue = "0".equals(attributeValue) ? OnOffType.OFF : OnOffType.ON;
305                             updateState(CHANNEL_CLEAN_ADVISE, newAttributeValue);
306                             break;
307                         case "FilterAdvise":
308                             newAttributeValue = "0".equals(attributeValue) ? OnOffType.OFF : OnOffType.ON;
309                             updateState(CHANNEL_FILTER_ADVISE, newAttributeValue);
310                             break;
311                         case "Brewed":
312                             newAttributeValue = getDateTimeState(attributeValue);
313                             if (newAttributeValue != null) {
314                                 updateState(CHANNEL_BREWED, newAttributeValue);
315                             }
316                             break;
317                         case "LastCleaned":
318                             newAttributeValue = getDateTimeState(attributeValue);
319                             if (newAttributeValue != null) {
320                                 updateState(CHANNEL_LAST_CLEANED, newAttributeValue);
321                             }
322                             break;
323                     }
324                 }
325                 updateStatus(ThingStatus.ONLINE);
326             } catch (Exception e) {
327                 logger.warn("Failed to parse attributeList for WeMo CoffeMaker '{}'", this.getThing().getUID(), e);
328             }
329         } catch (Exception e) {
330             logger.warn("Failed to get attributes for device '{}'", getThing().getUID(), e);
331             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
332         }
333     }
334
335     public @Nullable State getDateTimeState(String attributeValue) {
336         long value = 0;
337         try {
338             value = Long.parseLong(attributeValue);
339         } catch (NumberFormatException e) {
340             logger.warn("Unable to parse attributeValue '{}' for device '{}'; expected long", attributeValue,
341                     getThing().getUID());
342             return null;
343         }
344         ZonedDateTime zoned = ZonedDateTime.ofInstant(Instant.ofEpochSecond(value), TimeZone.getDefault().toZoneId());
345         State dateTimeState = new DateTimeType(zoned);
346         logger.trace("New attribute brewed '{}' received", dateTimeState);
347         return dateTimeState;
348     }
349 }