]> git.basschouten.com Git - openhab-addons.git/blob
0f0ac64ea5a69f744aeed512edebe734f8c4cf65
[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.jupnp.UpnpService;
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.DateTimeType;
37 import org.openhab.core.library.types.DecimalType;
38 import org.openhab.core.library.types.OnOffType;
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
55 /**
56  * The {@link WemoCoffeeHandler} 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  * @author Erdoan Hadzhiyusein - Adapted the class to work with the new DateTimeType
61  */
62 @NonNullByDefault
63 public class WemoCoffeeHandler extends WemoBaseThingHandler {
64
65     private final Logger logger = LoggerFactory.getLogger(WemoCoffeeHandler.class);
66
67     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_COFFEE);
68
69     private final Object jobLock = new Object();
70
71     private @Nullable ScheduledFuture<?> pollingJob;
72
73     public WemoCoffeeHandler(Thing thing, UpnpIOService upnpIOService, UpnpService upnpService,
74             WemoHttpCall wemoHttpCaller) {
75         super(thing, upnpIOService, upnpService, wemoHttpCaller);
76
77         logger.debug("Creating a WemoCoffeeHandler for thing '{}'", getThing().getUID());
78     }
79
80     @Override
81     public void initialize() {
82         super.initialize();
83         Configuration configuration = getConfig();
84
85         if (configuration.get(UDN) != null) {
86             logger.debug("Initializing WemoCoffeeHandler for UDN '{}'", configuration.get(UDN));
87             addSubscription(DEVICEEVENT);
88             pollingJob = scheduler.scheduleWithFixedDelay(this::poll, 0, DEFAULT_REFRESH_INTERVAL_SECONDS,
89                     TimeUnit.SECONDS);
90             updateStatus(ThingStatus.UNKNOWN);
91         } else {
92             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
93                     "@text/config-status.error.missing-udn");
94         }
95     }
96
97     @Override
98     public void dispose() {
99         logger.debug("WemoCoffeeHandler disposed.");
100         ScheduledFuture<?> job = this.pollingJob;
101         if (job != null && !job.isCancelled()) {
102             job.cancel(true);
103         }
104         this.pollingJob = null;
105         super.dispose();
106     }
107
108     private void poll() {
109         synchronized (jobLock) {
110             if (pollingJob == null) {
111                 return;
112             }
113             try {
114                 logger.debug("Polling job");
115
116                 // Check if the Wemo device is set in the UPnP service registry
117                 if (!isUpnpDeviceRegistered()) {
118                     logger.debug("UPnP device {} not yet registered", getUDN());
119                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
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_COFFEE_MODE, 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_COFFEE_MODE, newMode);
250                                     break;
251                                 case "1":
252                                     updateState(CHANNEL_STATE, OnOffType.OFF);
253                                     newMode = new StringType("PlaceCarafe");
254                                     updateState(CHANNEL_COFFEE_MODE, newMode);
255                                     break;
256                                 case "2":
257                                     updateState(CHANNEL_STATE, OnOffType.OFF);
258                                     newMode = new StringType("RefillWater");
259                                     updateState(CHANNEL_COFFEE_MODE, newMode);
260                                     break;
261                                 case "3":
262                                     updateState(CHANNEL_STATE, OnOffType.OFF);
263                                     newMode = new StringType("Ready");
264                                     updateState(CHANNEL_COFFEE_MODE, newMode);
265                                     break;
266                                 case "4":
267                                     updateState(CHANNEL_STATE, OnOffType.ON);
268                                     newMode = new StringType("Brewing");
269                                     updateState(CHANNEL_COFFEE_MODE, newMode);
270                                     break;
271                                 case "5":
272                                     updateState(CHANNEL_STATE, OnOffType.OFF);
273                                     newMode = new StringType("Brewed");
274                                     updateState(CHANNEL_COFFEE_MODE, newMode);
275                                     break;
276                                 case "6":
277                                     updateState(CHANNEL_STATE, OnOffType.OFF);
278                                     newMode = new StringType("CleaningBrewing");
279                                     updateState(CHANNEL_COFFEE_MODE, newMode);
280                                     break;
281                                 case "7":
282                                     updateState(CHANNEL_STATE, OnOffType.OFF);
283                                     newMode = new StringType("CleaningSoaking");
284                                     updateState(CHANNEL_COFFEE_MODE, newMode);
285                                     break;
286                                 case "8":
287                                     updateState(CHANNEL_STATE, OnOffType.OFF);
288                                     newMode = new StringType("BrewFailCarafeRemoved");
289                                     updateState(CHANNEL_COFFEE_MODE, newMode);
290                                     break;
291                             }
292                             break;
293                         case "ModeTime":
294                             newAttributeValue = new DecimalType(attributeValue);
295                             updateState(CHANNEL_MODE_TIME, newAttributeValue);
296                             break;
297                         case "TimeRemaining":
298                             newAttributeValue = new DecimalType(attributeValue);
299                             updateState(CHANNEL_TIME_REMAINING, newAttributeValue);
300                             break;
301                         case "WaterLevelReached":
302                             newAttributeValue = new DecimalType(attributeValue);
303                             updateState(CHANNEL_WATER_LEVEL_REACHED, newAttributeValue);
304                             break;
305                         case "CleanAdvise":
306                             newAttributeValue = "0".equals(attributeValue) ? OnOffType.OFF : OnOffType.ON;
307                             updateState(CHANNEL_CLEAN_ADVISE, newAttributeValue);
308                             break;
309                         case "FilterAdvise":
310                             newAttributeValue = "0".equals(attributeValue) ? OnOffType.OFF : OnOffType.ON;
311                             updateState(CHANNEL_FILTER_ADVISE, 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_LAST_CLEANED, 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 }