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.openhab.binding.wemo.internal.http.WemoHttpCall;
31 import org.openhab.core.config.core.Configuration;
32 import org.openhab.core.io.transport.upnp.UpnpIOService;
33 import org.openhab.core.library.types.DateTimeType;
34 import org.openhab.core.library.types.DecimalType;
35 import org.openhab.core.library.types.IncreaseDecreaseType;
36 import org.openhab.core.library.types.OnOffType;
37 import org.openhab.core.library.types.PercentType;
38 import org.openhab.core.thing.ChannelUID;
39 import org.openhab.core.thing.Thing;
40 import org.openhab.core.thing.ThingStatus;
41 import org.openhab.core.thing.ThingStatusDetail;
42 import org.openhab.core.thing.ThingTypeUID;
43 import org.openhab.core.types.Command;
44 import org.openhab.core.types.RefreshType;
45 import org.openhab.core.types.State;
46 import org.slf4j.Logger;
47 import org.slf4j.LoggerFactory;
50 * The {@link WemoDimmerHandler} is responsible for handling commands, which are
51 * sent to one of the channels and to update their states.
53 * @author Hans-Jörg Merk - Initial contribution
56 public class WemoDimmerHandler extends WemoBaseThingHandler {
58 private final Logger logger = LoggerFactory.getLogger(WemoDimmerHandler.class);
60 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_DIMMER);
62 private final Object jobLock = new Object();
64 private final Map<String, String> stateMap = Collections.synchronizedMap(new HashMap<>());
66 private @Nullable ScheduledFuture<?> pollingJob;
68 private int currentBrightness;
69 private int currentNightModeBrightness;
70 private @Nullable String currentNightModeState;
72 * Set dimming stepsize to 5%
74 private static final int DIM_STEPSIZE = 5;
76 public WemoDimmerHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemoHttpCaller) {
77 super(thing, upnpIOService, wemoHttpCaller);
79 logger.debug("Creating a WemoDimmerHandler for thing '{}'", getThing().getUID());
83 public void initialize() {
85 Configuration configuration = getConfig();
87 if (configuration.get(UDN) != null) {
88 logger.debug("Initializing WemoDimmerHandler for UDN '{}'", configuration.get(UDN));
89 addSubscription(BASICEVENT);
91 pollingJob = scheduler.scheduleWithFixedDelay(this::poll, 0, DEFAULT_REFRESH_INTERVAL_SECONDS,
93 updateStatus(ThingStatus.ONLINE);
95 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
96 "@text/config-status.error.missing-udn");
97 logger.debug("Cannot initalize WemoDimmerHandler. UDN not set.");
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");
121 // Check if the Wemo device is set in the UPnP service registry
122 // If not, set the thing state to ONLINE/CONFIG-PENDING and wait for the next poll
123 if (!isUpnpDeviceRegistered()) {
124 logger.debug("UPnP device {} not yet registered", getUDN());
125 updateStatus(ThingStatus.ONLINE, ThingStatusDetail.CONFIGURATION_PENDING,
126 "@text/config-status.pending.device-not-registered [\"" + getUDN() + "\"]");
130 } catch (Exception e) {
131 logger.debug("Exception during poll: {}", e.getMessage(), e);
137 public void handleCommand(ChannelUID channelUID, Command command) {
138 logger.trace("Command '{}' received for channel '{}'", command, channelUID);
139 if (command instanceof RefreshType) {
142 } catch (Exception e) {
143 logger.debug("Exception during poll", e);
146 String action = "SetBinaryState";
147 String argument = "BinaryState";
149 String timeStamp = null;
150 switch (channelUID.getId()) {
151 case CHANNEL_BRIGHTNESS:
152 String binaryState = this.stateMap.get("BinaryState");
153 if (command instanceof OnOffType) {
154 value = command.equals(OnOffType.OFF) ? "0" : "1";
155 setBinaryState(action, argument, value);
156 if (command.equals(OnOffType.OFF)) {
157 State brightnessState = new PercentType("0");
158 updateState(CHANNEL_BRIGHTNESS, brightnessState);
159 updateState(CHANNEL_TIMERSTART, OnOffType.OFF);
161 State brightnessState = new PercentType(currentBrightness);
162 updateState(CHANNEL_BRIGHTNESS, brightnessState);
164 } else if (command instanceof PercentType) {
165 int newBrightness = ((PercentType) command).intValue();
166 value = String.valueOf(newBrightness);
167 currentBrightness = newBrightness;
168 argument = "brightness";
169 if ("0".equals(value)) {
171 argument = "brightness";
172 setBinaryState(action, argument, "1");
174 argument = "BinaryState";
175 setBinaryState(action, argument, "0");
176 } else if ("0".equals(binaryState)) {
177 argument = "BinaryState";
178 setBinaryState(action, argument, "1");
180 argument = "brightness";
181 setBinaryState(action, argument, value);
182 } else if (command instanceof IncreaseDecreaseType) {
184 switch (command.toString()) {
186 newBrightness = currentBrightness + DIM_STEPSIZE;
187 if (newBrightness > 100) {
190 value = String.valueOf(newBrightness);
191 currentBrightness = newBrightness;
194 newBrightness = currentBrightness - DIM_STEPSIZE;
195 if (newBrightness < 0) {
198 value = String.valueOf(newBrightness);
199 currentBrightness = newBrightness;
202 argument = "brightness";
203 if ("0".equals(value)) {
205 argument = "brightness";
206 setBinaryState(action, argument, "1");
208 argument = "BinaryState";
209 setBinaryState(action, argument, "0");
210 } else if ("0".equals(binaryState)) {
211 argument = "BinaryState";
212 setBinaryState(action, argument, "1");
214 argument = "brightness";
215 setBinaryState(action, argument, value);
218 case CHANNEL_FADERCOUNTDOWNTIME:
220 if (command instanceof DecimalType) {
221 int commandValue = Integer.valueOf(String.valueOf(command));
222 commandValue = commandValue * 60;
223 String commandString = String.valueOf(commandValue);
224 value = "<BinaryState></BinaryState>" + "<Duration></Duration>" + "<EndAction></EndAction>"
225 + "<brightness></brightness>" + "<fader>" + commandString + ":-1:1:0:0</fader>"
227 setBinaryState(action, argument, value);
230 case CHANNEL_FADERENABLED:
232 if (command.equals(OnOffType.ON)) {
233 value = "<BinaryState></BinaryState>" + "<Duration></Duration>" + "<EndAction></EndAction>"
234 + "<brightness></brightness>" + "<fader>600:-1:1:0:0</fader>" + "<UDN></UDN>";
235 } else if (command.equals(OnOffType.OFF)) {
236 value = "<BinaryState></BinaryState>" + "<Duration></Duration>" + "<EndAction></EndAction>"
237 + "<brightness></brightness>" + "<fader>600:-1:0:0:0</fader>" + "<UDN></UDN>";
239 setBinaryState(action, argument, value);
241 case CHANNEL_TIMERSTART:
243 long ts = System.currentTimeMillis() / 1000;
244 timeStamp = String.valueOf(ts);
245 logger.info("timestamp '{}' created", timeStamp);
246 String faderSeconds = null;
247 String faderEnabled = null;
248 String fader = this.stateMap.get("fader");
250 String[] splitFader = fader.split(":");
251 if (splitFader[0] != null) {
252 faderSeconds = splitFader[0];
254 if (splitFader[0] != null) {
255 faderEnabled = splitFader[2];
258 if (faderSeconds != null && faderEnabled != null) {
259 if (OnOffType.ON.equals(command)) {
260 value = "<BinaryState></BinaryState>" + "<Duration></Duration>" + "<EndAction></EndAction>"
261 + "<brightness></brightness>" + "<fader>" + faderSeconds + ":" + timeStamp + ":"
262 + faderEnabled + ":0:0</fader>" + "<UDN></UDN>";
263 updateState(CHANNEL_STATE, OnOffType.ON);
264 } else if (OnOffType.OFF.equals(command)) {
265 value = "<BinaryState></BinaryState>" + "<Duration></Duration>" + "<EndAction></EndAction>"
266 + "<brightness></brightness>" + "<fader>" + faderSeconds + ":-1:" + faderEnabled
267 + ":0:0</fader>" + "<UDN></UDN>";
270 setBinaryState(action, argument, value);
272 case CHANNEL_NIGHTMODE:
273 action = "ConfigureNightMode";
274 argument = "NightModeConfiguration";
275 String nightModeBrightness = String.valueOf(currentNightModeBrightness);
276 if (OnOffType.ON.equals(command)) {
277 value = "<startTime>0</startTime> \\n<nightMode>1</nightMode> \\n<endTime>23400</endTime> \\n<nightModeBrightness>"
278 + nightModeBrightness + "</nightModeBrightness> \\n";
279 } else if (OnOffType.OFF.equals(command)) {
280 value = "<startTime>0</startTime> \\n<nightMode>0</nightMode> \\n<endTime>23400</endTime> \\n<nightModeBrightness>"
281 + nightModeBrightness + "</nightModeBrightness> \\n";
283 setBinaryState(action, argument, value);
285 case CHANNEL_NIGHTMODEBRIGHTNESS:
286 action = "ConfigureNightMode";
287 argument = "NightModeConfiguration";
288 if (command instanceof PercentType) {
289 int newBrightness = ((PercentType) command).intValue();
290 String newNightModeBrightness = String.valueOf(newBrightness);
291 value = "<startTime>0</startTime> \\n<nightMode>" + currentNightModeState
292 + "</nightMode> \\n<endTime>23400</endTime> \\n<nightModeBrightness>"
293 + newNightModeBrightness + "</nightModeBrightness> \\n";
294 currentNightModeBrightness = newBrightness;
295 } else if (command instanceof IncreaseDecreaseType) {
297 String newNightModeBrightness = null;
298 switch (command.toString()) {
300 newBrightness = currentNightModeBrightness + DIM_STEPSIZE;
301 if (newBrightness > 100) {
304 newNightModeBrightness = String.valueOf(newBrightness);
305 currentBrightness = newBrightness;
308 newBrightness = currentNightModeBrightness - DIM_STEPSIZE;
309 if (newBrightness < 0) {
312 newNightModeBrightness = String.valueOf(newBrightness);
313 currentNightModeBrightness = newBrightness;
316 value = "<startTime>0</startTime> \\n<nightMode>" + currentNightModeState
317 + "</nightMode> \\n<endTime>23400</endTime> \\n<nightModeBrightness>"
318 + newNightModeBrightness + "</nightModeBrightness> \\n";
320 setBinaryState(action, argument, value);
327 public void onValueReceived(@Nullable String variable, @Nullable String value, @Nullable String service) {
328 logger.debug("Received pair '{}':'{}' (service '{}') for thing '{}'",
329 new Object[] { variable, value, service, this.getThing().getUID() });
330 updateStatus(ThingStatus.ONLINE);
331 if (variable != null && value != null) {
332 String oldBinaryState = this.stateMap.get("BinaryState");
333 this.stateMap.put(variable, value);
336 if (oldBinaryState == null || !oldBinaryState.equals(value)) {
337 State state = "0".equals(value) ? OnOffType.OFF : OnOffType.ON;
338 logger.debug("State '{}' for device '{}' received", state, getThing().getUID());
339 updateState(CHANNEL_BRIGHTNESS, state);
340 if (state.equals(OnOffType.OFF)) {
341 updateState(CHANNEL_TIMERSTART, OnOffType.OFF);
346 logger.debug("brightness '{}' for device '{}' received", value, getThing().getUID());
347 int newBrightnessValue = Integer.valueOf(value);
348 State newBrightnessState = new PercentType(newBrightnessValue);
349 String binaryState = this.stateMap.get("BinaryState");
350 if (binaryState != null) {
351 if ("1".equals(binaryState)) {
352 updateState(CHANNEL_BRIGHTNESS, newBrightnessState);
355 currentBrightness = newBrightnessValue;
358 logger.debug("fader '{}' for device '{}' received", value, getThing().getUID());
359 String[] splitFader = value.split(":");
360 if (splitFader[0] != null) {
361 int faderSeconds = Integer.valueOf(splitFader[0]);
362 State faderMinutes = new DecimalType(faderSeconds / 60);
363 logger.debug("faderTime '{} minutes' for device '{}' received", faderMinutes,
364 getThing().getUID());
365 updateState(CHANNEL_FADERCOUNTDOWNTIME, faderMinutes);
367 if (splitFader[1] != null) {
368 State isTimerRunning = splitFader[1].equals("-1") ? OnOffType.OFF : OnOffType.ON;
369 logger.debug("isTimerRunning '{}' for device '{}' received", isTimerRunning,
370 getThing().getUID());
371 updateState(CHANNEL_TIMERSTART, isTimerRunning);
372 if (isTimerRunning.equals(OnOffType.ON)) {
373 updateState(CHANNEL_STATE, OnOffType.ON);
376 if (splitFader[2] != null) {
377 State isFaderEnabled = splitFader[1].equals("0") ? OnOffType.OFF : OnOffType.ON;
378 logger.debug("isFaderEnabled '{}' for device '{}' received", isFaderEnabled,
379 getThing().getUID());
380 updateState(CHANNEL_FADERENABLED, isFaderEnabled);
384 State nightModeState = "0".equals(value) ? OnOffType.OFF : OnOffType.ON;
385 currentNightModeState = value;
386 logger.debug("nightModeState '{}' for device '{}' received", nightModeState, getThing().getUID());
387 updateState(CHANNEL_NIGHTMODE, nightModeState);
390 State startTimeState = getDateTimeState(value);
391 logger.debug("startTimeState '{}' for device '{}' received", startTimeState, getThing().getUID());
392 if (startTimeState != null) {
393 updateState(CHANNEL_STARTTIME, startTimeState);
397 State endTimeState = getDateTimeState(value);
398 logger.debug("endTimeState '{}' for device '{}' received", endTimeState, getThing().getUID());
399 if (endTimeState != null) {
400 updateState(CHANNEL_ENDTIME, endTimeState);
403 case "nightModeBrightness":
404 int nightModeBrightnessValue = Integer.valueOf(value);
405 currentNightModeBrightness = nightModeBrightnessValue;
406 State nightModeBrightnessState = new PercentType(nightModeBrightnessValue);
407 logger.debug("nightModeBrightnessState '{}' for device '{}' received", nightModeBrightnessState,
408 getThing().getUID());
409 updateState(CHANNEL_NIGHTMODEBRIGHTNESS, nightModeBrightnessState);
416 * The {@link updateWemoState} polls the actual state of a WeMo device and
417 * calls {@link onValueReceived} to update the statemap and channels..
420 protected void updateWemoState() {
421 String localHost = getHost();
422 if (localHost.isEmpty()) {
423 logger.warn("Failed to get actual state for device '{}': IP address missing", getThing().getUID());
424 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
425 "@text/config-status.error.missing-ip");
428 String wemoURL = getWemoURL(localHost, BASICACTION);
429 if (wemoURL == null) {
430 logger.debug("Failed to get actual state for device '{}': URL cannot be created", getThing().getUID());
431 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
432 "@text/config-status.error.missing-url");
435 String action = "GetBinaryState";
436 String variable = null;
437 String actionService = BASICACTION;
439 String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
440 String content = createStateRequestContent(action, actionService);
442 String wemoCallResponse = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
443 value = substringBetween(wemoCallResponse, "<BinaryState>", "</BinaryState>");
444 variable = "BinaryState";
445 this.onValueReceived(variable, value, actionService + "1");
446 value = substringBetween(wemoCallResponse, "<brightness>", "</brightness>");
447 variable = "brightness";
448 this.onValueReceived(variable, value, actionService + "1");
449 value = substringBetween(wemoCallResponse, "<fader>", "</fader>");
451 this.onValueReceived(variable, value, actionService + "1");
452 updateStatus(ThingStatus.ONLINE);
453 } catch (Exception e) {
454 logger.debug("Failed to get actual state for device '{}': {}", getThing().getUID(), e.getMessage());
455 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
457 action = "GetNightModeConfiguration";
460 soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
461 content = createStateRequestContent(action, actionService);
463 String wemoCallResponse = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
464 value = substringBetween(wemoCallResponse, "<startTime>", "</startTime>");
465 variable = "startTime";
466 this.onValueReceived(variable, value, actionService + "1");
467 value = substringBetween(wemoCallResponse, "<endTime>", "</endTime>");
468 variable = "endTime";
469 this.onValueReceived(variable, value, actionService + "1");
470 value = substringBetween(wemoCallResponse, "<nightMode>", "</nightMode>");
471 variable = "nightMode";
472 this.onValueReceived(variable, value, actionService + "1");
473 value = substringBetween(wemoCallResponse, "<nightModeBrightness>", "</nightModeBrightness>");
474 variable = "nightModeBrightness";
475 this.onValueReceived(variable, value, actionService + "1");
476 updateStatus(ThingStatus.ONLINE);
477 } catch (Exception e) {
478 logger.debug("Failed to get actual NightMode state for device '{}': {}", getThing().getUID(),
480 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
484 public @Nullable State getDateTimeState(String attributeValue) {
487 value = Long.parseLong(attributeValue);
488 } catch (NumberFormatException e) {
489 logger.warn("Unable to parse attributeValue '{}' for device '{}'; expected long", attributeValue,
490 getThing().getUID());
493 ZonedDateTime zoned = ZonedDateTime.ofInstant(Instant.ofEpochSecond(value), TimeZone.getDefault().toZoneId());
494 State dateTimeState = new DateTimeType(zoned);
495 return dateTimeState;
498 public void setBinaryState(String action, String argument, String value) {
499 String localHost = getHost();
500 if (localHost.isEmpty()) {
501 logger.warn("Failed to set binary state for device '{}': IP address missing", getThing().getUID());
502 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
503 "@text/config-status.error.missing-ip");
506 String wemoURL = getWemoURL(localHost, BASICACTION);
507 if (wemoURL == null) {
508 logger.debug("Failed to set binary state for device '{}': URL cannot be created", getThing().getUID());
509 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
510 "@text/config-status.error.missing-url");
514 String soapHeader = "\"urn:Belkin:service:basicevent:1#SetBinaryState\"";
515 String content = "<?xml version=\"1.0\"?>"
516 + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
517 + "<s:Body>" + "<u:" + action + " xmlns:u=\"urn:Belkin:service:basicevent:1\">" + "<" + argument
518 + ">" + value + "</" + argument + ">" + "</u:" + action + ">" + "</s:Body>" + "</s:Envelope>";
520 wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
521 updateStatus(ThingStatus.ONLINE);
522 } catch (Exception e) {
523 logger.debug("Failed to set binaryState '{}' for device '{}': {}", value, getThing().getUID(),
525 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
529 public void setTimerStart(String action, String argument, String value) {
530 String localHost = getHost();
531 if (localHost.isEmpty()) {
532 logger.warn("Failed to set timerStart for device '{}': IP address missing", getThing().getUID());
533 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
534 "@text/config-status.error.missing-ip");
537 String wemoURL = getWemoURL(localHost, BASICACTION);
538 if (wemoURL == null) {
539 logger.warn("Failed to set timerStart for device '{}': URL cannot be created", getThing().getUID());
540 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
541 "@text/config-status.error.missing-url");
545 String soapHeader = "\"urn:Belkin:service:basicevent:1#SetBinaryState\"";
546 String content = "<?xml version=\"1.0\"?>"
547 + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
548 + "<s:Body>" + "<u:SetBinaryState xmlns:u=\"urn:Belkin:service:basicevent:1\">" + value
549 + "</u:SetBinaryState>" + "</s:Body>" + "</s:Envelope>";
550 wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
551 updateStatus(ThingStatus.ONLINE);
552 } catch (Exception e) {
553 logger.debug("Failed to set timerStart '{}' for device '{}': {}", value, getThing().getUID(),
555 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());