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.IOException;
19 import java.io.StringReader;
20 import java.util.Collections;
21 import java.util.HashMap;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
27 import javax.xml.parsers.DocumentBuilder;
28 import javax.xml.parsers.DocumentBuilderFactory;
29 import javax.xml.parsers.ParserConfigurationException;
31 import org.eclipse.jdt.annotation.NonNullByDefault;
32 import org.eclipse.jdt.annotation.Nullable;
33 import org.jupnp.UpnpService;
34 import org.openhab.binding.wemo.internal.http.WemoHttpCall;
35 import org.openhab.core.config.core.Configuration;
36 import org.openhab.core.io.transport.upnp.UpnpIOService;
37 import org.openhab.core.library.types.OnOffType;
38 import org.openhab.core.library.types.PercentType;
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 import org.xml.sax.SAXException;
57 * The {@link WemoHolmesHandler} is responsible for handling commands, which are
58 * sent to one of the channels and to update their states.
60 * @author Hans-Jörg Merk - Initial contribution;
63 public class WemoHolmesHandler extends WemoBaseThingHandler {
65 private final Logger logger = LoggerFactory.getLogger(WemoHolmesHandler.class);
67 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_PURIFIER);
69 private static final int FILTER_LIFE_DAYS = 330;
70 private static final int FILTER_LIFE_MINS = FILTER_LIFE_DAYS * 24 * 60;
72 private final Object jobLock = new Object();
74 private final Map<String, String> stateMap = Collections.synchronizedMap(new HashMap<>());
76 private @Nullable ScheduledFuture<?> pollingJob;
78 public WemoHolmesHandler(Thing thing, UpnpIOService upnpIOService, UpnpService upnpService,
79 WemoHttpCall wemoHttpCaller) {
80 super(thing, upnpIOService, upnpService, wemoHttpCaller);
82 logger.debug("Creating a WemoHolmesHandler for thing '{}'", getThing().getUID());
86 public void initialize() {
88 Configuration configuration = getConfig();
90 if (configuration.get(UDN) != null) {
91 logger.debug("Initializing WemoHolmesHandler for UDN '{}'", configuration.get(UDN));
92 addSubscription(BASICEVENT);
93 pollingJob = scheduler.scheduleWithFixedDelay(this::poll, 0, DEFAULT_REFRESH_INTERVAL_SECONDS,
95 updateStatus(ThingStatus.UNKNOWN);
97 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
98 "@text/config-status.error.missing-udn");
103 public void dispose() {
104 logger.debug("WemoHolmesHandler disposed.");
106 ScheduledFuture<?> job = this.pollingJob;
107 if (job != null && !job.isCancelled()) {
110 this.pollingJob = null;
114 private void poll() {
115 synchronized (jobLock) {
116 if (pollingJob == null) {
120 logger.debug("Polling job");
121 // Check if the Wemo device is set in the UPnP service registry
122 if (!isUpnpDeviceRegistered()) {
123 logger.debug("UPnP device {} not yet registered", getUDN());
124 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
125 "@text/config-status.pending.device-not-registered [\"" + getUDN() + "\"]");
129 } catch (Exception e) {
130 logger.debug("Exception during poll: {}", e.getMessage(), e);
136 public void handleCommand(ChannelUID channelUID, Command command) {
137 String wemoURL = getWemoURL(DEVICEACTION);
138 if (wemoURL == null) {
139 logger.debug("Failed to send command '{}' for device '{}': URL cannot be created", command,
140 getThing().getUID());
143 String attribute = null;
146 if (command instanceof RefreshType) {
148 } else if (CHANNEL_PURIFIER_MODE.equals(channelUID.getId())) {
150 String commandString = command.toString();
151 switch (commandString) {
168 } else if (CHANNEL_IONIZER.equals(channelUID.getId())) {
169 attribute = "Ionizer";
170 if (OnOffType.ON.equals(command)) {
172 } else if (OnOffType.OFF.equals(command)) {
175 } else if (CHANNEL_HUMIDIFIER_MODE.equals(channelUID.getId())) {
176 attribute = "FanMode";
177 String commandString = command.toString();
178 switch (commandString) {
198 } else if (CHANNEL_DESIRED_HUMIDITY.equals(channelUID.getId())) {
199 attribute = "DesiredHumidity";
200 String commandString = command.toString();
201 switch (commandString) {
218 } else if (CHANNEL_HEATER_MODE.equals(channelUID.getId())) {
220 String commandString = command.toString();
221 switch (commandString) {
238 } else if (CHANNEL_TARGET_TEMPERATURE.equals(channelUID.getId())) {
239 attribute = "SetTemperature";
240 value = command.toString();
243 String soapHeader = "\"urn:Belkin:service:deviceevent:1#SetAttributes\"";
244 String content = "<?xml version=\"1.0\"?>"
245 + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
246 + "<s:Body>" + "<u:SetAttributes xmlns:u=\"urn:Belkin:service:deviceevent:1\">"
247 + "<attributeList><attribute><name>" + attribute + "</name><value>" + value
248 + "</value></attribute></attributeList>" + "</u:SetAttributes>" + "</s:Body>"
250 wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
251 updateStatus(ThingStatus.ONLINE);
252 } catch (IOException e) {
253 logger.debug("Failed to send command '{}' for device '{}':", command, getThing().getUID(), e);
254 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
259 public void onValueReceived(@Nullable String variable, @Nullable String value, @Nullable String service) {
260 logger.debug("Received pair '{}':'{}' (service '{}') for thing '{}'", variable, value, service,
261 this.getThing().getUID());
263 updateStatus(ThingStatus.ONLINE);
264 if (variable != null && value != null) {
265 this.stateMap.put(variable, value);
270 * The {@link updateWemoState} polls the actual state of a WeMo device and
271 * calls {@link onValueReceived} to update the statemap and channels..
274 protected void updateWemoState() {
275 String actionService = DEVICEACTION;
276 String wemoURL = getWemoURL(actionService);
277 if (wemoURL == null) {
278 logger.debug("Failed to get actual state for device '{}': URL cannot be created", getThing().getUID());
282 String action = "GetAttributes";
283 String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
284 String content = createStateRequestContent(action, actionService);
285 String wemoCallResponse = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
286 String stringParser = substringBetween(wemoCallResponse, "<attributeList>", "</attributeList>");
288 // Due to Belkins bad response formatting, we need to run this twice.
289 stringParser = unescapeXml(stringParser);
290 stringParser = unescapeXml(stringParser);
292 logger.trace("AirPurifier response '{}' for device '{}' received", stringParser, getThing().getUID());
294 stringParser = "<data>" + stringParser + "</data>";
296 DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
298 // https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html
299 dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
300 dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
301 dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
302 dbf.setXIncludeAware(false);
303 dbf.setExpandEntityReferences(false);
304 DocumentBuilder db = dbf.newDocumentBuilder();
305 InputSource is = new InputSource();
306 is.setCharacterStream(new StringReader(stringParser));
308 Document doc = db.parse(is);
309 NodeList nodes = doc.getElementsByTagName("attribute");
311 // iterate the attributes
312 for (int i = 0; i < nodes.getLength(); i++) {
313 Element element = (Element) nodes.item(i);
315 NodeList deviceIndex = element.getElementsByTagName("name");
316 Element line = (Element) deviceIndex.item(0);
317 String attributeName = getCharacterDataFromElement(line);
318 logger.trace("attributeName: {}", attributeName);
320 NodeList deviceID = element.getElementsByTagName("value");
321 line = (Element) deviceID.item(0);
322 String attributeValue = getCharacterDataFromElement(line);
323 logger.trace("attributeValue: {}", attributeValue);
325 State newMode = new StringType();
326 switch (attributeName) {
328 if ("purifier".equals(getThing().getThingTypeUID().getId())) {
329 switch (attributeValue) {
331 newMode = new StringType("OFF");
334 newMode = new StringType("LOW");
337 newMode = new StringType("MED");
340 newMode = new StringType("HIGH");
343 newMode = new StringType("AUTO");
346 updateState(CHANNEL_PURIFIER_MODE, newMode);
348 switch (attributeValue) {
350 newMode = new StringType("OFF");
353 newMode = new StringType("FROSTPROTECT");
356 newMode = new StringType("HIGH");
359 newMode = new StringType("LOW");
362 newMode = new StringType("ECO");
365 updateState(CHANNEL_HEATER_MODE, newMode);
369 switch (attributeValue) {
371 newMode = OnOffType.OFF;
374 newMode = OnOffType.ON;
377 updateState(CHANNEL_IONIZER, newMode);
380 switch (attributeValue) {
382 newMode = new StringType("POOR");
385 newMode = new StringType("MODERATE");
388 newMode = new StringType("GOOD");
391 updateState(CHANNEL_AIR_QUALITY, newMode);
394 int filterLife = Integer.valueOf(attributeValue);
395 if ("purifier".equals(getThing().getThingTypeUID().getId())) {
396 filterLife = Math.round((filterLife / FILTER_LIFE_MINS) * 100);
398 filterLife = Math.round((filterLife / 60480) * 100);
400 updateState(CHANNEL_FILTER_LIFE, new PercentType(String.valueOf(filterLife)));
402 case "ExpiredFilterTime":
403 switch (attributeValue) {
405 newMode = OnOffType.OFF;
408 newMode = OnOffType.ON;
411 updateState(CHANNEL_EXPIRED_FILTER_TIME, newMode);
413 case "FilterPresent":
414 switch (attributeValue) {
416 newMode = OnOffType.OFF;
419 newMode = OnOffType.ON;
422 updateState(CHANNEL_FILTER_PRESENT, newMode);
425 switch (attributeValue) {
427 newMode = new StringType("OFF");
430 newMode = new StringType("LOW");
433 newMode = new StringType("MED");
436 newMode = new StringType("HIGH");
439 newMode = new StringType("AUTO");
442 updateState(CHANNEL_PURIFIER_MODE, newMode);
444 case "DesiredHumidity":
445 switch (attributeValue) {
447 newMode = new PercentType("45");
450 newMode = new PercentType("50");
453 newMode = new PercentType("55");
456 newMode = new PercentType("60");
459 newMode = new PercentType("100");
462 updateState(CHANNEL_DESIRED_HUMIDITY, newMode);
464 case "CurrentHumidity":
465 newMode = new StringType(attributeValue);
466 updateState(CHANNEL_CURRENT_HUMIDITY, newMode);
469 newMode = new StringType(attributeValue);
470 updateState(CHANNEL_CURRENT_TEMPERATURE, newMode);
472 case "SetTemperature":
473 newMode = new StringType(attributeValue);
474 updateState(CHANNEL_TARGET_TEMPERATURE, newMode);
477 newMode = new StringType(attributeValue);
478 updateState(CHANNEL_AUTO_OFF_TIME, newMode);
480 case "TimeRemaining":
481 newMode = new StringType(attributeValue);
482 updateState(CHANNEL_HEATING_REMAINING, newMode);
486 updateStatus(ThingStatus.ONLINE);
487 } catch (RuntimeException | ParserConfigurationException | SAXException | IOException e) {
488 logger.debug("Failed to get actual state for device '{}':", getThing().getUID(), e);
489 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());