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