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.*;
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 Map<String, Boolean> subscriptionState = new HashMap<>();
76 private final Map<String, String> stateMap = Collections.synchronizedMap(new HashMap<>());
78 protected UpnpIOService service;
79 private WemoHttpCall wemoCall;
81 private @Nullable ScheduledFuture<?> refreshJob;
83 private final Runnable refreshRunnable = new Runnable() {
88 if (!isUpnpDeviceRegistered()) {
89 logger.debug("WeMo UPnP device {} not yet registered", getUDN());
94 } catch (Exception e) {
95 logger.debug("Exception during poll", e);
96 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
101 public WemoHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemoHttpCaller) {
102 super(thing, wemoHttpCaller);
104 this.service = upnpIOService;
105 this.wemoCall = wemoHttpCaller;
107 logger.debug("Creating a WemoHandler for thing '{}'", getThing().getUID());
111 public void initialize() {
112 Configuration configuration = getConfig();
114 if (configuration.get("udn") != null) {
115 logger.debug("Initializing WemoHandler for UDN '{}'", configuration.get("udn"));
116 service.registerParticipant(this);
119 updateStatus(ThingStatus.ONLINE);
121 logger.debug("Cannot initalize WemoHandler. UDN not set.");
126 public void dispose() {
127 logger.debug("WeMoHandler disposed.");
129 ScheduledFuture<?> job = refreshJob;
130 if (job != null && !job.isCancelled()) {
134 removeSubscription();
138 public void handleCommand(ChannelUID channelUID, Command command) {
139 logger.trace("Command '{}' received for channel '{}'", command, channelUID);
141 if (command instanceof RefreshType) {
144 } catch (Exception e) {
145 logger.debug("Exception during poll", e);
147 } else if (channelUID.getId().equals(CHANNEL_STATE)) {
148 if (command instanceof OnOffType) {
150 String binaryState = null;
152 if (command.equals(OnOffType.ON)) {
154 } else if (command.equals(OnOffType.OFF)) {
158 String soapHeader = "\"urn:Belkin:service:basicevent:1#SetBinaryState\"";
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>"
166 URL descriptorURL = service.getDescriptorURL(this);
167 String wemoURL = getWemoURL(descriptorURL, "basicevent");
169 if (wemoURL != null) {
170 wemoCall.executeCall(wemoURL, soapHeader, content);
172 } catch (Exception e) {
173 logger.error("Failed to send command '{}' for device '{}': {}", command, getThing().getUID(),
175 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
177 updateStatus(ThingStatus.ONLINE);
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);
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() });
196 updateStatus(ThingStatus.ONLINE);
198 if (variable != null && value != null) {
199 this.stateMap.put(variable, value);
202 if (getThing().getThingTypeUID().getId().equals("insight")) {
203 String insightParams = stateMap.get("InsightParams");
205 if (insightParams != null) {
206 String[] splitInsightParams = insightParams.split("\\|");
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);
216 long lastChangedAt = 0;
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());
223 ZonedDateTime zoned = ZonedDateTime.ofInstant(Instant.ofEpochMilli(lastChangedAt),
224 TimeZone.getDefault().toZoneId());
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);
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);
238 State onToday = DecimalType.valueOf(splitInsightParams[3]);
239 logger.trace("New InsightParam onToday '{}' for device '{}' received", onToday, getThing().getUID());
240 updateState(CHANNEL_ONTODAY, onToday);
242 State onTotal = DecimalType.valueOf(splitInsightParams[4]);
243 logger.trace("New InsightParam onTotal '{}' for device '{}' received", onTotal, getThing().getUID());
244 updateState(CHANNEL_ONTOTAL, onTotal);
246 State timespan = DecimalType.valueOf(splitInsightParams[5]);
247 logger.trace("New InsightParam timespan '{}' for device '{}' received", timespan, getThing().getUID());
248 updateState(CHANNEL_TIMESPAN, timespan);
250 State averagePower = new QuantityType<>(DecimalType.valueOf(splitInsightParams[6]), Units.WATT); // natively
253 logger.trace("New InsightParam averagePower '{}' for device '{}' received", averagePower,
254 getThing().getUID());
255 updateState(CHANNEL_AVERAGEPOWER, averagePower);
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
261 logger.trace("New InsightParam currentPower '{}' for device '{}' received", currentPower,
262 getThing().getUID());
263 updateState(CHANNEL_CURRENTPOWER, currentPower);
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);
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);
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
285 logger.trace("New InsightParam standByLimit '{}' for device '{}' received", standByLimit,
286 getThing().getUID());
287 updateState(CHANNEL_STANDBYLIMIT, standByLimit);
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);
293 updateState(CHANNEL_ONSTANDBY, OnOffType.ON);
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);
308 updateState(CHANNEL_STATE, state);
314 private synchronized void onSubscription() {
315 if (service.isRegistered(this)) {
316 logger.debug("Checking WeMo GENA subscription for '{}'", this);
318 ThingTypeUID thingTypeUID = thing.getThingTypeUID();
319 String subscription = "basicevent1";
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);
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(),
332 service.addSubscription(this, subscription, SUBSCRIPTION_DURATION_SECONDS);
333 subscriptionState.put(subscription, true);
337 logger.debug("Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
342 private synchronized void removeSubscription() {
343 logger.debug("Removing WeMo GENA subscription for '{}'", this);
345 if (service.isRegistered(this)) {
346 ThingTypeUID thingTypeUID = thing.getThingTypeUID();
347 String subscription = "basicevent1";
349 if (subscriptionState.get(subscription) != null) {
350 logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription);
351 service.removeSubscription(this, subscription);
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);
361 subscriptionState = new HashMap<>();
362 service.unregisterParticipant(this);
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();
375 refreshJob = scheduler.scheduleWithFixedDelay(refreshRunnable, 0, refreshInterval, TimeUnit.SECONDS);
379 private boolean isUpnpDeviceRegistered() {
380 return service.isRegistered(this);
384 public String getUDN() {
385 return (String) this.getThing().getConfiguration().get(UDN);
389 * The {@link updateWemoState} polls the actual state of a WeMo device and
390 * calls {@link onValueReceived} to update the statemap and channels..
393 protected void updateWemoState() {
394 String action = "GetBinaryState";
395 String variable = "BinaryState";
396 String actionService = "basicevent";
399 if (getThing().getThingTypeUID().getId().equals("insight")) {
400 action = "GetInsightParams";
401 variable = "InsightParams";
402 actionService = "insight";
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>";
412 URL descriptorURL = service.getDescriptorURL(this);
413 String wemoURL = getWemoURL(descriptorURL, actionService);
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>");
422 value = substringBetween(wemoCallResponse, "<BinaryState>", "</BinaryState>");
424 logger.trace("New state '{}' for device '{}' received", value, getThing().getUID());
425 this.onValueReceived(variable, value, actionService + "1");
428 } catch (Exception e) {
429 logger.error("Failed to get actual state for device '{}': {}", getThing().getUID(), e.getMessage());
434 public void onStatusChanged(boolean status) {