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.*;
16 import static org.openhab.binding.wemo.internal.WemoUtil.*;
18 import java.math.BigDecimal;
20 import java.util.Collections;
21 import java.util.HashMap;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.openhab.binding.wemo.internal.http.WemoHttpCall;
30 import org.openhab.core.config.core.Configuration;
31 import org.openhab.core.io.transport.upnp.UpnpIOParticipant;
32 import org.openhab.core.io.transport.upnp.UpnpIOService;
33 import org.openhab.core.library.types.DecimalType;
34 import org.openhab.core.library.types.StringType;
35 import org.openhab.core.thing.ChannelUID;
36 import org.openhab.core.thing.Thing;
37 import org.openhab.core.thing.ThingStatus;
38 import org.openhab.core.thing.ThingStatusDetail;
39 import org.openhab.core.thing.ThingTypeUID;
40 import org.openhab.core.types.Command;
41 import org.openhab.core.types.RefreshType;
42 import org.openhab.core.types.State;
43 import org.slf4j.Logger;
44 import org.slf4j.LoggerFactory;
47 * The {@link WemoCrockpotHandler} is responsible for handling commands, which are
48 * sent to one of the channels and to update their states.
50 * @author Hans-Jörg Merk - Initial contribution;
53 public class WemoCrockpotHandler extends AbstractWemoHandler implements UpnpIOParticipant {
55 private final Logger logger = LoggerFactory.getLogger(WemoCrockpotHandler.class);
57 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_CROCKPOT);
59 private final Map<String, Boolean> subscriptionState = new HashMap<>();
60 private final Map<String, String> stateMap = Collections.synchronizedMap(new HashMap<>());
62 private UpnpIOService service;
64 private WemoHttpCall wemoCall;
66 private @Nullable ScheduledFuture<?> refreshJob;
68 private final Runnable refreshRunnable = () -> {
70 if (!isUpnpDeviceRegistered()) {
71 logger.debug("WeMo UPnP device {} not yet registered", getUDN());
77 public WemoCrockpotHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemoHttpCaller) {
78 super(thing, wemoHttpCaller);
80 this.wemoCall = wemoHttpCaller;
81 this.service = upnpIOService;
83 logger.debug("Creating a WemoCrockpotHandler for thing '{}'", getThing().getUID());
87 public void initialize() {
88 Configuration configuration = getConfig();
90 if (configuration.get("udn") != null) {
91 logger.debug("Initializing WemoCrockpotHandler for UDN '{}'", configuration.get("udn"));
92 service.registerParticipant(this);
95 updateStatus(ThingStatus.ONLINE);
97 logger.debug("Cannot initalize WemoCrockpotHandler. UDN not set.");
102 public void dispose() {
103 logger.debug("WeMoCrockpotHandler disposed.");
105 ScheduledFuture<?> job = refreshJob;
106 if (job != null && !job.isCancelled()) {
110 removeSubscription();
114 public void handleCommand(ChannelUID channelUID, Command command) {
115 logger.trace("Command '{}' received for channel '{}'", command, channelUID);
119 if (command instanceof RefreshType) {
121 } else if (CHANNEL_COOKMODE.equals(channelUID.getId())) {
122 String commandString = command.toString();
123 switch (commandString) {
139 String soapHeader = "\"urn:Belkin:service:basicevent:1#SetBinaryState\"";
140 String content = "<?xml version=\"1.0\"?>"
141 + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
142 + "<s:Body>" + "<u:SetCrockpotState xmlns:u=\"urn:Belkin:service:basicevent:1\">" + "<mode>"
143 + mode + "</mode>" + "<time>" + time + "</time>" + "</u:SetCrockpotState>" + "</s:Body>"
146 URL descriptorURL = service.getDescriptorURL(this);
147 String wemoURL = getWemoURL(descriptorURL, "basicevent");
149 if (wemoURL != null) {
150 wemoCall.executeCall(wemoURL, soapHeader, content);
152 } catch (RuntimeException e) {
153 logger.debug("Failed to send command '{}' for device '{}':", command, getThing().getUID(), e);
154 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
156 updateStatus(ThingStatus.ONLINE);
161 public void onServiceSubscribed(@Nullable String service, boolean succeeded) {
162 if (service != null) {
163 logger.debug("WeMo {}: Subscription to service {} {}", getUDN(), service,
164 succeeded ? "succeeded" : "failed");
165 subscriptionState.put(service, succeeded);
170 public void onValueReceived(@Nullable String variable, @Nullable String value, @Nullable String service) {
171 logger.debug("Received pair '{}':'{}' (service '{}') for thing '{}'", variable, value, service,
172 this.getThing().getUID());
174 updateStatus(ThingStatus.ONLINE);
175 if (variable != null && value != null) {
176 this.stateMap.put(variable, value);
180 private synchronized void onSubscription() {
181 if (service.isRegistered(this)) {
182 logger.debug("Checking WeMo GENA subscription for '{}'", this);
184 String subscription = "basicevent1";
186 if (subscriptionState.get(subscription) == null) {
187 logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(), subscription);
188 service.addSubscription(this, subscription, SUBSCRIPTION_DURATION_SECONDS);
189 subscriptionState.put(subscription, true);
193 logger.debug("Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
198 private synchronized void removeSubscription() {
199 logger.debug("Removing WeMo GENA subscription for '{}'", this);
201 if (service.isRegistered(this)) {
202 String subscription = "basicevent1";
204 if (subscriptionState.get(subscription) != null) {
205 logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription);
206 service.removeSubscription(this, subscription);
209 subscriptionState.remove(subscription);
210 service.unregisterParticipant(this);
214 private synchronized void onUpdate() {
215 ScheduledFuture<?> job = refreshJob;
216 if (job == null || job.isCancelled()) {
217 Configuration config = getThing().getConfiguration();
218 int refreshInterval = DEFAULT_REFRESH_INTERVALL_SECONDS;
219 Object refreshConfig = config.get("refresh");
220 refreshInterval = refreshConfig == null ? DEFAULT_REFRESH_INTERVALL_SECONDS
221 : ((BigDecimal) refreshConfig).intValue();
222 refreshJob = scheduler.scheduleWithFixedDelay(refreshRunnable, 0, refreshInterval, TimeUnit.SECONDS);
226 private boolean isUpnpDeviceRegistered() {
227 return service.isRegistered(this);
231 public String getUDN() {
232 return (String) this.getThing().getConfiguration().get(UDN);
236 * The {@link updateWemoState} polls the actual state of a WeMo device and
237 * calls {@link onValueReceived} to update the statemap and channels..
240 protected void updateWemoState() {
241 String action = "GetCrockpotState";
242 String actionService = "basicevent";
244 String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
245 String content = "<?xml version=\"1.0\"?>"
246 + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
247 + "<s:Body>" + "<u:" + action + " xmlns:u=\"urn:Belkin:service:" + actionService + ":1\">" + "</u:"
248 + action + ">" + "</s:Body>" + "</s:Envelope>";
251 URL descriptorURL = service.getDescriptorURL(this);
252 String wemoURL = getWemoURL(descriptorURL, actionService);
254 if (wemoURL != null) {
255 String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
256 if (wemoCallResponse != null) {
257 logger.trace("State response '{}' for device '{}' received", wemoCallResponse, getThing().getUID());
258 String mode = substringBetween(wemoCallResponse, "<mode>", "</mode>");
259 String time = substringBetween(wemoCallResponse, "<time>", "</time>");
260 String coockedTime = substringBetween(wemoCallResponse, "<coockedTime>", "</coockedTime>");
262 State newMode = new StringType(mode);
263 State newCoockedTime = DecimalType.valueOf(coockedTime);
266 newMode = new StringType("OFF");
269 newMode = new StringType("WARM");
270 State warmTime = DecimalType.valueOf(time);
271 updateState(CHANNEL_WARMCOOKTIME, warmTime);
274 newMode = new StringType("LOW");
275 State lowTime = DecimalType.valueOf(time);
276 updateState(CHANNEL_LOWCOOKTIME, lowTime);
279 newMode = new StringType("HIGH");
280 State highTime = DecimalType.valueOf(time);
281 updateState(CHANNEL_HIGHCOOKTIME, highTime);
284 updateState(CHANNEL_COOKMODE, newMode);
285 updateState(CHANNEL_COOKEDTIME, newCoockedTime);
288 } catch (RuntimeException e) {
289 logger.debug("Failed to get actual state for device '{}': {}", getThing().getUID(), e.getMessage(), e);
290 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
292 updateStatus(ThingStatus.ONLINE);
296 public void onStatusChanged(boolean status) {