]> git.basschouten.com Git - openhab-addons.git/blob
db5d386e8dffe078ccd7b82b08a29d55e3851434
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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             pollingJob = scheduler.scheduleWithFixedDelay(this::poll, DEFAULT_REFRESH_INITIAL_DELAY,
89                     DEFAULT_REFRESH_INTERVAL_SECONDS, TimeUnit.SECONDS);
90             updateStatus(ThingStatus.UNKNOWN);
91         } else {
92             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.BRIDGE_OFFLINE);
93         }
94     }
95
96     @Override
97     public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
98         if (bridgeStatusInfo.getStatus().equals(ThingStatus.ONLINE)) {
99             updateStatus(ThingStatus.ONLINE);
100         } else {
101             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
102             ScheduledFuture<?> job = this.pollingJob;
103             if (job != null && !job.isCancelled()) {
104                 job.cancel(true);
105             }
106             this.pollingJob = null;
107         }
108     }
109
110     @Override
111     public void dispose() {
112         logger.debug("WemoLightHandler disposed.");
113
114         ScheduledFuture<?> job = this.pollingJob;
115         if (job != null && !job.isCancelled()) {
116             job.cancel(true);
117         }
118         this.pollingJob = null;
119         super.dispose();
120     }
121
122     private synchronized @Nullable WemoBridgeHandler getWemoBridgeHandler() {
123         Bridge bridge = getBridge();
124         if (bridge == null) {
125             logger.warn("Required bridge not defined for device {}.", wemoLightID);
126             return null;
127         }
128         ThingHandler handler = bridge.getHandler();
129         if (handler instanceof WemoBridgeHandler wemoBridgeHandler) {
130             this.wemoBridgeHandler = wemoBridgeHandler;
131         } else {
132             logger.debug("No available bridge handler found for {} bridge {} .", wemoLightID, bridge.getUID());
133             return null;
134         }
135         return this.wemoBridgeHandler;
136     }
137
138     private void poll() {
139         synchronized (jobLock) {
140             if (pollingJob == null) {
141                 return;
142             }
143             try {
144                 logger.debug("Polling job");
145                 // Check if the Wemo device is set in the UPnP service registry
146                 if (!isUpnpDeviceRegistered()) {
147                     logger.debug("UPnP device {} not yet registered", getUDN());
148                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
149                             "@text/config-status.pending.device-not-registered [\"" + getUDN() + "\"]");
150                     return;
151                 }
152                 getDeviceState();
153             } catch (Exception e) {
154                 logger.debug("Exception during poll: {}", e.getMessage(), e);
155             }
156         }
157     }
158
159     @Override
160     public void handleCommand(ChannelUID channelUID, Command command) {
161         String wemoURL = getWemoURL(BASICACTION);
162         if (wemoURL == null) {
163             logger.debug("Failed to send command '{}' for device '{}': URL cannot be created", command,
164                     getThing().getUID());
165             return;
166         }
167         if (command instanceof RefreshType) {
168             try {
169                 getDeviceState();
170             } catch (Exception e) {
171                 logger.debug("Exception during poll", e);
172             }
173         } else {
174             Configuration configuration = getConfig();
175             configuration.get(DEVICE_ID);
176
177             WemoBridgeHandler wemoBridge = getWemoBridgeHandler();
178             if (wemoBridge == null) {
179                 logger.debug("wemoBridgeHandler not found, cannot handle command");
180                 return;
181             }
182             String devUDN = "uuid:" + wemoBridge.getThing().getConfiguration().get(UDN).toString();
183             logger.trace("WeMo Bridge to send command to : {}", devUDN);
184
185             String value = null;
186             String capability = null;
187             switch (channelUID.getId()) {
188                 case CHANNEL_BRIGHTNESS:
189                     capability = "10008";
190                     if (command instanceof PercentType percentCommand) {
191                         int newBrightness = percentCommand.intValue();
192                         logger.trace("wemoLight received Value {}", newBrightness);
193                         int value1 = Math.round(newBrightness * 255 / 100);
194                         value = value1 + ":0";
195                         currentBrightness = newBrightness;
196                     } else if (command instanceof OnOffType) {
197                         switch (command.toString()) {
198                             case "ON":
199                                 value = "255:0";
200                                 break;
201                             case "OFF":
202                                 value = "0:0";
203                                 break;
204                         }
205                     } else if (command instanceof IncreaseDecreaseType) {
206                         int newBrightness;
207                         switch (command.toString()) {
208                             case "INCREASE":
209                                 currentBrightness = currentBrightness + DIM_STEPSIZE;
210                                 newBrightness = Math.round(currentBrightness * 255 / 100);
211                                 if (newBrightness > 255) {
212                                     newBrightness = 255;
213                                 }
214                                 value = newBrightness + ":0";
215                                 break;
216                             case "DECREASE":
217                                 currentBrightness = currentBrightness - DIM_STEPSIZE;
218                                 newBrightness = Math.round(currentBrightness * 255 / 100);
219                                 if (newBrightness < 0) {
220                                     newBrightness = 0;
221                                 }
222                                 value = newBrightness + ":0";
223                                 break;
224                         }
225                     }
226                     break;
227                 case CHANNEL_STATE:
228                     capability = "10006";
229                     switch (command.toString()) {
230                         case "ON":
231                             value = "1";
232                             break;
233                         case "OFF":
234                             value = "0";
235                             break;
236                     }
237                     break;
238             }
239             try {
240                 if (capability != null && value != null) {
241                     String soapHeader = "\"urn:Belkin:service:bridge:1#SetDeviceStatus\"";
242                     String content = """
243                             <?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>\
246                             <u:SetDeviceStatus xmlns:u="urn:Belkin:service:bridge:1">\
247                             <DeviceStatusList>\
248                             &lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;DeviceStatus&gt;&lt;DeviceID&gt;\
249                             """
250                             + wemoLightID
251                             + "&lt;/DeviceID&gt;&lt;IsGroupAction&gt;NO&lt;/IsGroupAction&gt;&lt;CapabilityID&gt;"
252                             + capability + "&lt;/CapabilityID&gt;&lt;CapabilityValue&gt;" + value
253                             + "&lt;/CapabilityValue&gt;&lt;/DeviceStatus&gt;" + "</DeviceStatusList>"
254                             + "</u:SetDeviceStatus>" + "</s:Body>" + "</s:Envelope>";
255
256                     wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
257                     if ("10008".equals(capability)) {
258                         OnOffType binaryState = null;
259                         binaryState = "0".equals(value) ? OnOffType.OFF : OnOffType.ON;
260                         updateState(CHANNEL_STATE, binaryState);
261                     }
262                     updateStatus(ThingStatus.ONLINE);
263                 }
264             } catch (Exception e) {
265                 logger.warn("Failed to send command '{}' for device '{}': {}", command, getThing().getUID(),
266                         e.getMessage());
267                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
268             }
269         }
270     }
271
272     @Override
273     public @Nullable String getUDN() {
274         WemoBridgeHandler wemoBridge = getWemoBridgeHandler();
275         if (wemoBridge == null) {
276             logger.debug("wemoBridgeHandler not found");
277             return null;
278         }
279         return (String) wemoBridge.getThing().getConfiguration().get(UDN);
280     }
281
282     /**
283      * The {@link getDeviceState} is used for polling the actual state of a WeMo Light and updating the according
284      * channel states.
285      */
286     public void getDeviceState() {
287         logger.debug("Request actual state for LightID '{}'", wemoLightID);
288         String wemoURL = getWemoURL(BRIDGEACTION);
289         if (wemoURL == null) {
290             logger.debug("Failed to get actual state for device '{}': URL cannot be created", getThing().getUID());
291             return;
292         }
293         try {
294             String soapHeader = "\"urn:Belkin:service:bridge:1#GetDeviceStatus\"";
295             String content = """
296                     <?xml version="1.0"?>\
297                     <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">\
298                     <s:Body>\
299                     <u:GetDeviceStatus xmlns:u="urn:Belkin:service:bridge:1">\
300                     <DeviceIDs>\
301                     """
302                     + wemoLightID + "</DeviceIDs>" + "</u:GetDeviceStatus>" + "</s:Body>" + "</s:Envelope>";
303
304             String wemoCallResponse = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
305             wemoCallResponse = unescapeXml(wemoCallResponse);
306             String response = substringBetween(wemoCallResponse, "<CapabilityValue>", "</CapabilityValue>");
307             logger.trace("wemoNewLightState = {}", response);
308             String[] splitResponse = response.split(",");
309             if (splitResponse[0] != null) {
310                 OnOffType binaryState = null;
311                 binaryState = "0".equals(splitResponse[0]) ? OnOffType.OFF : OnOffType.ON;
312                 updateState(CHANNEL_STATE, binaryState);
313             }
314             if (splitResponse[1] != null) {
315                 String[] splitBrightness = splitResponse[1].split(":");
316                 if (splitBrightness[0] != null) {
317                     int newBrightnessValue = Integer.valueOf(splitBrightness[0]);
318                     int newBrightness = Math.round(newBrightnessValue * 100 / 255);
319                     logger.trace("newBrightness = {}", newBrightness);
320                     State newBrightnessState = new PercentType(newBrightness);
321                     updateState(CHANNEL_BRIGHTNESS, newBrightnessState);
322                     currentBrightness = newBrightness;
323                 }
324             }
325             updateStatus(ThingStatus.ONLINE);
326         } catch (Exception e) {
327             logger.debug("Could not retrieve new Wemo light state for '{}':", getThing().getUID(), e);
328             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
329         }
330     }
331
332     @Override
333     public void onValueReceived(@Nullable String variable, @Nullable String value, @Nullable String service) {
334         logger.trace("Received pair '{}':'{}' (service '{}') for thing '{}'",
335                 new Object[] { variable, value, service, this.getThing().getUID() });
336         String capabilityId = substringBetween(value, "<CapabilityId>", "</CapabilityId>");
337         String newValue = substringBetween(value, "<Value>", "</Value>");
338         switch (capabilityId) {
339             case "10006":
340                 OnOffType binaryState = null;
341                 binaryState = "0".equals(newValue) ? OnOffType.OFF : OnOffType.ON;
342                 updateState(CHANNEL_STATE, binaryState);
343                 break;
344             case "10008":
345                 String[] splitValue = newValue.split(":");
346                 if (splitValue[0] != null) {
347                     int newBrightnessValue = Integer.valueOf(splitValue[0]);
348                     int newBrightness = Math.round(newBrightnessValue * 100 / 255);
349                     State newBrightnessState = new PercentType(newBrightness);
350                     updateState(CHANNEL_BRIGHTNESS, newBrightnessState);
351                     currentBrightness = newBrightness;
352                 }
353                 break;
354         }
355     }
356 }