2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.wemo.internal.handler;
15 import static org.openhab.binding.wemo.internal.WemoBindingConstants.*;
17 import java.io.StringReader;
18 import java.math.BigDecimal;
20 import java.time.Instant;
21 import java.time.ZonedDateTime;
22 import java.util.Collections;
23 import java.util.HashMap;
26 import java.util.TimeZone;
27 import java.util.concurrent.ScheduledFuture;
28 import java.util.concurrent.TimeUnit;
30 import javax.xml.parsers.DocumentBuilder;
31 import javax.xml.parsers.DocumentBuilderFactory;
33 import org.apache.commons.lang.StringEscapeUtils;
34 import org.apache.commons.lang.StringUtils;
35 import org.openhab.binding.wemo.internal.http.WemoHttpCall;
36 import org.openhab.core.config.core.Configuration;
37 import org.openhab.core.io.transport.upnp.UpnpIOParticipant;
38 import org.openhab.core.io.transport.upnp.UpnpIOService;
39 import org.openhab.core.library.types.DateTimeType;
40 import org.openhab.core.library.types.DecimalType;
41 import org.openhab.core.library.types.OnOffType;
42 import org.openhab.core.library.types.StringType;
43 import org.openhab.core.thing.ChannelUID;
44 import org.openhab.core.thing.Thing;
45 import org.openhab.core.thing.ThingStatus;
46 import org.openhab.core.thing.ThingStatusDetail;
47 import org.openhab.core.thing.ThingTypeUID;
48 import org.openhab.core.types.Command;
49 import org.openhab.core.types.RefreshType;
50 import org.openhab.core.types.State;
51 import org.slf4j.Logger;
52 import org.slf4j.LoggerFactory;
53 import org.w3c.dom.CharacterData;
54 import org.w3c.dom.Document;
55 import org.w3c.dom.Element;
56 import org.w3c.dom.Node;
57 import org.w3c.dom.NodeList;
58 import org.xml.sax.InputSource;
61 * The {@link WemoCoffeeHandler} is responsible for handling commands, which are
62 * sent to one of the channels and to update their states.
64 * @author Hans-Jörg Merk - Initial contribution
65 * @author Erdoan Hadzhiyusein - Adapted the class to work with the new DateTimeType
68 public class WemoCoffeeHandler extends AbstractWemoHandler implements UpnpIOParticipant {
70 private final Logger logger = LoggerFactory.getLogger(WemoCoffeeHandler.class);
72 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_COFFEE);
74 private Map<String, Boolean> subscriptionState = new HashMap<>();
76 protected static final int SUBSCRIPTION_DURATION = 600;
78 private UpnpIOService service;
81 * The default refresh interval in Seconds.
83 private final int REFRESH_INTERVAL = 60;
85 private ScheduledFuture<?> refreshJob;
87 private final Runnable refreshRunnable = new Runnable() {
92 if (!isUpnpDeviceRegistered()) {
93 logger.debug("WeMo UPnP device {} not yet registered", getUDN());
98 } catch (Exception e) {
99 logger.debug("Exception during poll", e);
100 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
105 public WemoCoffeeHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemoHttpcaller) {
108 this.wemoHttpCaller = wemoHttpcaller;
110 logger.debug("Creating a WemoCoffeeHandler V0.4 for thing '{}'", getThing().getUID());
112 if (upnpIOService != null) {
113 this.service = upnpIOService;
115 logger.debug("upnpIOService not set.");
120 public void initialize() {
121 Configuration configuration = getConfig();
123 if (configuration.get("udn") != null) {
124 logger.debug("Initializing WemoCoffeeHandler for UDN '{}'", configuration.get("udn"));
127 updateStatus(ThingStatus.ONLINE);
129 logger.debug("Cannot initalize WemoCoffeeHandler. UDN not set.");
134 public void dispose() {
135 logger.debug("WeMoCoffeeHandler disposed.");
137 removeSubscription();
139 if (refreshJob != null && !refreshJob.isCancelled()) {
140 refreshJob.cancel(true);
146 public void handleCommand(ChannelUID channelUID, Command command) {
147 logger.trace("Command '{}' received for channel '{}'", command, channelUID);
149 if (command instanceof RefreshType) {
152 } catch (Exception e) {
153 logger.debug("Exception during poll", e);
155 } else if (channelUID.getId().equals(CHANNEL_STATE)) {
156 if (command instanceof OnOffType) {
157 if (command.equals(OnOffType.ON)) {
159 String soapHeader = "\"urn:Belkin:service:deviceevent:1#SetAttributes\"";
161 String content = "<?xml version=\"1.0\"?>"
162 + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
163 + "<s:Body>" + "<u:SetAttributes xmlns:u=\"urn:Belkin:service:deviceevent:1\">"
164 + "<attributeList><attribute><name>Brewed</name><value>NULL</value></attribute>"
165 + "<attribute><name>LastCleaned</name><value>NULL</value></attribute><attribute>"
166 + "<name>ModeTime</name><value>NULL</value></attribute><attribute><name>Brewing</name>"
167 + "<value>NULL</value></attribute><attribute><name>TimeRemaining</name><value>NULL</value>"
168 + "</attribute><attribute><name>WaterLevelReached</name><value>NULL</value></attribute><"
169 + "attribute><name>Mode</name><value>4</value></attribute><attribute><name>CleanAdvise</name>"
170 + "<value>NULL</value></attribute><attribute><name>FilterAdvise</name><value>NULL</value></attribute>"
171 + "<attribute><name>Cleaning</name><value>NULL</value></attribute></attributeList>"
172 + "</u:SetAttributes>" + "</s:Body>" + "</s:Envelope>";
174 String wemoURL = getWemoURL("deviceevent");
176 if (wemoURL != null) {
177 String wemoCallResponse = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
178 if (wemoCallResponse != null) {
179 updateState(CHANNEL_STATE, OnOffType.ON);
180 State newMode = new StringType("Brewing");
181 updateState(CHANNEL_COFFEEMODE, newMode);
184 } catch (Exception e) {
185 logger.error("Failed to send command '{}' for device '{}': {}", command, getThing().getUID(),
187 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
190 // if command.equals(OnOffType.OFF) we do nothing because WeMo Coffee Maker cannot be switched off
192 updateStatus(ThingStatus.ONLINE);
198 public void onServiceSubscribed(String service, boolean succeeded) {
199 logger.debug("WeMo {}: Subscription to service {} {}", getUDN(), service, succeeded ? "succeeded" : "failed");
200 subscriptionState.put(service, succeeded);
204 public void onValueReceived(String variable, String value, String service) {
205 // We can subscribe to GENA events, but there is no usefull response right now.
208 private synchronized void onSubscription() {
209 if (service.isRegistered(this)) {
210 logger.debug("Checking WeMo GENA subscription for '{}'", this);
212 String subscription = "deviceevent1";
213 if ((subscriptionState.get(subscription) == null) || !subscriptionState.get(subscription).booleanValue()) {
214 logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(), subscription);
215 service.addSubscription(this, subscription, SUBSCRIPTION_DURATION);
216 subscriptionState.put(subscription, true);
219 logger.debug("Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
224 private synchronized void removeSubscription() {
225 logger.debug("Removing WeMo GENA subscription for '{}'", this);
227 if (service.isRegistered(this)) {
228 String subscription = "deviceevent1";
229 if ((subscriptionState.get(subscription) != null) && subscriptionState.get(subscription).booleanValue()) {
230 logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription);
231 service.removeSubscription(this, subscription);
234 subscriptionState = new HashMap<>();
235 service.unregisterParticipant(this);
239 private synchronized void onUpdate() {
240 if (refreshJob == null || refreshJob.isCancelled()) {
241 Configuration config = getThing().getConfiguration();
242 int refreshInterval = REFRESH_INTERVAL;
243 Object refreshConfig = config.get("pollingInterval");
244 if (refreshConfig != null) {
245 refreshInterval = ((BigDecimal) refreshConfig).intValue();
246 logger.debug("Setting WemoCoffeeHandler refreshInterval to '{}' seconds", refreshInterval);
248 refreshJob = scheduler.scheduleWithFixedDelay(refreshRunnable, 0, refreshInterval, TimeUnit.SECONDS);
252 private boolean isUpnpDeviceRegistered() {
253 return service.isRegistered(this);
257 public String getUDN() {
258 return (String) this.getThing().getConfiguration().get(UDN);
262 * The {@link updateWemoState} polls the actual state of a WeMo CoffeeMaker.
264 protected void updateWemoState() {
265 String action = "GetAttributes";
266 String actionService = "deviceevent";
268 String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
269 String content = "<?xml version=\"1.0\"?>"
270 + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
271 + "<s:Body>" + "<u:" + action + " xmlns:u=\"urn:Belkin:service:" + actionService + ":1\">" + "</u:"
272 + action + ">" + "</s:Body>" + "</s:Envelope>";
275 String wemoURL = getWemoURL(actionService);
276 if (wemoURL != null) {
277 String wemoCallResponse = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
278 if (wemoCallResponse != null) {
280 String stringParser = StringUtils.substringBetween(wemoCallResponse, "<attributeList>",
283 // Due to Belkins bad response formatting, we need to run this twice.
284 stringParser = StringEscapeUtils.unescapeXml(stringParser);
285 stringParser = StringEscapeUtils.unescapeXml(stringParser);
287 logger.trace("CoffeeMaker response '{}' for device '{}' received", stringParser,
288 getThing().getUID());
290 stringParser = "<data>" + stringParser + "</data>";
292 DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
293 DocumentBuilder db = dbf.newDocumentBuilder();
294 InputSource is = new InputSource();
295 is.setCharacterStream(new StringReader(stringParser));
297 Document doc = db.parse(is);
298 NodeList nodes = doc.getElementsByTagName("attribute");
300 // iterate the attributes
301 for (int i = 0; i < nodes.getLength(); i++) {
302 Element element = (Element) nodes.item(i);
304 NodeList deviceIndex = element.getElementsByTagName("name");
305 Element line = (Element) deviceIndex.item(0);
306 String attributeName = getCharacterDataFromElement(line);
307 logger.trace("attributeName: {}", attributeName);
309 NodeList deviceID = element.getElementsByTagName("value");
310 line = (Element) deviceID.item(0);
311 String attributeValue = getCharacterDataFromElement(line);
312 logger.trace("attributeValue: {}", attributeValue);
314 switch (attributeName) {
316 State newMode = new StringType("Brewing");
317 switch (attributeValue) {
319 updateState(CHANNEL_STATE, OnOffType.ON);
320 newMode = new StringType("Refill");
321 updateState(CHANNEL_COFFEEMODE, newMode);
324 updateState(CHANNEL_STATE, OnOffType.OFF);
325 newMode = new StringType("PlaceCarafe");
326 updateState(CHANNEL_COFFEEMODE, newMode);
329 updateState(CHANNEL_STATE, OnOffType.OFF);
330 newMode = new StringType("RefillWater");
331 updateState(CHANNEL_COFFEEMODE, newMode);
334 updateState(CHANNEL_STATE, OnOffType.OFF);
335 newMode = new StringType("Ready");
336 updateState(CHANNEL_COFFEEMODE, newMode);
339 updateState(CHANNEL_STATE, OnOffType.ON);
340 newMode = new StringType("Brewing");
341 updateState(CHANNEL_COFFEEMODE, newMode);
344 updateState(CHANNEL_STATE, OnOffType.OFF);
345 newMode = new StringType("Brewed");
346 updateState(CHANNEL_COFFEEMODE, newMode);
349 updateState(CHANNEL_STATE, OnOffType.OFF);
350 newMode = new StringType("CleaningBrewing");
351 updateState(CHANNEL_COFFEEMODE, newMode);
354 updateState(CHANNEL_STATE, OnOffType.OFF);
355 newMode = new StringType("CleaningSoaking");
356 updateState(CHANNEL_COFFEEMODE, newMode);
359 updateState(CHANNEL_STATE, OnOffType.OFF);
360 newMode = new StringType("BrewFailCarafeRemoved");
361 updateState(CHANNEL_COFFEEMODE, newMode);
366 if (attributeValue != null) {
367 State newAttributeValue = new DecimalType(attributeValue);
368 updateState(CHANNEL_MODETIME, newAttributeValue);
371 case "TimeRemaining":
372 if (attributeValue != null) {
373 State newAttributeValue = new DecimalType(attributeValue);
374 updateState(CHANNEL_TIMEREMAINING, newAttributeValue);
377 case "WaterLevelReached":
378 if (attributeValue != null) {
379 State newAttributeValue = new DecimalType(attributeValue);
380 updateState(CHANNEL_WATERLEVELREACHED, newAttributeValue);
384 if (attributeValue != null) {
385 State newAttributeValue = attributeValue.equals("0") ? OnOffType.OFF
387 updateState(CHANNEL_CLEANADVISE, newAttributeValue);
391 if (attributeValue != null) {
392 State newAttributeValue = attributeValue.equals("0") ? OnOffType.OFF
394 updateState(CHANNEL_FILTERADVISE, newAttributeValue);
398 if (attributeValue != null) {
399 State newAttributeValue = getDateTimeState(attributeValue);
400 if (newAttributeValue != null) {
401 updateState(CHANNEL_BREWED, newAttributeValue);
406 if (attributeValue != null) {
407 State newAttributeValue = getDateTimeState(attributeValue);
408 if (newAttributeValue != null) {
409 updateState(CHANNEL_LASTCLEANED, newAttributeValue);
415 } catch (Exception e) {
416 logger.error("Failed to parse attributeList for WeMo CoffeMaker '{}'", this.getThing().getUID(),
421 } catch (Exception e) {
422 logger.error("Failed to get attributes for device '{}'", getThing().getUID(), e);
426 @SuppressWarnings("null")
427 public State getDateTimeState(String attributeValue) {
428 if (attributeValue != null) {
431 value = Long.parseLong(attributeValue) * 1000; // convert s to ms
432 } catch (NumberFormatException e) {
433 logger.error("Unable to parse attributeValue '{}' for device '{}'; expected long", attributeValue,
434 getThing().getUID());
437 ZonedDateTime zoned = ZonedDateTime.ofInstant(Instant.ofEpochMilli(value),
438 TimeZone.getDefault().toZoneId());
439 State dateTimeState = new DateTimeType(zoned);
440 if (dateTimeState != null) {
441 logger.trace("New attribute brewed '{}' received", dateTimeState);
442 return dateTimeState;
448 public String getWemoURL(String actionService) {
449 URL descriptorURL = service.getDescriptorURL(this);
450 String wemoURL = null;
451 if (descriptorURL != null) {
452 String deviceURL = StringUtils.substringBefore(descriptorURL.toString(), "/setup.xml");
453 wemoURL = deviceURL + "/upnp/control/" + actionService + "1";
459 public static String getCharacterDataFromElement(Element e) {
460 Node child = e.getFirstChild();
461 if (child instanceof CharacterData) {
462 CharacterData cd = (CharacterData) child;
469 public void onStatusChanged(boolean status) {