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.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.UpnpIOParticipant;
30 import org.openhab.core.io.transport.upnp.UpnpIOService;
31 import org.openhab.core.library.types.IncreaseDecreaseType;
32 import org.openhab.core.library.types.OnOffType;
33 import org.openhab.core.library.types.PercentType;
34 import org.openhab.core.thing.Bridge;
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.ThingStatusInfo;
40 import org.openhab.core.thing.binding.ThingHandler;
41 import org.openhab.core.types.Command;
42 import org.openhab.core.types.RefreshType;
43 import org.openhab.core.types.State;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
48 * {@link WemoLightHandler} is the handler for a WeMo light, responsible for handling commands and state updates for the
49 * different channels of a WeMo light.
51 * @author Hans-Jörg Merk - Initial contribution
54 public class WemoLightHandler extends AbstractWemoHandler implements UpnpIOParticipant {
56 private final Logger logger = LoggerFactory.getLogger(WemoLightHandler.class);
58 private Map<String, Boolean> subscriptionState = new HashMap<>();
60 private UpnpIOService service;
61 private WemoHttpCall wemoCall;
63 private @Nullable WemoBridgeHandler wemoBridgeHandler;
65 private @Nullable String wemoLightID;
67 private int currentBrightness;
70 * Set dimming stepsize to 5%
72 private static final int DIM_STEPSIZE = 5;
74 protected static final String SUBSCRIPTION = "bridge1";
77 * The default refresh initial delay in Seconds.
79 private static final int DEFAULT_REFRESH_INITIAL_DELAY = 15;
81 private @Nullable ScheduledFuture<?> refreshJob;
83 private final Runnable refreshRunnable = new Runnable() {
88 if (!isUpnpDeviceRegistered()) {
89 logger.debug("WeMo UPnP device {} not yet registered", getUDN());
94 } catch (Exception e) {
95 logger.debug("Exception during poll", e);
96 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
101 public WemoLightHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemoHttpcaller) {
102 super(thing, wemoHttpcaller);
104 this.service = upnpIOService;
105 this.wemoCall = wemoHttpcaller;
109 public void initialize() {
110 // initialize() is only called if the required parameter 'deviceID' is available
111 wemoLightID = (String) getConfig().get(DEVICE_ID);
113 final Bridge bridge = getBridge();
114 if (bridge != null && bridge.getStatus() == ThingStatus.ONLINE) {
115 updateStatus(ThingStatus.ONLINE);
119 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.BRIDGE_OFFLINE);
124 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
125 if (bridgeStatusInfo.getStatus().equals(ThingStatus.ONLINE)) {
126 updateStatus(ThingStatus.ONLINE);
130 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.BRIDGE_OFFLINE);
131 ScheduledFuture<?> job = refreshJob;
132 if (job != null && !job.isCancelled()) {
140 public void dispose() {
141 logger.debug("WeMoLightHandler disposed.");
143 ScheduledFuture<?> job = refreshJob;
144 if (job != null && !job.isCancelled()) {
148 removeSubscription();
151 private synchronized @Nullable WemoBridgeHandler getWemoBridgeHandler() {
152 Bridge bridge = getBridge();
153 if (bridge == null) {
154 logger.error("Required bridge not defined for device {}.", wemoLightID);
157 ThingHandler handler = bridge.getHandler();
158 if (handler instanceof WemoBridgeHandler) {
159 this.wemoBridgeHandler = (WemoBridgeHandler) handler;
161 logger.debug("No available bridge handler found for {} bridge {} .", wemoLightID, bridge.getUID());
164 return this.wemoBridgeHandler;
168 public void handleCommand(ChannelUID channelUID, Command command) {
169 if (command instanceof RefreshType) {
172 } catch (Exception e) {
173 logger.debug("Exception during poll", e);
176 Configuration configuration = getConfig();
177 configuration.get(DEVICE_ID);
179 WemoBridgeHandler wemoBridge = getWemoBridgeHandler();
180 if (wemoBridge == null) {
181 logger.debug("wemoBridgeHandler not found, cannot handle command");
184 String devUDN = "uuid:" + wemoBridge.getThing().getConfiguration().get(UDN).toString();
185 logger.trace("WeMo Bridge to send command to : {}", devUDN);
188 String capability = null;
189 switch (channelUID.getId()) {
190 case CHANNEL_BRIGHTNESS:
191 capability = "10008";
192 if (command instanceof PercentType) {
193 int newBrightness = ((PercentType) command).intValue();
194 logger.trace("wemoLight received Value {}", newBrightness);
195 int value1 = Math.round(newBrightness * 255 / 100);
196 value = value1 + ":0";
197 currentBrightness = newBrightness;
198 } else if (command instanceof OnOffType) {
199 switch (command.toString()) {
207 } else if (command instanceof IncreaseDecreaseType) {
209 switch (command.toString()) {
211 currentBrightness = currentBrightness + DIM_STEPSIZE;
212 newBrightness = Math.round(currentBrightness * 255 / 100);
213 if (newBrightness > 255) {
216 value = newBrightness + ":0";
219 currentBrightness = currentBrightness - DIM_STEPSIZE;
220 newBrightness = Math.round(currentBrightness * 255 / 100);
221 if (newBrightness < 0) {
224 value = newBrightness + ":0";
230 capability = "10006";
231 switch (command.toString()) {
242 String soapHeader = "\"urn:Belkin:service:bridge:1#SetDeviceStatus\"";
243 String content = "<?xml version=\"1.0\"?>"
244 + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
245 + "<s:Body>" + "<u:SetDeviceStatus xmlns:u=\"urn:Belkin:service:bridge:1\">"
246 + "<DeviceStatusList>"
247 + "<?xml version="1.0" encoding="UTF-8"?><DeviceStatus><DeviceID>"
249 + "</DeviceID><IsGroupAction>NO</IsGroupAction><CapabilityID>"
250 + capability + "</CapabilityID><CapabilityValue>" + value
251 + "</CapabilityValue></DeviceStatus>" + "</DeviceStatusList>"
252 + "</u:SetDeviceStatus>" + "</s:Body>" + "</s:Envelope>";
254 URL descriptorURL = service.getDescriptorURL(this);
255 String wemoURL = getWemoURL(descriptorURL, "bridge");
257 if (wemoURL != null && capability != null && value != null) {
258 String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
259 if (wemoCallResponse != null) {
260 if (capability.equals("10008")) {
261 OnOffType binaryState = null;
262 binaryState = value.equals("0") ? OnOffType.OFF : OnOffType.ON;
263 updateState(CHANNEL_STATE, binaryState);
267 } catch (Exception e) {
268 throw new IllegalStateException("Could not send command to WeMo Bridge", e);
274 public @Nullable String getUDN() {
275 WemoBridgeHandler wemoBridge = getWemoBridgeHandler();
276 if (wemoBridge == null) {
277 logger.debug("wemoBridgeHandler not found");
280 return (String) wemoBridge.getThing().getConfiguration().get(UDN);
284 * The {@link getDeviceState} is used for polling the actual state of a WeMo Light and updating the according
287 public void getDeviceState() {
288 logger.debug("Request actual state for LightID '{}'", wemoLightID);
290 String soapHeader = "\"urn:Belkin:service:bridge:1#GetDeviceStatus\"";
291 String content = "<?xml version=\"1.0\"?>"
292 + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
293 + "<s:Body>" + "<u:GetDeviceStatus xmlns:u=\"urn:Belkin:service:bridge:1\">" + "<DeviceIDs>"
294 + wemoLightID + "</DeviceIDs>" + "</u:GetDeviceStatus>" + "</s:Body>" + "</s:Envelope>";
296 URL descriptorURL = service.getDescriptorURL(this);
297 String wemoURL = getWemoURL(descriptorURL, "bridge");
299 if (wemoURL != null) {
300 String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
301 if (wemoCallResponse != null) {
302 wemoCallResponse = unescapeXml(wemoCallResponse);
303 String response = substringBetween(wemoCallResponse, "<CapabilityValue>", "</CapabilityValue>");
304 logger.trace("wemoNewLightState = {}", response);
305 String[] splitResponse = response.split(",");
306 if (splitResponse[0] != null) {
307 OnOffType binaryState = null;
308 binaryState = splitResponse[0].equals("0") ? OnOffType.OFF : OnOffType.ON;
309 updateState(CHANNEL_STATE, binaryState);
311 if (splitResponse[1] != null) {
312 String splitBrightness[] = splitResponse[1].split(":");
313 if (splitBrightness[0] != null) {
314 int newBrightnessValue = Integer.valueOf(splitBrightness[0]);
315 int newBrightness = Math.round(newBrightnessValue * 100 / 255);
316 logger.trace("newBrightness = {}", newBrightness);
317 State newBrightnessState = new PercentType(newBrightness);
318 updateState(CHANNEL_BRIGHTNESS, newBrightnessState);
319 currentBrightness = newBrightness;
324 } catch (Exception e) {
325 throw new IllegalStateException("Could not retrieve new Wemo light state", e);
330 public void onServiceSubscribed(@Nullable String service, boolean succeeded) {
334 public void onValueReceived(@Nullable String variable, @Nullable String value, @Nullable String service) {
335 logger.trace("Received pair '{}':'{}' (service '{}') for thing '{}'",
336 new Object[] { variable, value, service, this.getThing().getUID() });
337 String capabilityId = substringBetween(value, "<CapabilityId>", "</CapabilityId>");
338 String newValue = substringBetween(value, "<Value>", "</Value>");
339 switch (capabilityId) {
341 OnOffType binaryState = null;
342 binaryState = newValue.equals("0") ? OnOffType.OFF : OnOffType.ON;
343 updateState(CHANNEL_STATE, binaryState);
346 String splitValue[] = newValue.split(":");
347 if (splitValue[0] != null) {
348 int newBrightnessValue = Integer.valueOf(splitValue[0]);
349 int newBrightness = Math.round(newBrightnessValue * 100 / 255);
350 State newBrightnessState = new PercentType(newBrightness);
351 updateState(CHANNEL_BRIGHTNESS, newBrightnessState);
352 currentBrightness = newBrightness;
359 public void onStatusChanged(boolean status) {
362 private synchronized void onSubscription() {
363 if (service.isRegistered(this)) {
364 logger.debug("Checking WeMo GENA subscription for '{}'", this);
366 if (subscriptionState.get(SUBSCRIPTION) == null) {
367 logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(), SUBSCRIPTION);
368 service.addSubscription(this, SUBSCRIPTION, SUBSCRIPTION_DURATION_SECONDS);
369 subscriptionState.put(SUBSCRIPTION, true);
372 logger.debug("Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
377 private synchronized void removeSubscription() {
378 if (service.isRegistered(this)) {
379 logger.debug("Removing WeMo GENA subscription for '{}'", this);
381 if (subscriptionState.get(SUBSCRIPTION) != null) {
382 logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), SUBSCRIPTION);
383 service.removeSubscription(this, SUBSCRIPTION);
386 subscriptionState = new HashMap<>();
387 service.unregisterParticipant(this);
391 private synchronized void onUpdate() {
392 ScheduledFuture<?> job = refreshJob;
393 if (job == null || job.isCancelled()) {
394 Configuration config = getThing().getConfiguration();
395 int refreshInterval = DEFAULT_REFRESH_INTERVALL_SECONDS;
396 Object refreshConfig = config.get("refresh");
397 if (refreshConfig != null) {
398 refreshInterval = ((BigDecimal) refreshConfig).intValue();
400 logger.trace("Start polling job for LightID '{}'", wemoLightID);
401 refreshJob = scheduler.scheduleWithFixedDelay(refreshRunnable, DEFAULT_REFRESH_INITIAL_DELAY,
402 refreshInterval, TimeUnit.SECONDS);
406 private boolean isUpnpDeviceRegistered() {
407 return service.isRegistered(this);