2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.wemo.internal.handler;
15 import static org.openhab.binding.wemo.internal.WemoBindingConstants.*;
17 import java.io.IOException;
18 import java.math.BigDecimal;
19 import java.math.RoundingMode;
21 import java.time.Instant;
22 import java.time.ZonedDateTime;
23 import java.util.Collections;
24 import java.util.HashMap;
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;
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;
54 * The {@link WemoHandler} is responsible for handling commands, which are
55 * sent to one of the channels and to update their states.
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
64 public class WemoHandler extends AbstractWemoHandler implements UpnpIOParticipant {
66 private final Logger logger = LoggerFactory.getLogger(WemoHandler.class);
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());
72 private Map<String, Boolean> subscriptionState = new HashMap<>();
74 private final Map<String, String> stateMap = Collections.synchronizedMap(new HashMap<>());
76 // protected static final int SUBSCRIPTION_DURATION = WemoBindingConstants.SUBSCRIPTION_DURATION;
78 private UpnpIOService service;
81 * The default refresh interval in Seconds.
83 private final int DEFAULT_REFRESH_INTERVAL = 120;
85 private ScheduledFuture<?> refreshJob;
87 private final Runnable refreshRunnable = new Runnable() {
92 if (!isUpnpDeviceRegistered()) {
93 logger.debug("WeMo UPnP device {} not yet registered", getUDN());
98 } catch (Exception e) {
99 logger.debug("Exception during poll", e);
100 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
105 public WemoHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemohttpCaller) {
108 this.wemoHttpCaller = wemohttpCaller;
110 logger.debug("Creating a WemoHandler for thing '{}'", getThing().getUID());
112 if (upnpIOService != null) {
113 this.service = upnpIOService;
115 logger.debug("upnpIOService not set.");
120 public void initialize() {
121 Configuration configuration = getConfig();
123 if (configuration.get("udn") != null) {
124 logger.debug("Initializing WemoHandler for UDN '{}'", configuration.get("udn"));
125 service.registerParticipant(this);
128 updateStatus(ThingStatus.ONLINE);
130 logger.debug("Cannot initalize WemoHandler. UDN not set.");
135 public void dispose() {
136 logger.debug("WeMoHandler disposed.");
138 removeSubscription();
140 if (refreshJob != null && !refreshJob.isCancelled()) {
141 refreshJob.cancel(true);
147 public void handleCommand(ChannelUID channelUID, Command command) {
148 logger.trace("Command '{}' received for channel '{}'", command, channelUID);
150 if (command instanceof RefreshType) {
153 } catch (Exception e) {
154 logger.debug("Exception during poll", e);
156 } else if (channelUID.getId().equals(CHANNEL_STATE)) {
157 if (command instanceof OnOffType) {
159 String binaryState = null;
161 if (command.equals(OnOffType.ON)) {
163 } else if (command.equals(OnOffType.OFF)) {
167 String soapHeader = "\"urn:Belkin:service:basicevent:1#SetBinaryState\"";
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>"
175 String wemoURL = getWemoURL("basicevent");
177 if (wemoURL != null) {
178 wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
180 } catch (Exception e) {
181 logger.error("Failed to send command '{}' for device '{}': {}", command, getThing().getUID(),
183 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
185 updateStatus(ThingStatus.ONLINE);
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);
196 @SuppressWarnings("null")
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() });
202 updateStatus(ThingStatus.ONLINE);
204 this.stateMap.put(variable, value);
206 if (getThing().getThingTypeUID().getId().equals("insight")) {
207 String insightParams = stateMap.get("InsightParams");
209 if (insightParams != null) {
210 String[] splitInsightParams = insightParams.split("\\|");
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);
222 long lastChangedAt = 0;
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());
229 ZonedDateTime zoned = ZonedDateTime.ofInstant(Instant.ofEpochMilli(lastChangedAt),
230 TimeZone.getDefault().toZoneId());
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);
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);
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);
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);
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);
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);
274 BigDecimal currentMW = new BigDecimal(splitInsightParams[7]);
275 State currentPower = new DecimalType(currentMW.divide(new BigDecimal(1000), RoundingMode.HALF_UP)); // recalculate
277 if (currentPower != null) {
278 logger.trace("New InsightParam currentPower '{}' for device '{}' received", currentPower,
279 getThing().getUID());
280 updateState(CHANNEL_CURRENTPOWER, currentPower);
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);
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);
303 BigDecimal standByLimitMW = new BigDecimal(splitInsightParams[10]);
304 State standByLimit = new DecimalType(standByLimitMW.divide(new BigDecimal(1000), RoundingMode.HALF_UP)); // recalculate
306 if (standByLimit != null) {
307 logger.trace("New InsightParam standByLimit '{}' for device '{}' received", standByLimit,
308 getThing().getUID());
309 updateState(CHANNEL_STANDBYLIMIT, standByLimit);
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);
315 updateState(CHANNEL_ONSTANDBY, OnOffType.ON);
319 State state = stateMap.get("BinaryState").equals("0") ? OnOffType.OFF : OnOffType.ON;
321 logger.debug("State '{}' for device '{}' received", state, getThing().getUID());
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);
331 updateState(CHANNEL_STATE, state);
337 private synchronized void onSubscription() {
338 if (service.isRegistered(this)) {
339 logger.debug("Checking WeMo GENA subscription for '{}'", this);
341 ThingTypeUID thingTypeUID = thing.getThingTypeUID();
342 String subscription = "basicevent1";
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);
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(),
356 service.addSubscription(this, subscription, SUBSCRIPTION_DURATION);
357 subscriptionState.put(subscription, true);
361 logger.debug("Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
366 private synchronized void removeSubscription() {
367 logger.debug("Removing WeMo GENA subscription for '{}'", this);
369 if (service.isRegistered(this)) {
370 ThingTypeUID thingTypeUID = thing.getThingTypeUID();
371 String subscription = "basicevent1";
373 if ((subscriptionState.get(subscription) != null) && subscriptionState.get(subscription).booleanValue()) {
374 logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription);
375 service.removeSubscription(this, subscription);
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);
386 subscriptionState = new HashMap<>();
387 service.unregisterParticipant(this);
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();
399 refreshJob = scheduler.scheduleWithFixedDelay(refreshRunnable, 0, refreshInterval, TimeUnit.SECONDS);
403 private boolean isUpnpDeviceRegistered() {
404 return service.isRegistered(this);
408 public String getUDN() {
409 return (String) this.getThing().getConfiguration().get(UDN);
413 * The {@link updateWemoState} polls the actual state of a WeMo device and
414 * calls {@link onValueReceived} to update the statemap and channels..
417 protected void updateWemoState() {
418 String action = "GetBinaryState";
419 String variable = "BinaryState";
420 String actionService = "basicevent";
423 if (getThing().getThingTypeUID().getId().equals("insight")) {
424 action = "GetInsightParams";
425 variable = "InsightParams";
426 actionService = "insight";
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>";
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>");
444 value = StringUtils.substringBetween(wemoCallResponse, "<BinaryState>", "</BinaryState>");
447 logger.trace("New state '{}' for device '{}' received", value, getThing().getUID());
448 this.onValueReceived(variable, value, actionService + "1");
452 } catch (Exception e) {
453 logger.error("Failed to get actual state for device '{}': {}", getThing().getUID(), e.getMessage());
457 public String getWemoURL(String actionService) {
458 URL descriptorURL = service.getDescriptorURL(this);
459 int portCheckStart = 49151;
460 int portCheckStop = 49157;
461 String wemoURL = null;
464 if (descriptorURL != null) {
465 host = StringUtils.substringBetween(descriptorURL.toString(), "://", ":");
466 for (int i = portCheckStart; i < portCheckStop; i++) {
468 boolean portFound = servicePing(host, i);
470 logger.trace("WeMo device {} responded at Port {}", getUDN(), i);
471 port = String.valueOf(i);
474 } catch (Exception e) {
477 wemoURL = "http://" + host + ":" + port + "/upnp/control/" + actionService + "1";
478 logger.trace("WeMo url {}", wemoURL);
484 public boolean servicePing(String host, int port) {
485 logger.trace("Ping WeMo device at '{}:{}'", host, port);
487 HttpUtil.executeUrl("GET", "http://" + host + ":" + port, 250);
488 } catch (IOException e) {
495 public void onStatusChanged(boolean status) {