]> git.basschouten.com Git - openhab-addons.git/blob
23c2a99a42964ad01ed154002df31fb1d6606491
[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.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 final Object upnpLock = new Object();
75     private final Object jobLock = new Object();
76
77     private final Map<String, String> stateMap = Collections.synchronizedMap(new HashMap<>());
78
79     private @Nullable UpnpIOService service;
80
81     private WemoHttpCall wemoCall;
82
83     private Map<String, Boolean> subscriptionState = new HashMap<>();
84
85     private @Nullable ScheduledFuture<?> pollingJob;
86
87     private String host = "";
88
89     public WemoHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemoHttpCaller) {
90         super(thing, wemoHttpCaller);
91
92         this.service = upnpIOService;
93         this.wemoCall = wemoHttpCaller;
94
95         logger.debug("Creating a WemoHandler for thing '{}'", getThing().getUID());
96     }
97
98     @Override
99     public void initialize() {
100         Configuration configuration = getConfig();
101
102         if (configuration.get(UDN) != null) {
103             logger.debug("Initializing WemoHandler for UDN '{}'", configuration.get(UDN));
104             UpnpIOService localService = service;
105             if (localService != null) {
106                 localService.registerParticipant(this);
107             }
108             host = getHost();
109             pollingJob = scheduler.scheduleWithFixedDelay(this::poll, 0, DEFAULT_REFRESH_INTERVALL_SECONDS,
110                     TimeUnit.SECONDS);
111             updateStatus(ThingStatus.ONLINE);
112         } else {
113             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
114                     "@text/config-status.error.missing-udn");
115             logger.debug("Cannot initalize WemoHandler. UDN not set.");
116         }
117     }
118
119     @Override
120     public void dispose() {
121         logger.debug("WemoHandler disposed for thing {}", getThing().getUID());
122
123         ScheduledFuture<?> job = this.pollingJob;
124         if (job != null) {
125             job.cancel(true);
126         }
127         this.pollingJob = null;
128         removeSubscription();
129     }
130
131     private void poll() {
132         synchronized (jobLock) {
133             if (pollingJob == null) {
134                 return;
135             }
136             try {
137                 logger.debug("Polling job");
138                 host = getHost();
139                 // Check if the Wemo device is set in the UPnP service registry
140                 // If not, set the thing state to ONLINE/CONFIG-PENDING and wait for the next poll
141                 if (!isUpnpDeviceRegistered()) {
142                     logger.debug("UPnP device {} not yet registered", getUDN());
143                     updateStatus(ThingStatus.ONLINE, ThingStatusDetail.CONFIGURATION_PENDING,
144                             "@text/config-status.pending.device-not-registered [\"" + getUDN() + "\"]");
145                     synchronized (upnpLock) {
146                         subscriptionState = new HashMap<>();
147                     }
148                     return;
149                 }
150                 updateStatus(ThingStatus.ONLINE);
151                 updateWemoState();
152                 addSubscription();
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 localHost = getHost();
162         if (localHost.isEmpty()) {
163             logger.error("Failed to send command '{}' for device '{}': IP address missing", command,
164                     getThing().getUID());
165             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
166                     "@text/config-status.error.missing-ip");
167             return;
168         }
169         String wemoURL = getWemoURL(localHost, BASICACTION);
170         if (wemoURL == null) {
171             logger.error("Failed to send command '{}' for device '{}': URL cannot be created", command,
172                     getThing().getUID());
173             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
174                     "@text/config-status.error.missing-url");
175             return;
176         }
177         if (command instanceof RefreshType) {
178             try {
179                 updateWemoState();
180             } catch (Exception e) {
181                 logger.debug("Exception during poll", e);
182             }
183         } else if (CHANNEL_STATE.equals(channelUID.getId())) {
184             if (command instanceof OnOffType) {
185                 try {
186                     boolean binaryState = OnOffType.ON.equals(command) ? true : false;
187                     String soapHeader = "\"urn:Belkin:service:basicevent:1#SetBinaryState\"";
188                     String content = createBinaryStateContent(binaryState);
189                     String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
190                     if (wemoCallResponse != null && logger.isTraceEnabled()) {
191                         logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
192                         logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader, getThing().getUID());
193                         logger.trace("wemoCall with content '{}' for device '{}'", content, getThing().getUID());
194                         logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse,
195                                 getThing().getUID());
196                     }
197                 } catch (Exception e) {
198                     logger.error("Failed to send command '{}' for device '{}': {}", command, getThing().getUID(),
199                             e.getMessage());
200                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
201                 }
202                 updateStatus(ThingStatus.ONLINE);
203             }
204         }
205     }
206
207     @Override
208     public void onServiceSubscribed(@Nullable String service, boolean succeeded) {
209         if (service != null) {
210             logger.debug("WeMo {}: Subscription to service {} {}", getUDN(), service,
211                     succeeded ? "succeeded" : "failed");
212             subscriptionState.put(service, succeeded);
213         }
214     }
215
216     @Override
217     public void onValueReceived(@Nullable String variable, @Nullable String value, @Nullable String service) {
218         logger.debug("Received pair '{}':'{}' (service '{}') for thing '{}'",
219                 new Object[] { variable, value, service, this.getThing().getUID() });
220
221         updateStatus(ThingStatus.ONLINE);
222
223         if (!"BinaryState".equals(variable) && !"InsightParams".equals(variable)) {
224             return;
225         }
226
227         String oldValue = this.stateMap.get(variable);
228         if (variable != null && value != null) {
229             this.stateMap.put(variable, value);
230         }
231
232         if (value != null && value.length() > 1) {
233             String insightParams = stateMap.get(variable);
234
235             if (insightParams != null) {
236                 String[] splitInsightParams = insightParams.split("\\|");
237
238                 if (splitInsightParams[0] != null) {
239                     OnOffType binaryState = "0".equals(splitInsightParams[0]) ? OnOffType.OFF : OnOffType.ON;
240                     logger.trace("New InsightParam binaryState '{}' for device '{}' received", binaryState,
241                             getThing().getUID());
242                     updateState(CHANNEL_STATE, binaryState);
243                 }
244
245                 long lastChangedAt = 0;
246                 try {
247                     lastChangedAt = Long.parseLong(splitInsightParams[1]) * 1000; // convert s to ms
248                 } catch (NumberFormatException e) {
249                     logger.error("Unable to parse lastChangedAt value '{}' for device '{}'; expected long",
250                             splitInsightParams[1], getThing().getUID());
251                 }
252                 ZonedDateTime zoned = ZonedDateTime.ofInstant(Instant.ofEpochMilli(lastChangedAt),
253                         TimeZone.getDefault().toZoneId());
254
255                 State lastChangedAtState = new DateTimeType(zoned);
256                 if (lastChangedAt != 0) {
257                     logger.trace("New InsightParam lastChangedAt '{}' for device '{}' received", lastChangedAtState,
258                             getThing().getUID());
259                     updateState(CHANNEL_LASTCHANGEDAT, lastChangedAtState);
260                 }
261
262                 State lastOnFor = DecimalType.valueOf(splitInsightParams[2]);
263                 logger.trace("New InsightParam lastOnFor '{}' for device '{}' received", lastOnFor,
264                         getThing().getUID());
265                 updateState(CHANNEL_LASTONFOR, lastOnFor);
266
267                 State onToday = DecimalType.valueOf(splitInsightParams[3]);
268                 logger.trace("New InsightParam onToday '{}' for device '{}' received", onToday, getThing().getUID());
269                 updateState(CHANNEL_ONTODAY, onToday);
270
271                 State onTotal = DecimalType.valueOf(splitInsightParams[4]);
272                 logger.trace("New InsightParam onTotal '{}' for device '{}' received", onTotal, getThing().getUID());
273                 updateState(CHANNEL_ONTOTAL, onTotal);
274
275                 State timespan = DecimalType.valueOf(splitInsightParams[5]);
276                 logger.trace("New InsightParam timespan '{}' for device '{}' received", timespan, getThing().getUID());
277                 updateState(CHANNEL_TIMESPAN, timespan);
278
279                 State averagePower = new QuantityType<>(DecimalType.valueOf(splitInsightParams[6]), Units.WATT); // natively
280                                                                                                                  // given
281                                                                                                                  // in W
282                 logger.trace("New InsightParam averagePower '{}' for device '{}' received", averagePower,
283                         getThing().getUID());
284                 updateState(CHANNEL_AVERAGEPOWER, averagePower);
285
286                 BigDecimal currentMW = new BigDecimal(splitInsightParams[7]);
287                 State currentPower = new QuantityType<>(currentMW.divide(new BigDecimal(1000), 0, RoundingMode.HALF_UP),
288                         Units.WATT); // recalculate
289                 // mW to W
290                 logger.trace("New InsightParam currentPower '{}' for device '{}' received", currentPower,
291                         getThing().getUID());
292                 updateState(CHANNEL_CURRENTPOWER, currentPower);
293
294                 BigDecimal energyTodayMWMin = new BigDecimal(splitInsightParams[8]);
295                 // recalculate mW-mins to Wh
296                 State energyToday = new QuantityType<>(
297                         energyTodayMWMin.divide(new BigDecimal(60000), 0, RoundingMode.HALF_UP), Units.WATT_HOUR);
298                 logger.trace("New InsightParam energyToday '{}' for device '{}' received", energyToday,
299                         getThing().getUID());
300                 updateState(CHANNEL_ENERGYTODAY, energyToday);
301
302                 BigDecimal energyTotalMWMin = new BigDecimal(splitInsightParams[9]);
303                 // recalculate mW-mins to Wh
304                 State energyTotal = new QuantityType<>(
305                         energyTotalMWMin.divide(new BigDecimal(60000), 0, RoundingMode.HALF_UP), Units.WATT_HOUR);
306                 logger.trace("New InsightParam energyTotal '{}' for device '{}' received", energyTotal,
307                         getThing().getUID());
308                 updateState(CHANNEL_ENERGYTOTAL, energyTotal);
309
310                 if (splitInsightParams.length > 10 && splitInsightParams[10] != null) {
311                     BigDecimal standByLimitMW = new BigDecimal(splitInsightParams[10]);
312                     State standByLimit = new QuantityType<>(
313                             standByLimitMW.divide(new BigDecimal(1000), 0, RoundingMode.HALF_UP), Units.WATT); // recalculate
314                     // mW to W
315                     logger.trace("New InsightParam standByLimit '{}' for device '{}' received", standByLimit,
316                             getThing().getUID());
317                     updateState(CHANNEL_STANDBYLIMIT, standByLimit);
318
319                     if (currentMW.divide(new BigDecimal(1000), 0, RoundingMode.HALF_UP).intValue() > standByLimitMW
320                             .divide(new BigDecimal(1000), 0, RoundingMode.HALF_UP).intValue()) {
321                         updateState(CHANNEL_ONSTANDBY, OnOffType.OFF);
322                     } else {
323                         updateState(CHANNEL_ONSTANDBY, OnOffType.ON);
324                     }
325                 }
326             }
327         } else if (value != null && value.length() == 1) {
328             String binaryState = stateMap.get("BinaryState");
329             if (binaryState != null) {
330                 if (oldValue == null || !oldValue.equals(binaryState)) {
331                     State state = "0".equals(binaryState) ? OnOffType.OFF : OnOffType.ON;
332                     logger.debug("State '{}' for device '{}' received", state, getThing().getUID());
333                     if ("motion".equals(getThing().getThingTypeUID().getId())) {
334                         updateState(CHANNEL_MOTIONDETECTION, state);
335                         if (OnOffType.ON.equals(state)) {
336                             State lastMotionDetected = new DateTimeType();
337                             updateState(CHANNEL_LASTMOTIONDETECTED, lastMotionDetected);
338                         }
339                     } else {
340                         updateState(CHANNEL_STATE, state);
341                     }
342                 }
343             }
344         }
345     }
346
347     private synchronized void addSubscription() {
348         synchronized (upnpLock) {
349             UpnpIOService localService = service;
350             if (localService != null) {
351                 if (localService.isRegistered(this)) {
352                     logger.debug("Checking WeMo GENA subscription for '{}'", getThing().getUID());
353
354                     ThingTypeUID thingTypeUID = thing.getThingTypeUID();
355                     String subscription = BASICEVENT;
356
357                     if (subscriptionState.get(subscription) == null) {
358                         logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(),
359                                 subscription);
360                         localService.addSubscription(this, subscription, SUBSCRIPTION_DURATION_SECONDS);
361                         subscriptionState.put(subscription, true);
362                     }
363
364                     if (THING_TYPE_INSIGHT.equals(thingTypeUID)) {
365                         subscription = INSIGHTEVENT;
366                         if (subscriptionState.get(subscription) == null) {
367                             logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(),
368                                     subscription);
369                             localService.addSubscription(this, subscription, SUBSCRIPTION_DURATION_SECONDS);
370                             subscriptionState.put(subscription, true);
371                         }
372                     }
373                 } else {
374                     logger.debug(
375                             "Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
376                             getThing().getUID());
377                 }
378             }
379         }
380     }
381
382     private synchronized void removeSubscription() {
383         synchronized (upnpLock) {
384             UpnpIOService localService = service;
385             if (localService != null) {
386                 if (localService.isRegistered(this)) {
387                     logger.debug("Removing WeMo GENA subscription for '{}'", getThing().getUID());
388                     ThingTypeUID thingTypeUID = thing.getThingTypeUID();
389                     String subscription = BASICEVENT;
390
391                     if (subscriptionState.get(subscription) != null) {
392                         logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription);
393                         localService.removeSubscription(this, subscription);
394                     }
395
396                     if (THING_TYPE_INSIGHT.equals(thingTypeUID)) {
397                         subscription = INSIGHTEVENT;
398                         if (subscriptionState.get(subscription) != null) {
399                             logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription);
400                             localService.removeSubscription(this, subscription);
401                         }
402                     }
403                     subscriptionState = new HashMap<>();
404                     localService.unregisterParticipant(this);
405                 }
406             }
407         }
408     }
409
410     private boolean isUpnpDeviceRegistered() {
411         UpnpIOService localService = service;
412         if (localService != null) {
413             return localService.isRegistered(this);
414         }
415         return false;
416     }
417
418     @Override
419     public String getUDN() {
420         return (String) this.getThing().getConfiguration().get(UDN);
421     }
422
423     public String getHost() {
424         String localHost = host;
425         if (!localHost.isEmpty()) {
426             return localHost;
427         }
428         UpnpIOService localService = service;
429         if (localService != null) {
430             URL descriptorURL = localService.getDescriptorURL(this);
431             if (descriptorURL != null) {
432                 return descriptorURL.getHost();
433             }
434         }
435         return "";
436     }
437
438     /**
439      * The {@link updateWemoState} polls the actual state of a WeMo device and
440      * calls {@link onValueReceived} to update the statemap and channels..
441      *
442      */
443     protected void updateWemoState() {
444         String actionService = BASICACTION;
445         String localhost = getHost();
446         if (localhost.isEmpty()) {
447             logger.error("Failed to get actual state for device '{}': IP address missing", getThing().getUID());
448             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
449                     "@text/config-status.error.missing-ip");
450             return;
451         }
452         String action = "GetBinaryState";
453         String variable = "BinaryState";
454         String value = null;
455         if ("insight".equals(getThing().getThingTypeUID().getId())) {
456             action = "GetInsightParams";
457             variable = "InsightParams";
458             actionService = INSIGHTACTION;
459         }
460         String wemoURL = getWemoURL(localhost, actionService);
461         if (wemoURL == null) {
462             logger.error("Failed to get actual state for device '{}': URL cannot be created", getThing().getUID());
463             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
464                     "@text/config-status.error.missing-url");
465             return;
466         }
467         String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
468         String content = createStateRequestContent(action, actionService);
469         try {
470             String wemoCallResponse = wemoCall.executeCall(wemoURL, soapHeader, content);
471             if (wemoCallResponse != null) {
472                 if (logger.isTraceEnabled()) {
473                     logger.trace("wemoCall to URL '{}' for device '{}'", wemoURL, getThing().getUID());
474                     logger.trace("wemoCall with soapHeader '{}' for device '{}'", soapHeader, getThing().getUID());
475                     logger.trace("wemoCall with content '{}' for device '{}'", content, getThing().getUID());
476                     logger.trace("wemoCall with response '{}' for device '{}'", wemoCallResponse, getThing().getUID());
477                 }
478                 if ("InsightParams".equals(variable)) {
479                     value = substringBetween(wemoCallResponse, "<InsightParams>", "</InsightParams>");
480                 } else {
481                     value = substringBetween(wemoCallResponse, "<BinaryState>", "</BinaryState>");
482                 }
483                 if (value.length() != 0) {
484                     logger.trace("New state '{}' for device '{}' received", value, getThing().getUID());
485                     this.onValueReceived(variable, value, actionService + "1");
486                 }
487             }
488         } catch (Exception e) {
489             logger.error("Failed to get actual state for device '{}': {}", getThing().getUID(), e.getMessage());
490         }
491     }
492
493     @Override
494     public void onStatusChanged(boolean status) {
495     }
496 }