2 * Copyright (c) 2010-2023 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 = Set.of(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);
90 pollingJob = scheduler.scheduleWithFixedDelay(this::poll, 0, DEFAULT_REFRESH_INTERVAL_SECONDS,
92 updateStatus(ThingStatus.UNKNOWN);
94 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
95 "@text/config-status.error.missing-udn");
100 public void dispose() {
101 logger.debug("WeMoDimmerHandler disposed.");
103 ScheduledFuture<?> job = this.pollingJob;
104 if (job != null && !job.isCancelled()) {
107 this.pollingJob = null;
111 private void poll() {
112 synchronized (jobLock) {
113 if (pollingJob == null) {
117 logger.debug("Polling job");
118 // Check if the Wemo device is set in the UPnP service registry
119 if (!isUpnpDeviceRegistered()) {
120 logger.debug("UPnP device {} not yet registered", getUDN());
121 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
122 "@text/config-status.pending.device-not-registered [\"" + getUDN() + "\"]");
126 } catch (Exception e) {
127 logger.debug("Exception during poll: {}", e.getMessage(), e);
133 public void handleCommand(ChannelUID channelUID, Command command) {
134 logger.trace("Command '{}' received for channel '{}'", command, channelUID);
135 if (command instanceof RefreshType) {
138 } catch (Exception e) {
139 logger.debug("Exception during poll", e);
142 String action = "SetBinaryState";
143 String argument = "BinaryState";
145 String timeStamp = null;
146 switch (channelUID.getId()) {
147 case CHANNEL_BRIGHTNESS:
148 String binaryState = this.stateMap.get("BinaryState");
149 if (command instanceof OnOffType) {
150 value = command.equals(OnOffType.OFF) ? "0" : "1";
151 setBinaryState(action, argument, value);
152 if (command.equals(OnOffType.OFF)) {
153 State brightnessState = new PercentType("0");
154 updateState(CHANNEL_BRIGHTNESS, brightnessState);
155 updateState(CHANNEL_TIMER_START, OnOffType.OFF);
157 State brightnessState = new PercentType(currentBrightness);
158 updateState(CHANNEL_BRIGHTNESS, brightnessState);
160 } else if (command instanceof PercentType percentCommand) {
161 int newBrightness = percentCommand.intValue();
162 value = String.valueOf(newBrightness);
163 currentBrightness = newBrightness;
164 argument = "brightness";
165 if ("0".equals(value)) {
167 argument = "brightness";
168 setBinaryState(action, argument, "1");
170 argument = "BinaryState";
171 setBinaryState(action, argument, "0");
172 } else if ("0".equals(binaryState)) {
173 argument = "BinaryState";
174 setBinaryState(action, argument, "1");
176 argument = "brightness";
177 setBinaryState(action, argument, value);
178 } else if (command instanceof IncreaseDecreaseType) {
180 switch (command.toString()) {
182 newBrightness = currentBrightness + DIM_STEPSIZE;
183 if (newBrightness > 100) {
186 value = String.valueOf(newBrightness);
187 currentBrightness = newBrightness;
190 newBrightness = currentBrightness - DIM_STEPSIZE;
191 if (newBrightness < 0) {
194 value = String.valueOf(newBrightness);
195 currentBrightness = newBrightness;
198 argument = "brightness";
199 if ("0".equals(value)) {
201 argument = "brightness";
202 setBinaryState(action, argument, "1");
204 argument = "BinaryState";
205 setBinaryState(action, argument, "0");
206 } else if ("0".equals(binaryState)) {
207 argument = "BinaryState";
208 setBinaryState(action, argument, "1");
210 argument = "brightness";
211 setBinaryState(action, argument, value);
214 case CHANNEL_FADER_COUNT_DOWN_TIME:
216 if (command instanceof DecimalType) {
217 int commandValue = Integer.valueOf(String.valueOf(command));
218 commandValue = commandValue * 60;
219 String commandString = String.valueOf(commandValue);
221 <BinaryState></BinaryState>\
222 <Duration></Duration>\
223 <EndAction></EndAction>\
224 <brightness></brightness>\
226 """ + commandString + ":-1:1:0:0</fader><UDN></UDN>";
227 setBinaryState(action, argument, value);
230 case CHANNEL_FADER_ENABLED:
232 if (command.equals(OnOffType.ON)) {
234 <BinaryState></BinaryState>\
235 <Duration></Duration>\
236 <EndAction></EndAction>\
237 <brightness></brightness>\
238 <fader>600:-1:1:0:0</fader>\
241 } else if (command.equals(OnOffType.OFF)) {
243 <BinaryState></BinaryState>\
244 <Duration></Duration>\
245 <EndAction></EndAction>\
246 <brightness></brightness>\
247 <fader>600:-1:0:0:0</fader>\
251 setBinaryState(action, argument, value);
253 case CHANNEL_TIMER_START:
255 long ts = System.currentTimeMillis() / 1000;
256 timeStamp = String.valueOf(ts);
257 logger.info("timestamp '{}' created", timeStamp);
258 String faderSeconds = null;
259 String faderEnabled = null;
260 String fader = this.stateMap.get("fader");
262 String[] splitFader = fader.split(":");
263 if (splitFader[0] != null) {
264 faderSeconds = splitFader[0];
266 if (splitFader[0] != null) {
267 faderEnabled = splitFader[2];
270 if (faderSeconds != null && faderEnabled != null) {
271 if (OnOffType.ON.equals(command)) {
273 <BinaryState></BinaryState>\
274 <Duration></Duration>\
275 <EndAction></EndAction>\
276 <brightness></brightness>\
278 """ + faderSeconds + ":" + timeStamp + ":" + faderEnabled + ":0:0</fader>"
280 updateState(CHANNEL_STATE, OnOffType.ON);
281 } else if (OnOffType.OFF.equals(command)) {
283 <BinaryState></BinaryState>\
284 <Duration></Duration>\
285 <EndAction></EndAction>\
286 <brightness></brightness>\
288 """ + faderSeconds + ":-1:" + faderEnabled + ":0:0</fader>" + "<UDN></UDN>";
291 setBinaryState(action, argument, value);
293 case CHANNEL_NIGHT_MODE:
294 action = "ConfigureNightMode";
295 argument = "NightModeConfiguration";
296 String nightModeBrightness = String.valueOf(currentNightModeBrightness);
297 if (OnOffType.ON.equals(command)) {
298 value = "<startTime>0</startTime> \\n<nightMode>1</nightMode> \\n<endTime>23400</endTime> \\n<nightModeBrightness>"
299 + nightModeBrightness + "</nightModeBrightness> \\n";
300 } else if (OnOffType.OFF.equals(command)) {
301 value = "<startTime>0</startTime> \\n<nightMode>0</nightMode> \\n<endTime>23400</endTime> \\n<nightModeBrightness>"
302 + nightModeBrightness + "</nightModeBrightness> \\n";
304 setBinaryState(action, argument, value);
306 case CHANNEL_NIGHT_MODE_BRIGHTNESS:
307 action = "ConfigureNightMode";
308 argument = "NightModeConfiguration";
309 if (command instanceof PercentType percentCommand) {
310 int newBrightness = percentCommand.intValue();
311 String newNightModeBrightness = String.valueOf(newBrightness);
312 value = "<startTime>0</startTime> \\n<nightMode>" + currentNightModeState
313 + "</nightMode> \\n<endTime>23400</endTime> \\n<nightModeBrightness>"
314 + newNightModeBrightness + "</nightModeBrightness> \\n";
315 currentNightModeBrightness = newBrightness;
316 } else if (command instanceof IncreaseDecreaseType) {
318 String newNightModeBrightness = null;
319 switch (command.toString()) {
321 newBrightness = currentNightModeBrightness + DIM_STEPSIZE;
322 if (newBrightness > 100) {
325 newNightModeBrightness = String.valueOf(newBrightness);
326 currentBrightness = newBrightness;
329 newBrightness = currentNightModeBrightness - DIM_STEPSIZE;
330 if (newBrightness < 0) {
333 newNightModeBrightness = String.valueOf(newBrightness);
334 currentNightModeBrightness = newBrightness;
337 value = "<startTime>0</startTime> \\n<nightMode>" + currentNightModeState
338 + "</nightMode> \\n<endTime>23400</endTime> \\n<nightModeBrightness>"
339 + newNightModeBrightness + "</nightModeBrightness> \\n";
341 setBinaryState(action, argument, value);
348 public void onValueReceived(@Nullable String variable, @Nullable String value, @Nullable String service) {
349 logger.debug("Received pair '{}':'{}' (service '{}') for thing '{}'",
350 new Object[] { variable, value, service, this.getThing().getUID() });
351 updateStatus(ThingStatus.ONLINE);
352 if (variable != null && value != null) {
353 String oldBinaryState = this.stateMap.get("BinaryState");
354 this.stateMap.put(variable, value);
357 if (oldBinaryState == null || !oldBinaryState.equals(value)) {
358 State state = OnOffType.from(!"0".equals(value));
359 logger.debug("State '{}' for device '{}' received", state, getThing().getUID());
360 updateState(CHANNEL_BRIGHTNESS, state);
361 if (state.equals(OnOffType.OFF)) {
362 updateState(CHANNEL_TIMER_START, OnOffType.OFF);
367 logger.debug("brightness '{}' for device '{}' received", value, getThing().getUID());
368 int newBrightnessValue = Integer.valueOf(value);
369 State newBrightnessState = new PercentType(newBrightnessValue);
370 String binaryState = this.stateMap.get("BinaryState");
371 if (binaryState != null) {
372 if ("1".equals(binaryState)) {
373 updateState(CHANNEL_BRIGHTNESS, newBrightnessState);
376 currentBrightness = newBrightnessValue;
379 logger.debug("fader '{}' for device '{}' received", value, getThing().getUID());
380 String[] splitFader = value.split(":");
381 if (splitFader[0] != null) {
382 int faderSeconds = Integer.valueOf(splitFader[0]);
383 State faderMinutes = new DecimalType(faderSeconds / 60);
384 logger.debug("faderTime '{} minutes' for device '{}' received", faderMinutes,
385 getThing().getUID());
386 updateState(CHANNEL_FADER_COUNT_DOWN_TIME, faderMinutes);
388 if (splitFader[1] != null) {
389 State isTimerRunning = OnOffType.from(!"-1".equals(splitFader[1]));
390 logger.debug("isTimerRunning '{}' for device '{}' received", isTimerRunning,
391 getThing().getUID());
392 updateState(CHANNEL_TIMER_START, isTimerRunning);
393 if (isTimerRunning.equals(OnOffType.ON)) {
394 updateState(CHANNEL_STATE, OnOffType.ON);
397 if (splitFader[2] != null) {
398 State isFaderEnabled = OnOffType.from(!"0".equals(splitFader[1]));
399 logger.debug("isFaderEnabled '{}' for device '{}' received", isFaderEnabled,
400 getThing().getUID());
401 updateState(CHANNEL_FADER_ENABLED, isFaderEnabled);
405 State nightModeState = OnOffType.from(!"0".equals(value));
406 currentNightModeState = value;
407 logger.debug("nightModeState '{}' for device '{}' received", nightModeState, getThing().getUID());
408 updateState(CHANNEL_NIGHT_MODE, nightModeState);
411 State startTimeState = getDateTimeState(value);
412 logger.debug("startTimeState '{}' for device '{}' received", startTimeState, getThing().getUID());
413 if (startTimeState != null) {
414 updateState(CHANNEL_START_TIME, startTimeState);
418 State endTimeState = getDateTimeState(value);
419 logger.debug("endTimeState '{}' for device '{}' received", endTimeState, getThing().getUID());
420 if (endTimeState != null) {
421 updateState(CHANNEL_END_TIME, endTimeState);
424 case "nightModeBrightness":
425 int nightModeBrightnessValue = Integer.valueOf(value);
426 currentNightModeBrightness = nightModeBrightnessValue;
427 State nightModeBrightnessState = new PercentType(nightModeBrightnessValue);
428 logger.debug("nightModeBrightnessState '{}' for device '{}' received", nightModeBrightnessState,
429 getThing().getUID());
430 updateState(CHANNEL_NIGHT_MODE_BRIGHTNESS, nightModeBrightnessState);
437 * The {@link updateWemoState} polls the actual state of a WeMo device and
438 * calls {@link onValueReceived} to update the statemap and channels..
441 protected void updateWemoState() {
442 String wemoURL = getWemoURL(BASICACTION);
443 if (wemoURL == null) {
444 logger.debug("Failed to get actual state for device '{}': URL cannot be created", getThing().getUID());
447 String action = "GetBinaryState";
448 String variable = null;
449 String actionService = BASICACTION;
451 String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
452 String content = createStateRequestContent(action, actionService);
454 String wemoCallResponse = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
455 value = substringBetween(wemoCallResponse, "<BinaryState>", "</BinaryState>");
456 variable = "BinaryState";
457 this.onValueReceived(variable, value, actionService + "1");
458 value = substringBetween(wemoCallResponse, "<brightness>", "</brightness>");
459 variable = "brightness";
460 this.onValueReceived(variable, value, actionService + "1");
461 value = substringBetween(wemoCallResponse, "<fader>", "</fader>");
463 this.onValueReceived(variable, value, actionService + "1");
464 updateStatus(ThingStatus.ONLINE);
465 } catch (Exception e) {
466 logger.debug("Failed to get actual state for device '{}': {}", getThing().getUID(), e.getMessage());
467 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
469 action = "GetNightModeConfiguration";
472 soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
473 content = createStateRequestContent(action, actionService);
475 String wemoCallResponse = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
476 value = substringBetween(wemoCallResponse, "<startTime>", "</startTime>");
477 variable = "startTime";
478 this.onValueReceived(variable, value, actionService + "1");
479 value = substringBetween(wemoCallResponse, "<endTime>", "</endTime>");
480 variable = "endTime";
481 this.onValueReceived(variable, value, actionService + "1");
482 value = substringBetween(wemoCallResponse, "<nightMode>", "</nightMode>");
483 variable = "nightMode";
484 this.onValueReceived(variable, value, actionService + "1");
485 value = substringBetween(wemoCallResponse, "<nightModeBrightness>", "</nightModeBrightness>");
486 variable = "nightModeBrightness";
487 this.onValueReceived(variable, value, actionService + "1");
488 updateStatus(ThingStatus.ONLINE);
489 } catch (Exception e) {
490 logger.debug("Failed to get actual NightMode state for device '{}': {}", getThing().getUID(),
492 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
496 public @Nullable State getDateTimeState(String attributeValue) {
499 value = Long.parseLong(attributeValue);
500 } catch (NumberFormatException e) {
501 logger.warn("Unable to parse attributeValue '{}' for device '{}'; expected long", attributeValue,
502 getThing().getUID());
505 ZonedDateTime zoned = ZonedDateTime.ofInstant(Instant.ofEpochSecond(value), TimeZone.getDefault().toZoneId());
506 return new DateTimeType(zoned);
509 public void setBinaryState(String action, String argument, String value) {
510 String wemoURL = getWemoURL(BASICACTION);
511 if (wemoURL == null) {
512 logger.debug("Failed to set binary state for device '{}': URL cannot be created", getThing().getUID());
516 String soapHeader = "\"urn:Belkin:service:basicevent:1#SetBinaryState\"";
518 <?xml version="1.0"?>\
519 <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">\
523 + action + " xmlns:u=\"urn:Belkin:service:basicevent:1\">" + "<" + argument + ">" + value + "</"
524 + argument + ">" + "</u:" + action + ">" + "</s:Body>" + "</s:Envelope>";
526 wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
527 updateStatus(ThingStatus.ONLINE);
528 } catch (Exception e) {
529 logger.debug("Failed to set binaryState '{}' for device '{}': {}", value, getThing().getUID(),
531 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
535 public void setTimerStart(String action, String argument, String value) {
536 String wemoURL = getWemoURL(BASICACTION);
537 if (wemoURL == null) {
538 logger.warn("Failed to set timerStart for device '{}': URL cannot be created", getThing().getUID());
542 String soapHeader = "\"urn:Belkin:service:basicevent:1#SetBinaryState\"";
544 <?xml version="1.0"?>\
545 <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">\
547 <u:SetBinaryState xmlns:u="urn:Belkin:service:basicevent:1">\
549 + value + "</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());