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.HashMap;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.openhab.binding.wemo.internal.http.WemoHttpCall;
27 import org.openhab.core.config.core.Configuration;
28 import org.openhab.core.io.transport.upnp.UpnpIOParticipant;
29 import org.openhab.core.io.transport.upnp.UpnpIOService;
30 import org.openhab.core.library.types.IncreaseDecreaseType;
31 import org.openhab.core.library.types.OnOffType;
32 import org.openhab.core.library.types.PercentType;
33 import org.openhab.core.thing.Bridge;
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.ThingStatusInfo;
39 import org.openhab.core.thing.binding.ThingHandler;
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 * {@link WemoLightHandler} is the handler for a WeMo light, responsible for handling commands and state updates for the
48 * different channels of a WeMo light.
50 * @author Hans-Jörg Merk - Initial contribution
53 public class WemoLightHandler extends AbstractWemoHandler implements UpnpIOParticipant {
55 private final Logger logger = LoggerFactory.getLogger(WemoLightHandler.class);
57 private Map<String, Boolean> subscriptionState = new HashMap<>();
59 private final Object upnpLock = new Object();
60 private final Object jobLock = new Object();
62 private @Nullable WemoBridgeHandler wemoBridgeHandler;
64 private @Nullable UpnpIOService service;
66 private String host = "";
68 private @Nullable String wemoLightID;
70 private int currentBrightness;
72 private WemoHttpCall wemoCall;
75 * Set dimming stepsize to 5%
77 private static final int DIM_STEPSIZE = 5;
79 protected static final String SUBSCRIPTION = "bridge1";
82 * The default refresh initial delay in Seconds.
84 private static final int DEFAULT_REFRESH_INITIAL_DELAY = 15;
86 private @Nullable ScheduledFuture<?> pollingJob;
88 public WemoLightHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemoHttpcaller) {
89 super(thing, wemoHttpcaller);
91 this.service = upnpIOService;
92 this.wemoCall = wemoHttpcaller;
94 logger.debug("Creating a WemoLightHandler for thing '{}'", getThing().getUID());
98 public void initialize() {
99 // initialize() is only called if the required parameter 'deviceID' is available
100 wemoLightID = (String) getConfig().get(DEVICE_ID);
102 final Bridge bridge = getBridge();
103 if (bridge != null && bridge.getStatus() == ThingStatus.ONLINE) {
104 UpnpIOService localService = service;
105 if (localService != null) {
106 localService.registerParticipant(this);
109 pollingJob = scheduler.scheduleWithFixedDelay(this::poll, DEFAULT_REFRESH_INITIAL_DELAY,
110 DEFAULT_REFRESH_INTERVALL_SECONDS, TimeUnit.SECONDS);
111 updateStatus(ThingStatus.ONLINE);
113 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.BRIDGE_OFFLINE);
118 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
119 if (bridgeStatusInfo.getStatus().equals(ThingStatus.ONLINE)) {
120 updateStatus(ThingStatus.ONLINE);
122 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.BRIDGE_OFFLINE);
123 ScheduledFuture<?> job = this.pollingJob;
124 if (job != null && !job.isCancelled()) {
127 this.pollingJob = null;
132 public void dispose() {
133 logger.debug("WeMoLightHandler disposed.");
135 ScheduledFuture<?> job = this.pollingJob;
136 if (job != null && !job.isCancelled()) {
139 this.pollingJob = null;
140 removeSubscription();
143 private synchronized @Nullable WemoBridgeHandler getWemoBridgeHandler() {
144 Bridge bridge = getBridge();
145 if (bridge == null) {
146 logger.error("Required bridge not defined for device {}.", wemoLightID);
149 ThingHandler handler = bridge.getHandler();
150 if (handler instanceof WemoBridgeHandler) {
151 this.wemoBridgeHandler = (WemoBridgeHandler) handler;
153 logger.debug("No available bridge handler found for {} bridge {} .", wemoLightID, bridge.getUID());
156 return this.wemoBridgeHandler;
159 private void poll() {
160 synchronized (jobLock) {
161 if (pollingJob == null) {
165 logger.debug("Polling job");
167 // Check if the Wemo device is set in the UPnP service registry
168 // If not, set the thing state to ONLINE/CONFIG-PENDING and wait for the next poll
169 if (!isUpnpDeviceRegistered()) {
170 logger.debug("UPnP device {} not yet registered", getUDN());
171 updateStatus(ThingStatus.ONLINE, ThingStatusDetail.CONFIGURATION_PENDING,
172 "@text/config-status.pending.device-not-registered [\"" + getUDN() + "\"]");
173 synchronized (upnpLock) {
174 subscriptionState = new HashMap<>();
178 updateStatus(ThingStatus.ONLINE);
181 } catch (Exception e) {
182 logger.debug("Exception during poll: {}", e.getMessage(), e);
188 public void handleCommand(ChannelUID channelUID, Command command) {
189 String localHost = getHost();
190 if (localHost.isEmpty()) {
191 logger.error("Failed to send command '{}' for device '{}': IP address missing", command,
192 getThing().getUID());
193 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
194 "@text/config-status.error.missing-ip");
197 String wemoURL = getWemoURL(localHost, BASICACTION);
198 if (wemoURL == null) {
199 logger.error("Failed to send command '{}' for device '{}': URL cannot be created", command,
200 getThing().getUID());
201 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
202 "@text/config-status.error.missing-url");
205 if (command instanceof RefreshType) {
208 } catch (Exception e) {
209 logger.debug("Exception during poll", e);
212 Configuration configuration = getConfig();
213 configuration.get(DEVICE_ID);
215 WemoBridgeHandler wemoBridge = getWemoBridgeHandler();
216 if (wemoBridge == null) {
217 logger.debug("wemoBridgeHandler not found, cannot handle command");
220 String devUDN = "uuid:" + wemoBridge.getThing().getConfiguration().get(UDN).toString();
221 logger.trace("WeMo Bridge to send command to : {}", devUDN);
224 String capability = null;
225 switch (channelUID.getId()) {
226 case CHANNEL_BRIGHTNESS:
227 capability = "10008";
228 if (command instanceof PercentType) {
229 int newBrightness = ((PercentType) command).intValue();
230 logger.trace("wemoLight received Value {}", newBrightness);
231 int value1 = Math.round(newBrightness * 255 / 100);
232 value = value1 + ":0";
233 currentBrightness = newBrightness;
234 } else if (command instanceof OnOffType) {
235 switch (command.toString()) {
243 } else if (command instanceof IncreaseDecreaseType) {
245 switch (command.toString()) {
247 currentBrightness = currentBrightness + DIM_STEPSIZE;
248 newBrightness = Math.round(currentBrightness * 255 / 100);
249 if (newBrightness > 255) {
252 value = newBrightness + ":0";
255 currentBrightness = currentBrightness - DIM_STEPSIZE;
256 newBrightness = Math.round(currentBrightness * 255 / 100);
257 if (newBrightness < 0) {
260 value = newBrightness + ":0";
266 capability = "10006";
267 switch (command.toString()) {
278 if (capability != null && value != null) {
279 String soapHeader = "\"urn:Belkin:service:bridge:1#SetDeviceStatus\"";
280 String content = "<?xml version=\"1.0\"?>"
281 + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
282 + "<s:Body>" + "<u:SetDeviceStatus xmlns:u=\"urn:Belkin:service:bridge:1\">"
283 + "<DeviceStatusList>"
284 + "<?xml version="1.0" encoding="UTF-8"?><DeviceStatus><DeviceID>"
286 + "</DeviceID><IsGroupAction>NO</IsGroupAction><CapabilityID>"
287 + capability + "</CapabilityID><CapabilityValue>" + value
288 + "</CapabilityValue></DeviceStatus>" + "</DeviceStatusList>"
289 + "</u:SetDeviceStatus>" + "</s:Body>" + "</s:Envelope>";
291 String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
292 if (wemoCallResponse != null) {
293 if (logger.isTraceEnabled()) {
294 logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
295 logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader,
296 getThing().getUID());
297 logger.trace("wemoCall with content '{}' for device '{}'", content, getThing().getUID());
298 logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse,
299 getThing().getUID());
301 if ("10008".equals(capability)) {
302 OnOffType binaryState = null;
303 binaryState = "0".equals(value) ? OnOffType.OFF : OnOffType.ON;
304 updateState(CHANNEL_STATE, binaryState);
308 } catch (Exception e) {
309 throw new IllegalStateException("Could not send command to WeMo Bridge", e);
315 public @Nullable String getUDN() {
316 WemoBridgeHandler wemoBridge = getWemoBridgeHandler();
317 if (wemoBridge == null) {
318 logger.debug("wemoBridgeHandler not found");
321 return (String) wemoBridge.getThing().getConfiguration().get(UDN);
325 * The {@link getDeviceState} is used for polling the actual state of a WeMo Light and updating the according
328 public void getDeviceState() {
329 String localHost = getHost();
330 if (localHost.isEmpty()) {
331 logger.error("Failed to get actual state for device '{}': IP address missing", getThing().getUID());
332 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
333 "@text/config-status.error.missing-ip");
336 logger.debug("Request actual state for LightID '{}'", wemoLightID);
337 String wemoURL = getWemoURL(localHost, BRIDGEACTION);
338 if (wemoURL == null) {
339 logger.error("Failed to get actual state for device '{}': URL cannot be created", getThing().getUID());
340 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
341 "@text/config-status.error.missing-url");
345 String soapHeader = "\"urn:Belkin:service:bridge:1#GetDeviceStatus\"";
346 String content = "<?xml version=\"1.0\"?>"
347 + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
348 + "<s:Body>" + "<u:GetDeviceStatus xmlns:u=\"urn:Belkin:service:bridge:1\">" + "<DeviceIDs>"
349 + wemoLightID + "</DeviceIDs>" + "</u:GetDeviceStatus>" + "</s:Body>" + "</s:Envelope>";
351 String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
352 if (wemoCallResponse != null) {
353 if (logger.isTraceEnabled()) {
354 logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
355 logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader, getThing().getUID());
356 logger.trace("wemoCall with content '{}' for device '{}'", content, getThing().getUID());
357 logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse, getThing().getUID());
359 wemoCallResponse = unescapeXml(wemoCallResponse);
360 String response = substringBetween(wemoCallResponse, "<CapabilityValue>", "</CapabilityValue>");
361 logger.trace("wemoNewLightState = {}", response);
362 String[] splitResponse = response.split(",");
363 if (splitResponse[0] != null) {
364 OnOffType binaryState = null;
365 binaryState = "0".equals(splitResponse[0]) ? OnOffType.OFF : OnOffType.ON;
366 updateState(CHANNEL_STATE, binaryState);
368 if (splitResponse[1] != null) {
369 String splitBrightness[] = splitResponse[1].split(":");
370 if (splitBrightness[0] != null) {
371 int newBrightnessValue = Integer.valueOf(splitBrightness[0]);
372 int newBrightness = Math.round(newBrightnessValue * 100 / 255);
373 logger.trace("newBrightness = {}", newBrightness);
374 State newBrightnessState = new PercentType(newBrightness);
375 updateState(CHANNEL_BRIGHTNESS, newBrightnessState);
376 currentBrightness = newBrightness;
380 } catch (Exception e) {
381 throw new IllegalStateException("Could not retrieve new Wemo light state", e);
386 public void onServiceSubscribed(@Nullable String service, boolean succeeded) {
390 public void onValueReceived(@Nullable String variable, @Nullable String value, @Nullable String service) {
391 logger.trace("Received pair '{}':'{}' (service '{}') for thing '{}'",
392 new Object[] { variable, value, service, this.getThing().getUID() });
393 String capabilityId = substringBetween(value, "<CapabilityId>", "</CapabilityId>");
394 String newValue = substringBetween(value, "<Value>", "</Value>");
395 switch (capabilityId) {
397 OnOffType binaryState = null;
398 binaryState = "0".equals(newValue) ? OnOffType.OFF : OnOffType.ON;
399 updateState(CHANNEL_STATE, binaryState);
402 String splitValue[] = newValue.split(":");
403 if (splitValue[0] != null) {
404 int newBrightnessValue = Integer.valueOf(splitValue[0]);
405 int newBrightness = Math.round(newBrightnessValue * 100 / 255);
406 State newBrightnessState = new PercentType(newBrightness);
407 updateState(CHANNEL_BRIGHTNESS, newBrightnessState);
408 currentBrightness = newBrightness;
415 public void onStatusChanged(boolean status) {
418 private synchronized void addSubscription() {
419 synchronized (upnpLock) {
420 UpnpIOService localService = service;
421 if (localService != null) {
422 if (localService.isRegistered(this)) {
423 logger.debug("Checking WeMo GENA subscription for '{}'", getThing().getUID());
425 if (subscriptionState.get(SUBSCRIPTION) == null) {
426 logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(),
428 localService.addSubscription(this, SUBSCRIPTION, SUBSCRIPTION_DURATION_SECONDS);
429 subscriptionState.put(SUBSCRIPTION, true);
433 "Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
434 getThing().getUID());
440 private synchronized void removeSubscription() {
441 synchronized (upnpLock) {
442 UpnpIOService localService = service;
443 if (localService != null) {
444 if (localService.isRegistered(this)) {
445 logger.debug("Removing WeMo GENA subscription for '{}'", getThing().getUID());
447 if (subscriptionState.get(SUBSCRIPTION) != null) {
448 logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), SUBSCRIPTION);
449 localService.removeSubscription(this, SUBSCRIPTION);
451 subscriptionState = new HashMap<>();
452 localService.unregisterParticipant(this);
458 private boolean isUpnpDeviceRegistered() {
459 UpnpIOService localService = service;
460 if (localService != null) {
461 return localService.isRegistered(this);
466 public String getHost() {
467 String localHost = host;
468 if (!localHost.isEmpty()) {
471 UpnpIOService localService = service;
472 if (localService != null) {
473 URL descriptorURL = localService.getDescriptorURL(this);
474 if (descriptorURL != null) {
475 return descriptorURL.getHost();