]> git.basschouten.com Git - openhab-addons.git/blob
59c0969680b7389ee4ff2f31fcc34bef8ace6e9a
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.wemo.internal.handler;
14
15 import static org.openhab.binding.wemo.internal.WemoBindingConstants.*;
16
17 import java.math.BigDecimal;
18 import java.net.URL;
19 import java.util.HashMap;
20 import java.util.Map;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
23
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;
45
46 /**
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.
49  *
50  * @author Hans-Jörg Merk - Initial contribution
51  */
52 public class WemoLightHandler extends AbstractWemoHandler implements UpnpIOParticipant {
53
54     private final Logger logger = LoggerFactory.getLogger(WemoLightHandler.class);
55
56     private Map<String, Boolean> subscriptionState = new HashMap<>();
57
58     private UpnpIOService service;
59
60     private WemoBridgeHandler wemoBridgeHandler;
61
62     private String wemoLightID;
63
64     private int currentBrightness;
65
66     /**
67      * Set dimming stepsize to 5%
68      */
69     private static final int DIM_STEPSIZE = 5;
70
71     protected static final String SUBSCRIPTION = "bridge1";
72
73     protected static final int SUBSCRIPTION_DURATION = 600;
74
75     /**
76      * The default refresh interval in Seconds.
77      */
78     private final int DEFAULT_REFRESH_INTERVAL = 60;
79
80     /**
81      * The default refresh initial delay in Seconds.
82      */
83     private static final int DEFAULT_REFRESH_INITIAL_DELAY = 15;
84
85     private ScheduledFuture<?> refreshJob;
86
87     private final Runnable refreshRunnable = new Runnable() {
88
89         @Override
90         public void run() {
91             try {
92                 if (!isUpnpDeviceRegistered()) {
93                     logger.debug("WeMo UPnP device {} not yet registered", getUDN());
94                 }
95
96                 getDeviceState();
97                 onSubscription();
98             } catch (Exception e) {
99                 logger.debug("Exception during poll", e);
100                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
101             }
102         }
103     };
104
105     public WemoLightHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemoHttpcaller) {
106         super(thing);
107
108         this.wemoHttpCaller = wemoHttpcaller;
109
110         if (upnpIOService != null) {
111             logger.debug("UPnPIOService '{}'", upnpIOService);
112             this.service = upnpIOService;
113         } else {
114             logger.debug("upnpIOService not set.");
115         }
116     }
117
118     @Override
119     public void initialize() {
120         // initialize() is only called if the required parameter 'deviceID' is available
121         wemoLightID = (String) getConfig().get(DEVICE_ID);
122
123         if (getBridge() != null) {
124             logger.debug("Initializing WemoLightHandler for LightID '{}'", wemoLightID);
125             if (getBridge().getStatus() == ThingStatus.ONLINE) {
126                 updateStatus(ThingStatus.ONLINE);
127                 onSubscription();
128                 onUpdate();
129             } else {
130                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.BRIDGE_OFFLINE);
131             }
132         } else {
133             updateStatus(ThingStatus.OFFLINE);
134         }
135     }
136
137     @Override
138     public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
139         if (bridgeStatusInfo.getStatus().equals(ThingStatus.ONLINE)) {
140             updateStatus(ThingStatus.ONLINE);
141             onSubscription();
142             onUpdate();
143         } else {
144             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.BRIDGE_OFFLINE);
145             if (refreshJob != null && !refreshJob.isCancelled()) {
146                 refreshJob.cancel(true);
147                 refreshJob = null;
148             }
149         }
150     }
151
152     @Override
153     public void dispose() {
154         logger.debug("WeMoLightHandler disposed.");
155
156         removeSubscription();
157
158         if (refreshJob != null && !refreshJob.isCancelled()) {
159             refreshJob.cancel(true);
160             refreshJob = null;
161         }
162     }
163
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);
169                 return null;
170             }
171             ThingHandler handler = bridge.getHandler();
172             if (handler instanceof WemoBridgeHandler) {
173                 this.wemoBridgeHandler = (WemoBridgeHandler) handler;
174             } else {
175                 logger.debug("No available bridge handler found for {} bridge {} .", wemoLightID, bridge.getUID());
176                 return null;
177             }
178         }
179         return this.wemoBridgeHandler;
180     }
181
182     @Override
183     public void handleCommand(ChannelUID channelUID, Command command) {
184         if (command instanceof RefreshType) {
185             try {
186                 getDeviceState();
187             } catch (Exception e) {
188                 logger.debug("Exception during poll", e);
189             }
190         } else {
191             Configuration configuration = getConfig();
192             configuration.get(DEVICE_ID);
193
194             WemoBridgeHandler wemoBridge = getWemoBridgeHandler();
195             if (wemoBridge == null) {
196                 logger.debug("wemoBridgeHandler not found, cannot handle command");
197                 return;
198             }
199             String devUDN = "uuid:" + wemoBridge.getThing().getConfiguration().get(UDN).toString();
200             logger.trace("WeMo Bridge to send command to : {}", devUDN);
201
202             String value = null;
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()) {
215                             case "ON":
216                                 value = "255:0";
217                                 break;
218                             case "OFF":
219                                 value = "0:0";
220                                 break;
221                         }
222                     } else if (command instanceof IncreaseDecreaseType) {
223                         int newBrightness;
224                         switch (command.toString()) {
225                             case "INCREASE":
226                                 currentBrightness = currentBrightness + DIM_STEPSIZE;
227                                 newBrightness = Math.round(currentBrightness * 255 / 100);
228                                 if (newBrightness > 255) {
229                                     newBrightness = 255;
230                                 }
231                                 value = newBrightness + ":0";
232                                 break;
233                             case "DECREASE":
234                                 currentBrightness = currentBrightness - DIM_STEPSIZE;
235                                 newBrightness = Math.round(currentBrightness * 255 / 100);
236                                 if (newBrightness < 0) {
237                                     newBrightness = 0;
238                                 }
239                                 value = newBrightness + ":0";
240                                 break;
241                         }
242                     }
243                     break;
244                 case CHANNEL_STATE:
245                     capability = "10006";
246                     switch (command.toString()) {
247                         case "ON":
248                             value = "1";
249                             break;
250                         case "OFF":
251                             value = "0";
252                             break;
253                     }
254                     break;
255             }
256             try {
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                         + "&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;DeviceStatus&gt;&lt;DeviceID&gt;"
263                         + wemoLightID
264                         + "&lt;/DeviceID&gt;&lt;IsGroupAction&gt;NO&lt;/IsGroupAction&gt;&lt;CapabilityID&gt;"
265                         + capability + "&lt;/CapabilityID&gt;&lt;CapabilityValue&gt;" + value
266                         + "&lt;/CapabilityValue&gt;&lt;/DeviceStatus&gt;" + "</DeviceStatusList>"
267                         + "</u:SetDeviceStatus>" + "</s:Body>" + "</s:Envelope>";
268
269                 String wemoURL = getWemoURL();
270
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);
278                         }
279                     }
280                 }
281             } catch (Exception e) {
282                 throw new IllegalStateException("Could not send command to WeMo Bridge", e);
283             }
284         }
285     }
286
287     @Override
288     public String getUDN() {
289         WemoBridgeHandler wemoBridge = getWemoBridgeHandler();
290         if (wemoBridge == null) {
291             logger.debug("wemoBridgeHandler not found");
292             return null;
293         }
294         return (String) wemoBridge.getThing().getConfiguration().get(UDN);
295     }
296
297     /**
298      * The {@link getDeviceState} is used for polling the actual state of a WeMo Light and updating the according
299      * channel states.
300      */
301     public void getDeviceState() {
302         logger.debug("Request actual state for LightID '{}'", wemoLightID);
303         try {
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>";
309
310             String wemoURL = getWemoURL();
311
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);
324                     }
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;
334                         }
335                     }
336                 }
337             }
338         } catch (Exception e) {
339             throw new IllegalStateException("Could not retrieve new Wemo light state", e);
340         }
341     }
342
343     @Override
344     public void onServiceSubscribed(String service, boolean succeeded) {
345     }
346
347     @Override
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) {
354             case "10006":
355                 OnOffType binaryState = null;
356                 binaryState = newValue.equals("0") ? OnOffType.OFF : OnOffType.ON;
357                 updateState(CHANNEL_STATE, binaryState);
358                 break;
359             case "10008":
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;
367                 }
368                 break;
369         }
370     }
371
372     @Override
373     public void onStatusChanged(boolean status) {
374     }
375
376     private synchronized void onSubscription() {
377         if (service.isRegistered(this)) {
378             logger.debug("Checking WeMo GENA subscription for '{}'", this);
379
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);
384             }
385         } else {
386             logger.debug("Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
387                     this);
388         }
389     }
390
391     private synchronized void removeSubscription() {
392         if (service.isRegistered(this)) {
393             logger.debug("Removing WeMo GENA subscription for '{}'", this);
394
395             if ((subscriptionState.get(SUBSCRIPTION) != null) && subscriptionState.get(SUBSCRIPTION).booleanValue()) {
396                 logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), SUBSCRIPTION);
397                 service.removeSubscription(this, SUBSCRIPTION);
398             }
399
400             subscriptionState = new HashMap<>();
401             service.unregisterParticipant(this);
402         }
403     }
404
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();
412             }
413             logger.trace("Start polling job for LightID '{}'", wemoLightID);
414             refreshJob = scheduler.scheduleWithFixedDelay(refreshRunnable, DEFAULT_REFRESH_INITIAL_DELAY,
415                     refreshInterval, TimeUnit.SECONDS);
416         }
417     }
418
419     private boolean isUpnpDeviceRegistered() {
420         return service.isRegistered(this);
421     }
422
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";
429             return wemoURL;
430         }
431         return null;
432     }
433 }