]> git.basschouten.com Git - openhab-addons.git/blob
41a53c4deb5fb136d823d28d217c8aa26155825e
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.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.eclipse.jdt.annotation.NonNullByDefault;
34 import org.eclipse.jdt.annotation.Nullable;
35 import org.openhab.binding.wemo.internal.http.WemoHttpCall;
36 import org.openhab.core.config.core.Configuration;
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
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 @NonNullByDefault
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 UpnpIOService service;
77     private WemoHttpCall wemoCall;
78
79     private @Nullable ScheduledFuture<?> refreshJob;
80
81     private final Runnable refreshRunnable = new Runnable() {
82
83         @Override
84         public void run() {
85             try {
86                 if (!isUpnpDeviceRegistered()) {
87                     logger.debug("WeMo UPnP device {} not yet registered", getUDN());
88                 }
89
90                 updateWemoState();
91                 onSubscription();
92             } catch (Exception e) {
93                 logger.debug("Exception during poll", e);
94                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
95             }
96         }
97     };
98
99     public WemoHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemoHttpCaller) {
100         super(thing, wemoHttpCaller);
101
102         this.service = upnpIOService;
103         this.wemoCall = wemoHttpCaller;
104
105         logger.debug("Creating a WemoHandler for thing '{}'", getThing().getUID());
106     }
107
108     @Override
109     public void initialize() {
110         Configuration configuration = getConfig();
111
112         if (configuration.get("udn") != null) {
113             logger.debug("Initializing WemoHandler for UDN '{}'", configuration.get("udn"));
114             service.registerParticipant(this);
115             onSubscription();
116             onUpdate();
117             updateStatus(ThingStatus.ONLINE);
118         } else {
119             logger.debug("Cannot initalize WemoHandler. UDN not set.");
120         }
121     }
122
123     @Override
124     public void dispose() {
125         logger.debug("WeMoHandler disposed.");
126
127         ScheduledFuture<?> job = refreshJob;
128         if (job != null && !job.isCancelled()) {
129             job.cancel(true);
130         }
131         refreshJob = null;
132         removeSubscription();
133     }
134
135     @Override
136     public void handleCommand(ChannelUID channelUID, Command command) {
137         logger.trace("Command '{}' received for channel '{}'", command, channelUID);
138
139         if (command instanceof RefreshType) {
140             try {
141                 updateWemoState();
142             } catch (Exception e) {
143                 logger.debug("Exception during poll", e);
144             }
145         } else if (channelUID.getId().equals(CHANNEL_STATE)) {
146             if (command instanceof OnOffType) {
147                 try {
148                     String binaryState = null;
149
150                     if (command.equals(OnOffType.ON)) {
151                         binaryState = "1";
152                     } else if (command.equals(OnOffType.OFF)) {
153                         binaryState = "0";
154                     }
155
156                     String soapHeader = "\"urn:Belkin:service:basicevent:1#SetBinaryState\"";
157
158                     String content = "<?xml version=\"1.0\"?>"
159                             + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
160                             + "<s:Body>" + "<u:SetBinaryState xmlns:u=\"urn:Belkin:service:basicevent:1\">"
161                             + "<BinaryState>" + binaryState + "</BinaryState>" + "</u:SetBinaryState>" + "</s:Body>"
162                             + "</s:Envelope>";
163
164                     URL descriptorURL = service.getDescriptorURL(this);
165                     String wemoURL = getWemoURL(descriptorURL, "basicevent");
166
167                     if (wemoURL != null) {
168                         wemoCall.executeCall(wemoURL, soapHeader, content);
169                     }
170                 } catch (Exception e) {
171                     logger.error("Failed to send command '{}' for device '{}': {}", command, getThing().getUID(),
172                             e.getMessage());
173                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
174                 }
175                 updateStatus(ThingStatus.ONLINE);
176             }
177         }
178     }
179
180     @Override
181     public void onServiceSubscribed(@Nullable String service, boolean succeeded) {
182         if (service != null) {
183             logger.debug("WeMo {}: Subscription to service {} {}", getUDN(), service,
184                     succeeded ? "succeeded" : "failed");
185             subscriptionState.put(service, succeeded);
186         }
187     }
188
189     @Override
190     public void onValueReceived(@Nullable String variable, @Nullable String value, @Nullable String service) {
191         logger.debug("Received pair '{}':'{}' (service '{}') for thing '{}'",
192                 new Object[] { variable, value, service, this.getThing().getUID() });
193
194         updateStatus(ThingStatus.ONLINE);
195
196         if (variable != null && value != null) {
197             this.stateMap.put(variable, value);
198         }
199
200         if (getThing().getThingTypeUID().getId().equals("insight")) {
201             String insightParams = stateMap.get("InsightParams");
202
203             if (insightParams != null) {
204                 String[] splitInsightParams = insightParams.split("\\|");
205
206                 if (splitInsightParams[0] != null) {
207                     OnOffType binaryState = null;
208                     binaryState = splitInsightParams[0].equals("0") ? OnOffType.OFF : OnOffType.ON;
209                     logger.trace("New InsightParam binaryState '{}' for device '{}' received", binaryState,
210                             getThing().getUID());
211                     updateState(CHANNEL_STATE, binaryState);
212                 }
213
214                 long lastChangedAt = 0;
215                 try {
216                     lastChangedAt = Long.parseLong(splitInsightParams[1]) * 1000; // convert s to ms
217                 } catch (NumberFormatException e) {
218                     logger.error("Unable to parse lastChangedAt value '{}' for device '{}'; expected long",
219                             splitInsightParams[1], getThing().getUID());
220                 }
221                 ZonedDateTime zoned = ZonedDateTime.ofInstant(Instant.ofEpochMilli(lastChangedAt),
222                         TimeZone.getDefault().toZoneId());
223
224                 State lastChangedAtState = new DateTimeType(zoned);
225                 if (lastChangedAt != 0) {
226                     logger.trace("New InsightParam lastChangedAt '{}' for device '{}' received", lastChangedAtState,
227                             getThing().getUID());
228                     updateState(CHANNEL_LASTCHANGEDAT, lastChangedAtState);
229                 }
230
231                 State lastOnFor = DecimalType.valueOf(splitInsightParams[2]);
232                 logger.trace("New InsightParam lastOnFor '{}' for device '{}' received", lastOnFor,
233                         getThing().getUID());
234                 updateState(CHANNEL_LASTONFOR, lastOnFor);
235
236                 State onToday = DecimalType.valueOf(splitInsightParams[3]);
237                 logger.trace("New InsightParam onToday '{}' for device '{}' received", onToday, getThing().getUID());
238                 updateState(CHANNEL_ONTODAY, onToday);
239
240                 State onTotal = DecimalType.valueOf(splitInsightParams[4]);
241                 logger.trace("New InsightParam onTotal '{}' for device '{}' received", onTotal, getThing().getUID());
242                 updateState(CHANNEL_ONTOTAL, onTotal);
243
244                 State timespan = DecimalType.valueOf(splitInsightParams[5]);
245                 logger.trace("New InsightParam timespan '{}' for device '{}' received", timespan, getThing().getUID());
246                 updateState(CHANNEL_TIMESPAN, timespan);
247
248                 State averagePower = DecimalType.valueOf(splitInsightParams[6]); // natively given in W
249                 logger.trace("New InsightParam averagePower '{}' for device '{}' received", averagePower,
250                         getThing().getUID());
251                 updateState(CHANNEL_AVERAGEPOWER, averagePower);
252
253                 BigDecimal currentMW = new BigDecimal(splitInsightParams[7]);
254                 State currentPower = new DecimalType(currentMW.divide(new BigDecimal(1000), RoundingMode.HALF_UP)); // recalculate
255                 // mW to W
256                 logger.trace("New InsightParam currentPower '{}' for device '{}' received", currentPower,
257                         getThing().getUID());
258                 updateState(CHANNEL_CURRENTPOWER, currentPower);
259
260                 BigDecimal energyTodayMWMin = new BigDecimal(splitInsightParams[8]);
261                 // recalculate mW-mins to Wh
262                 State energyToday = new DecimalType(
263                         energyTodayMWMin.divide(new BigDecimal(60000), RoundingMode.HALF_UP));
264                 logger.trace("New InsightParam energyToday '{}' for device '{}' received", energyToday,
265                         getThing().getUID());
266                 updateState(CHANNEL_ENERGYTODAY, energyToday);
267
268                 BigDecimal energyTotalMWMin = new BigDecimal(splitInsightParams[9]);
269                 // recalculate mW-mins to Wh
270                 State energyTotal = new DecimalType(
271                         energyTotalMWMin.divide(new BigDecimal(60000), RoundingMode.HALF_UP));
272                 logger.trace("New InsightParam energyTotal '{}' for device '{}' received", energyTotal,
273                         getThing().getUID());
274                 updateState(CHANNEL_ENERGYTOTAL, energyTotal);
275
276                 BigDecimal standByLimitMW = new BigDecimal(splitInsightParams[10]);
277                 State standByLimit = new DecimalType(standByLimitMW.divide(new BigDecimal(1000), RoundingMode.HALF_UP)); // recalculate
278                 // mW to W
279                 logger.trace("New InsightParam standByLimit '{}' for device '{}' received", standByLimit,
280                         getThing().getUID());
281                 updateState(CHANNEL_STANDBYLIMIT, standByLimit);
282
283                 if (currentMW.divide(new BigDecimal(1000), RoundingMode.HALF_UP).intValue() > standByLimitMW
284                         .divide(new BigDecimal(1000), RoundingMode.HALF_UP).intValue()) {
285                     updateState(CHANNEL_ONSTANDBY, OnOffType.OFF);
286                 } else {
287                     updateState(CHANNEL_ONSTANDBY, OnOffType.ON);
288                 }
289             }
290         } else {
291             String binaryState = stateMap.get("BinaryState");
292             if (binaryState != null) {
293                 State state = binaryState.equals("0") ? OnOffType.OFF : OnOffType.ON;
294                 logger.debug("State '{}' for device '{}' received", state, getThing().getUID());
295                 if (getThing().getThingTypeUID().getId().equals("motion")) {
296                     updateState(CHANNEL_MOTIONDETECTION, state);
297                     if (state.equals(OnOffType.ON)) {
298                         State lastMotionDetected = new DateTimeType();
299                         updateState(CHANNEL_LASTMOTIONDETECTED, lastMotionDetected);
300                     }
301                 } else {
302                     updateState(CHANNEL_STATE, state);
303                 }
304             }
305         }
306     }
307
308     private synchronized void onSubscription() {
309         if (service.isRegistered(this)) {
310             logger.debug("Checking WeMo GENA subscription for '{}'", this);
311
312             ThingTypeUID thingTypeUID = thing.getThingTypeUID();
313             String subscription = "basicevent1";
314
315             if (subscriptionState.get(subscription) == null) {
316                 logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(), subscription);
317                 service.addSubscription(this, subscription, SUBSCRIPTION_DURATION_SECONDS);
318                 subscriptionState.put(subscription, true);
319             }
320
321             if (thingTypeUID.equals(THING_TYPE_INSIGHT)) {
322                 subscription = "insight1";
323                 if (subscriptionState.get(subscription) == null) {
324                     logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(),
325                             subscription);
326                     service.addSubscription(this, subscription, SUBSCRIPTION_DURATION_SECONDS);
327                     subscriptionState.put(subscription, true);
328                 }
329             }
330         } else {
331             logger.debug("Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
332                     this);
333         }
334     }
335
336     private synchronized void removeSubscription() {
337         logger.debug("Removing WeMo GENA subscription for '{}'", this);
338
339         if (service.isRegistered(this)) {
340             ThingTypeUID thingTypeUID = thing.getThingTypeUID();
341             String subscription = "basicevent1";
342
343             if (subscriptionState.get(subscription) != null) {
344                 logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription);
345                 service.removeSubscription(this, subscription);
346             }
347
348             if (thingTypeUID.equals(THING_TYPE_INSIGHT)) {
349                 subscription = "insight1";
350                 if (subscriptionState.get(subscription) != null) {
351                     logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription);
352                     service.removeSubscription(this, subscription);
353                 }
354             }
355             subscriptionState = new HashMap<>();
356             service.unregisterParticipant(this);
357         }
358     }
359
360     private synchronized void onUpdate() {
361         ScheduledFuture<?> job = refreshJob;
362         if (job == null || job.isCancelled()) {
363             Configuration config = getThing().getConfiguration();
364             int refreshInterval = DEFAULT_REFRESH_INTERVALL_SECONDS;
365             Object refreshConfig = config.get("refresh");
366             if (refreshConfig != null) {
367                 refreshInterval = ((BigDecimal) refreshConfig).intValue();
368             }
369             refreshJob = scheduler.scheduleWithFixedDelay(refreshRunnable, 0, refreshInterval, TimeUnit.SECONDS);
370         }
371     }
372
373     private boolean isUpnpDeviceRegistered() {
374         return service.isRegistered(this);
375     }
376
377     @Override
378     public String getUDN() {
379         return (String) this.getThing().getConfiguration().get(UDN);
380     }
381
382     /**
383      * The {@link updateWemoState} polls the actual state of a WeMo device and
384      * calls {@link onValueReceived} to update the statemap and channels..
385      *
386      */
387     protected void updateWemoState() {
388         String action = "GetBinaryState";
389         String variable = "BinaryState";
390         String actionService = "basicevent";
391         String value = null;
392
393         if (getThing().getThingTypeUID().getId().equals("insight")) {
394             action = "GetInsightParams";
395             variable = "InsightParams";
396             actionService = "insight";
397         }
398
399         String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
400         String content = "<?xml version=\"1.0\"?>"
401                 + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
402                 + "<s:Body>" + "<u:" + action + " xmlns:u=\"urn:Belkin:service:" + actionService + ":1\">" + "</u:"
403                 + action + ">" + "</s:Body>" + "</s:Envelope>";
404
405         try {
406             URL descriptorURL = service.getDescriptorURL(this);
407             String wemoURL = getWemoURL(descriptorURL, actionService);
408
409             if (wemoURL != null) {
410                 String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
411                 if (wemoCallResponse != null) {
412                     logger.trace("State response '{}' for device '{}' received", wemoCallResponse, getThing().getUID());
413                     if (variable.equals("InsightParams")) {
414                         value = substringBetween(wemoCallResponse, "<InsightParams>", "</InsightParams>");
415                     } else {
416                         value = substringBetween(wemoCallResponse, "<BinaryState>", "</BinaryState>");
417                     }
418                     logger.trace("New state '{}' for device '{}' received", value, getThing().getUID());
419                     this.onValueReceived(variable, value, actionService + "1");
420                 }
421             }
422         } catch (Exception e) {
423             logger.error("Failed to get actual state for device '{}': {}", getThing().getUID(), e.getMessage());
424         }
425     }
426
427     @Override
428     public void onStatusChanged(boolean status) {
429     }
430 }