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.*;
17 import java.math.BigDecimal;
19 import java.util.HashMap;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
24 import org.apache.commons.lang3.StringEscapeUtils;
25 import org.apache.commons.lang3.StringUtils;
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
52 public class WemoLightHandler extends AbstractWemoHandler implements UpnpIOParticipant {
54 private final Logger logger = LoggerFactory.getLogger(WemoLightHandler.class);
56 private Map<String, Boolean> subscriptionState = new HashMap<>();
58 private UpnpIOService service;
60 private WemoBridgeHandler wemoBridgeHandler;
62 private String wemoLightID;
64 private int currentBrightness;
67 * Set dimming stepsize to 5%
69 private static final int DIM_STEPSIZE = 5;
71 protected static final String SUBSCRIPTION = "bridge1";
73 protected static final int SUBSCRIPTION_DURATION = 600;
76 * The default refresh interval in Seconds.
78 private final int DEFAULT_REFRESH_INTERVAL = 60;
81 * The default refresh initial delay in Seconds.
83 private static final int DEFAULT_REFRESH_INITIAL_DELAY = 15;
85 private ScheduledFuture<?> refreshJob;
87 private final Runnable refreshRunnable = new Runnable() {
92 if (!isUpnpDeviceRegistered()) {
93 logger.debug("WeMo UPnP device {} not yet registered", getUDN());
98 } catch (Exception e) {
99 logger.debug("Exception during poll", e);
100 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
105 public WemoLightHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemoHttpcaller) {
108 this.wemoHttpCaller = wemoHttpcaller;
110 if (upnpIOService != null) {
111 logger.debug("UPnPIOService '{}'", upnpIOService);
112 this.service = upnpIOService;
114 logger.debug("upnpIOService not set.");
119 public void initialize() {
120 // initialize() is only called if the required parameter 'deviceID' is available
121 wemoLightID = (String) getConfig().get(DEVICE_ID);
123 if (getBridge() != null) {
124 logger.debug("Initializing WemoLightHandler for LightID '{}'", wemoLightID);
125 if (getBridge().getStatus() == ThingStatus.ONLINE) {
126 updateStatus(ThingStatus.ONLINE);
130 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.BRIDGE_OFFLINE);
133 updateStatus(ThingStatus.OFFLINE);
138 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
139 if (bridgeStatusInfo.getStatus().equals(ThingStatus.ONLINE)) {
140 updateStatus(ThingStatus.ONLINE);
144 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.BRIDGE_OFFLINE);
145 if (refreshJob != null && !refreshJob.isCancelled()) {
146 refreshJob.cancel(true);
153 public void dispose() {
154 logger.debug("WeMoLightHandler disposed.");
156 removeSubscription();
158 if (refreshJob != null && !refreshJob.isCancelled()) {
159 refreshJob.cancel(true);
164 private synchronized WemoBridgeHandler getWemoBridgeHandler() {
165 if (this.wemoBridgeHandler == null) {
166 Bridge bridge = getBridge();
167 if (bridge == null) {
168 logger.error("Required bridge not defined for device {}.", wemoLightID);
171 ThingHandler handler = bridge.getHandler();
172 if (handler instanceof WemoBridgeHandler) {
173 this.wemoBridgeHandler = (WemoBridgeHandler) handler;
175 logger.debug("No available bridge handler found for {} bridge {} .", wemoLightID, bridge.getUID());
179 return this.wemoBridgeHandler;
183 public void handleCommand(ChannelUID channelUID, Command command) {
184 if (command instanceof RefreshType) {
187 } catch (Exception e) {
188 logger.debug("Exception during poll", e);
191 Configuration configuration = getConfig();
192 configuration.get(DEVICE_ID);
194 WemoBridgeHandler wemoBridge = getWemoBridgeHandler();
195 if (wemoBridge == null) {
196 logger.debug("wemoBridgeHandler not found, cannot handle command");
199 String devUDN = "uuid:" + wemoBridge.getThing().getConfiguration().get(UDN).toString();
200 logger.trace("WeMo Bridge to send command to : {}", devUDN);
203 String capability = null;
204 switch (channelUID.getId()) {
205 case CHANNEL_BRIGHTNESS:
206 capability = "10008";
207 if (command instanceof PercentType) {
208 int newBrightness = ((PercentType) command).intValue();
209 logger.trace("wemoLight received Value {}", newBrightness);
210 int value1 = Math.round(newBrightness * 255 / 100);
211 value = value1 + ":0";
212 currentBrightness = newBrightness;
213 } else if (command instanceof OnOffType) {
214 switch (command.toString()) {
222 } else if (command instanceof IncreaseDecreaseType) {
224 switch (command.toString()) {
226 currentBrightness = currentBrightness + DIM_STEPSIZE;
227 newBrightness = Math.round(currentBrightness * 255 / 100);
228 if (newBrightness > 255) {
231 value = newBrightness + ":0";
234 currentBrightness = currentBrightness - DIM_STEPSIZE;
235 newBrightness = Math.round(currentBrightness * 255 / 100);
236 if (newBrightness < 0) {
239 value = newBrightness + ":0";
245 capability = "10006";
246 switch (command.toString()) {
257 String soapHeader = "\"urn:Belkin:service:bridge:1#SetDeviceStatus\"";
258 String content = "<?xml version=\"1.0\"?>"
259 + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
260 + "<s:Body>" + "<u:SetDeviceStatus xmlns:u=\"urn:Belkin:service:bridge:1\">"
261 + "<DeviceStatusList>"
262 + "<?xml version="1.0" encoding="UTF-8"?><DeviceStatus><DeviceID>"
264 + "</DeviceID><IsGroupAction>NO</IsGroupAction><CapabilityID>"
265 + capability + "</CapabilityID><CapabilityValue>" + value
266 + "</CapabilityValue></DeviceStatus>" + "</DeviceStatusList>"
267 + "</u:SetDeviceStatus>" + "</s:Body>" + "</s:Envelope>";
269 String wemoURL = getWemoURL();
271 if (wemoURL != null && capability != null && value != null) {
272 String wemoCallResponse = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
273 if (wemoCallResponse != null) {
274 if (capability.equals("10008")) {
275 OnOffType binaryState = null;
276 binaryState = value.equals("0") ? OnOffType.OFF : OnOffType.ON;
277 updateState(CHANNEL_STATE, binaryState);
281 } catch (Exception e) {
282 throw new IllegalStateException("Could not send command to WeMo Bridge", e);
288 public String getUDN() {
289 WemoBridgeHandler wemoBridge = getWemoBridgeHandler();
290 if (wemoBridge == null) {
291 logger.debug("wemoBridgeHandler not found");
294 return (String) wemoBridge.getThing().getConfiguration().get(UDN);
298 * The {@link getDeviceState} is used for polling the actual state of a WeMo Light and updating the according
301 public void getDeviceState() {
302 logger.debug("Request actual state for LightID '{}'", wemoLightID);
304 String soapHeader = "\"urn:Belkin:service:bridge:1#GetDeviceStatus\"";
305 String content = "<?xml version=\"1.0\"?>"
306 + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
307 + "<s:Body>" + "<u:GetDeviceStatus xmlns:u=\"urn:Belkin:service:bridge:1\">" + "<DeviceIDs>"
308 + wemoLightID + "</DeviceIDs>" + "</u:GetDeviceStatus>" + "</s:Body>" + "</s:Envelope>";
310 String wemoURL = getWemoURL();
312 if (wemoURL != null) {
313 String wemoCallResponse = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
314 if (wemoCallResponse != null) {
315 wemoCallResponse = StringEscapeUtils.unescapeXml(wemoCallResponse);
316 String response = StringUtils.substringBetween(wemoCallResponse, "<CapabilityValue>",
317 "</CapabilityValue>");
318 logger.trace("wemoNewLightState = {}", response);
319 String[] splitResponse = response.split(",");
320 if (splitResponse[0] != null) {
321 OnOffType binaryState = null;
322 binaryState = splitResponse[0].equals("0") ? OnOffType.OFF : OnOffType.ON;
323 updateState(CHANNEL_STATE, binaryState);
325 if (splitResponse[1] != null) {
326 String splitBrightness[] = splitResponse[1].split(":");
327 if (splitBrightness[0] != null) {
328 int newBrightnessValue = Integer.valueOf(splitBrightness[0]);
329 int newBrightness = Math.round(newBrightnessValue * 100 / 255);
330 logger.trace("newBrightness = {}", newBrightness);
331 State newBrightnessState = new PercentType(newBrightness);
332 updateState(CHANNEL_BRIGHTNESS, newBrightnessState);
333 currentBrightness = newBrightness;
338 } catch (Exception e) {
339 throw new IllegalStateException("Could not retrieve new Wemo light state", e);
344 public void onServiceSubscribed(String service, boolean succeeded) {
348 public void onValueReceived(String variable, String value, String service) {
349 logger.trace("Received pair '{}':'{}' (service '{}') for thing '{}'",
350 new Object[] { variable, value, service, this.getThing().getUID() });
351 String capabilityId = StringUtils.substringBetween(value, "<CapabilityId>", "</CapabilityId>");
352 String newValue = StringUtils.substringBetween(value, "<Value>", "</Value>");
353 switch (capabilityId) {
355 OnOffType binaryState = null;
356 binaryState = newValue.equals("0") ? OnOffType.OFF : OnOffType.ON;
357 updateState(CHANNEL_STATE, binaryState);
360 String splitValue[] = newValue.split(":");
361 if (splitValue[0] != null) {
362 int newBrightnessValue = Integer.valueOf(splitValue[0]);
363 int newBrightness = Math.round(newBrightnessValue * 100 / 255);
364 State newBrightnessState = new PercentType(newBrightness);
365 updateState(CHANNEL_BRIGHTNESS, newBrightnessState);
366 currentBrightness = newBrightness;
373 public void onStatusChanged(boolean status) {
376 private synchronized void onSubscription() {
377 if (service.isRegistered(this)) {
378 logger.debug("Checking WeMo GENA subscription for '{}'", this);
380 if ((subscriptionState.get(SUBSCRIPTION) == null) || !subscriptionState.get(SUBSCRIPTION).booleanValue()) {
381 logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(), SUBSCRIPTION);
382 service.addSubscription(this, SUBSCRIPTION, SUBSCRIPTION_DURATION);
383 subscriptionState.put(SUBSCRIPTION, true);
386 logger.debug("Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
391 private synchronized void removeSubscription() {
392 if (service.isRegistered(this)) {
393 logger.debug("Removing WeMo GENA subscription for '{}'", this);
395 if ((subscriptionState.get(SUBSCRIPTION) != null) && subscriptionState.get(SUBSCRIPTION).booleanValue()) {
396 logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), SUBSCRIPTION);
397 service.removeSubscription(this, SUBSCRIPTION);
400 subscriptionState = new HashMap<>();
401 service.unregisterParticipant(this);
405 private synchronized void onUpdate() {
406 if (refreshJob == null || refreshJob.isCancelled()) {
407 Configuration config = getThing().getConfiguration();
408 int refreshInterval = DEFAULT_REFRESH_INTERVAL;
409 Object refreshConfig = config.get("refresh");
410 if (refreshConfig != null) {
411 refreshInterval = ((BigDecimal) refreshConfig).intValue();
413 logger.trace("Start polling job for LightID '{}'", wemoLightID);
414 refreshJob = scheduler.scheduleWithFixedDelay(refreshRunnable, DEFAULT_REFRESH_INITIAL_DELAY,
415 refreshInterval, TimeUnit.SECONDS);
419 private boolean isUpnpDeviceRegistered() {
420 return service.isRegistered(this);
423 public String getWemoURL() {
424 URL descriptorURL = service.getDescriptorURL(this);
425 String wemoURL = null;
426 if (descriptorURL != null) {
427 String deviceURL = StringUtils.substringBefore(descriptorURL.toString(), "/setup.xml");
428 wemoURL = deviceURL + "/upnp/control/bridge1";