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.time.Instant;
19 import java.time.ZonedDateTime;
20 import java.util.Collections;
21 import java.util.HashMap;
24 import java.util.TimeZone;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.jupnp.UpnpService;
31 import org.openhab.binding.wemo.internal.http.WemoHttpCall;
32 import org.openhab.core.config.core.Configuration;
33 import org.openhab.core.io.transport.upnp.UpnpIOService;
34 import org.openhab.core.library.types.DateTimeType;
35 import org.openhab.core.library.types.DecimalType;
36 import org.openhab.core.library.types.IncreaseDecreaseType;
37 import org.openhab.core.library.types.OnOffType;
38 import org.openhab.core.library.types.PercentType;
39 import org.openhab.core.thing.ChannelUID;
40 import org.openhab.core.thing.Thing;
41 import org.openhab.core.thing.ThingStatus;
42 import org.openhab.core.thing.ThingStatusDetail;
43 import org.openhab.core.thing.ThingTypeUID;
44 import org.openhab.core.types.Command;
45 import org.openhab.core.types.RefreshType;
46 import org.openhab.core.types.State;
47 import org.slf4j.Logger;
48 import org.slf4j.LoggerFactory;
51 * The {@link WemoDimmerHandler} is responsible for handling commands, which are
52 * sent to one of the channels and to update their states.
54 * @author Hans-Jörg Merk - Initial contribution
57 public class WemoDimmerHandler extends WemoBaseThingHandler {
59 private final Logger logger = LoggerFactory.getLogger(WemoDimmerHandler.class);
61 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_DIMMER);
63 private final Object jobLock = new Object();
65 private final Map<String, String> stateMap = Collections.synchronizedMap(new HashMap<>());
67 private @Nullable ScheduledFuture<?> pollingJob;
69 private int currentBrightness;
70 private int currentNightModeBrightness;
71 private @Nullable String currentNightModeState;
73 * Set dimming stepsize to 5%
75 private static final int DIM_STEPSIZE = 5;
77 public WemoDimmerHandler(Thing thing, UpnpIOService upnpIOService, UpnpService upnpService,
78 WemoHttpCall wemoHttpCaller) {
79 super(thing, upnpIOService, upnpService, wemoHttpCaller);
81 logger.debug("Creating a WemoDimmerHandler for thing '{}'", getThing().getUID());
85 public void initialize() {
87 Configuration configuration = getConfig();
89 if (configuration.get(UDN) != null) {
90 logger.debug("Initializing WemoDimmerHandler for UDN '{}'", configuration.get(UDN));
91 addSubscription(BASICEVENT);
92 pollingJob = scheduler.scheduleWithFixedDelay(this::poll, 0, DEFAULT_REFRESH_INTERVAL_SECONDS,
94 updateStatus(ThingStatus.UNKNOWN);
96 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
97 "@text/config-status.error.missing-udn");
102 public void dispose() {
103 logger.debug("WeMoDimmerHandler disposed.");
105 ScheduledFuture<?> job = this.pollingJob;
106 if (job != null && !job.isCancelled()) {
109 this.pollingJob = null;
113 private void poll() {
114 synchronized (jobLock) {
115 if (pollingJob == null) {
119 logger.debug("Polling job");
120 // Check if the Wemo device is set in the UPnP service registry
121 if (!isUpnpDeviceRegistered()) {
122 logger.debug("UPnP device {} not yet registered", getUDN());
123 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
124 "@text/config-status.pending.device-not-registered [\"" + getUDN() + "\"]");
128 } catch (Exception e) {
129 logger.debug("Exception during poll: {}", e.getMessage(), e);
135 public void handleCommand(ChannelUID channelUID, Command command) {
136 logger.trace("Command '{}' received for channel '{}'", command, channelUID);
137 if (command instanceof RefreshType) {
140 } catch (Exception e) {
141 logger.debug("Exception during poll", e);
144 String action = "SetBinaryState";
145 String argument = "BinaryState";
147 String timeStamp = null;
148 switch (channelUID.getId()) {
149 case CHANNEL_BRIGHTNESS:
150 String binaryState = this.stateMap.get("BinaryState");
151 if (command instanceof OnOffType) {
152 value = command.equals(OnOffType.OFF) ? "0" : "1";
153 setBinaryState(action, argument, value);
154 if (command.equals(OnOffType.OFF)) {
155 State brightnessState = new PercentType("0");
156 updateState(CHANNEL_BRIGHTNESS, brightnessState);
157 updateState(CHANNEL_TIMER_START, OnOffType.OFF);
159 State brightnessState = new PercentType(currentBrightness);
160 updateState(CHANNEL_BRIGHTNESS, brightnessState);
162 } else if (command instanceof PercentType) {
163 int newBrightness = ((PercentType) command).intValue();
164 value = String.valueOf(newBrightness);
165 currentBrightness = newBrightness;
166 argument = "brightness";
167 if ("0".equals(value)) {
169 argument = "brightness";
170 setBinaryState(action, argument, "1");
172 argument = "BinaryState";
173 setBinaryState(action, argument, "0");
174 } else if ("0".equals(binaryState)) {
175 argument = "BinaryState";
176 setBinaryState(action, argument, "1");
178 argument = "brightness";
179 setBinaryState(action, argument, value);
180 } else if (command instanceof IncreaseDecreaseType) {
182 switch (command.toString()) {
184 newBrightness = currentBrightness + DIM_STEPSIZE;
185 if (newBrightness > 100) {
188 value = String.valueOf(newBrightness);
189 currentBrightness = newBrightness;
192 newBrightness = currentBrightness - DIM_STEPSIZE;
193 if (newBrightness < 0) {
196 value = String.valueOf(newBrightness);
197 currentBrightness = newBrightness;
200 argument = "brightness";
201 if ("0".equals(value)) {
203 argument = "brightness";
204 setBinaryState(action, argument, "1");
206 argument = "BinaryState";
207 setBinaryState(action, argument, "0");
208 } else if ("0".equals(binaryState)) {
209 argument = "BinaryState";
210 setBinaryState(action, argument, "1");
212 argument = "brightness";
213 setBinaryState(action, argument, value);
216 case CHANNEL_FADER_COUNT_DOWN_TIME:
218 if (command instanceof DecimalType) {
219 int commandValue = Integer.valueOf(String.valueOf(command));
220 commandValue = commandValue * 60;
221 String commandString = String.valueOf(commandValue);
222 value = "<BinaryState></BinaryState>" + "<Duration></Duration>" + "<EndAction></EndAction>"
223 + "<brightness></brightness>" + "<fader>" + commandString + ":-1:1:0:0</fader>"
225 setBinaryState(action, argument, value);
228 case CHANNEL_FADER_ENABLED:
230 if (command.equals(OnOffType.ON)) {
231 value = "<BinaryState></BinaryState>" + "<Duration></Duration>" + "<EndAction></EndAction>"
232 + "<brightness></brightness>" + "<fader>600:-1:1:0:0</fader>" + "<UDN></UDN>";
233 } else if (command.equals(OnOffType.OFF)) {
234 value = "<BinaryState></BinaryState>" + "<Duration></Duration>" + "<EndAction></EndAction>"
235 + "<brightness></brightness>" + "<fader>600:-1:0:0:0</fader>" + "<UDN></UDN>";
237 setBinaryState(action, argument, value);
239 case CHANNEL_TIMER_START:
241 long ts = System.currentTimeMillis() / 1000;
242 timeStamp = String.valueOf(ts);
243 logger.info("timestamp '{}' created", timeStamp);
244 String faderSeconds = null;
245 String faderEnabled = null;
246 String fader = this.stateMap.get("fader");
248 String[] splitFader = fader.split(":");
249 if (splitFader[0] != null) {
250 faderSeconds = splitFader[0];
252 if (splitFader[0] != null) {
253 faderEnabled = splitFader[2];
256 if (faderSeconds != null && faderEnabled != null) {
257 if (OnOffType.ON.equals(command)) {
258 value = "<BinaryState></BinaryState>" + "<Duration></Duration>" + "<EndAction></EndAction>"
259 + "<brightness></brightness>" + "<fader>" + faderSeconds + ":" + timeStamp + ":"
260 + faderEnabled + ":0:0</fader>" + "<UDN></UDN>";
261 updateState(CHANNEL_STATE, OnOffType.ON);
262 } else if (OnOffType.OFF.equals(command)) {
263 value = "<BinaryState></BinaryState>" + "<Duration></Duration>" + "<EndAction></EndAction>"
264 + "<brightness></brightness>" + "<fader>" + faderSeconds + ":-1:" + faderEnabled
265 + ":0:0</fader>" + "<UDN></UDN>";
268 setBinaryState(action, argument, value);
270 case CHANNEL_NIGHT_MODE:
271 action = "ConfigureNightMode";
272 argument = "NightModeConfiguration";
273 String nightModeBrightness = String.valueOf(currentNightModeBrightness);
274 if (OnOffType.ON.equals(command)) {
275 value = "<startTime>0</startTime> \\n<nightMode>1</nightMode> \\n<endTime>23400</endTime> \\n<nightModeBrightness>"
276 + nightModeBrightness + "</nightModeBrightness> \\n";
277 } else if (OnOffType.OFF.equals(command)) {
278 value = "<startTime>0</startTime> \\n<nightMode>0</nightMode> \\n<endTime>23400</endTime> \\n<nightModeBrightness>"
279 + nightModeBrightness + "</nightModeBrightness> \\n";
281 setBinaryState(action, argument, value);
283 case CHANNEL_NIGHT_MODE_BRIGHTNESS:
284 action = "ConfigureNightMode";
285 argument = "NightModeConfiguration";
286 if (command instanceof PercentType) {
287 int newBrightness = ((PercentType) command).intValue();
288 String newNightModeBrightness = String.valueOf(newBrightness);
289 value = "<startTime>0</startTime> \\n<nightMode>" + currentNightModeState
290 + "</nightMode> \\n<endTime>23400</endTime> \\n<nightModeBrightness>"
291 + newNightModeBrightness + "</nightModeBrightness> \\n";
292 currentNightModeBrightness = newBrightness;
293 } else if (command instanceof IncreaseDecreaseType) {
295 String newNightModeBrightness = null;
296 switch (command.toString()) {
298 newBrightness = currentNightModeBrightness + DIM_STEPSIZE;
299 if (newBrightness > 100) {
302 newNightModeBrightness = String.valueOf(newBrightness);
303 currentBrightness = newBrightness;
306 newBrightness = currentNightModeBrightness - DIM_STEPSIZE;
307 if (newBrightness < 0) {
310 newNightModeBrightness = String.valueOf(newBrightness);
311 currentNightModeBrightness = newBrightness;
314 value = "<startTime>0</startTime> \\n<nightMode>" + currentNightModeState
315 + "</nightMode> \\n<endTime>23400</endTime> \\n<nightModeBrightness>"
316 + newNightModeBrightness + "</nightModeBrightness> \\n";
318 setBinaryState(action, argument, value);
325 public void onValueReceived(@Nullable String variable, @Nullable String value, @Nullable String service) {
326 logger.debug("Received pair '{}':'{}' (service '{}') for thing '{}'",
327 new Object[] { variable, value, service, this.getThing().getUID() });
328 updateStatus(ThingStatus.ONLINE);
329 if (variable != null && value != null) {
330 String oldBinaryState = this.stateMap.get("BinaryState");
331 this.stateMap.put(variable, value);
334 if (oldBinaryState == null || !oldBinaryState.equals(value)) {
335 State state = "0".equals(value) ? OnOffType.OFF : OnOffType.ON;
336 logger.debug("State '{}' for device '{}' received", state, getThing().getUID());
337 updateState(CHANNEL_BRIGHTNESS, state);
338 if (state.equals(OnOffType.OFF)) {
339 updateState(CHANNEL_TIMER_START, OnOffType.OFF);
344 logger.debug("brightness '{}' for device '{}' received", value, getThing().getUID());
345 int newBrightnessValue = Integer.valueOf(value);
346 State newBrightnessState = new PercentType(newBrightnessValue);
347 String binaryState = this.stateMap.get("BinaryState");
348 if (binaryState != null) {
349 if ("1".equals(binaryState)) {
350 updateState(CHANNEL_BRIGHTNESS, newBrightnessState);
353 currentBrightness = newBrightnessValue;
356 logger.debug("fader '{}' for device '{}' received", value, getThing().getUID());
357 String[] splitFader = value.split(":");
358 if (splitFader[0] != null) {
359 int faderSeconds = Integer.valueOf(splitFader[0]);
360 State faderMinutes = new DecimalType(faderSeconds / 60);
361 logger.debug("faderTime '{} minutes' for device '{}' received", faderMinutes,
362 getThing().getUID());
363 updateState(CHANNEL_FADER_COUNT_DOWN_TIME, faderMinutes);
365 if (splitFader[1] != null) {
366 State isTimerRunning = splitFader[1].equals("-1") ? OnOffType.OFF : OnOffType.ON;
367 logger.debug("isTimerRunning '{}' for device '{}' received", isTimerRunning,
368 getThing().getUID());
369 updateState(CHANNEL_TIMER_START, isTimerRunning);
370 if (isTimerRunning.equals(OnOffType.ON)) {
371 updateState(CHANNEL_STATE, OnOffType.ON);
374 if (splitFader[2] != null) {
375 State isFaderEnabled = splitFader[1].equals("0") ? OnOffType.OFF : OnOffType.ON;
376 logger.debug("isFaderEnabled '{}' for device '{}' received", isFaderEnabled,
377 getThing().getUID());
378 updateState(CHANNEL_FADER_ENABLED, isFaderEnabled);
382 State nightModeState = "0".equals(value) ? OnOffType.OFF : OnOffType.ON;
383 currentNightModeState = value;
384 logger.debug("nightModeState '{}' for device '{}' received", nightModeState, getThing().getUID());
385 updateState(CHANNEL_NIGHT_MODE, nightModeState);
388 State startTimeState = getDateTimeState(value);
389 logger.debug("startTimeState '{}' for device '{}' received", startTimeState, getThing().getUID());
390 if (startTimeState != null) {
391 updateState(CHANNEL_START_TIME, startTimeState);
395 State endTimeState = getDateTimeState(value);
396 logger.debug("endTimeState '{}' for device '{}' received", endTimeState, getThing().getUID());
397 if (endTimeState != null) {
398 updateState(CHANNEL_END_TIME, endTimeState);
401 case "nightModeBrightness":
402 int nightModeBrightnessValue = Integer.valueOf(value);
403 currentNightModeBrightness = nightModeBrightnessValue;
404 State nightModeBrightnessState = new PercentType(nightModeBrightnessValue);
405 logger.debug("nightModeBrightnessState '{}' for device '{}' received", nightModeBrightnessState,
406 getThing().getUID());
407 updateState(CHANNEL_NIGHT_MODE_BRIGHTNESS, nightModeBrightnessState);
414 * The {@link updateWemoState} polls the actual state of a WeMo device and
415 * calls {@link onValueReceived} to update the statemap and channels..
418 protected void updateWemoState() {
419 String wemoURL = getWemoURL(BASICACTION);
420 if (wemoURL == null) {
421 logger.debug("Failed to get actual state for device '{}': URL cannot be created", getThing().getUID());
424 String action = "GetBinaryState";
425 String variable = null;
426 String actionService = BASICACTION;
428 String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
429 String content = createStateRequestContent(action, actionService);
431 String wemoCallResponse = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
432 value = substringBetween(wemoCallResponse, "<BinaryState>", "</BinaryState>");
433 variable = "BinaryState";
434 this.onValueReceived(variable, value, actionService + "1");
435 value = substringBetween(wemoCallResponse, "<brightness>", "</brightness>");
436 variable = "brightness";
437 this.onValueReceived(variable, value, actionService + "1");
438 value = substringBetween(wemoCallResponse, "<fader>", "</fader>");
440 this.onValueReceived(variable, value, actionService + "1");
441 updateStatus(ThingStatus.ONLINE);
442 } catch (Exception e) {
443 logger.debug("Failed to get actual state for device '{}': {}", getThing().getUID(), e.getMessage());
444 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
446 action = "GetNightModeConfiguration";
449 soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
450 content = createStateRequestContent(action, actionService);
452 String wemoCallResponse = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
453 value = substringBetween(wemoCallResponse, "<startTime>", "</startTime>");
454 variable = "startTime";
455 this.onValueReceived(variable, value, actionService + "1");
456 value = substringBetween(wemoCallResponse, "<endTime>", "</endTime>");
457 variable = "endTime";
458 this.onValueReceived(variable, value, actionService + "1");
459 value = substringBetween(wemoCallResponse, "<nightMode>", "</nightMode>");
460 variable = "nightMode";
461 this.onValueReceived(variable, value, actionService + "1");
462 value = substringBetween(wemoCallResponse, "<nightModeBrightness>", "</nightModeBrightness>");
463 variable = "nightModeBrightness";
464 this.onValueReceived(variable, value, actionService + "1");
465 updateStatus(ThingStatus.ONLINE);
466 } catch (Exception e) {
467 logger.debug("Failed to get actual NightMode state for device '{}': {}", getThing().getUID(),
469 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
473 public @Nullable State getDateTimeState(String attributeValue) {
476 value = Long.parseLong(attributeValue);
477 } catch (NumberFormatException e) {
478 logger.warn("Unable to parse attributeValue '{}' for device '{}'; expected long", attributeValue,
479 getThing().getUID());
482 ZonedDateTime zoned = ZonedDateTime.ofInstant(Instant.ofEpochSecond(value), TimeZone.getDefault().toZoneId());
483 State dateTimeState = new DateTimeType(zoned);
484 return dateTimeState;
487 public void setBinaryState(String action, String argument, String value) {
488 String wemoURL = getWemoURL(BASICACTION);
489 if (wemoURL == null) {
490 logger.debug("Failed to set binary state for device '{}': URL cannot be created", getThing().getUID());
494 String soapHeader = "\"urn:Belkin:service:basicevent:1#SetBinaryState\"";
495 String content = "<?xml version=\"1.0\"?>"
496 + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
497 + "<s:Body>" + "<u:" + action + " xmlns:u=\"urn:Belkin:service:basicevent:1\">" + "<" + argument
498 + ">" + value + "</" + argument + ">" + "</u:" + action + ">" + "</s:Body>" + "</s:Envelope>";
500 wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
501 updateStatus(ThingStatus.ONLINE);
502 } catch (Exception e) {
503 logger.debug("Failed to set binaryState '{}' for device '{}': {}", value, getThing().getUID(),
505 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
509 public void setTimerStart(String action, String argument, String value) {
510 String wemoURL = getWemoURL(BASICACTION);
511 if (wemoURL == null) {
512 logger.warn("Failed to set timerStart for device '{}': URL cannot be created", getThing().getUID());
516 String soapHeader = "\"urn:Belkin:service:basicevent:1#SetBinaryState\"";
517 String content = "<?xml version=\"1.0\"?>"
518 + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
519 + "<s:Body>" + "<u:SetBinaryState xmlns:u=\"urn:Belkin:service:basicevent:1\">" + value
520 + "</u:SetBinaryState>" + "</s:Body>" + "</s:Envelope>";
521 wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
522 updateStatus(ThingStatus.ONLINE);
523 } catch (Exception e) {
524 logger.debug("Failed to set timerStart '{}' for device '{}': {}", value, getThing().getUID(),
526 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());