]> git.basschouten.com Git - openhab-addons.git/blob
3a37aca080fbf6c9594252aca217e1af7a924c88
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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
17 import java.io.IOException;
18 import java.math.BigDecimal;
19 import java.math.RoundingMode;
20 import java.net.URL;
21 import java.time.Instant;
22 import java.time.ZonedDateTime;
23 import java.util.Collections;
24 import java.util.HashMap;
25 import java.util.Map;
26 import java.util.Set;
27 import java.util.TimeZone;
28 import java.util.concurrent.ScheduledFuture;
29 import java.util.concurrent.TimeUnit;
30 import java.util.stream.Collectors;
31 import java.util.stream.Stream;
32
33 import org.apache.commons.lang.StringUtils;
34 import org.openhab.binding.wemo.internal.http.WemoHttpCall;
35 import org.openhab.core.config.core.Configuration;
36 import org.openhab.core.io.net.http.HttpUtil;
37 import org.openhab.core.io.transport.upnp.UpnpIOParticipant;
38 import org.openhab.core.io.transport.upnp.UpnpIOService;
39 import org.openhab.core.library.types.DateTimeType;
40 import org.openhab.core.library.types.DecimalType;
41 import org.openhab.core.library.types.OnOffType;
42 import org.openhab.core.thing.ChannelUID;
43 import org.openhab.core.thing.Thing;
44 import org.openhab.core.thing.ThingStatus;
45 import org.openhab.core.thing.ThingStatusDetail;
46 import org.openhab.core.thing.ThingTypeUID;
47 import org.openhab.core.types.Command;
48 import org.openhab.core.types.RefreshType;
49 import org.openhab.core.types.State;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
52
53 /**
54  * The {@link WemoHandler} is responsible for handling commands, which are
55  * sent to one of the channels and to update their states.
56  *
57  * @author Hans-Jörg Merk - Initial contribution; Added support for WeMo Insight energy measurement
58  * @author Kai Kreuzer - some refactoring for performance and simplification
59  * @author Stefan Bußweiler - Added new thing status handling
60  * @author Erdoan Hadzhiyusein - Adapted the class to work with the new DateTimeType
61  * @author Mihir Patil - Added standby switch
62  */
63
64 public class WemoHandler extends AbstractWemoHandler implements UpnpIOParticipant {
65
66     private final Logger logger = LoggerFactory.getLogger(WemoHandler.class);
67
68     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Stream
69             .of(THING_TYPE_SOCKET, THING_TYPE_INSIGHT, THING_TYPE_LIGHTSWITCH, THING_TYPE_MOTION)
70             .collect(Collectors.toSet());
71
72     private Map<String, Boolean> subscriptionState = new HashMap<>();
73
74     private final Map<String, String> stateMap = Collections.synchronizedMap(new HashMap<>());
75
76     // protected static final int SUBSCRIPTION_DURATION = WemoBindingConstants.SUBSCRIPTION_DURATION;
77
78     private UpnpIOService service;
79
80     /**
81      * The default refresh interval in Seconds.
82      */
83     private final int DEFAULT_REFRESH_INTERVAL = 120;
84
85     private ScheduledFuture<?> refreshJob;
86
87     private final Runnable refreshRunnable = new Runnable() {
88
89         @Override
90         public void run() {
91             try {
92                 if (!isUpnpDeviceRegistered()) {
93                     logger.debug("WeMo UPnP device {} not yet registered", getUDN());
94                 }
95
96                 updateWemoState();
97                 onSubscription();
98             } catch (Exception e) {
99                 logger.debug("Exception during poll", e);
100                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
101             }
102         }
103     };
104
105     public WemoHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemohttpCaller) {
106         super(thing);
107
108         this.wemoHttpCaller = wemohttpCaller;
109
110         logger.debug("Creating a WemoHandler for thing '{}'", getThing().getUID());
111
112         if (upnpIOService != null) {
113             this.service = upnpIOService;
114         } else {
115             logger.debug("upnpIOService not set.");
116         }
117     }
118
119     @Override
120     public void initialize() {
121         Configuration configuration = getConfig();
122
123         if (configuration.get("udn") != null) {
124             logger.debug("Initializing WemoHandler for UDN '{}'", configuration.get("udn"));
125             service.registerParticipant(this);
126             onSubscription();
127             onUpdate();
128             updateStatus(ThingStatus.ONLINE);
129         } else {
130             logger.debug("Cannot initalize WemoHandler. UDN not set.");
131         }
132     }
133
134     @Override
135     public void dispose() {
136         logger.debug("WeMoHandler disposed.");
137
138         removeSubscription();
139
140         if (refreshJob != null && !refreshJob.isCancelled()) {
141             refreshJob.cancel(true);
142             refreshJob = null;
143         }
144     }
145
146     @Override
147     public void handleCommand(ChannelUID channelUID, Command command) {
148         logger.trace("Command '{}' received for channel '{}'", command, channelUID);
149
150         if (command instanceof RefreshType) {
151             try {
152                 updateWemoState();
153             } catch (Exception e) {
154                 logger.debug("Exception during poll", e);
155             }
156         } else if (channelUID.getId().equals(CHANNEL_STATE)) {
157             if (command instanceof OnOffType) {
158                 try {
159                     String binaryState = null;
160
161                     if (command.equals(OnOffType.ON)) {
162                         binaryState = "1";
163                     } else if (command.equals(OnOffType.OFF)) {
164                         binaryState = "0";
165                     }
166
167                     String soapHeader = "\"urn:Belkin:service:basicevent:1#SetBinaryState\"";
168
169                     String content = "<?xml version=\"1.0\"?>"
170                             + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
171                             + "<s:Body>" + "<u:SetBinaryState xmlns:u=\"urn:Belkin:service:basicevent:1\">"
172                             + "<BinaryState>" + binaryState + "</BinaryState>" + "</u:SetBinaryState>" + "</s:Body>"
173                             + "</s:Envelope>";
174
175                     String wemoURL = getWemoURL("basicevent");
176
177                     if (wemoURL != null) {
178                         wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
179                     }
180                 } catch (Exception e) {
181                     logger.error("Failed to send command '{}' for device '{}': {}", command, getThing().getUID(),
182                             e.getMessage());
183                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
184                 }
185                 updateStatus(ThingStatus.ONLINE);
186             }
187         }
188     }
189
190     @Override
191     public void onServiceSubscribed(String service, boolean succeeded) {
192         logger.debug("WeMo {}: Subscription to service {} {}", getUDN(), service, succeeded ? "succeeded" : "failed");
193         subscriptionState.put(service, succeeded);
194     }
195
196     @SuppressWarnings("null")
197     @Override
198     public void onValueReceived(String variable, String value, String service) {
199         logger.debug("Received pair '{}':'{}' (service '{}') for thing '{}'",
200                 new Object[] { variable, value, service, this.getThing().getUID() });
201
202         updateStatus(ThingStatus.ONLINE);
203
204         this.stateMap.put(variable, value);
205
206         if (getThing().getThingTypeUID().getId().equals("insight")) {
207             String insightParams = stateMap.get("InsightParams");
208
209             if (insightParams != null) {
210                 String[] splitInsightParams = insightParams.split("\\|");
211
212                 if (splitInsightParams[0] != null) {
213                     OnOffType binaryState = null;
214                     binaryState = splitInsightParams[0].equals("0") ? OnOffType.OFF : OnOffType.ON;
215                     if (binaryState != null) {
216                         logger.trace("New InsightParam binaryState '{}' for device '{}' received", binaryState,
217                                 getThing().getUID());
218                         updateState(CHANNEL_STATE, binaryState);
219                     }
220                 }
221
222                 long lastChangedAt = 0;
223                 try {
224                     lastChangedAt = Long.parseLong(splitInsightParams[1]) * 1000; // convert s to ms
225                 } catch (NumberFormatException e) {
226                     logger.error("Unable to parse lastChangedAt value '{}' for device '{}'; expected long",
227                             splitInsightParams[1], getThing().getUID());
228                 }
229                 ZonedDateTime zoned = ZonedDateTime.ofInstant(Instant.ofEpochMilli(lastChangedAt),
230                         TimeZone.getDefault().toZoneId());
231
232                 State lastChangedAtState = new DateTimeType(zoned);
233                 if (lastChangedAt != 0) {
234                     logger.trace("New InsightParam lastChangedAt '{}' for device '{}' received", lastChangedAtState,
235                             getThing().getUID());
236                     updateState(CHANNEL_LASTCHANGEDAT, lastChangedAtState);
237                 }
238
239                 State lastOnFor = DecimalType.valueOf(splitInsightParams[2]);
240                 if (lastOnFor != null) {
241                     logger.trace("New InsightParam lastOnFor '{}' for device '{}' received", lastOnFor,
242                             getThing().getUID());
243                     updateState(CHANNEL_LASTONFOR, lastOnFor);
244                 }
245
246                 State onToday = DecimalType.valueOf(splitInsightParams[3]);
247                 if (onToday != null) {
248                     logger.trace("New InsightParam onToday '{}' for device '{}' received", onToday,
249                             getThing().getUID());
250                     updateState(CHANNEL_ONTODAY, onToday);
251                 }
252
253                 State onTotal = DecimalType.valueOf(splitInsightParams[4]);
254                 if (onTotal != null) {
255                     logger.trace("New InsightParam onTotal '{}' for device '{}' received", onTotal,
256                             getThing().getUID());
257                     updateState(CHANNEL_ONTOTAL, onTotal);
258                 }
259
260                 State timespan = DecimalType.valueOf(splitInsightParams[5]);
261                 if (timespan != null) {
262                     logger.trace("New InsightParam timespan '{}' for device '{}' received", timespan,
263                             getThing().getUID());
264                     updateState(CHANNEL_TIMESPAN, timespan);
265                 }
266
267                 State averagePower = DecimalType.valueOf(splitInsightParams[6]); // natively given in W
268                 if (averagePower != null) {
269                     logger.trace("New InsightParam averagePower '{}' for device '{}' received", averagePower,
270                             getThing().getUID());
271                     updateState(CHANNEL_AVERAGEPOWER, averagePower);
272                 }
273
274                 BigDecimal currentMW = new BigDecimal(splitInsightParams[7]);
275                 State currentPower = new DecimalType(currentMW.divide(new BigDecimal(1000), RoundingMode.HALF_UP)); // recalculate
276                 // mW to W
277                 if (currentPower != null) {
278                     logger.trace("New InsightParam currentPower '{}' for device '{}' received", currentPower,
279                             getThing().getUID());
280                     updateState(CHANNEL_CURRENTPOWER, currentPower);
281                 }
282
283                 BigDecimal energyTodayMWMin = new BigDecimal(splitInsightParams[8]);
284                 // recalculate mW-mins to Wh
285                 State energyToday = new DecimalType(
286                         energyTodayMWMin.divide(new BigDecimal(60000), RoundingMode.HALF_UP));
287                 if (energyToday != null) {
288                     logger.trace("New InsightParam energyToday '{}' for device '{}' received", energyToday,
289                             getThing().getUID());
290                     updateState(CHANNEL_ENERGYTODAY, energyToday);
291                 }
292
293                 BigDecimal energyTotalMWMin = new BigDecimal(splitInsightParams[9]);
294                 // recalculate mW-mins to Wh
295                 State energyTotal = new DecimalType(
296                         energyTotalMWMin.divide(new BigDecimal(60000), RoundingMode.HALF_UP));
297                 if (energyTotal != null) {
298                     logger.trace("New InsightParam energyTotal '{}' for device '{}' received", energyTotal,
299                             getThing().getUID());
300                     updateState(CHANNEL_ENERGYTOTAL, energyTotal);
301                 }
302
303                 BigDecimal standByLimitMW = new BigDecimal(splitInsightParams[10]);
304                 State standByLimit = new DecimalType(standByLimitMW.divide(new BigDecimal(1000), RoundingMode.HALF_UP)); // recalculate
305                 // mW to W
306                 if (standByLimit != null) {
307                     logger.trace("New InsightParam standByLimit '{}' for device '{}' received", standByLimit,
308                             getThing().getUID());
309                     updateState(CHANNEL_STANDBYLIMIT, standByLimit);
310                 }
311                 if (currentMW.divide(new BigDecimal(1000), RoundingMode.HALF_UP).intValue() > standByLimitMW
312                         .divide(new BigDecimal(1000), RoundingMode.HALF_UP).intValue()) {
313                     updateState(CHANNEL_ONSTANDBY, OnOffType.OFF);
314                 } else {
315                     updateState(CHANNEL_ONSTANDBY, OnOffType.ON);
316                 }
317             }
318         } else {
319             State state = stateMap.get("BinaryState").equals("0") ? OnOffType.OFF : OnOffType.ON;
320
321             logger.debug("State '{}' for device '{}' received", state, getThing().getUID());
322
323             if (state != null) {
324                 if (getThing().getThingTypeUID().getId().equals("motion")) {
325                     updateState(CHANNEL_MOTIONDETECTION, state);
326                     if (state.equals(OnOffType.ON)) {
327                         State lastMotionDetected = new DateTimeType();
328                         updateState(CHANNEL_LASTMOTIONDETECTED, lastMotionDetected);
329                     }
330                 } else {
331                     updateState(CHANNEL_STATE, state);
332                 }
333             }
334         }
335     }
336
337     private synchronized void onSubscription() {
338         if (service.isRegistered(this)) {
339             logger.debug("Checking WeMo GENA subscription for '{}'", this);
340
341             ThingTypeUID thingTypeUID = thing.getThingTypeUID();
342             String subscription = "basicevent1";
343
344             if ((subscriptionState.get(subscription) == null) || !subscriptionState.get(subscription).booleanValue()) {
345                 logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(), subscription);
346                 service.addSubscription(this, subscription, SUBSCRIPTION_DURATION);
347                 subscriptionState.put(subscription, true);
348             }
349
350             if (thingTypeUID.equals(THING_TYPE_INSIGHT)) {
351                 subscription = "insight1";
352                 if ((subscriptionState.get(subscription) == null)
353                         || !subscriptionState.get(subscription).booleanValue()) {
354                     logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(),
355                             subscription);
356                     service.addSubscription(this, subscription, SUBSCRIPTION_DURATION);
357                     subscriptionState.put(subscription, true);
358                 }
359             }
360         } else {
361             logger.debug("Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
362                     this);
363         }
364     }
365
366     private synchronized void removeSubscription() {
367         logger.debug("Removing WeMo GENA subscription for '{}'", this);
368
369         if (service.isRegistered(this)) {
370             ThingTypeUID thingTypeUID = thing.getThingTypeUID();
371             String subscription = "basicevent1";
372
373             if ((subscriptionState.get(subscription) != null) && subscriptionState.get(subscription).booleanValue()) {
374                 logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription);
375                 service.removeSubscription(this, subscription);
376             }
377
378             if (thingTypeUID.equals(THING_TYPE_INSIGHT)) {
379                 subscription = "insight1";
380                 if ((subscriptionState.get(subscription) != null)
381                         && subscriptionState.get(subscription).booleanValue()) {
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         if (refreshJob == null || refreshJob.isCancelled()) {
393             Configuration config = getThing().getConfiguration();
394             int refreshInterval = DEFAULT_REFRESH_INTERVAL;
395             Object refreshConfig = config.get("refresh");
396             if (refreshConfig != null) {
397                 refreshInterval = ((BigDecimal) refreshConfig).intValue();
398             }
399             refreshJob = scheduler.scheduleWithFixedDelay(refreshRunnable, 0, refreshInterval, TimeUnit.SECONDS);
400         }
401     }
402
403     private boolean isUpnpDeviceRegistered() {
404         return service.isRegistered(this);
405     }
406
407     @Override
408     public String getUDN() {
409         return (String) this.getThing().getConfiguration().get(UDN);
410     }
411
412     /**
413      * The {@link updateWemoState} polls the actual state of a WeMo device and
414      * calls {@link onValueReceived} to update the statemap and channels..
415      *
416      */
417     protected void updateWemoState() {
418         String action = "GetBinaryState";
419         String variable = "BinaryState";
420         String actionService = "basicevent";
421         String value = null;
422
423         if (getThing().getThingTypeUID().getId().equals("insight")) {
424             action = "GetInsightParams";
425             variable = "InsightParams";
426             actionService = "insight";
427         }
428
429         String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
430         String content = "<?xml version=\"1.0\"?>"
431                 + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
432                 + "<s:Body>" + "<u:" + action + " xmlns:u=\"urn:Belkin:service:" + actionService + ":1\">" + "</u:"
433                 + action + ">" + "</s:Body>" + "</s:Envelope>";
434
435         try {
436             String wemoURL = getWemoURL(actionService);
437             if (wemoURL != null) {
438                 String wemoCallResponse = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
439                 if (wemoCallResponse != null) {
440                     logger.trace("State response '{}' for device '{}' received", wemoCallResponse, getThing().getUID());
441                     if (variable.equals("InsightParams")) {
442                         value = StringUtils.substringBetween(wemoCallResponse, "<InsightParams>", "</InsightParams>");
443                     } else {
444                         value = StringUtils.substringBetween(wemoCallResponse, "<BinaryState>", "</BinaryState>");
445                     }
446                     if (value != null) {
447                         logger.trace("New state '{}' for device '{}' received", value, getThing().getUID());
448                         this.onValueReceived(variable, value, actionService + "1");
449                     }
450                 }
451             }
452         } catch (Exception e) {
453             logger.error("Failed to get actual state for device '{}': {}", getThing().getUID(), e.getMessage());
454         }
455     }
456
457     public String getWemoURL(String actionService) {
458         URL descriptorURL = service.getDescriptorURL(this);
459         int portCheckStart = 49151;
460         int portCheckStop = 49157;
461         String wemoURL = null;
462         String host = null;
463         String port = null;
464         if (descriptorURL != null) {
465             host = StringUtils.substringBetween(descriptorURL.toString(), "://", ":");
466             for (int i = portCheckStart; i < portCheckStop; i++) {
467                 try {
468                     boolean portFound = servicePing(host, i);
469                     if (portFound) {
470                         logger.trace("WeMo device {} responded at Port {}", getUDN(), i);
471                         port = String.valueOf(i);
472                         break;
473                     }
474                 } catch (Exception e) {
475                 }
476             }
477             wemoURL = "http://" + host + ":" + port + "/upnp/control/" + actionService + "1";
478             logger.trace("WeMo url {}", wemoURL);
479             return wemoURL;
480         }
481         return wemoURL;
482     }
483
484     public boolean servicePing(String host, int port) {
485         logger.trace("Ping WeMo device at '{}:{}'", host, port);
486         try {
487             HttpUtil.executeUrl("GET", "http://" + host + ":" + port, 250);
488         } catch (IOException e) {
489             return false;
490         }
491         return true;
492     }
493
494     @Override
495     public void onStatusChanged(boolean status) {
496     }
497 }