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.util.Collections;
19 import java.util.HashMap;
22 import java.util.concurrent.ScheduledFuture;
23 import java.util.concurrent.TimeUnit;
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
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.UpnpIOService;
30 import org.openhab.core.library.types.DecimalType;
31 import org.openhab.core.library.types.StringType;
32 import org.openhab.core.thing.ChannelUID;
33 import org.openhab.core.thing.Thing;
34 import org.openhab.core.thing.ThingStatus;
35 import org.openhab.core.thing.ThingStatusDetail;
36 import org.openhab.core.thing.ThingTypeUID;
37 import org.openhab.core.types.Command;
38 import org.openhab.core.types.RefreshType;
39 import org.openhab.core.types.State;
40 import org.slf4j.Logger;
41 import org.slf4j.LoggerFactory;
44 * The {@link WemoCrockpotHandler} is responsible for handling commands, which are
45 * sent to one of the channels and to update their states.
47 * @author Hans-Jörg Merk - Initial contribution;
50 public class WemoCrockpotHandler extends WemoBaseThingHandler {
52 private final Logger logger = LoggerFactory.getLogger(WemoCrockpotHandler.class);
54 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_CROCKPOT);
56 private final Object upnpLock = new Object();
57 private final Object jobLock = new Object();
59 private final Map<String, String> stateMap = Collections.synchronizedMap(new HashMap<>());
61 private Map<String, Boolean> subscriptionState = new HashMap<>();
63 private @Nullable ScheduledFuture<?> pollingJob;
65 public WemoCrockpotHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemoHttpCaller) {
66 super(thing, upnpIOService, wemoHttpCaller);
68 logger.debug("Creating a WemoCrockpotHandler for thing '{}'", getThing().getUID());
72 public void initialize() {
73 Configuration configuration = getConfig();
75 if (configuration.get(UDN) != null) {
76 logger.debug("Initializing WemoCrockpotHandler for UDN '{}'", configuration.get(UDN));
77 UpnpIOService localService = service;
78 if (localService != null) {
79 localService.registerParticipant(this);
82 pollingJob = scheduler.scheduleWithFixedDelay(this::poll, 0, DEFAULT_REFRESH_INTERVAL_SECONDS,
84 updateStatus(ThingStatus.ONLINE);
86 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
87 "@text/config-status.error.missing-udn");
88 logger.debug("Cannot initalize WemoCrockpotHandler. UDN not set.");
93 public void dispose() {
94 logger.debug("WeMoCrockpotHandler disposed.");
95 ScheduledFuture<?> job = this.pollingJob;
96 if (job != null && !job.isCancelled()) {
99 this.pollingJob = null;
100 removeSubscription();
103 private void poll() {
104 synchronized (jobLock) {
105 if (pollingJob == null) {
109 logger.debug("Polling job");
111 // Check if the Wemo device is set in the UPnP service registry
112 // If not, set the thing state to ONLINE/CONFIG-PENDING and wait for the next poll
113 if (!isUpnpDeviceRegistered()) {
114 logger.debug("UPnP device {} not yet registered", getUDN());
115 updateStatus(ThingStatus.ONLINE, ThingStatusDetail.CONFIGURATION_PENDING,
116 "@text/config-status.pending.device-not-registered [\"" + getUDN() + "\"]");
117 synchronized (upnpLock) {
118 subscriptionState = new HashMap<>();
122 updateStatus(ThingStatus.ONLINE);
125 } catch (Exception e) {
126 logger.debug("Exception during poll: {}", e.getMessage(), e);
132 public void handleCommand(ChannelUID channelUID, Command command) {
133 String localHost = getHost();
134 if (localHost.isEmpty()) {
135 logger.error("Failed to send command '{}' for device '{}': IP address missing", command,
136 getThing().getUID());
137 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
138 "@text/config-status.error.missing-ip");
141 String wemoURL = getWemoURL(localHost, BASICACTION);
142 if (wemoURL == null) {
143 logger.error("Failed to send command '{}' for device '{}': URL cannot be created", command,
144 getThing().getUID());
145 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
146 "@text/config-status.error.missing-url");
152 if (command instanceof RefreshType) {
154 } else if (CHANNEL_COOKMODE.equals(channelUID.getId())) {
155 String commandString = command.toString();
156 switch (commandString) {
172 String soapHeader = "\"urn:Belkin:service:basicevent:1#SetBinaryState\"";
173 String content = "<?xml version=\"1.0\"?>"
174 + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
175 + "<s:Body>" + "<u:SetCrockpotState xmlns:u=\"urn:Belkin:service:basicevent:1\">" + "<mode>"
176 + mode + "</mode>" + "<time>" + time + "</time>" + "</u:SetCrockpotState>" + "</s:Body>"
178 String wemoCallResponse = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
179 if (wemoCallResponse != null && logger.isTraceEnabled()) {
180 logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
181 logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader, getThing().getUID());
182 logger.trace("wemoCall with content '{}' for device '{}'", content, getThing().getUID());
183 logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse, getThing().getUID());
185 } catch (RuntimeException e) {
186 logger.debug("Failed to send command '{}' for device '{}':", command, getThing().getUID(), e);
187 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
189 updateStatus(ThingStatus.ONLINE);
194 public void onServiceSubscribed(@Nullable String service, boolean succeeded) {
195 if (service != null) {
196 logger.debug("WeMo {}: Subscription to service {} {}", getUDN(), service,
197 succeeded ? "succeeded" : "failed");
198 subscriptionState.put(service, succeeded);
203 public void onValueReceived(@Nullable String variable, @Nullable String value, @Nullable String service) {
204 logger.debug("Received pair '{}':'{}' (service '{}') for thing '{}'", variable, value, service,
205 this.getThing().getUID());
207 updateStatus(ThingStatus.ONLINE);
208 if (variable != null && value != null) {
209 this.stateMap.put(variable, value);
213 private synchronized void addSubscription() {
214 synchronized (upnpLock) {
215 UpnpIOService localService = service;
216 if (localService != null) {
217 if (localService.isRegistered(this)) {
218 logger.debug("Checking WeMo GENA subscription for '{}'", getThing().getUID());
220 String subscription = BASICEVENT;
222 if (subscriptionState.get(subscription) == null) {
223 logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(),
225 localService.addSubscription(this, subscription, SUBSCRIPTION_DURATION_SECONDS);
226 subscriptionState.put(subscription, true);
230 "Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
231 getThing().getUID());
237 private synchronized void removeSubscription() {
238 synchronized (upnpLock) {
239 UpnpIOService localService = service;
240 if (localService != null) {
241 if (localService.isRegistered(this)) {
242 logger.debug("Removing WeMo GENA subscription for '{}'", getThing().getUID());
243 String subscription = BASICEVENT;
245 if (subscriptionState.get(subscription) != null) {
246 logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription);
247 localService.removeSubscription(this, subscription);
249 subscriptionState.remove(subscription);
250 localService.unregisterParticipant(this);
257 * The {@link updateWemoState} polls the actual state of a WeMo device and
258 * calls {@link onValueReceived} to update the statemap and channels..
261 protected void updateWemoState() {
262 String localHost = getHost();
263 if (localHost.isEmpty()) {
264 logger.error("Failed to get actual state for device '{}': IP address missing", getThing().getUID());
265 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
266 "@text/config-status.error.missing-ip");
269 String actionService = BASICEVENT;
270 String wemoURL = getWemoURL(localHost, actionService);
271 if (wemoURL == null) {
272 logger.error("Failed to get actual state for device '{}': URL cannot be created", getThing().getUID());
273 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
274 "@text/config-status.error.missing-url");
278 String action = "GetCrockpotState";
279 String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
280 String content = createStateRequestContent(action, actionService);
281 String wemoCallResponse = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
282 if (wemoCallResponse != null) {
283 if (logger.isTraceEnabled()) {
284 logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
285 logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader, getThing().getUID());
286 logger.trace("wemoCall with content '{}' for device '{}'", content, getThing().getUID());
287 logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse, getThing().getUID());
289 String mode = substringBetween(wemoCallResponse, "<mode>", "</mode>");
290 String time = substringBetween(wemoCallResponse, "<time>", "</time>");
291 String coockedTime = substringBetween(wemoCallResponse, "<coockedTime>", "</coockedTime>");
293 State newMode = new StringType(mode);
294 State newCoockedTime = DecimalType.valueOf(coockedTime);
297 newMode = new StringType("OFF");
300 newMode = new StringType("WARM");
301 State warmTime = DecimalType.valueOf(time);
302 updateState(CHANNEL_WARMCOOKTIME, warmTime);
305 newMode = new StringType("LOW");
306 State lowTime = DecimalType.valueOf(time);
307 updateState(CHANNEL_LOWCOOKTIME, lowTime);
310 newMode = new StringType("HIGH");
311 State highTime = DecimalType.valueOf(time);
312 updateState(CHANNEL_HIGHCOOKTIME, highTime);
315 updateState(CHANNEL_COOKMODE, newMode);
316 updateState(CHANNEL_COOKEDTIME, newCoockedTime);
318 } catch (RuntimeException e) {
319 logger.debug("Failed to get actual state for device '{}': {}", getThing().getUID(), e.getMessage(), e);
320 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
322 updateStatus(ThingStatus.ONLINE);