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.concurrent.ScheduledFuture;
19 import java.util.concurrent.TimeUnit;
21 import org.eclipse.jdt.annotation.NonNullByDefault;
22 import org.eclipse.jdt.annotation.Nullable;
23 import org.openhab.binding.wemo.internal.http.WemoHttpCall;
24 import org.openhab.core.config.core.Configuration;
25 import org.openhab.core.io.transport.upnp.UpnpIOService;
26 import org.openhab.core.library.types.IncreaseDecreaseType;
27 import org.openhab.core.library.types.OnOffType;
28 import org.openhab.core.library.types.PercentType;
29 import org.openhab.core.thing.Bridge;
30 import org.openhab.core.thing.ChannelUID;
31 import org.openhab.core.thing.Thing;
32 import org.openhab.core.thing.ThingStatus;
33 import org.openhab.core.thing.ThingStatusDetail;
34 import org.openhab.core.thing.ThingStatusInfo;
35 import org.openhab.core.thing.binding.ThingHandler;
36 import org.openhab.core.types.Command;
37 import org.openhab.core.types.RefreshType;
38 import org.openhab.core.types.State;
39 import org.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
43 * {@link WemoLightHandler} is the handler for a WeMo light, responsible for handling commands and state updates for the
44 * different channels of a WeMo light.
46 * @author Hans-Jörg Merk - Initial contribution
49 public class WemoLightHandler extends WemoBaseThingHandler {
51 private final Logger logger = LoggerFactory.getLogger(WemoLightHandler.class);
53 private final Object jobLock = new Object();
55 private @Nullable WemoBridgeHandler wemoBridgeHandler;
57 private @Nullable String wemoLightID;
59 private int currentBrightness;
62 * Set dimming stepsize to 5%
64 private static final int DIM_STEPSIZE = 5;
67 * The default refresh initial delay in Seconds.
69 private static final int DEFAULT_REFRESH_INITIAL_DELAY = 15;
71 private @Nullable ScheduledFuture<?> pollingJob;
73 public WemoLightHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemoHttpcaller) {
74 super(thing, upnpIOService, wemoHttpcaller);
76 logger.debug("Creating a WemoLightHandler for thing '{}'", getThing().getUID());
80 public void initialize() {
82 // initialize() is only called if the required parameter 'deviceID' is available
83 wemoLightID = (String) getConfig().get(DEVICE_ID);
85 final Bridge bridge = getBridge();
86 if (bridge != null && bridge.getStatus() == ThingStatus.ONLINE) {
87 addSubscription(BRIDGEEVENT);
89 pollingJob = scheduler.scheduleWithFixedDelay(this::poll, DEFAULT_REFRESH_INITIAL_DELAY,
90 DEFAULT_REFRESH_INTERVAL_SECONDS, TimeUnit.SECONDS);
91 updateStatus(ThingStatus.ONLINE);
93 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.BRIDGE_OFFLINE);
98 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
99 if (bridgeStatusInfo.getStatus().equals(ThingStatus.ONLINE)) {
100 updateStatus(ThingStatus.ONLINE);
102 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.BRIDGE_OFFLINE);
103 ScheduledFuture<?> job = this.pollingJob;
104 if (job != null && !job.isCancelled()) {
107 this.pollingJob = null;
112 public void dispose() {
113 logger.debug("WemoLightHandler disposed.");
115 ScheduledFuture<?> job = this.pollingJob;
116 if (job != null && !job.isCancelled()) {
119 this.pollingJob = null;
123 private synchronized @Nullable WemoBridgeHandler getWemoBridgeHandler() {
124 Bridge bridge = getBridge();
125 if (bridge == null) {
126 logger.warn("Required bridge not defined for device {}.", wemoLightID);
129 ThingHandler handler = bridge.getHandler();
130 if (handler instanceof WemoBridgeHandler) {
131 this.wemoBridgeHandler = (WemoBridgeHandler) handler;
133 logger.debug("No available bridge handler found for {} bridge {} .", wemoLightID, bridge.getUID());
136 return this.wemoBridgeHandler;
139 private void poll() {
140 synchronized (jobLock) {
141 if (pollingJob == null) {
145 logger.debug("Polling job");
147 // Check if the Wemo device is set in the UPnP service registry
148 // If not, set the thing state to ONLINE/CONFIG-PENDING and wait for the next poll
149 if (!isUpnpDeviceRegistered()) {
150 logger.debug("UPnP device {} not yet registered", getUDN());
151 updateStatus(ThingStatus.ONLINE, ThingStatusDetail.CONFIGURATION_PENDING,
152 "@text/config-status.pending.device-not-registered [\"" + getUDN() + "\"]");
156 } catch (Exception e) {
157 logger.debug("Exception during poll: {}", e.getMessage(), e);
163 public void handleCommand(ChannelUID channelUID, Command command) {
164 String localHost = getHost();
165 if (localHost.isEmpty()) {
166 logger.warn("Failed to send command '{}' for device '{}': IP address missing", command,
167 getThing().getUID());
168 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
169 "@text/config-status.error.missing-ip");
172 String wemoURL = getWemoURL(localHost, BASICACTION);
173 if (wemoURL == null) {
174 logger.debug("Failed to send command '{}' for device '{}': URL cannot be created", command,
175 getThing().getUID());
176 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
177 "@text/config-status.error.missing-url");
180 if (command instanceof RefreshType) {
183 } catch (Exception e) {
184 logger.debug("Exception during poll", e);
187 Configuration configuration = getConfig();
188 configuration.get(DEVICE_ID);
190 WemoBridgeHandler wemoBridge = getWemoBridgeHandler();
191 if (wemoBridge == null) {
192 logger.debug("wemoBridgeHandler not found, cannot handle command");
195 String devUDN = "uuid:" + wemoBridge.getThing().getConfiguration().get(UDN).toString();
196 logger.trace("WeMo Bridge to send command to : {}", devUDN);
199 String capability = null;
200 switch (channelUID.getId()) {
201 case CHANNEL_BRIGHTNESS:
202 capability = "10008";
203 if (command instanceof PercentType) {
204 int newBrightness = ((PercentType) command).intValue();
205 logger.trace("wemoLight received Value {}", newBrightness);
206 int value1 = Math.round(newBrightness * 255 / 100);
207 value = value1 + ":0";
208 currentBrightness = newBrightness;
209 } else if (command instanceof OnOffType) {
210 switch (command.toString()) {
218 } else if (command instanceof IncreaseDecreaseType) {
220 switch (command.toString()) {
222 currentBrightness = currentBrightness + DIM_STEPSIZE;
223 newBrightness = Math.round(currentBrightness * 255 / 100);
224 if (newBrightness > 255) {
227 value = newBrightness + ":0";
230 currentBrightness = currentBrightness - DIM_STEPSIZE;
231 newBrightness = Math.round(currentBrightness * 255 / 100);
232 if (newBrightness < 0) {
235 value = newBrightness + ":0";
241 capability = "10006";
242 switch (command.toString()) {
253 if (capability != null && value != null) {
254 String soapHeader = "\"urn:Belkin:service:bridge:1#SetDeviceStatus\"";
255 String content = "<?xml version=\"1.0\"?>"
256 + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
257 + "<s:Body>" + "<u:SetDeviceStatus xmlns:u=\"urn:Belkin:service:bridge:1\">"
258 + "<DeviceStatusList>"
259 + "<?xml version="1.0" encoding="UTF-8"?><DeviceStatus><DeviceID>"
261 + "</DeviceID><IsGroupAction>NO</IsGroupAction><CapabilityID>"
262 + capability + "</CapabilityID><CapabilityValue>" + value
263 + "</CapabilityValue></DeviceStatus>" + "</DeviceStatusList>"
264 + "</u:SetDeviceStatus>" + "</s:Body>" + "</s:Envelope>";
266 wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
267 if ("10008".equals(capability)) {
268 OnOffType binaryState = null;
269 binaryState = "0".equals(value) ? OnOffType.OFF : OnOffType.ON;
270 updateState(CHANNEL_STATE, binaryState);
272 updateStatus(ThingStatus.ONLINE);
274 } catch (Exception e) {
275 logger.warn("Failed to send command '{}' for device '{}': {}", command, getThing().getUID(),
277 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
283 public @Nullable String getUDN() {
284 WemoBridgeHandler wemoBridge = getWemoBridgeHandler();
285 if (wemoBridge == null) {
286 logger.debug("wemoBridgeHandler not found");
289 return (String) wemoBridge.getThing().getConfiguration().get(UDN);
293 * The {@link getDeviceState} is used for polling the actual state of a WeMo Light and updating the according
296 public void getDeviceState() {
297 String localHost = getHost();
298 if (localHost.isEmpty()) {
299 logger.warn("Failed to get actual state for device '{}': IP address missing", getThing().getUID());
300 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
301 "@text/config-status.error.missing-ip");
304 logger.debug("Request actual state for LightID '{}'", wemoLightID);
305 String wemoURL = getWemoURL(localHost, BRIDGEACTION);
306 if (wemoURL == null) {
307 logger.debug("Failed to get actual state for device '{}': URL cannot be created", getThing().getUID());
308 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
309 "@text/config-status.error.missing-url");
313 String soapHeader = "\"urn:Belkin:service:bridge:1#GetDeviceStatus\"";
314 String content = "<?xml version=\"1.0\"?>"
315 + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
316 + "<s:Body>" + "<u:GetDeviceStatus xmlns:u=\"urn:Belkin:service:bridge:1\">" + "<DeviceIDs>"
317 + wemoLightID + "</DeviceIDs>" + "</u:GetDeviceStatus>" + "</s:Body>" + "</s:Envelope>";
319 String wemoCallResponse = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
320 wemoCallResponse = unescapeXml(wemoCallResponse);
321 String response = substringBetween(wemoCallResponse, "<CapabilityValue>", "</CapabilityValue>");
322 logger.trace("wemoNewLightState = {}", response);
323 String[] splitResponse = response.split(",");
324 if (splitResponse[0] != null) {
325 OnOffType binaryState = null;
326 binaryState = "0".equals(splitResponse[0]) ? OnOffType.OFF : OnOffType.ON;
327 updateState(CHANNEL_STATE, binaryState);
329 if (splitResponse[1] != null) {
330 String splitBrightness[] = splitResponse[1].split(":");
331 if (splitBrightness[0] != null) {
332 int newBrightnessValue = Integer.valueOf(splitBrightness[0]);
333 int newBrightness = Math.round(newBrightnessValue * 100 / 255);
334 logger.trace("newBrightness = {}", newBrightness);
335 State newBrightnessState = new PercentType(newBrightness);
336 updateState(CHANNEL_BRIGHTNESS, newBrightnessState);
337 currentBrightness = newBrightness;
340 updateStatus(ThingStatus.ONLINE);
341 } catch (Exception e) {
342 logger.debug("Could not retrieve new Wemo light state for '{}':", getThing().getUID(), e);
343 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
348 public void onValueReceived(@Nullable String variable, @Nullable String value, @Nullable String service) {
349 logger.trace("Received pair '{}':'{}' (service '{}') for thing '{}'",
350 new Object[] { variable, value, service, this.getThing().getUID() });
351 String capabilityId = substringBetween(value, "<CapabilityId>", "</CapabilityId>");
352 String newValue = substringBetween(value, "<Value>", "</Value>");
353 switch (capabilityId) {
355 OnOffType binaryState = null;
356 binaryState = "0".equals(newValue) ? 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;