2 * Copyright (c) 2010-2021 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.IOException;
18 import java.io.StringReader;
19 import java.math.BigDecimal;
21 import java.util.Collections;
22 import java.util.HashMap;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
28 import javax.xml.parsers.DocumentBuilder;
29 import javax.xml.parsers.DocumentBuilderFactory;
30 import javax.xml.parsers.ParserConfigurationException;
32 import org.apache.commons.lang.StringEscapeUtils;
33 import org.apache.commons.lang.StringUtils;
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.UpnpIOParticipant;
37 import org.openhab.core.io.transport.upnp.UpnpIOService;
38 import org.openhab.core.library.types.OnOffType;
39 import org.openhab.core.library.types.PercentType;
40 import org.openhab.core.library.types.StringType;
41 import org.openhab.core.thing.ChannelUID;
42 import org.openhab.core.thing.Thing;
43 import org.openhab.core.thing.ThingStatus;
44 import org.openhab.core.thing.ThingStatusDetail;
45 import org.openhab.core.thing.ThingTypeUID;
46 import org.openhab.core.types.Command;
47 import org.openhab.core.types.RefreshType;
48 import org.openhab.core.types.State;
49 import org.slf4j.Logger;
50 import org.slf4j.LoggerFactory;
51 import org.w3c.dom.CharacterData;
52 import org.w3c.dom.Document;
53 import org.w3c.dom.Element;
54 import org.w3c.dom.Node;
55 import org.w3c.dom.NodeList;
56 import org.xml.sax.InputSource;
57 import org.xml.sax.SAXException;
60 * The {@link WemoHolmesHandler} is responsible for handling commands, which are
61 * sent to one of the channels and to update their states.
63 * @author Hans-Jörg Merk - Initial contribution;
66 public class WemoHolmesHandler extends AbstractWemoHandler implements UpnpIOParticipant {
68 private final Logger logger = LoggerFactory.getLogger(WemoHolmesHandler.class);
70 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_PURIFIER);
73 * The default refresh interval in Seconds.
75 private static final int DEFAULT_REFRESH_INTERVAL_SECONDS = 120;
76 private static final int FILTER_LIFE_DAYS = 330;
77 private static final int FILTER_LIFE_MINS = FILTER_LIFE_DAYS * 24 * 60;
78 private final Map<String, Boolean> subscriptionState = new HashMap<>();
79 private final Map<String, String> stateMap = Collections.synchronizedMap(new HashMap<>());
81 private UpnpIOService service;
83 private ScheduledFuture<?> refreshJob;
85 private final Runnable refreshRunnable = () -> {
86 if (!isUpnpDeviceRegistered()) {
87 logger.debug("WeMo UPnP device {} not yet registered", getUDN());
94 public WemoHolmesHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemohttpCaller) {
97 this.wemoHttpCaller = wemohttpCaller;
99 logger.debug("Creating a WemoHolmesHandler for thing '{}'", getThing().getUID());
101 if (upnpIOService != null) {
102 this.service = upnpIOService;
104 logger.debug("upnpIOService not set.");
109 public void initialize() {
110 Configuration configuration = getConfig();
112 if (configuration.get("udn") != null) {
113 logger.debug("Initializing WemoHolmesHandler for UDN '{}'", configuration.get("udn"));
114 service.registerParticipant(this);
117 updateStatus(ThingStatus.ONLINE);
119 logger.debug("Cannot initalize WemoHolmesHandler. UDN not set.");
124 public void dispose() {
125 logger.debug("WemoHolmesHandler disposed.");
127 removeSubscription();
129 if (refreshJob != null && !refreshJob.isCancelled()) {
130 refreshJob.cancel(true);
136 public void handleCommand(ChannelUID channelUID, Command command) {
137 logger.trace("Command '{}' received for channel '{}'", command, channelUID);
139 String attribute = null;
142 if (command instanceof RefreshType) {
144 } else if (CHANNEL_PURIFIERMODE.equals(channelUID.getId())) {
146 String commandString = command.toString();
147 switch (commandString) {
164 } else if (CHANNEL_IONIZER.equals(channelUID.getId())) {
165 attribute = "Ionizer";
166 if (OnOffType.ON.equals(command)) {
168 } else if (OnOffType.OFF.equals(command)) {
171 } else if (CHANNEL_HUMIDIFIERMODE.equals(channelUID.getId())) {
172 attribute = "FanMode";
173 String commandString = command.toString();
174 switch (commandString) {
194 } else if (CHANNEL_DESIREDHUMIDITY.equals(channelUID.getId())) {
195 attribute = "DesiredHumidity";
196 String commandString = command.toString();
197 switch (commandString) {
214 } else if (CHANNEL_HEATERMODE.equals(channelUID.getId())) {
216 String commandString = command.toString();
217 switch (commandString) {
234 } else if (CHANNEL_TARGETTEMP.equals(channelUID.getId())) {
235 attribute = "SetTemperature";
236 value = command.toString();
239 String soapHeader = "\"urn:Belkin:service:deviceevent:1#SetAttributes\"";
240 String content = "<?xml version=\"1.0\"?>"
241 + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
242 + "<s:Body>" + "<u:SetAttributes xmlns:u=\"urn:Belkin:service:deviceevent:1\">"
243 + "<attributeList><attribute><name>" + attribute + "</name><value>" + value
244 + "</value></attribute></attributeList>" + "</u:SetAttributes>" + "</s:Body>"
246 String wemoURL = getWemoURL("deviceevent");
248 if (wemoURL != null) {
249 wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
251 } catch (RuntimeException e) {
252 logger.debug("Failed to send command '{}' for device '{}':", command, getThing().getUID(), e);
253 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
255 updateStatus(ThingStatus.ONLINE);
259 public void onServiceSubscribed(String service, boolean succeeded) {
260 logger.debug("WeMo {}: Subscription to service {} {}", getUDN(), service, succeeded ? "succeeded" : "failed");
261 subscriptionState.put(service, succeeded);
265 public void onValueReceived(String variable, String value, String service) {
266 logger.debug("Received pair '{}':'{}' (service '{}') for thing '{}'", variable, value, service,
267 this.getThing().getUID());
269 updateStatus(ThingStatus.ONLINE);
270 this.stateMap.put(variable, value);
273 private synchronized void onSubscription() {
274 if (service.isRegistered(this)) {
275 logger.debug("Checking WeMo GENA subscription for '{}'", this);
277 String subscription = "basicevent1";
279 if ((subscriptionState.get(subscription) == null) || !subscriptionState.get(subscription).booleanValue()) {
280 logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(), subscription);
281 service.addSubscription(this, subscription, SUBSCRIPTION_DURATION);
282 subscriptionState.put(subscription, true);
286 logger.debug("Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
291 private synchronized void removeSubscription() {
292 logger.debug("Removing WeMo GENA subscription for '{}'", this);
294 if (service.isRegistered(this)) {
295 String subscription = "basicevent1";
297 if ((subscriptionState.get(subscription) != null) && subscriptionState.get(subscription).booleanValue()) {
298 logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription);
299 service.removeSubscription(this, subscription);
302 subscriptionState.remove(subscription);
303 service.unregisterParticipant(this);
307 private synchronized void onUpdate() {
308 if (refreshJob == null || refreshJob.isCancelled()) {
309 Configuration config = getThing().getConfiguration();
310 int refreshInterval = DEFAULT_REFRESH_INTERVAL_SECONDS;
311 Object refreshConfig = config.get("refresh");
312 refreshInterval = refreshConfig == null ? DEFAULT_REFRESH_INTERVAL_SECONDS
313 : ((BigDecimal) refreshConfig).intValue();
314 refreshJob = scheduler.scheduleWithFixedDelay(refreshRunnable, 0, refreshInterval, TimeUnit.SECONDS);
318 private boolean isUpnpDeviceRegistered() {
319 return service.isRegistered(this);
323 public String getUDN() {
324 return (String) this.getThing().getConfiguration().get(UDN);
328 * The {@link updateWemoState} polls the actual state of a WeMo device and
329 * calls {@link onValueReceived} to update the statemap and channels..
332 protected void updateWemoState() {
333 String action = "GetAttributes";
334 String actionService = "deviceevent";
336 String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
337 String content = "<?xml version=\"1.0\"?>"
338 + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
339 + "<s:Body>" + "<u:" + action + " xmlns:u=\"urn:Belkin:service:" + actionService + ":1\">" + "</u:"
340 + action + ">" + "</s:Body>" + "</s:Envelope>";
343 String wemoURL = getWemoURL(actionService);
344 if (wemoURL != null) {
345 String wemoCallResponse = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
346 if (wemoCallResponse != null) {
347 logger.trace("State response '{}' for device '{}' received", wemoCallResponse, getThing().getUID());
349 String stringParser = StringUtils.substringBetween(wemoCallResponse, "<attributeList>",
352 // Due to Belkins bad response formatting, we need to run this twice.
353 stringParser = StringEscapeUtils.unescapeXml(stringParser);
354 stringParser = StringEscapeUtils.unescapeXml(stringParser);
356 logger.trace("AirPurifier response '{}' for device '{}' received", stringParser,
357 getThing().getUID());
359 stringParser = "<data>" + stringParser + "</data>";
361 DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
362 DocumentBuilder db = dbf.newDocumentBuilder();
363 InputSource is = new InputSource();
364 is.setCharacterStream(new StringReader(stringParser));
366 Document doc = db.parse(is);
367 NodeList nodes = doc.getElementsByTagName("attribute");
369 // iterate the attributes
370 for (int i = 0; i < nodes.getLength(); i++) {
371 Element element = (Element) nodes.item(i);
373 NodeList deviceIndex = element.getElementsByTagName("name");
374 Element line = (Element) deviceIndex.item(0);
375 String attributeName = getCharacterDataFromElement(line);
376 logger.trace("attributeName: {}", attributeName);
378 NodeList deviceID = element.getElementsByTagName("value");
379 line = (Element) deviceID.item(0);
380 String attributeValue = getCharacterDataFromElement(line);
381 logger.trace("attributeValue: {}", attributeValue);
383 State newMode = new StringType();
384 switch (attributeName) {
386 if ("purifier".equals(getThing().getThingTypeUID().getId())) {
387 switch (attributeValue) {
389 newMode = new StringType("OFF");
392 newMode = new StringType("LOW");
395 newMode = new StringType("MED");
398 newMode = new StringType("HIGH");
401 newMode = new StringType("AUTO");
404 updateState(CHANNEL_PURIFIERMODE, newMode);
406 switch (attributeValue) {
408 newMode = new StringType("OFF");
411 newMode = new StringType("FROSTPROTECT");
414 newMode = new StringType("HIGH");
417 newMode = new StringType("LOW");
420 newMode = new StringType("ECO");
423 updateState(CHANNEL_HEATERMODE, newMode);
427 switch (attributeValue) {
429 newMode = OnOffType.OFF;
432 newMode = OnOffType.ON;
435 updateState(CHANNEL_IONIZER, newMode);
438 switch (attributeValue) {
440 newMode = new StringType("POOR");
443 newMode = new StringType("MODERATE");
446 newMode = new StringType("GOOD");
449 updateState(CHANNEL_AIRQUALITY, newMode);
452 int filterLife = Integer.valueOf(attributeValue);
453 if ("purifier".equals(getThing().getThingTypeUID().getId())) {
454 filterLife = Math.round((filterLife / FILTER_LIFE_MINS) * 100);
456 filterLife = Math.round((filterLife / 60480) * 100);
458 updateState(CHANNEL_FILTERLIFE, new PercentType(String.valueOf(filterLife)));
460 case "ExpiredFilterTime":
461 switch (attributeValue) {
463 newMode = OnOffType.OFF;
466 newMode = OnOffType.ON;
469 updateState(CHANNEL_EXPIREDFILTERTIME, newMode);
471 case "FilterPresent":
472 switch (attributeValue) {
474 newMode = OnOffType.OFF;
477 newMode = OnOffType.ON;
480 updateState(CHANNEL_FILTERPRESENT, newMode);
483 switch (attributeValue) {
485 newMode = new StringType("OFF");
488 newMode = new StringType("LOW");
491 newMode = new StringType("MED");
494 newMode = new StringType("HIGH");
497 newMode = new StringType("AUTO");
500 updateState(CHANNEL_PURIFIERMODE, newMode);
502 case "DesiredHumidity":
503 switch (attributeValue) {
505 newMode = new PercentType("45");
508 newMode = new PercentType("50");
511 newMode = new PercentType("55");
514 newMode = new PercentType("60");
517 newMode = new PercentType("100");
520 updateState(CHANNEL_DESIREDHUMIDITY, newMode);
522 case "CurrentHumidity":
523 newMode = new StringType(attributeValue);
524 updateState(CHANNEL_CURRENTHUMIDITY, newMode);
527 newMode = new StringType(attributeValue);
528 updateState(CHANNEL_CURRENTTEMP, newMode);
530 case "SetTemperature":
531 newMode = new StringType(attributeValue);
532 updateState(CHANNEL_TARGETTEMP, newMode);
535 newMode = new StringType(attributeValue);
536 updateState(CHANNEL_AUTOOFFTIME, newMode);
538 case "TimeRemaining":
539 newMode = new StringType(attributeValue);
540 updateState(CHANNEL_HEATINGREMAINING, newMode);
546 } catch (RuntimeException | ParserConfigurationException | SAXException | IOException e) {
547 logger.debug("Failed to get actual state for device '{}':", getThing().getUID(), e);
548 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
550 updateStatus(ThingStatus.ONLINE);
553 public String getWemoURL(String actionService) {
554 URL descriptorURL = service.getDescriptorURL(this);
555 String wemoURL = null;
556 if (descriptorURL != null) {
557 String deviceURL = StringUtils.substringBefore(descriptorURL.toString(), "/setup.xml");
558 wemoURL = deviceURL + "/upnp/control/" + actionService + "1";
564 public static String getCharacterDataFromElement(Element e) {
565 Node child = e.getFirstChild();
566 if (child instanceof CharacterData) {
567 CharacterData cd = (CharacterData) child;
574 public void onStatusChanged(boolean status) {