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.HashMap;
20 import java.util.concurrent.ScheduledFuture;
21 import java.util.concurrent.TimeUnit;
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.binding.wemo.internal.http.WemoHttpCall;
26 import org.openhab.core.config.core.Configuration;
27 import org.openhab.core.io.transport.upnp.UpnpIOService;
28 import org.openhab.core.library.types.IncreaseDecreaseType;
29 import org.openhab.core.library.types.OnOffType;
30 import org.openhab.core.library.types.PercentType;
31 import org.openhab.core.thing.Bridge;
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.ThingStatusInfo;
37 import org.openhab.core.thing.binding.ThingHandler;
38 import org.openhab.core.types.Command;
39 import org.openhab.core.types.RefreshType;
40 import org.openhab.core.types.State;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
45 * {@link WemoLightHandler} is the handler for a WeMo light, responsible for handling commands and state updates for the
46 * different channels of a WeMo light.
48 * @author Hans-Jörg Merk - Initial contribution
51 public class WemoLightHandler extends WemoBaseThingHandler {
53 private final Logger logger = LoggerFactory.getLogger(WemoLightHandler.class);
55 private Map<String, Boolean> subscriptionState = new HashMap<>();
57 private final Object upnpLock = new Object();
58 private final Object jobLock = new Object();
60 private @Nullable WemoBridgeHandler wemoBridgeHandler;
62 private @Nullable 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";
74 * The default refresh initial delay in Seconds.
76 private static final int DEFAULT_REFRESH_INITIAL_DELAY = 15;
78 private @Nullable ScheduledFuture<?> pollingJob;
80 public WemoLightHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemoHttpcaller) {
81 super(thing, upnpIOService, wemoHttpcaller);
83 logger.debug("Creating a WemoLightHandler for thing '{}'", getThing().getUID());
87 public void initialize() {
88 // initialize() is only called if the required parameter 'deviceID' is available
89 wemoLightID = (String) getConfig().get(DEVICE_ID);
91 final Bridge bridge = getBridge();
92 if (bridge != null && bridge.getStatus() == ThingStatus.ONLINE) {
93 UpnpIOService localService = service;
94 if (localService != null) {
95 localService.registerParticipant(this);
98 pollingJob = scheduler.scheduleWithFixedDelay(this::poll, DEFAULT_REFRESH_INITIAL_DELAY,
99 DEFAULT_REFRESH_INTERVAL_SECONDS, TimeUnit.SECONDS);
100 updateStatus(ThingStatus.ONLINE);
102 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.BRIDGE_OFFLINE);
107 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
108 if (bridgeStatusInfo.getStatus().equals(ThingStatus.ONLINE)) {
109 updateStatus(ThingStatus.ONLINE);
111 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.BRIDGE_OFFLINE);
112 ScheduledFuture<?> job = this.pollingJob;
113 if (job != null && !job.isCancelled()) {
116 this.pollingJob = null;
121 public void dispose() {
122 logger.debug("WeMoLightHandler disposed.");
124 ScheduledFuture<?> job = this.pollingJob;
125 if (job != null && !job.isCancelled()) {
128 this.pollingJob = null;
129 removeSubscription();
132 private synchronized @Nullable WemoBridgeHandler getWemoBridgeHandler() {
133 Bridge bridge = getBridge();
134 if (bridge == null) {
135 logger.error("Required bridge not defined for device {}.", wemoLightID);
138 ThingHandler handler = bridge.getHandler();
139 if (handler instanceof WemoBridgeHandler) {
140 this.wemoBridgeHandler = (WemoBridgeHandler) handler;
142 logger.debug("No available bridge handler found for {} bridge {} .", wemoLightID, bridge.getUID());
145 return this.wemoBridgeHandler;
148 private void poll() {
149 synchronized (jobLock) {
150 if (pollingJob == null) {
154 logger.debug("Polling job");
156 // Check if the Wemo device is set in the UPnP service registry
157 // If not, set the thing state to ONLINE/CONFIG-PENDING and wait for the next poll
158 if (!isUpnpDeviceRegistered()) {
159 logger.debug("UPnP device {} not yet registered", getUDN());
160 updateStatus(ThingStatus.ONLINE, ThingStatusDetail.CONFIGURATION_PENDING,
161 "@text/config-status.pending.device-not-registered [\"" + getUDN() + "\"]");
162 synchronized (upnpLock) {
163 subscriptionState = new HashMap<>();
167 updateStatus(ThingStatus.ONLINE);
170 } catch (Exception e) {
171 logger.debug("Exception during poll: {}", e.getMessage(), e);
177 public void handleCommand(ChannelUID channelUID, Command command) {
178 String localHost = getHost();
179 if (localHost.isEmpty()) {
180 logger.error("Failed to send command '{}' for device '{}': IP address missing", command,
181 getThing().getUID());
182 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
183 "@text/config-status.error.missing-ip");
186 String wemoURL = getWemoURL(localHost, BASICACTION);
187 if (wemoURL == null) {
188 logger.error("Failed to send command '{}' for device '{}': URL cannot be created", command,
189 getThing().getUID());
190 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
191 "@text/config-status.error.missing-url");
194 if (command instanceof RefreshType) {
197 } catch (Exception e) {
198 logger.debug("Exception during poll", e);
201 Configuration configuration = getConfig();
202 configuration.get(DEVICE_ID);
204 WemoBridgeHandler wemoBridge = getWemoBridgeHandler();
205 if (wemoBridge == null) {
206 logger.debug("wemoBridgeHandler not found, cannot handle command");
209 String devUDN = "uuid:" + wemoBridge.getThing().getConfiguration().get(UDN).toString();
210 logger.trace("WeMo Bridge to send command to : {}", devUDN);
213 String capability = null;
214 switch (channelUID.getId()) {
215 case CHANNEL_BRIGHTNESS:
216 capability = "10008";
217 if (command instanceof PercentType) {
218 int newBrightness = ((PercentType) command).intValue();
219 logger.trace("wemoLight received Value {}", newBrightness);
220 int value1 = Math.round(newBrightness * 255 / 100);
221 value = value1 + ":0";
222 currentBrightness = newBrightness;
223 } else if (command instanceof OnOffType) {
224 switch (command.toString()) {
232 } else if (command instanceof IncreaseDecreaseType) {
234 switch (command.toString()) {
236 currentBrightness = currentBrightness + DIM_STEPSIZE;
237 newBrightness = Math.round(currentBrightness * 255 / 100);
238 if (newBrightness > 255) {
241 value = newBrightness + ":0";
244 currentBrightness = currentBrightness - DIM_STEPSIZE;
245 newBrightness = Math.round(currentBrightness * 255 / 100);
246 if (newBrightness < 0) {
249 value = newBrightness + ":0";
255 capability = "10006";
256 switch (command.toString()) {
267 if (capability != null && value != null) {
268 String soapHeader = "\"urn:Belkin:service:bridge:1#SetDeviceStatus\"";
269 String content = "<?xml version=\"1.0\"?>"
270 + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
271 + "<s:Body>" + "<u:SetDeviceStatus xmlns:u=\"urn:Belkin:service:bridge:1\">"
272 + "<DeviceStatusList>"
273 + "<?xml version="1.0" encoding="UTF-8"?><DeviceStatus><DeviceID>"
275 + "</DeviceID><IsGroupAction>NO</IsGroupAction><CapabilityID>"
276 + capability + "</CapabilityID><CapabilityValue>" + value
277 + "</CapabilityValue></DeviceStatus>" + "</DeviceStatusList>"
278 + "</u:SetDeviceStatus>" + "</s:Body>" + "</s:Envelope>";
280 String wemoCallResponse = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
281 if (wemoCallResponse != null) {
282 if (logger.isTraceEnabled()) {
283 logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
284 logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader,
285 getThing().getUID());
286 logger.trace("wemoCall with content '{}' for device '{}'", content, getThing().getUID());
287 logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse,
288 getThing().getUID());
290 if ("10008".equals(capability)) {
291 OnOffType binaryState = null;
292 binaryState = "0".equals(value) ? OnOffType.OFF : OnOffType.ON;
293 updateState(CHANNEL_STATE, binaryState);
297 } catch (Exception e) {
298 throw new IllegalStateException("Could not send command to WeMo Bridge", e);
304 public @Nullable String getUDN() {
305 WemoBridgeHandler wemoBridge = getWemoBridgeHandler();
306 if (wemoBridge == null) {
307 logger.debug("wemoBridgeHandler not found");
310 return (String) wemoBridge.getThing().getConfiguration().get(UDN);
314 * The {@link getDeviceState} is used for polling the actual state of a WeMo Light and updating the according
317 public void getDeviceState() {
318 String localHost = getHost();
319 if (localHost.isEmpty()) {
320 logger.error("Failed to get actual state for device '{}': IP address missing", getThing().getUID());
321 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
322 "@text/config-status.error.missing-ip");
325 logger.debug("Request actual state for LightID '{}'", wemoLightID);
326 String wemoURL = getWemoURL(localHost, BRIDGEACTION);
327 if (wemoURL == null) {
328 logger.error("Failed to get actual state for device '{}': URL cannot be created", getThing().getUID());
329 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
330 "@text/config-status.error.missing-url");
334 String soapHeader = "\"urn:Belkin:service:bridge:1#GetDeviceStatus\"";
335 String content = "<?xml version=\"1.0\"?>"
336 + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
337 + "<s:Body>" + "<u:GetDeviceStatus xmlns:u=\"urn:Belkin:service:bridge:1\">" + "<DeviceIDs>"
338 + wemoLightID + "</DeviceIDs>" + "</u:GetDeviceStatus>" + "</s:Body>" + "</s:Envelope>";
340 String wemoCallResponse = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
341 if (wemoCallResponse != null) {
342 if (logger.isTraceEnabled()) {
343 logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
344 logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader, getThing().getUID());
345 logger.trace("wemoCall with content '{}' for device '{}'", content, getThing().getUID());
346 logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse, getThing().getUID());
348 wemoCallResponse = unescapeXml(wemoCallResponse);
349 String response = substringBetween(wemoCallResponse, "<CapabilityValue>", "</CapabilityValue>");
350 logger.trace("wemoNewLightState = {}", response);
351 String[] splitResponse = response.split(",");
352 if (splitResponse[0] != null) {
353 OnOffType binaryState = null;
354 binaryState = "0".equals(splitResponse[0]) ? OnOffType.OFF : OnOffType.ON;
355 updateState(CHANNEL_STATE, binaryState);
357 if (splitResponse[1] != null) {
358 String splitBrightness[] = splitResponse[1].split(":");
359 if (splitBrightness[0] != null) {
360 int newBrightnessValue = Integer.valueOf(splitBrightness[0]);
361 int newBrightness = Math.round(newBrightnessValue * 100 / 255);
362 logger.trace("newBrightness = {}", newBrightness);
363 State newBrightnessState = new PercentType(newBrightness);
364 updateState(CHANNEL_BRIGHTNESS, newBrightnessState);
365 currentBrightness = newBrightness;
369 } catch (Exception e) {
370 throw new IllegalStateException("Could not retrieve new Wemo light state", e);
375 public void onServiceSubscribed(@Nullable String service, boolean succeeded) {
379 public void onValueReceived(@Nullable String variable, @Nullable String value, @Nullable String service) {
380 logger.trace("Received pair '{}':'{}' (service '{}') for thing '{}'",
381 new Object[] { variable, value, service, this.getThing().getUID() });
382 String capabilityId = substringBetween(value, "<CapabilityId>", "</CapabilityId>");
383 String newValue = substringBetween(value, "<Value>", "</Value>");
384 switch (capabilityId) {
386 OnOffType binaryState = null;
387 binaryState = "0".equals(newValue) ? OnOffType.OFF : OnOffType.ON;
388 updateState(CHANNEL_STATE, binaryState);
391 String splitValue[] = newValue.split(":");
392 if (splitValue[0] != null) {
393 int newBrightnessValue = Integer.valueOf(splitValue[0]);
394 int newBrightness = Math.round(newBrightnessValue * 100 / 255);
395 State newBrightnessState = new PercentType(newBrightness);
396 updateState(CHANNEL_BRIGHTNESS, newBrightnessState);
397 currentBrightness = newBrightness;
403 private synchronized void addSubscription() {
404 synchronized (upnpLock) {
405 UpnpIOService localService = service;
406 if (localService != null) {
407 if (localService.isRegistered(this)) {
408 logger.debug("Checking WeMo GENA subscription for '{}'", getThing().getUID());
410 if (subscriptionState.get(SUBSCRIPTION) == null) {
411 logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(),
413 localService.addSubscription(this, SUBSCRIPTION, SUBSCRIPTION_DURATION_SECONDS);
414 subscriptionState.put(SUBSCRIPTION, true);
418 "Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
419 getThing().getUID());
425 private synchronized void removeSubscription() {
426 synchronized (upnpLock) {
427 UpnpIOService localService = service;
428 if (localService != null) {
429 if (localService.isRegistered(this)) {
430 logger.debug("Removing WeMo GENA subscription for '{}'", getThing().getUID());
432 if (subscriptionState.get(SUBSCRIPTION) != null) {
433 logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), SUBSCRIPTION);
434 localService.removeSubscription(this, SUBSCRIPTION);
436 subscriptionState = new HashMap<>();
437 localService.unregisterParticipant(this);