]> git.basschouten.com Git - openhab-addons.git/blob
745e33db631adf5642a89ded25cce017700b2241
[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.concurrent.ScheduledFuture;
19 import java.util.concurrent.TimeUnit;
20
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;
41
42 /**
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.
45  *
46  * @author Hans-Jörg Merk - Initial contribution
47  */
48 @NonNullByDefault
49 public class WemoLightHandler extends WemoBaseThingHandler {
50
51     private final Logger logger = LoggerFactory.getLogger(WemoLightHandler.class);
52
53     private final Object jobLock = new Object();
54
55     private @Nullable WemoBridgeHandler wemoBridgeHandler;
56
57     private @Nullable String wemoLightID;
58
59     private int currentBrightness;
60
61     /**
62      * Set dimming stepsize to 5%
63      */
64     private static final int DIM_STEPSIZE = 5;
65
66     /**
67      * The default refresh initial delay in Seconds.
68      */
69     private static final int DEFAULT_REFRESH_INITIAL_DELAY = 15;
70
71     private @Nullable ScheduledFuture<?> pollingJob;
72
73     public WemoLightHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemoHttpcaller) {
74         super(thing, upnpIOService, wemoHttpcaller);
75
76         logger.debug("Creating a WemoLightHandler for thing '{}'", getThing().getUID());
77     }
78
79     @Override
80     public void initialize() {
81         super.initialize();
82         // initialize() is only called if the required parameter 'deviceID' is available
83         wemoLightID = (String) getConfig().get(DEVICE_ID);
84
85         final Bridge bridge = getBridge();
86         if (bridge != null && bridge.getStatus() == ThingStatus.ONLINE) {
87             addSubscription(BRIDGEEVENT);
88             host = getHost();
89             pollingJob = scheduler.scheduleWithFixedDelay(this::poll, DEFAULT_REFRESH_INITIAL_DELAY,
90                     DEFAULT_REFRESH_INTERVAL_SECONDS, TimeUnit.SECONDS);
91             updateStatus(ThingStatus.ONLINE);
92         } else {
93             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.BRIDGE_OFFLINE);
94         }
95     }
96
97     @Override
98     public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
99         if (bridgeStatusInfo.getStatus().equals(ThingStatus.ONLINE)) {
100             updateStatus(ThingStatus.ONLINE);
101         } else {
102             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.BRIDGE_OFFLINE);
103             ScheduledFuture<?> job = this.pollingJob;
104             if (job != null && !job.isCancelled()) {
105                 job.cancel(true);
106             }
107             this.pollingJob = null;
108         }
109     }
110
111     @Override
112     public void dispose() {
113         logger.debug("WemoLightHandler disposed.");
114
115         ScheduledFuture<?> job = this.pollingJob;
116         if (job != null && !job.isCancelled()) {
117             job.cancel(true);
118         }
119         this.pollingJob = null;
120         super.dispose();
121     }
122
123     private synchronized @Nullable WemoBridgeHandler getWemoBridgeHandler() {
124         Bridge bridge = getBridge();
125         if (bridge == null) {
126             logger.warn("Required bridge not defined for device {}.", wemoLightID);
127             return null;
128         }
129         ThingHandler handler = bridge.getHandler();
130         if (handler instanceof WemoBridgeHandler) {
131             this.wemoBridgeHandler = (WemoBridgeHandler) handler;
132         } else {
133             logger.debug("No available bridge handler found for {} bridge {} .", wemoLightID, bridge.getUID());
134             return null;
135         }
136         return this.wemoBridgeHandler;
137     }
138
139     private void poll() {
140         synchronized (jobLock) {
141             if (pollingJob == null) {
142                 return;
143             }
144             try {
145                 logger.debug("Polling job");
146                 host = getHost();
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() + "\"]");
153                     return;
154                 }
155                 getDeviceState();
156             } catch (Exception e) {
157                 logger.debug("Exception during poll: {}", e.getMessage(), e);
158             }
159         }
160     }
161
162     @Override
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");
170             return;
171         }
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");
178             return;
179         }
180         if (command instanceof RefreshType) {
181             try {
182                 getDeviceState();
183             } catch (Exception e) {
184                 logger.debug("Exception during poll", e);
185             }
186         } else {
187             Configuration configuration = getConfig();
188             configuration.get(DEVICE_ID);
189
190             WemoBridgeHandler wemoBridge = getWemoBridgeHandler();
191             if (wemoBridge == null) {
192                 logger.debug("wemoBridgeHandler not found, cannot handle command");
193                 return;
194             }
195             String devUDN = "uuid:" + wemoBridge.getThing().getConfiguration().get(UDN).toString();
196             logger.trace("WeMo Bridge to send command to : {}", devUDN);
197
198             String value = null;
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()) {
211                             case "ON":
212                                 value = "255:0";
213                                 break;
214                             case "OFF":
215                                 value = "0:0";
216                                 break;
217                         }
218                     } else if (command instanceof IncreaseDecreaseType) {
219                         int newBrightness;
220                         switch (command.toString()) {
221                             case "INCREASE":
222                                 currentBrightness = currentBrightness + DIM_STEPSIZE;
223                                 newBrightness = Math.round(currentBrightness * 255 / 100);
224                                 if (newBrightness > 255) {
225                                     newBrightness = 255;
226                                 }
227                                 value = newBrightness + ":0";
228                                 break;
229                             case "DECREASE":
230                                 currentBrightness = currentBrightness - DIM_STEPSIZE;
231                                 newBrightness = Math.round(currentBrightness * 255 / 100);
232                                 if (newBrightness < 0) {
233                                     newBrightness = 0;
234                                 }
235                                 value = newBrightness + ":0";
236                                 break;
237                         }
238                     }
239                     break;
240                 case CHANNEL_STATE:
241                     capability = "10006";
242                     switch (command.toString()) {
243                         case "ON":
244                             value = "1";
245                             break;
246                         case "OFF":
247                             value = "0";
248                             break;
249                     }
250                     break;
251             }
252             try {
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                             + "&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;DeviceStatus&gt;&lt;DeviceID&gt;"
260                             + wemoLightID
261                             + "&lt;/DeviceID&gt;&lt;IsGroupAction&gt;NO&lt;/IsGroupAction&gt;&lt;CapabilityID&gt;"
262                             + capability + "&lt;/CapabilityID&gt;&lt;CapabilityValue&gt;" + value
263                             + "&lt;/CapabilityValue&gt;&lt;/DeviceStatus&gt;" + "</DeviceStatusList>"
264                             + "</u:SetDeviceStatus>" + "</s:Body>" + "</s:Envelope>";
265
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);
271                     }
272                     updateStatus(ThingStatus.ONLINE);
273                 }
274             } catch (Exception e) {
275                 logger.warn("Failed to send command '{}' for device '{}': {}", command, getThing().getUID(),
276                         e.getMessage());
277                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
278             }
279         }
280     }
281
282     @Override
283     public @Nullable String getUDN() {
284         WemoBridgeHandler wemoBridge = getWemoBridgeHandler();
285         if (wemoBridge == null) {
286             logger.debug("wemoBridgeHandler not found");
287             return null;
288         }
289         return (String) wemoBridge.getThing().getConfiguration().get(UDN);
290     }
291
292     /**
293      * The {@link getDeviceState} is used for polling the actual state of a WeMo Light and updating the according
294      * channel states.
295      */
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");
302             return;
303         }
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");
310             return;
311         }
312         try {
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>";
318
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);
328             }
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;
338                 }
339             }
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);
344         }
345     }
346
347     @Override
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) {
354             case "10006":
355                 OnOffType binaryState = null;
356                 binaryState = "0".equals(newValue) ? 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 }