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