2 * Copyright (c) 2010-2022 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.*;
16 import static org.openhab.binding.wemo.internal.WemoUtil.*;
18 import java.io.StringReader;
19 import java.math.BigDecimal;
21 import java.time.Instant;
22 import java.time.ZonedDateTime;
23 import java.util.Collections;
24 import java.util.HashMap;
27 import java.util.TimeZone;
28 import java.util.concurrent.ScheduledFuture;
29 import java.util.concurrent.TimeUnit;
31 import javax.xml.parsers.DocumentBuilder;
32 import javax.xml.parsers.DocumentBuilderFactory;
34 import org.eclipse.jdt.annotation.NonNullByDefault;
35 import org.eclipse.jdt.annotation.Nullable;
36 import org.openhab.binding.wemo.internal.http.WemoHttpCall;
37 import org.openhab.core.config.core.Configuration;
38 import org.openhab.core.io.transport.upnp.UpnpIOParticipant;
39 import org.openhab.core.io.transport.upnp.UpnpIOService;
40 import org.openhab.core.library.types.DateTimeType;
41 import org.openhab.core.library.types.DecimalType;
42 import org.openhab.core.library.types.OnOffType;
43 import org.openhab.core.library.types.StringType;
44 import org.openhab.core.thing.ChannelUID;
45 import org.openhab.core.thing.Thing;
46 import org.openhab.core.thing.ThingStatus;
47 import org.openhab.core.thing.ThingStatusDetail;
48 import org.openhab.core.thing.ThingTypeUID;
49 import org.openhab.core.types.Command;
50 import org.openhab.core.types.RefreshType;
51 import org.openhab.core.types.State;
52 import org.slf4j.Logger;
53 import org.slf4j.LoggerFactory;
54 import org.w3c.dom.CharacterData;
55 import org.w3c.dom.Document;
56 import org.w3c.dom.Element;
57 import org.w3c.dom.Node;
58 import org.w3c.dom.NodeList;
59 import org.xml.sax.InputSource;
62 * The {@link WemoCoffeeHandler} is responsible for handling commands, which are
63 * sent to one of the channels and to update their states.
65 * @author Hans-Jörg Merk - Initial contribution
66 * @author Erdoan Hadzhiyusein - Adapted the class to work with the new DateTimeType
69 public class WemoCoffeeHandler extends AbstractWemoHandler implements UpnpIOParticipant {
71 private final Logger logger = LoggerFactory.getLogger(WemoCoffeeHandler.class);
73 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_COFFEE);
75 private Map<String, Boolean> subscriptionState = new HashMap<>();
77 private UpnpIOService service;
79 private WemoHttpCall wemoCall;
81 private @Nullable ScheduledFuture<?> refreshJob;
83 private final Runnable refreshRunnable = new Runnable() {
88 if (!isUpnpDeviceRegistered()) {
89 logger.debug("WeMo UPnP device {} not yet registered", getUDN());
94 } catch (Exception e) {
95 logger.debug("Exception during poll", e);
96 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
101 public WemoCoffeeHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemoHttpCaller) {
102 super(thing, wemoHttpCaller);
104 this.wemoCall = wemoHttpCaller;
105 this.service = upnpIOService;
107 logger.debug("Creating a WemoCoffeeHandler V0.4 for thing '{}'", getThing().getUID());
111 public void initialize() {
112 Configuration configuration = getConfig();
114 if (configuration.get("udn") != null) {
115 logger.debug("Initializing WemoCoffeeHandler for UDN '{}'", configuration.get("udn"));
118 updateStatus(ThingStatus.ONLINE);
120 logger.debug("Cannot initalize WemoCoffeeHandler. UDN not set.");
125 public void dispose() {
126 logger.debug("WeMoCoffeeHandler disposed.");
128 ScheduledFuture<?> job = refreshJob;
129 if (job != null && !job.isCancelled()) {
133 removeSubscription();
137 public void handleCommand(ChannelUID channelUID, Command command) {
138 logger.trace("Command '{}' received for channel '{}'", command, channelUID);
140 if (command instanceof RefreshType) {
143 } catch (Exception e) {
144 logger.debug("Exception during poll", e);
146 } else if (channelUID.getId().equals(CHANNEL_STATE)) {
147 if (command instanceof OnOffType) {
148 if (command.equals(OnOffType.ON)) {
150 String soapHeader = "\"urn:Belkin:service:deviceevent:1#SetAttributes\"";
152 String content = "<?xml version=\"1.0\"?>"
153 + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
154 + "<s:Body>" + "<u:SetAttributes xmlns:u=\"urn:Belkin:service:deviceevent:1\">"
155 + "<attributeList><attribute><name>Brewed</name><value>NULL</value></attribute>"
156 + "<attribute><name>LastCleaned</name><value>NULL</value></attribute><attribute>"
157 + "<name>ModeTime</name><value>NULL</value></attribute><attribute><name>Brewing</name>"
158 + "<value>NULL</value></attribute><attribute><name>TimeRemaining</name><value>NULL</value>"
159 + "</attribute><attribute><name>WaterLevelReached</name><value>NULL</value></attribute><"
160 + "attribute><name>Mode</name><value>4</value></attribute><attribute><name>CleanAdvise</name>"
161 + "<value>NULL</value></attribute><attribute><name>FilterAdvise</name><value>NULL</value></attribute>"
162 + "<attribute><name>Cleaning</name><value>NULL</value></attribute></attributeList>"
163 + "</u:SetAttributes>" + "</s:Body>" + "</s:Envelope>";
165 URL descriptorURL = service.getDescriptorURL(this);
166 String wemoURL = getWemoURL(descriptorURL, "basicevent");
168 if (wemoURL != null) {
169 String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
170 if (wemoCallResponse != null) {
171 updateState(CHANNEL_STATE, OnOffType.ON);
172 State newMode = new StringType("Brewing");
173 updateState(CHANNEL_COFFEEMODE, newMode);
176 } catch (Exception e) {
177 logger.error("Failed to send command '{}' for device '{}': {}", command, getThing().getUID(),
179 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
182 // if command.equals(OnOffType.OFF) we do nothing because WeMo Coffee Maker cannot be switched off
184 updateStatus(ThingStatus.ONLINE);
190 public void onServiceSubscribed(@Nullable String service, boolean succeeded) {
191 if (service != null) {
192 logger.debug("WeMo {}: Subscription to service {} {}", getUDN(), service,
193 succeeded ? "succeeded" : "failed");
194 subscriptionState.put(service, succeeded);
199 public void onValueReceived(@Nullable String variable, @Nullable String value, @Nullable String service) {
200 // We can subscribe to GENA events, but there is no usefull response right now.
203 private synchronized void onSubscription() {
204 if (service.isRegistered(this)) {
205 logger.debug("Checking WeMo GENA subscription for '{}'", this);
207 String subscription = "deviceevent1";
208 if (subscriptionState.get(subscription) == null) {
209 logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(), subscription);
210 service.addSubscription(this, subscription, SUBSCRIPTION_DURATION_SECONDS);
211 subscriptionState.put(subscription, true);
214 logger.debug("Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
219 private synchronized void removeSubscription() {
220 logger.debug("Removing WeMo GENA subscription for '{}'", this);
222 if (service.isRegistered(this)) {
223 String subscription = "deviceevent1";
224 if (subscriptionState.get(subscription) != null) {
225 logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription);
226 service.removeSubscription(this, subscription);
229 subscriptionState = new HashMap<>();
230 service.unregisterParticipant(this);
234 private synchronized void onUpdate() {
235 ScheduledFuture<?> job = refreshJob;
236 if (job == null || job.isCancelled()) {
237 Configuration config = getThing().getConfiguration();
238 int refreshInterval = DEFAULT_REFRESH_INTERVALL_SECONDS;
239 Object refreshConfig = config.get("pollingInterval");
240 if (refreshConfig != null) {
241 refreshInterval = ((BigDecimal) refreshConfig).intValue();
242 logger.debug("Setting WemoCoffeeHandler refreshInterval to '{}' seconds", refreshInterval);
244 refreshJob = scheduler.scheduleWithFixedDelay(refreshRunnable, 0, refreshInterval, TimeUnit.SECONDS);
248 private boolean isUpnpDeviceRegistered() {
249 return service.isRegistered(this);
253 public String getUDN() {
254 return (String) this.getThing().getConfiguration().get(UDN);
258 * The {@link updateWemoState} polls the actual state of a WeMo CoffeeMaker.
260 protected void updateWemoState() {
261 String action = "GetAttributes";
262 String actionService = "deviceevent";
264 String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
265 String content = "<?xml version=\"1.0\"?>"
266 + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
267 + "<s:Body>" + "<u:" + action + " xmlns:u=\"urn:Belkin:service:" + actionService + ":1\">" + "</u:"
268 + action + ">" + "</s:Body>" + "</s:Envelope>";
271 URL descriptorURL = service.getDescriptorURL(this);
272 String wemoURL = getWemoURL(descriptorURL, actionService);
274 if (wemoURL != null) {
275 String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
276 if (wemoCallResponse != null) {
278 String stringParser = substringBetween(wemoCallResponse, "<attributeList>", "</attributeList>");
280 // Due to Belkins bad response formatting, we need to run this twice.
281 stringParser = unescapeXml(stringParser);
282 stringParser = unescapeXml(stringParser);
284 logger.trace("CoffeeMaker response '{}' for device '{}' received", stringParser,
285 getThing().getUID());
287 stringParser = "<data>" + stringParser + "</data>";
289 DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
291 // https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html
292 dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
293 dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
294 dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
295 dbf.setXIncludeAware(false);
296 dbf.setExpandEntityReferences(false);
297 DocumentBuilder db = dbf.newDocumentBuilder();
298 InputSource is = new InputSource();
299 is.setCharacterStream(new StringReader(stringParser));
301 Document doc = db.parse(is);
302 NodeList nodes = doc.getElementsByTagName("attribute");
304 // iterate the attributes
305 for (int i = 0; i < nodes.getLength(); i++) {
306 Element element = (Element) nodes.item(i);
308 NodeList deviceIndex = element.getElementsByTagName("name");
309 Element line = (Element) deviceIndex.item(0);
310 String attributeName = getCharacterDataFromElement(line);
311 logger.trace("attributeName: {}", attributeName);
313 NodeList deviceID = element.getElementsByTagName("value");
314 line = (Element) deviceID.item(0);
315 String attributeValue = getCharacterDataFromElement(line);
316 logger.trace("attributeValue: {}", attributeValue);
318 switch (attributeName) {
320 State newMode = new StringType("Brewing");
321 State newAttributeValue;
323 switch (attributeValue) {
325 updateState(CHANNEL_STATE, OnOffType.ON);
326 newMode = new StringType("Refill");
327 updateState(CHANNEL_COFFEEMODE, newMode);
330 updateState(CHANNEL_STATE, OnOffType.OFF);
331 newMode = new StringType("PlaceCarafe");
332 updateState(CHANNEL_COFFEEMODE, newMode);
335 updateState(CHANNEL_STATE, OnOffType.OFF);
336 newMode = new StringType("RefillWater");
337 updateState(CHANNEL_COFFEEMODE, newMode);
340 updateState(CHANNEL_STATE, OnOffType.OFF);
341 newMode = new StringType("Ready");
342 updateState(CHANNEL_COFFEEMODE, newMode);
345 updateState(CHANNEL_STATE, OnOffType.ON);
346 newMode = new StringType("Brewing");
347 updateState(CHANNEL_COFFEEMODE, newMode);
350 updateState(CHANNEL_STATE, OnOffType.OFF);
351 newMode = new StringType("Brewed");
352 updateState(CHANNEL_COFFEEMODE, newMode);
355 updateState(CHANNEL_STATE, OnOffType.OFF);
356 newMode = new StringType("CleaningBrewing");
357 updateState(CHANNEL_COFFEEMODE, newMode);
360 updateState(CHANNEL_STATE, OnOffType.OFF);
361 newMode = new StringType("CleaningSoaking");
362 updateState(CHANNEL_COFFEEMODE, newMode);
365 updateState(CHANNEL_STATE, OnOffType.OFF);
366 newMode = new StringType("BrewFailCarafeRemoved");
367 updateState(CHANNEL_COFFEEMODE, newMode);
372 newAttributeValue = new DecimalType(attributeValue);
373 updateState(CHANNEL_MODETIME, newAttributeValue);
375 case "TimeRemaining":
376 newAttributeValue = new DecimalType(attributeValue);
377 updateState(CHANNEL_TIMEREMAINING, newAttributeValue);
379 case "WaterLevelReached":
380 newAttributeValue = new DecimalType(attributeValue);
381 updateState(CHANNEL_WATERLEVELREACHED, newAttributeValue);
384 newAttributeValue = attributeValue.equals("0") ? OnOffType.OFF : OnOffType.ON;
385 updateState(CHANNEL_CLEANADVISE, newAttributeValue);
388 newAttributeValue = attributeValue.equals("0") ? OnOffType.OFF : OnOffType.ON;
389 updateState(CHANNEL_FILTERADVISE, newAttributeValue);
392 newAttributeValue = getDateTimeState(attributeValue);
393 if (newAttributeValue != null) {
394 updateState(CHANNEL_BREWED, newAttributeValue);
398 newAttributeValue = getDateTimeState(attributeValue);
399 if (newAttributeValue != null) {
400 updateState(CHANNEL_LASTCLEANED, newAttributeValue);
405 } catch (Exception e) {
406 logger.error("Failed to parse attributeList for WeMo CoffeMaker '{}'", this.getThing().getUID(),
411 } catch (Exception e) {
412 logger.error("Failed to get attributes for device '{}'", getThing().getUID(), e);
416 public @Nullable State getDateTimeState(String attributeValue) {
419 value = Long.parseLong(attributeValue);
420 } catch (NumberFormatException e) {
421 logger.error("Unable to parse attributeValue '{}' for device '{}'; expected long", attributeValue,
422 getThing().getUID());
425 ZonedDateTime zoned = ZonedDateTime.ofInstant(Instant.ofEpochSecond(value), TimeZone.getDefault().toZoneId());
426 State dateTimeState = new DateTimeType(zoned);
427 logger.trace("New attribute brewed '{}' received", dateTimeState);
428 return dateTimeState;
431 public static String getCharacterDataFromElement(Element e) {
432 Node child = e.getFirstChild();
433 if (child instanceof CharacterData) {
434 CharacterData cd = (CharacterData) child;
441 public void onStatusChanged(boolean status) {