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.*;
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.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.wemo.internal.http.WemoHttpCall;
29 import org.openhab.core.config.core.Configuration;
30 import org.openhab.core.io.transport.upnp.UpnpIOParticipant;
31 import org.openhab.core.io.transport.upnp.UpnpIOService;
32 import org.openhab.core.library.types.DecimalType;
33 import org.openhab.core.library.types.StringType;
34 import org.openhab.core.thing.ChannelUID;
35 import org.openhab.core.thing.Thing;
36 import org.openhab.core.thing.ThingStatus;
37 import org.openhab.core.thing.ThingStatusDetail;
38 import org.openhab.core.thing.ThingTypeUID;
39 import org.openhab.core.types.Command;
40 import org.openhab.core.types.RefreshType;
41 import org.openhab.core.types.State;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
46 * The {@link WemoCrockpotHandler} is responsible for handling commands, which are
47 * sent to one of the channels and to update their states.
49 * @author Hans-Jörg Merk - Initial contribution;
52 public class WemoCrockpotHandler extends AbstractWemoHandler implements UpnpIOParticipant {
54 private final Logger logger = LoggerFactory.getLogger(WemoCrockpotHandler.class);
56 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_CROCKPOT);
58 private final Object upnpLock = new Object();
59 private final Object jobLock = new Object();
61 private final Map<String, String> stateMap = Collections.synchronizedMap(new HashMap<>());
63 private @Nullable UpnpIOService service;
65 private WemoHttpCall wemoCall;
67 private String host = "";
69 private Map<String, Boolean> subscriptionState = new HashMap<>();
71 private @Nullable ScheduledFuture<?> pollingJob;
73 public WemoCrockpotHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemoHttpCaller) {
74 super(thing, wemoHttpCaller);
76 this.wemoCall = wemoHttpCaller;
77 this.service = upnpIOService;
79 logger.debug("Creating a WemoCrockpotHandler for thing '{}'", getThing().getUID());
83 public void initialize() {
84 Configuration configuration = getConfig();
86 if (configuration.get(UDN) != null) {
87 logger.debug("Initializing WemoCrockpotHandler for UDN '{}'", configuration.get(UDN));
88 UpnpIOService localService = service;
89 if (localService != null) {
90 localService.registerParticipant(this);
93 pollingJob = scheduler.scheduleWithFixedDelay(this::poll, 0, DEFAULT_REFRESH_INTERVALL_SECONDS,
95 updateStatus(ThingStatus.ONLINE);
97 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
98 "@text/config-status.error.missing-udn");
99 logger.debug("Cannot initalize WemoCrockpotHandler. UDN not set.");
104 public void dispose() {
105 logger.debug("WeMoCrockpotHandler disposed.");
106 ScheduledFuture<?> job = this.pollingJob;
107 if (job != null && !job.isCancelled()) {
110 this.pollingJob = null;
111 removeSubscription();
114 private void poll() {
115 synchronized (jobLock) {
116 if (pollingJob == null) {
120 logger.debug("Polling job");
122 // Check if the Wemo device is set in the UPnP service registry
123 // If not, set the thing state to ONLINE/CONFIG-PENDING and wait for the next poll
124 if (!isUpnpDeviceRegistered()) {
125 logger.debug("UPnP device {} not yet registered", getUDN());
126 updateStatus(ThingStatus.ONLINE, ThingStatusDetail.CONFIGURATION_PENDING,
127 "@text/config-status.pending.device-not-registered [\"" + getUDN() + "\"]");
128 synchronized (upnpLock) {
129 subscriptionState = new HashMap<>();
133 updateStatus(ThingStatus.ONLINE);
136 } catch (Exception e) {
137 logger.debug("Exception during poll: {}", e.getMessage(), e);
143 public void handleCommand(ChannelUID channelUID, Command command) {
144 String localHost = getHost();
145 if (localHost.isEmpty()) {
146 logger.error("Failed to send command '{}' for device '{}': IP address missing", command,
147 getThing().getUID());
148 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
149 "@text/config-status.error.missing-ip");
152 String wemoURL = getWemoURL(localHost, BASICACTION);
153 if (wemoURL == null) {
154 logger.error("Failed to send command '{}' for device '{}': URL cannot be created", command,
155 getThing().getUID());
156 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
157 "@text/config-status.error.missing-url");
163 if (command instanceof RefreshType) {
165 } else if (CHANNEL_COOKMODE.equals(channelUID.getId())) {
166 String commandString = command.toString();
167 switch (commandString) {
183 String soapHeader = "\"urn:Belkin:service:basicevent:1#SetBinaryState\"";
184 String content = "<?xml version=\"1.0\"?>"
185 + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
186 + "<s:Body>" + "<u:SetCrockpotState xmlns:u=\"urn:Belkin:service:basicevent:1\">" + "<mode>"
187 + mode + "</mode>" + "<time>" + time + "</time>" + "</u:SetCrockpotState>" + "</s:Body>"
189 String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
190 if (wemoCallResponse != null && logger.isTraceEnabled()) {
191 logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
192 logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader, getThing().getUID());
193 logger.trace("wemoCall with content '{}' for device '{}'", content, getThing().getUID());
194 logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse, getThing().getUID());
196 } catch (RuntimeException e) {
197 logger.debug("Failed to send command '{}' for device '{}':", command, getThing().getUID(), e);
198 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
200 updateStatus(ThingStatus.ONLINE);
205 public void onServiceSubscribed(@Nullable String service, boolean succeeded) {
206 if (service != null) {
207 logger.debug("WeMo {}: Subscription to service {} {}", getUDN(), service,
208 succeeded ? "succeeded" : "failed");
209 subscriptionState.put(service, succeeded);
214 public void onValueReceived(@Nullable String variable, @Nullable String value, @Nullable String service) {
215 logger.debug("Received pair '{}':'{}' (service '{}') for thing '{}'", variable, value, service,
216 this.getThing().getUID());
218 updateStatus(ThingStatus.ONLINE);
219 if (variable != null && value != null) {
220 this.stateMap.put(variable, value);
224 private synchronized void addSubscription() {
225 synchronized (upnpLock) {
226 UpnpIOService localService = service;
227 if (localService != null) {
228 if (localService.isRegistered(this)) {
229 logger.debug("Checking WeMo GENA subscription for '{}'", getThing().getUID());
231 String subscription = BASICEVENT;
233 if (subscriptionState.get(subscription) == null) {
234 logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(),
236 localService.addSubscription(this, subscription, SUBSCRIPTION_DURATION_SECONDS);
237 subscriptionState.put(subscription, true);
241 "Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
242 getThing().getUID());
248 private synchronized void removeSubscription() {
249 synchronized (upnpLock) {
250 UpnpIOService localService = service;
251 if (localService != null) {
252 if (localService.isRegistered(this)) {
253 logger.debug("Removing WeMo GENA subscription for '{}'", getThing().getUID());
254 String subscription = BASICEVENT;
256 if (subscriptionState.get(subscription) != null) {
257 logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription);
258 localService.removeSubscription(this, subscription);
260 subscriptionState.remove(subscription);
261 localService.unregisterParticipant(this);
267 private boolean isUpnpDeviceRegistered() {
268 UpnpIOService localService = service;
269 if (localService != null) {
270 return localService.isRegistered(this);
276 public String getUDN() {
277 return (String) this.getThing().getConfiguration().get(UDN);
281 * The {@link updateWemoState} polls the actual state of a WeMo device and
282 * calls {@link onValueReceived} to update the statemap and channels..
285 protected void updateWemoState() {
286 String localHost = getHost();
287 if (localHost.isEmpty()) {
288 logger.error("Failed to get actual state for device '{}': IP address missing", getThing().getUID());
289 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
290 "@text/config-status.error.missing-ip");
293 String actionService = BASICEVENT;
294 String wemoURL = getWemoURL(localHost, actionService);
295 if (wemoURL == null) {
296 logger.error("Failed to get actual state for device '{}': URL cannot be created", getThing().getUID());
297 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
298 "@text/config-status.error.missing-url");
302 String action = "GetCrockpotState";
303 String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
304 String content = createStateRequestContent(action, actionService);
305 String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
306 if (wemoCallResponse != null) {
307 if (logger.isTraceEnabled()) {
308 logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
309 logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader, getThing().getUID());
310 logger.trace("wemoCall with content '{}' for device '{}'", content, getThing().getUID());
311 logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse, getThing().getUID());
313 String mode = substringBetween(wemoCallResponse, "<mode>", "</mode>");
314 String time = substringBetween(wemoCallResponse, "<time>", "</time>");
315 String coockedTime = substringBetween(wemoCallResponse, "<coockedTime>", "</coockedTime>");
317 State newMode = new StringType(mode);
318 State newCoockedTime = DecimalType.valueOf(coockedTime);
321 newMode = new StringType("OFF");
324 newMode = new StringType("WARM");
325 State warmTime = DecimalType.valueOf(time);
326 updateState(CHANNEL_WARMCOOKTIME, warmTime);
329 newMode = new StringType("LOW");
330 State lowTime = DecimalType.valueOf(time);
331 updateState(CHANNEL_LOWCOOKTIME, lowTime);
334 newMode = new StringType("HIGH");
335 State highTime = DecimalType.valueOf(time);
336 updateState(CHANNEL_HIGHCOOKTIME, highTime);
339 updateState(CHANNEL_COOKMODE, newMode);
340 updateState(CHANNEL_COOKEDTIME, newCoockedTime);
342 } catch (RuntimeException e) {
343 logger.debug("Failed to get actual state for device '{}': {}", getThing().getUID(), e.getMessage(), e);
344 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
346 updateStatus(ThingStatus.ONLINE);
350 public void onStatusChanged(boolean status) {
353 public String getHost() {
354 String localHost = host;
355 if (!localHost.isEmpty()) {
358 UpnpIOService localService = service;
359 if (localService != null) {
360 URL descriptorURL = localService.getDescriptorURL(this);
361 if (descriptorURL != null) {
362 return descriptorURL.getHost();