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.math.BigDecimal;
19 import java.util.Collections;
20 import java.util.HashMap;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
26 import org.apache.commons.lang.StringUtils;
27 import org.openhab.binding.wemo.internal.http.WemoHttpCall;
28 import org.openhab.core.config.core.Configuration;
29 import org.openhab.core.io.transport.upnp.UpnpIOParticipant;
30 import org.openhab.core.io.transport.upnp.UpnpIOService;
31 import org.openhab.core.library.types.DecimalType;
32 import org.openhab.core.library.types.StringType;
33 import org.openhab.core.thing.ChannelUID;
34 import org.openhab.core.thing.Thing;
35 import org.openhab.core.thing.ThingStatus;
36 import org.openhab.core.thing.ThingStatusDetail;
37 import org.openhab.core.thing.ThingTypeUID;
38 import org.openhab.core.types.Command;
39 import org.openhab.core.types.RefreshType;
40 import org.openhab.core.types.State;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
45 * The {@link WemoCrockpotHandler} is responsible for handling commands, which are
46 * sent to one of the channels and to update their states.
48 * @author Hans-Jörg Merk - Initial contribution;
51 public class WemoCrockpotHandler extends AbstractWemoHandler implements UpnpIOParticipant {
53 private final Logger logger = LoggerFactory.getLogger(WemoCrockpotHandler.class);
55 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_CROCKPOT);
57 * The default refresh interval in Seconds.
59 private static final int DEFAULT_REFRESH_INTERVAL_SECONDS = 120;
60 private final Map<String, Boolean> subscriptionState = new HashMap<>();
61 private final Map<String, String> stateMap = Collections.synchronizedMap(new HashMap<>());
63 private UpnpIOService service;
65 private ScheduledFuture<?> refreshJob;
67 private final Runnable refreshRunnable = () -> {
69 if (!isUpnpDeviceRegistered()) {
70 logger.debug("WeMo UPnP device {} not yet registered", getUDN());
76 public WemoCrockpotHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemohttpCaller) {
79 this.wemoHttpCaller = wemohttpCaller;
81 logger.debug("Creating a WemoCrockpotHandler for thing '{}'", getThing().getUID());
83 if (upnpIOService != null) {
84 this.service = upnpIOService;
86 logger.debug("upnpIOService not set.");
91 public void initialize() {
92 Configuration configuration = getConfig();
94 if (configuration.get("udn") != null) {
95 logger.debug("Initializing WemoCrockpotHandler for UDN '{}'", configuration.get("udn"));
96 service.registerParticipant(this);
99 updateStatus(ThingStatus.ONLINE);
101 logger.debug("Cannot initalize WemoCrockpotHandler. UDN not set.");
106 public void dispose() {
107 logger.debug("WeMoCrockpotHandler disposed.");
109 removeSubscription();
111 if (refreshJob != null && !refreshJob.isCancelled()) {
112 refreshJob.cancel(true);
118 public void handleCommand(ChannelUID channelUID, Command command) {
119 logger.trace("Command '{}' received for channel '{}'", command, channelUID);
123 if (command instanceof RefreshType) {
125 } else if (CHANNEL_COOKMODE.equals(channelUID.getId())) {
126 String commandString = command.toString();
127 switch (commandString) {
143 String soapHeader = "\"urn:Belkin:service:basicevent:1#SetBinaryState\"";
144 String content = "<?xml version=\"1.0\"?>"
145 + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
146 + "<s:Body>" + "<u:SetCrockpotState xmlns:u=\"urn:Belkin:service:basicevent:1\">" + "<mode>"
147 + mode + "</mode>" + "<time>" + time + "</time>" + "</u:SetCrockpotState>" + "</s:Body>"
149 String wemoURL = getWemoURL("basicevent");
151 if (wemoURL != null) {
152 wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
154 } catch (RuntimeException e) {
155 logger.debug("Failed to send command '{}' for device '{}':", command, getThing().getUID(), e);
156 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
158 updateStatus(ThingStatus.ONLINE);
163 public void onServiceSubscribed(String service, boolean succeeded) {
164 logger.debug("WeMo {}: Subscription to service {} {}", getUDN(), service, succeeded ? "succeeded" : "failed");
165 subscriptionState.put(service, succeeded);
169 public void onValueReceived(String variable, String value, String service) {
170 logger.debug("Received pair '{}':'{}' (service '{}') for thing '{}'", variable, value, service,
171 this.getThing().getUID());
173 updateStatus(ThingStatus.ONLINE);
174 this.stateMap.put(variable, value);
177 private synchronized void onSubscription() {
178 if (service.isRegistered(this)) {
179 logger.debug("Checking WeMo GENA subscription for '{}'", this);
181 String subscription = "basicevent1";
183 if ((subscriptionState.get(subscription) == null) || !subscriptionState.get(subscription).booleanValue()) {
184 logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(), subscription);
185 service.addSubscription(this, subscription, SUBSCRIPTION_DURATION);
186 subscriptionState.put(subscription, true);
190 logger.debug("Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
195 private synchronized void removeSubscription() {
196 logger.debug("Removing WeMo GENA subscription for '{}'", this);
198 if (service.isRegistered(this)) {
199 String subscription = "basicevent1";
201 if ((subscriptionState.get(subscription) != null) && subscriptionState.get(subscription).booleanValue()) {
202 logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription);
203 service.removeSubscription(this, subscription);
206 subscriptionState.remove(subscription);
207 service.unregisterParticipant(this);
211 private synchronized void onUpdate() {
212 if (refreshJob == null || refreshJob.isCancelled()) {
213 Configuration config = getThing().getConfiguration();
214 int refreshInterval = DEFAULT_REFRESH_INTERVAL_SECONDS;
215 Object refreshConfig = config.get("refresh");
216 refreshInterval = refreshConfig == null ? DEFAULT_REFRESH_INTERVAL_SECONDS
217 : ((BigDecimal) refreshConfig).intValue();
218 refreshJob = scheduler.scheduleWithFixedDelay(refreshRunnable, 0, refreshInterval, TimeUnit.SECONDS);
222 private boolean isUpnpDeviceRegistered() {
223 return service.isRegistered(this);
227 public String getUDN() {
228 return (String) this.getThing().getConfiguration().get(UDN);
232 * The {@link updateWemoState} polls the actual state of a WeMo device and
233 * calls {@link onValueReceived} to update the statemap and channels..
236 protected void updateWemoState() {
237 String action = "GetCrockpotState";
238 String actionService = "basicevent";
240 String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
241 String content = "<?xml version=\"1.0\"?>"
242 + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
243 + "<s:Body>" + "<u:" + action + " xmlns:u=\"urn:Belkin:service:" + actionService + ":1\">" + "</u:"
244 + action + ">" + "</s:Body>" + "</s:Envelope>";
247 String wemoURL = getWemoURL(actionService);
248 if (wemoURL != null) {
249 String wemoCallResponse = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
250 if (wemoCallResponse != null) {
251 logger.trace("State response '{}' for device '{}' received", wemoCallResponse, getThing().getUID());
252 String mode = StringUtils.substringBetween(wemoCallResponse, "<mode>", "</mode>");
253 String time = StringUtils.substringBetween(wemoCallResponse, "<time>", "</time>");
254 String coockedTime = StringUtils.substringBetween(wemoCallResponse, "<coockedTime>",
257 if (mode != null && time != null && coockedTime != null) {
258 State newMode = new StringType(mode);
259 State newCoockedTime = DecimalType.valueOf(coockedTime);
262 newMode = new StringType("OFF");
265 newMode = new StringType("WARM");
266 State warmTime = DecimalType.valueOf(time);
267 updateState(CHANNEL_WARMCOOKTIME, warmTime);
270 newMode = new StringType("LOW");
271 State lowTime = DecimalType.valueOf(time);
272 updateState(CHANNEL_LOWCOOKTIME, lowTime);
275 newMode = new StringType("HIGH");
276 State highTime = DecimalType.valueOf(time);
277 updateState(CHANNEL_HIGHCOOKTIME, highTime);
280 updateState(CHANNEL_COOKMODE, newMode);
281 updateState(CHANNEL_COOKEDTIME, newCoockedTime);
285 } catch (RuntimeException e) {
286 logger.debug("Failed to get actual state for device '{}': {}", getThing().getUID(), e.getMessage(), e);
287 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
289 updateStatus(ThingStatus.ONLINE);
292 public String getWemoURL(String actionService) {
293 URL descriptorURL = service.getDescriptorURL(this);
294 String wemoURL = null;
295 if (descriptorURL != null) {
296 String deviceURL = StringUtils.substringBefore(descriptorURL.toString(), "/setup.xml");
297 wemoURL = deviceURL + "/upnp/control/" + actionService + "1";
304 public void onStatusChanged(boolean status) {