]> git.basschouten.com Git - openhab-addons.git/blob
94c5df71461222396c4d7a5d8f2764b1d62b5e46
[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.util.HashMap;
19 import java.util.Map;
20 import java.util.concurrent.ScheduledFuture;
21 import java.util.concurrent.TimeUnit;
22
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;
43
44 /**
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.
47  *
48  * @author Hans-Jörg Merk - Initial contribution
49  */
50 @NonNullByDefault
51 public class WemoLightHandler extends WemoBaseThingHandler {
52
53     private final Logger logger = LoggerFactory.getLogger(WemoLightHandler.class);
54
55     private Map<String, Boolean> subscriptionState = new HashMap<>();
56
57     private final Object upnpLock = new Object();
58     private final Object jobLock = new Object();
59
60     private @Nullable WemoBridgeHandler wemoBridgeHandler;
61
62     private @Nullable 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     /**
74      * The default refresh initial delay in Seconds.
75      */
76     private static final int DEFAULT_REFRESH_INITIAL_DELAY = 15;
77
78     private @Nullable ScheduledFuture<?> pollingJob;
79
80     public WemoLightHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemoHttpcaller) {
81         super(thing, upnpIOService, wemoHttpcaller);
82
83         logger.debug("Creating a WemoLightHandler for thing '{}'", getThing().getUID());
84     }
85
86     @Override
87     public void initialize() {
88         // initialize() is only called if the required parameter 'deviceID' is available
89         wemoLightID = (String) getConfig().get(DEVICE_ID);
90
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);
96             }
97             host = getHost();
98             pollingJob = scheduler.scheduleWithFixedDelay(this::poll, DEFAULT_REFRESH_INITIAL_DELAY,
99                     DEFAULT_REFRESH_INTERVAL_SECONDS, TimeUnit.SECONDS);
100             updateStatus(ThingStatus.ONLINE);
101         } else {
102             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.BRIDGE_OFFLINE);
103         }
104     }
105
106     @Override
107     public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
108         if (bridgeStatusInfo.getStatus().equals(ThingStatus.ONLINE)) {
109             updateStatus(ThingStatus.ONLINE);
110         } else {
111             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.BRIDGE_OFFLINE);
112             ScheduledFuture<?> job = this.pollingJob;
113             if (job != null && !job.isCancelled()) {
114                 job.cancel(true);
115             }
116             this.pollingJob = null;
117         }
118     }
119
120     @Override
121     public void dispose() {
122         logger.debug("WeMoLightHandler disposed.");
123
124         ScheduledFuture<?> job = this.pollingJob;
125         if (job != null && !job.isCancelled()) {
126             job.cancel(true);
127         }
128         this.pollingJob = null;
129         removeSubscription();
130     }
131
132     private synchronized @Nullable WemoBridgeHandler getWemoBridgeHandler() {
133         Bridge bridge = getBridge();
134         if (bridge == null) {
135             logger.error("Required bridge not defined for device {}.", wemoLightID);
136             return null;
137         }
138         ThingHandler handler = bridge.getHandler();
139         if (handler instanceof WemoBridgeHandler) {
140             this.wemoBridgeHandler = (WemoBridgeHandler) handler;
141         } else {
142             logger.debug("No available bridge handler found for {} bridge {} .", wemoLightID, bridge.getUID());
143             return null;
144         }
145         return this.wemoBridgeHandler;
146     }
147
148     private void poll() {
149         synchronized (jobLock) {
150             if (pollingJob == null) {
151                 return;
152             }
153             try {
154                 logger.debug("Polling job");
155                 host = getHost();
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<>();
164                     }
165                     return;
166                 }
167                 updateStatus(ThingStatus.ONLINE);
168                 getDeviceState();
169                 addSubscription();
170             } catch (Exception e) {
171                 logger.debug("Exception during poll: {}", e.getMessage(), e);
172             }
173         }
174     }
175
176     @Override
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");
184             return;
185         }
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");
192             return;
193         }
194         if (command instanceof RefreshType) {
195             try {
196                 getDeviceState();
197             } catch (Exception e) {
198                 logger.debug("Exception during poll", e);
199             }
200         } else {
201             Configuration configuration = getConfig();
202             configuration.get(DEVICE_ID);
203
204             WemoBridgeHandler wemoBridge = getWemoBridgeHandler();
205             if (wemoBridge == null) {
206                 logger.debug("wemoBridgeHandler not found, cannot handle command");
207                 return;
208             }
209             String devUDN = "uuid:" + wemoBridge.getThing().getConfiguration().get(UDN).toString();
210             logger.trace("WeMo Bridge to send command to : {}", devUDN);
211
212             String value = null;
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()) {
225                             case "ON":
226                                 value = "255:0";
227                                 break;
228                             case "OFF":
229                                 value = "0:0";
230                                 break;
231                         }
232                     } else if (command instanceof IncreaseDecreaseType) {
233                         int newBrightness;
234                         switch (command.toString()) {
235                             case "INCREASE":
236                                 currentBrightness = currentBrightness + DIM_STEPSIZE;
237                                 newBrightness = Math.round(currentBrightness * 255 / 100);
238                                 if (newBrightness > 255) {
239                                     newBrightness = 255;
240                                 }
241                                 value = newBrightness + ":0";
242                                 break;
243                             case "DECREASE":
244                                 currentBrightness = currentBrightness - DIM_STEPSIZE;
245                                 newBrightness = Math.round(currentBrightness * 255 / 100);
246                                 if (newBrightness < 0) {
247                                     newBrightness = 0;
248                                 }
249                                 value = newBrightness + ":0";
250                                 break;
251                         }
252                     }
253                     break;
254                 case CHANNEL_STATE:
255                     capability = "10006";
256                     switch (command.toString()) {
257                         case "ON":
258                             value = "1";
259                             break;
260                         case "OFF":
261                             value = "0";
262                             break;
263                     }
264                     break;
265             }
266             try {
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                             + "&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;DeviceStatus&gt;&lt;DeviceID&gt;"
274                             + wemoLightID
275                             + "&lt;/DeviceID&gt;&lt;IsGroupAction&gt;NO&lt;/IsGroupAction&gt;&lt;CapabilityID&gt;"
276                             + capability + "&lt;/CapabilityID&gt;&lt;CapabilityValue&gt;" + value
277                             + "&lt;/CapabilityValue&gt;&lt;/DeviceStatus&gt;" + "</DeviceStatusList>"
278                             + "</u:SetDeviceStatus>" + "</s:Body>" + "</s:Envelope>";
279
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());
289                         }
290                         if ("10008".equals(capability)) {
291                             OnOffType binaryState = null;
292                             binaryState = "0".equals(value) ? OnOffType.OFF : OnOffType.ON;
293                             updateState(CHANNEL_STATE, binaryState);
294                         }
295                     }
296                 }
297             } catch (Exception e) {
298                 throw new IllegalStateException("Could not send command to WeMo Bridge", e);
299             }
300         }
301     }
302
303     @Override
304     public @Nullable String getUDN() {
305         WemoBridgeHandler wemoBridge = getWemoBridgeHandler();
306         if (wemoBridge == null) {
307             logger.debug("wemoBridgeHandler not found");
308             return null;
309         }
310         return (String) wemoBridge.getThing().getConfiguration().get(UDN);
311     }
312
313     /**
314      * The {@link getDeviceState} is used for polling the actual state of a WeMo Light and updating the according
315      * channel states.
316      */
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");
323             return;
324         }
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");
331             return;
332         }
333         try {
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>";
339
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());
347                 }
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);
356                 }
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;
366                     }
367                 }
368             }
369         } catch (Exception e) {
370             throw new IllegalStateException("Could not retrieve new Wemo light state", e);
371         }
372     }
373
374     @Override
375     public void onServiceSubscribed(@Nullable String service, boolean succeeded) {
376     }
377
378     @Override
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) {
385             case "10006":
386                 OnOffType binaryState = null;
387                 binaryState = "0".equals(newValue) ? OnOffType.OFF : OnOffType.ON;
388                 updateState(CHANNEL_STATE, binaryState);
389                 break;
390             case "10008":
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;
398                 }
399                 break;
400         }
401     }
402
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());
409
410                     if (subscriptionState.get(SUBSCRIPTION) == null) {
411                         logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(),
412                                 SUBSCRIPTION);
413                         localService.addSubscription(this, SUBSCRIPTION, SUBSCRIPTION_DURATION_SECONDS);
414                         subscriptionState.put(SUBSCRIPTION, true);
415                     }
416                 } else {
417                     logger.debug(
418                             "Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
419                             getThing().getUID());
420                 }
421             }
422         }
423     }
424
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());
431
432                     if (subscriptionState.get(SUBSCRIPTION) != null) {
433                         logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), SUBSCRIPTION);
434                         localService.removeSubscription(this, SUBSCRIPTION);
435                     }
436                     subscriptionState = new HashMap<>();
437                     localService.unregisterParticipant(this);
438                 }
439             }
440         }
441     }
442 }