2 * Copyright (c) 2010-2022 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.*;
16 import static org.openhab.binding.wemo.internal.WemoUtil.*;
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.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;
56 * The {@link WemoHandler} is responsible for handling commands, which are
57 * sent to one of the channels and to update their states.
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
66 public class WemoHandler extends AbstractWemoHandler implements UpnpIOParticipant {
68 private final Logger logger = LoggerFactory.getLogger(WemoHandler.class);
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());
74 private final Object upnpLock = new Object();
75 private final Object jobLock = new Object();
77 private final Map<String, String> stateMap = Collections.synchronizedMap(new HashMap<>());
79 private @Nullable UpnpIOService service;
81 private WemoHttpCall wemoCall;
83 private Map<String, Boolean> subscriptionState = new HashMap<>();
85 private @Nullable ScheduledFuture<?> pollingJob;
87 private String host = "";
89 public WemoHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemoHttpCaller) {
90 super(thing, wemoHttpCaller);
92 this.service = upnpIOService;
93 this.wemoCall = wemoHttpCaller;
95 logger.debug("Creating a WemoHandler for thing '{}'", getThing().getUID());
99 public void initialize() {
100 Configuration configuration = getConfig();
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);
109 pollingJob = scheduler.scheduleWithFixedDelay(this::poll, 0, DEFAULT_REFRESH_INTERVALL_SECONDS,
111 updateStatus(ThingStatus.ONLINE);
113 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
114 "@text/config-status.error.missing-udn");
115 logger.debug("Cannot initalize WemoHandler. UDN not set.");
120 public void dispose() {
121 logger.debug("WemoHandler disposed for thing {}", getThing().getUID());
123 ScheduledFuture<?> job = this.pollingJob;
127 this.pollingJob = null;
128 removeSubscription();
131 private void poll() {
132 synchronized (jobLock) {
133 if (pollingJob == null) {
137 logger.debug("Polling job");
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<>();
150 updateStatus(ThingStatus.ONLINE);
153 } catch (Exception e) {
154 logger.debug("Exception during poll: {}", e.getMessage(), e);
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");
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");
177 if (command instanceof RefreshType) {
180 } catch (Exception e) {
181 logger.debug("Exception during poll", e);
183 } else if (CHANNEL_STATE.equals(channelUID.getId())) {
184 if (command instanceof OnOffType) {
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());
197 } catch (Exception e) {
198 logger.error("Failed to send command '{}' for device '{}': {}", command, getThing().getUID(),
200 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
202 updateStatus(ThingStatus.ONLINE);
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);
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() });
221 updateStatus(ThingStatus.ONLINE);
223 if (!"BinaryState".equals(variable) && !"InsightParams".equals(variable)) {
227 String oldValue = this.stateMap.get(variable);
228 if (variable != null && value != null) {
229 this.stateMap.put(variable, value);
232 if (value != null && value.length() > 1) {
233 String insightParams = stateMap.get(variable);
235 if (insightParams != null) {
236 String[] splitInsightParams = insightParams.split("\\|");
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);
245 long lastChangedAt = 0;
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());
252 ZonedDateTime zoned = ZonedDateTime.ofInstant(Instant.ofEpochMilli(lastChangedAt),
253 TimeZone.getDefault().toZoneId());
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);
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);
267 State onToday = DecimalType.valueOf(splitInsightParams[3]);
268 logger.trace("New InsightParam onToday '{}' for device '{}' received", onToday, getThing().getUID());
269 updateState(CHANNEL_ONTODAY, onToday);
271 State onTotal = DecimalType.valueOf(splitInsightParams[4]);
272 logger.trace("New InsightParam onTotal '{}' for device '{}' received", onTotal, getThing().getUID());
273 updateState(CHANNEL_ONTOTAL, onTotal);
275 State timespan = DecimalType.valueOf(splitInsightParams[5]);
276 logger.trace("New InsightParam timespan '{}' for device '{}' received", timespan, getThing().getUID());
277 updateState(CHANNEL_TIMESPAN, timespan);
279 State averagePower = new QuantityType<>(DecimalType.valueOf(splitInsightParams[6]), Units.WATT); // natively
282 logger.trace("New InsightParam averagePower '{}' for device '{}' received", averagePower,
283 getThing().getUID());
284 updateState(CHANNEL_AVERAGEPOWER, averagePower);
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
290 logger.trace("New InsightParam currentPower '{}' for device '{}' received", currentPower,
291 getThing().getUID());
292 updateState(CHANNEL_CURRENTPOWER, currentPower);
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);
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);
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
315 logger.trace("New InsightParam standByLimit '{}' for device '{}' received", standByLimit,
316 getThing().getUID());
317 updateState(CHANNEL_STANDBYLIMIT, standByLimit);
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);
323 updateState(CHANNEL_ONSTANDBY, OnOffType.ON);
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);
340 updateState(CHANNEL_STATE, state);
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());
354 ThingTypeUID thingTypeUID = thing.getThingTypeUID();
355 String subscription = BASICEVENT;
357 if (subscriptionState.get(subscription) == null) {
358 logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(),
360 localService.addSubscription(this, subscription, SUBSCRIPTION_DURATION_SECONDS);
361 subscriptionState.put(subscription, true);
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(),
369 localService.addSubscription(this, subscription, SUBSCRIPTION_DURATION_SECONDS);
370 subscriptionState.put(subscription, true);
375 "Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
376 getThing().getUID());
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;
391 if (subscriptionState.get(subscription) != null) {
392 logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription);
393 localService.removeSubscription(this, subscription);
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);
403 subscriptionState = new HashMap<>();
404 localService.unregisterParticipant(this);
410 private boolean isUpnpDeviceRegistered() {
411 UpnpIOService localService = service;
412 if (localService != null) {
413 return localService.isRegistered(this);
419 public String getUDN() {
420 return (String) this.getThing().getConfiguration().get(UDN);
423 public String getHost() {
424 String localHost = host;
425 if (!localHost.isEmpty()) {
428 UpnpIOService localService = service;
429 if (localService != null) {
430 URL descriptorURL = localService.getDescriptorURL(this);
431 if (descriptorURL != null) {
432 return descriptorURL.getHost();
439 * The {@link updateWemoState} polls the actual state of a WeMo device and
440 * calls {@link onValueReceived} to update the statemap and channels..
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");
452 String action = "GetBinaryState";
453 String variable = "BinaryState";
455 if ("insight".equals(getThing().getThingTypeUID().getId())) {
456 action = "GetInsightParams";
457 variable = "InsightParams";
458 actionService = INSIGHTACTION;
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");
467 String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
468 String content = createStateRequestContent(action, actionService);
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());
478 if ("InsightParams".equals(variable)) {
479 value = substringBetween(wemoCallResponse, "<InsightParams>", "</InsightParams>");
481 value = substringBetween(wemoCallResponse, "<BinaryState>", "</BinaryState>");
483 if (value.length() != 0) {
484 logger.trace("New state '{}' for device '{}' received", value, getThing().getUID());
485 this.onValueReceived(variable, value, actionService + "1");
488 } catch (Exception e) {
489 logger.error("Failed to get actual state for device '{}': {}", getThing().getUID(), e.getMessage());
494 public void onStatusChanged(boolean status) {