]> git.basschouten.com Git - openhab-addons.git/blob
af8d43bc916d6afbb50fa82c934217c143f865e0
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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 import static org.openhab.binding.wemo.internal.WemoUtil.*;
17
18 import java.math.BigDecimal;
19 import java.net.URL;
20 import java.util.HashMap;
21 import java.util.Map;
22 import java.util.concurrent.ScheduledFuture;
23 import java.util.concurrent.TimeUnit;
24
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;
46
47 /**
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.
50  *
51  * @author Hans-Jörg Merk - Initial contribution
52  */
53 @NonNullByDefault
54 public class WemoLightHandler extends AbstractWemoHandler implements UpnpIOParticipant {
55
56     private final Logger logger = LoggerFactory.getLogger(WemoLightHandler.class);
57
58     private Map<String, Boolean> subscriptionState = new HashMap<>();
59
60     private UpnpIOService service;
61     private WemoHttpCall wemoCall;
62
63     private @Nullable WemoBridgeHandler wemoBridgeHandler;
64
65     private @Nullable String wemoLightID;
66
67     private int currentBrightness;
68
69     /**
70      * Set dimming stepsize to 5%
71      */
72     private static final int DIM_STEPSIZE = 5;
73
74     protected static final String SUBSCRIPTION = "bridge1";
75
76     /**
77      * The default refresh initial delay in Seconds.
78      */
79     private static final int DEFAULT_REFRESH_INITIAL_DELAY = 15;
80
81     private @Nullable ScheduledFuture<?> refreshJob;
82
83     private final Runnable refreshRunnable = new Runnable() {
84
85         @Override
86         public void run() {
87             try {
88                 if (!isUpnpDeviceRegistered()) {
89                     logger.debug("WeMo UPnP device {} not yet registered", getUDN());
90                 }
91
92                 getDeviceState();
93                 onSubscription();
94             } catch (Exception e) {
95                 logger.debug("Exception during poll", e);
96                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
97             }
98         }
99     };
100
101     public WemoLightHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemoHttpcaller) {
102         super(thing, wemoHttpcaller);
103
104         this.service = upnpIOService;
105         this.wemoCall = wemoHttpcaller;
106     }
107
108     @Override
109     public void initialize() {
110         // initialize() is only called if the required parameter 'deviceID' is available
111         wemoLightID = (String) getConfig().get(DEVICE_ID);
112
113         final Bridge bridge = getBridge();
114         if (bridge != null && bridge.getStatus() == ThingStatus.ONLINE) {
115             updateStatus(ThingStatus.ONLINE);
116             onSubscription();
117             onUpdate();
118         } else {
119             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.BRIDGE_OFFLINE);
120         }
121     }
122
123     @Override
124     public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
125         if (bridgeStatusInfo.getStatus().equals(ThingStatus.ONLINE)) {
126             updateStatus(ThingStatus.ONLINE);
127             onSubscription();
128             onUpdate();
129         } else {
130             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.BRIDGE_OFFLINE);
131             ScheduledFuture<?> job = refreshJob;
132             if (job != null && !job.isCancelled()) {
133                 job.cancel(true);
134             }
135             refreshJob = null;
136         }
137     }
138
139     @Override
140     public void dispose() {
141         logger.debug("WeMoLightHandler disposed.");
142
143         ScheduledFuture<?> job = refreshJob;
144         if (job != null && !job.isCancelled()) {
145             job.cancel(true);
146         }
147         refreshJob = null;
148         removeSubscription();
149     }
150
151     private synchronized @Nullable WemoBridgeHandler getWemoBridgeHandler() {
152         Bridge bridge = getBridge();
153         if (bridge == null) {
154             logger.error("Required bridge not defined for device {}.", wemoLightID);
155             return null;
156         }
157         ThingHandler handler = bridge.getHandler();
158         if (handler instanceof WemoBridgeHandler) {
159             this.wemoBridgeHandler = (WemoBridgeHandler) handler;
160         } else {
161             logger.debug("No available bridge handler found for {} bridge {} .", wemoLightID, bridge.getUID());
162             return null;
163         }
164         return this.wemoBridgeHandler;
165     }
166
167     @Override
168     public void handleCommand(ChannelUID channelUID, Command command) {
169         if (command instanceof RefreshType) {
170             try {
171                 getDeviceState();
172             } catch (Exception e) {
173                 logger.debug("Exception during poll", e);
174             }
175         } else {
176             Configuration configuration = getConfig();
177             configuration.get(DEVICE_ID);
178
179             WemoBridgeHandler wemoBridge = getWemoBridgeHandler();
180             if (wemoBridge == null) {
181                 logger.debug("wemoBridgeHandler not found, cannot handle command");
182                 return;
183             }
184             String devUDN = "uuid:" + wemoBridge.getThing().getConfiguration().get(UDN).toString();
185             logger.trace("WeMo Bridge to send command to : {}", devUDN);
186
187             String value = null;
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()) {
200                             case "ON":
201                                 value = "255:0";
202                                 break;
203                             case "OFF":
204                                 value = "0:0";
205                                 break;
206                         }
207                     } else if (command instanceof IncreaseDecreaseType) {
208                         int newBrightness;
209                         switch (command.toString()) {
210                             case "INCREASE":
211                                 currentBrightness = currentBrightness + DIM_STEPSIZE;
212                                 newBrightness = Math.round(currentBrightness * 255 / 100);
213                                 if (newBrightness > 255) {
214                                     newBrightness = 255;
215                                 }
216                                 value = newBrightness + ":0";
217                                 break;
218                             case "DECREASE":
219                                 currentBrightness = currentBrightness - DIM_STEPSIZE;
220                                 newBrightness = Math.round(currentBrightness * 255 / 100);
221                                 if (newBrightness < 0) {
222                                     newBrightness = 0;
223                                 }
224                                 value = newBrightness + ":0";
225                                 break;
226                         }
227                     }
228                     break;
229                 case CHANNEL_STATE:
230                     capability = "10006";
231                     switch (command.toString()) {
232                         case "ON":
233                             value = "1";
234                             break;
235                         case "OFF":
236                             value = "0";
237                             break;
238                     }
239                     break;
240             }
241             try {
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                         + "&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;DeviceStatus&gt;&lt;DeviceID&gt;"
248                         + wemoLightID
249                         + "&lt;/DeviceID&gt;&lt;IsGroupAction&gt;NO&lt;/IsGroupAction&gt;&lt;CapabilityID&gt;"
250                         + capability + "&lt;/CapabilityID&gt;&lt;CapabilityValue&gt;" + value
251                         + "&lt;/CapabilityValue&gt;&lt;/DeviceStatus&gt;" + "</DeviceStatusList>"
252                         + "</u:SetDeviceStatus>" + "</s:Body>" + "</s:Envelope>";
253
254                 URL descriptorURL = service.getDescriptorURL(this);
255                 String wemoURL = getWemoURL(descriptorURL, "bridge");
256
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);
264                         }
265                     }
266                 }
267             } catch (Exception e) {
268                 throw new IllegalStateException("Could not send command to WeMo Bridge", e);
269             }
270         }
271     }
272
273     @Override
274     public @Nullable String getUDN() {
275         WemoBridgeHandler wemoBridge = getWemoBridgeHandler();
276         if (wemoBridge == null) {
277             logger.debug("wemoBridgeHandler not found");
278             return null;
279         }
280         return (String) wemoBridge.getThing().getConfiguration().get(UDN);
281     }
282
283     /**
284      * The {@link getDeviceState} is used for polling the actual state of a WeMo Light and updating the according
285      * channel states.
286      */
287     public void getDeviceState() {
288         logger.debug("Request actual state for LightID '{}'", wemoLightID);
289         try {
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>";
295
296             URL descriptorURL = service.getDescriptorURL(this);
297             String wemoURL = getWemoURL(descriptorURL, "bridge");
298
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);
310                     }
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;
320                         }
321                     }
322                 }
323             }
324         } catch (Exception e) {
325             throw new IllegalStateException("Could not retrieve new Wemo light state", e);
326         }
327     }
328
329     @Override
330     public void onServiceSubscribed(@Nullable String service, boolean succeeded) {
331     }
332
333     @Override
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) {
340             case "10006":
341                 OnOffType binaryState = null;
342                 binaryState = newValue.equals("0") ? OnOffType.OFF : OnOffType.ON;
343                 updateState(CHANNEL_STATE, binaryState);
344                 break;
345             case "10008":
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;
353                 }
354                 break;
355         }
356     }
357
358     @Override
359     public void onStatusChanged(boolean status) {
360     }
361
362     private synchronized void onSubscription() {
363         if (service.isRegistered(this)) {
364             logger.debug("Checking WeMo GENA subscription for '{}'", this);
365
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);
370             }
371         } else {
372             logger.debug("Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
373                     this);
374         }
375     }
376
377     private synchronized void removeSubscription() {
378         if (service.isRegistered(this)) {
379             logger.debug("Removing WeMo GENA subscription for '{}'", this);
380
381             if (subscriptionState.get(SUBSCRIPTION) != null) {
382                 logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), SUBSCRIPTION);
383                 service.removeSubscription(this, SUBSCRIPTION);
384             }
385
386             subscriptionState = new HashMap<>();
387             service.unregisterParticipant(this);
388         }
389     }
390
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();
399             }
400             logger.trace("Start polling job for LightID '{}'", wemoLightID);
401             refreshJob = scheduler.scheduleWithFixedDelay(refreshRunnable, DEFAULT_REFRESH_INITIAL_DELAY,
402                     refreshInterval, TimeUnit.SECONDS);
403         }
404     }
405
406     private boolean isUpnpDeviceRegistered() {
407         return service.isRegistered(this);
408     }
409 }