]> git.basschouten.com Git - openhab-addons.git/blob
1aad41ee31cf7dfd4583640bab109d045e9e9a13
[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.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.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
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 @NonNullByDefault
53 public class WemoLightHandler extends AbstractWemoHandler implements UpnpIOParticipant {
54
55     private final Logger logger = LoggerFactory.getLogger(WemoLightHandler.class);
56
57     private Map<String, Boolean> subscriptionState = new HashMap<>();
58
59     private final Object upnpLock = new Object();
60     private final Object jobLock = new Object();
61
62     private @Nullable WemoBridgeHandler wemoBridgeHandler;
63
64     private @Nullable UpnpIOService service;
65
66     private String host = "";
67
68     private @Nullable String wemoLightID;
69
70     private int currentBrightness;
71
72     private WemoHttpCall wemoCall;
73
74     /**
75      * Set dimming stepsize to 5%
76      */
77     private static final int DIM_STEPSIZE = 5;
78
79     protected static final String SUBSCRIPTION = "bridge1";
80
81     /**
82      * The default refresh initial delay in Seconds.
83      */
84     private static final int DEFAULT_REFRESH_INITIAL_DELAY = 15;
85
86     private @Nullable ScheduledFuture<?> pollingJob;
87
88     public WemoLightHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemoHttpcaller) {
89         super(thing, wemoHttpcaller);
90
91         this.service = upnpIOService;
92         this.wemoCall = wemoHttpcaller;
93
94         logger.debug("Creating a WemoLightHandler for thing '{}'", getThing().getUID());
95     }
96
97     @Override
98     public void initialize() {
99         // initialize() is only called if the required parameter 'deviceID' is available
100         wemoLightID = (String) getConfig().get(DEVICE_ID);
101
102         final Bridge bridge = getBridge();
103         if (bridge != null && bridge.getStatus() == ThingStatus.ONLINE) {
104             UpnpIOService localService = service;
105             if (localService != null) {
106                 localService.registerParticipant(this);
107             }
108             host = getHost();
109             pollingJob = scheduler.scheduleWithFixedDelay(this::poll, DEFAULT_REFRESH_INITIAL_DELAY,
110                     DEFAULT_REFRESH_INTERVALL_SECONDS, TimeUnit.SECONDS);
111             updateStatus(ThingStatus.ONLINE);
112         } else {
113             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.BRIDGE_OFFLINE);
114         }
115     }
116
117     @Override
118     public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
119         if (bridgeStatusInfo.getStatus().equals(ThingStatus.ONLINE)) {
120             updateStatus(ThingStatus.ONLINE);
121         } else {
122             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.BRIDGE_OFFLINE);
123             ScheduledFuture<?> job = this.pollingJob;
124             if (job != null && !job.isCancelled()) {
125                 job.cancel(true);
126             }
127             this.pollingJob = null;
128         }
129     }
130
131     @Override
132     public void dispose() {
133         logger.debug("WeMoLightHandler disposed.");
134
135         ScheduledFuture<?> job = this.pollingJob;
136         if (job != null && !job.isCancelled()) {
137             job.cancel(true);
138         }
139         this.pollingJob = null;
140         removeSubscription();
141     }
142
143     private synchronized @Nullable WemoBridgeHandler getWemoBridgeHandler() {
144         Bridge bridge = getBridge();
145         if (bridge == null) {
146             logger.error("Required bridge not defined for device {}.", wemoLightID);
147             return null;
148         }
149         ThingHandler handler = bridge.getHandler();
150         if (handler instanceof WemoBridgeHandler) {
151             this.wemoBridgeHandler = (WemoBridgeHandler) handler;
152         } else {
153             logger.debug("No available bridge handler found for {} bridge {} .", wemoLightID, bridge.getUID());
154             return null;
155         }
156         return this.wemoBridgeHandler;
157     }
158
159     private void poll() {
160         synchronized (jobLock) {
161             if (pollingJob == null) {
162                 return;
163             }
164             try {
165                 logger.debug("Polling job");
166                 host = getHost();
167                 // Check if the Wemo device is set in the UPnP service registry
168                 // If not, set the thing state to ONLINE/CONFIG-PENDING and wait for the next poll
169                 if (!isUpnpDeviceRegistered()) {
170                     logger.debug("UPnP device {} not yet registered", getUDN());
171                     updateStatus(ThingStatus.ONLINE, ThingStatusDetail.CONFIGURATION_PENDING,
172                             "@text/config-status.pending.device-not-registered [\"" + getUDN() + "\"]");
173                     synchronized (upnpLock) {
174                         subscriptionState = new HashMap<>();
175                     }
176                     return;
177                 }
178                 updateStatus(ThingStatus.ONLINE);
179                 getDeviceState();
180                 addSubscription();
181             } catch (Exception e) {
182                 logger.debug("Exception during poll: {}", e.getMessage(), e);
183             }
184         }
185     }
186
187     @Override
188     public void handleCommand(ChannelUID channelUID, Command command) {
189         String localHost = getHost();
190         if (localHost.isEmpty()) {
191             logger.error("Failed to send command '{}' for device '{}': IP address missing", command,
192                     getThing().getUID());
193             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
194                     "@text/config-status.error.missing-ip");
195             return;
196         }
197         String wemoURL = getWemoURL(localHost, BASICACTION);
198         if (wemoURL == null) {
199             logger.error("Failed to send command '{}' for device '{}': URL cannot be created", command,
200                     getThing().getUID());
201             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
202                     "@text/config-status.error.missing-url");
203             return;
204         }
205         if (command instanceof RefreshType) {
206             try {
207                 getDeviceState();
208             } catch (Exception e) {
209                 logger.debug("Exception during poll", e);
210             }
211         } else {
212             Configuration configuration = getConfig();
213             configuration.get(DEVICE_ID);
214
215             WemoBridgeHandler wemoBridge = getWemoBridgeHandler();
216             if (wemoBridge == null) {
217                 logger.debug("wemoBridgeHandler not found, cannot handle command");
218                 return;
219             }
220             String devUDN = "uuid:" + wemoBridge.getThing().getConfiguration().get(UDN).toString();
221             logger.trace("WeMo Bridge to send command to : {}", devUDN);
222
223             String value = null;
224             String capability = null;
225             switch (channelUID.getId()) {
226                 case CHANNEL_BRIGHTNESS:
227                     capability = "10008";
228                     if (command instanceof PercentType) {
229                         int newBrightness = ((PercentType) command).intValue();
230                         logger.trace("wemoLight received Value {}", newBrightness);
231                         int value1 = Math.round(newBrightness * 255 / 100);
232                         value = value1 + ":0";
233                         currentBrightness = newBrightness;
234                     } else if (command instanceof OnOffType) {
235                         switch (command.toString()) {
236                             case "ON":
237                                 value = "255:0";
238                                 break;
239                             case "OFF":
240                                 value = "0:0";
241                                 break;
242                         }
243                     } else if (command instanceof IncreaseDecreaseType) {
244                         int newBrightness;
245                         switch (command.toString()) {
246                             case "INCREASE":
247                                 currentBrightness = currentBrightness + DIM_STEPSIZE;
248                                 newBrightness = Math.round(currentBrightness * 255 / 100);
249                                 if (newBrightness > 255) {
250                                     newBrightness = 255;
251                                 }
252                                 value = newBrightness + ":0";
253                                 break;
254                             case "DECREASE":
255                                 currentBrightness = currentBrightness - DIM_STEPSIZE;
256                                 newBrightness = Math.round(currentBrightness * 255 / 100);
257                                 if (newBrightness < 0) {
258                                     newBrightness = 0;
259                                 }
260                                 value = newBrightness + ":0";
261                                 break;
262                         }
263                     }
264                     break;
265                 case CHANNEL_STATE:
266                     capability = "10006";
267                     switch (command.toString()) {
268                         case "ON":
269                             value = "1";
270                             break;
271                         case "OFF":
272                             value = "0";
273                             break;
274                     }
275                     break;
276             }
277             try {
278                 if (capability != null && value != null) {
279                     String soapHeader = "\"urn:Belkin:service:bridge:1#SetDeviceStatus\"";
280                     String content = "<?xml version=\"1.0\"?>"
281                             + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
282                             + "<s:Body>" + "<u:SetDeviceStatus xmlns:u=\"urn:Belkin:service:bridge:1\">"
283                             + "<DeviceStatusList>"
284                             + "&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;DeviceStatus&gt;&lt;DeviceID&gt;"
285                             + wemoLightID
286                             + "&lt;/DeviceID&gt;&lt;IsGroupAction&gt;NO&lt;/IsGroupAction&gt;&lt;CapabilityID&gt;"
287                             + capability + "&lt;/CapabilityID&gt;&lt;CapabilityValue&gt;" + value
288                             + "&lt;/CapabilityValue&gt;&lt;/DeviceStatus&gt;" + "</DeviceStatusList>"
289                             + "</u:SetDeviceStatus>" + "</s:Body>" + "</s:Envelope>";
290
291                     String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
292                     if (wemoCallResponse != null) {
293                         if (logger.isTraceEnabled()) {
294                             logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
295                             logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader,
296                                     getThing().getUID());
297                             logger.trace("wemoCall with content '{}' for device '{}'", content, getThing().getUID());
298                             logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse,
299                                     getThing().getUID());
300                         }
301                         if ("10008".equals(capability)) {
302                             OnOffType binaryState = null;
303                             binaryState = "0".equals(value) ? OnOffType.OFF : OnOffType.ON;
304                             updateState(CHANNEL_STATE, binaryState);
305                         }
306                     }
307                 }
308             } catch (Exception e) {
309                 throw new IllegalStateException("Could not send command to WeMo Bridge", e);
310             }
311         }
312     }
313
314     @Override
315     public @Nullable String getUDN() {
316         WemoBridgeHandler wemoBridge = getWemoBridgeHandler();
317         if (wemoBridge == null) {
318             logger.debug("wemoBridgeHandler not found");
319             return null;
320         }
321         return (String) wemoBridge.getThing().getConfiguration().get(UDN);
322     }
323
324     /**
325      * The {@link getDeviceState} is used for polling the actual state of a WeMo Light and updating the according
326      * channel states.
327      */
328     public void getDeviceState() {
329         String localHost = getHost();
330         if (localHost.isEmpty()) {
331             logger.error("Failed to get actual state for device '{}': IP address missing", getThing().getUID());
332             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
333                     "@text/config-status.error.missing-ip");
334             return;
335         }
336         logger.debug("Request actual state for LightID '{}'", wemoLightID);
337         String wemoURL = getWemoURL(localHost, BRIDGEACTION);
338         if (wemoURL == null) {
339             logger.error("Failed to get actual state for device '{}': URL cannot be created", getThing().getUID());
340             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
341                     "@text/config-status.error.missing-url");
342             return;
343         }
344         try {
345             String soapHeader = "\"urn:Belkin:service:bridge:1#GetDeviceStatus\"";
346             String content = "<?xml version=\"1.0\"?>"
347                     + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
348                     + "<s:Body>" + "<u:GetDeviceStatus xmlns:u=\"urn:Belkin:service:bridge:1\">" + "<DeviceIDs>"
349                     + wemoLightID + "</DeviceIDs>" + "</u:GetDeviceStatus>" + "</s:Body>" + "</s:Envelope>";
350
351             String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
352             if (wemoCallResponse != null) {
353                 if (logger.isTraceEnabled()) {
354                     logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
355                     logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader, getThing().getUID());
356                     logger.trace("wemoCall with content '{}' for device '{}'", content, getThing().getUID());
357                     logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse, getThing().getUID());
358                 }
359                 wemoCallResponse = unescapeXml(wemoCallResponse);
360                 String response = substringBetween(wemoCallResponse, "<CapabilityValue>", "</CapabilityValue>");
361                 logger.trace("wemoNewLightState = {}", response);
362                 String[] splitResponse = response.split(",");
363                 if (splitResponse[0] != null) {
364                     OnOffType binaryState = null;
365                     binaryState = "0".equals(splitResponse[0]) ? OnOffType.OFF : OnOffType.ON;
366                     updateState(CHANNEL_STATE, binaryState);
367                 }
368                 if (splitResponse[1] != null) {
369                     String splitBrightness[] = splitResponse[1].split(":");
370                     if (splitBrightness[0] != null) {
371                         int newBrightnessValue = Integer.valueOf(splitBrightness[0]);
372                         int newBrightness = Math.round(newBrightnessValue * 100 / 255);
373                         logger.trace("newBrightness = {}", newBrightness);
374                         State newBrightnessState = new PercentType(newBrightness);
375                         updateState(CHANNEL_BRIGHTNESS, newBrightnessState);
376                         currentBrightness = newBrightness;
377                     }
378                 }
379             }
380         } catch (Exception e) {
381             throw new IllegalStateException("Could not retrieve new Wemo light state", e);
382         }
383     }
384
385     @Override
386     public void onServiceSubscribed(@Nullable String service, boolean succeeded) {
387     }
388
389     @Override
390     public void onValueReceived(@Nullable String variable, @Nullable String value, @Nullable String service) {
391         logger.trace("Received pair '{}':'{}' (service '{}') for thing '{}'",
392                 new Object[] { variable, value, service, this.getThing().getUID() });
393         String capabilityId = substringBetween(value, "<CapabilityId>", "</CapabilityId>");
394         String newValue = substringBetween(value, "<Value>", "</Value>");
395         switch (capabilityId) {
396             case "10006":
397                 OnOffType binaryState = null;
398                 binaryState = "0".equals(newValue) ? OnOffType.OFF : OnOffType.ON;
399                 updateState(CHANNEL_STATE, binaryState);
400                 break;
401             case "10008":
402                 String splitValue[] = newValue.split(":");
403                 if (splitValue[0] != null) {
404                     int newBrightnessValue = Integer.valueOf(splitValue[0]);
405                     int newBrightness = Math.round(newBrightnessValue * 100 / 255);
406                     State newBrightnessState = new PercentType(newBrightness);
407                     updateState(CHANNEL_BRIGHTNESS, newBrightnessState);
408                     currentBrightness = newBrightness;
409                 }
410                 break;
411         }
412     }
413
414     @Override
415     public void onStatusChanged(boolean status) {
416     }
417
418     private synchronized void addSubscription() {
419         synchronized (upnpLock) {
420             UpnpIOService localService = service;
421             if (localService != null) {
422                 if (localService.isRegistered(this)) {
423                     logger.debug("Checking WeMo GENA subscription for '{}'", getThing().getUID());
424
425                     if (subscriptionState.get(SUBSCRIPTION) == null) {
426                         logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(),
427                                 SUBSCRIPTION);
428                         localService.addSubscription(this, SUBSCRIPTION, SUBSCRIPTION_DURATION_SECONDS);
429                         subscriptionState.put(SUBSCRIPTION, true);
430                     }
431                 } else {
432                     logger.debug(
433                             "Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
434                             getThing().getUID());
435                 }
436             }
437         }
438     }
439
440     private synchronized void removeSubscription() {
441         synchronized (upnpLock) {
442             UpnpIOService localService = service;
443             if (localService != null) {
444                 if (localService.isRegistered(this)) {
445                     logger.debug("Removing WeMo GENA subscription for '{}'", getThing().getUID());
446
447                     if (subscriptionState.get(SUBSCRIPTION) != null) {
448                         logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), SUBSCRIPTION);
449                         localService.removeSubscription(this, SUBSCRIPTION);
450                     }
451                     subscriptionState = new HashMap<>();
452                     localService.unregisterParticipant(this);
453                 }
454             }
455         }
456     }
457
458     private boolean isUpnpDeviceRegistered() {
459         UpnpIOService localService = service;
460         if (localService != null) {
461             return localService.isRegistered(this);
462         }
463         return false;
464     }
465
466     public String getHost() {
467         String localHost = host;
468         if (!localHost.isEmpty()) {
469             return localHost;
470         }
471         UpnpIOService localService = service;
472         if (localService != null) {
473             URL descriptorURL = localService.getDescriptorURL(this);
474             if (descriptorURL != null) {
475                 return descriptorURL.getHost();
476             }
477         }
478         return "";
479     }
480 }