2 * Copyright (c) 2010-2023 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.time.Instant;
20 import java.time.ZonedDateTime;
22 import java.util.TimeZone;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
26 import javax.xml.parsers.DocumentBuilder;
27 import javax.xml.parsers.DocumentBuilderFactory;
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.openhab.binding.wemo.internal.http.WemoHttpCall;
32 import org.openhab.core.config.core.Configuration;
33 import org.openhab.core.io.transport.upnp.UpnpIOService;
34 import org.openhab.core.library.types.DateTimeType;
35 import org.openhab.core.library.types.DecimalType;
36 import org.openhab.core.library.types.OnOffType;
37 import org.openhab.core.library.types.StringType;
38 import org.openhab.core.thing.ChannelUID;
39 import org.openhab.core.thing.Thing;
40 import org.openhab.core.thing.ThingStatus;
41 import org.openhab.core.thing.ThingStatusDetail;
42 import org.openhab.core.thing.ThingTypeUID;
43 import org.openhab.core.types.Command;
44 import org.openhab.core.types.RefreshType;
45 import org.openhab.core.types.State;
46 import org.slf4j.Logger;
47 import org.slf4j.LoggerFactory;
48 import org.w3c.dom.Document;
49 import org.w3c.dom.Element;
50 import org.w3c.dom.NodeList;
51 import org.xml.sax.InputSource;
54 * The {@link WemoCoffeeHandler} is responsible for handling commands, which are
55 * sent to one of the channels and to update their states.
57 * @author Hans-Jörg Merk - Initial contribution
58 * @author Erdoan Hadzhiyusein - Adapted the class to work with the new DateTimeType
61 public class WemoCoffeeHandler extends WemoBaseThingHandler {
63 private final Logger logger = LoggerFactory.getLogger(WemoCoffeeHandler.class);
65 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_COFFEE);
67 private final Object jobLock = new Object();
69 private @Nullable ScheduledFuture<?> pollingJob;
71 public WemoCoffeeHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemoHttpCaller) {
72 super(thing, upnpIOService, wemoHttpCaller);
74 logger.debug("Creating a WemoCoffeeHandler for thing '{}'", getThing().getUID());
78 public void initialize() {
80 Configuration configuration = getConfig();
82 if (configuration.get(UDN) != null) {
83 logger.debug("Initializing WemoCoffeeHandler for UDN '{}'", configuration.get(UDN));
84 addSubscription(DEVICEEVENT);
85 pollingJob = scheduler.scheduleWithFixedDelay(this::poll, 0, DEFAULT_REFRESH_INTERVAL_SECONDS,
87 updateStatus(ThingStatus.UNKNOWN);
89 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
90 "@text/config-status.error.missing-udn");
95 public void dispose() {
96 logger.debug("WemoCoffeeHandler disposed.");
97 ScheduledFuture<?> job = this.pollingJob;
98 if (job != null && !job.isCancelled()) {
101 this.pollingJob = null;
105 private void poll() {
106 synchronized (jobLock) {
107 if (pollingJob == null) {
111 logger.debug("Polling job");
113 // Check if the Wemo device is set in the UPnP service registry
114 if (!isUpnpDeviceRegistered()) {
115 logger.debug("UPnP device {} not yet registered", getUDN());
116 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
117 "@text/config-status.pending.device-not-registered [\"" + getUDN() + "\"]");
121 } catch (Exception e) {
122 logger.debug("Exception during poll: {}", e.getMessage(), e);
128 public void handleCommand(ChannelUID channelUID, Command command) {
129 String wemoURL = getWemoURL(BASICACTION);
130 if (wemoURL == null) {
131 logger.debug("Failed to send command '{}' for device '{}': URL cannot be created", command,
132 getThing().getUID());
135 if (command instanceof RefreshType) {
138 } catch (Exception e) {
139 logger.debug("Exception during poll", e);
141 } else if (channelUID.getId().equals(CHANNEL_STATE)) {
142 if (command instanceof OnOffType) {
143 if (command.equals(OnOffType.ON)) {
145 String soapHeader = "\"urn:Belkin:service:deviceevent:1#SetAttributes\"";
148 <?xml version="1.0"?>\
149 <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">\
151 <u:SetAttributes xmlns:u="urn:Belkin:service:deviceevent:1">\
152 <attributeList><attribute><name>Brewed</name><value>NULL</value></attribute>\
153 <attribute><name>LastCleaned</name><value>NULL</value></attribute><attribute>\
154 <name>ModeTime</name><value>NULL</value></attribute><attribute><name>Brewing</name>\
155 <value>NULL</value></attribute><attribute><name>TimeRemaining</name><value>NULL</value>\
156 </attribute><attribute><name>WaterLevelReached</name><value>NULL</value></attribute><\
157 attribute><name>Mode</name><value>4</value></attribute><attribute><name>CleanAdvise</name>\
158 <value>NULL</value></attribute><attribute><name>FilterAdvise</name><value>NULL</value></attribute>\
159 <attribute><name>Cleaning</name><value>NULL</value></attribute></attributeList>\
165 wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
166 updateState(CHANNEL_STATE, OnOffType.ON);
167 State newMode = new StringType("Brewing");
168 updateState(CHANNEL_COFFEE_MODE, newMode);
169 updateStatus(ThingStatus.ONLINE);
170 } catch (Exception e) {
171 logger.warn("Failed to send command '{}' for device '{}': {}", command, getThing().getUID(),
173 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
176 // if command.equals(OnOffType.OFF) we do nothing because WeMo Coffee Maker cannot be switched
183 public void onValueReceived(@Nullable String variable, @Nullable String value, @Nullable String service) {
184 // We can subscribe to GENA events, but there is no usefull response right now.
188 * The {@link updateWemoState} polls the actual state of a WeMo CoffeeMaker.
190 protected void updateWemoState() {
191 String actionService = DEVICEACTION;
192 String wemoURL = getWemoURL(actionService);
193 if (wemoURL == null) {
194 logger.debug("Failed to get actual state for device '{}': URL cannot be created", getThing().getUID());
198 String action = "GetAttributes";
199 String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
200 String content = createStateRequestContent(action, actionService);
201 String wemoCallResponse = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
203 String stringParser = substringBetween(wemoCallResponse, "<attributeList>", "</attributeList>");
205 // Due to Belkins bad response formatting, we need to run this twice.
206 stringParser = unescapeXml(stringParser);
207 stringParser = unescapeXml(stringParser);
209 logger.trace("CoffeeMaker response '{}' for device '{}' received", stringParser, getThing().getUID());
211 stringParser = "<data>" + stringParser + "</data>";
213 DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
215 // https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html
216 dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
217 dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
218 dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
219 dbf.setXIncludeAware(false);
220 dbf.setExpandEntityReferences(false);
221 DocumentBuilder db = dbf.newDocumentBuilder();
222 InputSource is = new InputSource();
223 is.setCharacterStream(new StringReader(stringParser));
225 Document doc = db.parse(is);
226 NodeList nodes = doc.getElementsByTagName("attribute");
228 // iterate the attributes
229 for (int i = 0; i < nodes.getLength(); i++) {
230 Element element = (Element) nodes.item(i);
232 NodeList deviceIndex = element.getElementsByTagName("name");
233 Element line = (Element) deviceIndex.item(0);
234 String attributeName = getCharacterDataFromElement(line);
235 logger.trace("attributeName: {}", attributeName);
237 NodeList deviceID = element.getElementsByTagName("value");
238 line = (Element) deviceID.item(0);
239 String attributeValue = getCharacterDataFromElement(line);
240 logger.trace("attributeValue: {}", attributeValue);
242 switch (attributeName) {
244 State newMode = new StringType("Brewing");
245 State newAttributeValue;
247 switch (attributeValue) {
249 updateState(CHANNEL_STATE, OnOffType.ON);
250 newMode = new StringType("Refill");
251 updateState(CHANNEL_COFFEE_MODE, newMode);
254 updateState(CHANNEL_STATE, OnOffType.OFF);
255 newMode = new StringType("PlaceCarafe");
256 updateState(CHANNEL_COFFEE_MODE, newMode);
259 updateState(CHANNEL_STATE, OnOffType.OFF);
260 newMode = new StringType("RefillWater");
261 updateState(CHANNEL_COFFEE_MODE, newMode);
264 updateState(CHANNEL_STATE, OnOffType.OFF);
265 newMode = new StringType("Ready");
266 updateState(CHANNEL_COFFEE_MODE, newMode);
269 updateState(CHANNEL_STATE, OnOffType.ON);
270 newMode = new StringType("Brewing");
271 updateState(CHANNEL_COFFEE_MODE, newMode);
274 updateState(CHANNEL_STATE, OnOffType.OFF);
275 newMode = new StringType("Brewed");
276 updateState(CHANNEL_COFFEE_MODE, newMode);
279 updateState(CHANNEL_STATE, OnOffType.OFF);
280 newMode = new StringType("CleaningBrewing");
281 updateState(CHANNEL_COFFEE_MODE, newMode);
284 updateState(CHANNEL_STATE, OnOffType.OFF);
285 newMode = new StringType("CleaningSoaking");
286 updateState(CHANNEL_COFFEE_MODE, newMode);
289 updateState(CHANNEL_STATE, OnOffType.OFF);
290 newMode = new StringType("BrewFailCarafeRemoved");
291 updateState(CHANNEL_COFFEE_MODE, newMode);
296 newAttributeValue = new DecimalType(attributeValue);
297 updateState(CHANNEL_MODE_TIME, newAttributeValue);
299 case "TimeRemaining":
300 newAttributeValue = new DecimalType(attributeValue);
301 updateState(CHANNEL_TIME_REMAINING, newAttributeValue);
303 case "WaterLevelReached":
304 newAttributeValue = new DecimalType(attributeValue);
305 updateState(CHANNEL_WATER_LEVEL_REACHED, newAttributeValue);
308 newAttributeValue = "0".equals(attributeValue) ? OnOffType.OFF : OnOffType.ON;
309 updateState(CHANNEL_CLEAN_ADVISE, newAttributeValue);
312 newAttributeValue = "0".equals(attributeValue) ? OnOffType.OFF : OnOffType.ON;
313 updateState(CHANNEL_FILTER_ADVISE, newAttributeValue);
316 newAttributeValue = getDateTimeState(attributeValue);
317 if (newAttributeValue != null) {
318 updateState(CHANNEL_BREWED, newAttributeValue);
322 newAttributeValue = getDateTimeState(attributeValue);
323 if (newAttributeValue != null) {
324 updateState(CHANNEL_LAST_CLEANED, newAttributeValue);
329 updateStatus(ThingStatus.ONLINE);
330 } catch (Exception e) {
331 logger.warn("Failed to parse attributeList for WeMo CoffeMaker '{}'", this.getThing().getUID(), e);
333 } catch (Exception e) {
334 logger.warn("Failed to get attributes for device '{}'", getThing().getUID(), e);
335 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
339 public @Nullable State getDateTimeState(String attributeValue) {
342 value = Long.parseLong(attributeValue);
343 } catch (NumberFormatException e) {
344 logger.warn("Unable to parse attributeValue '{}' for device '{}'; expected long", attributeValue,
345 getThing().getUID());
348 ZonedDateTime zoned = ZonedDateTime.ofInstant(Instant.ofEpochSecond(value), TimeZone.getDefault().toZoneId());
349 State dateTimeState = new DateTimeType(zoned);
350 logger.trace("New attribute brewed '{}' received", dateTimeState);
351 return dateTimeState;