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 = 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);
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) {
161 int newBrightness = ((PercentType) command).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);
220 value = "<BinaryState></BinaryState>" + "<Duration></Duration>" + "<EndAction></EndAction>"
221 + "<brightness></brightness>" + "<fader>" + commandString + ":-1:1:0:0</fader>"
223 setBinaryState(action, argument, value);
226 case CHANNEL_FADER_ENABLED:
228 if (command.equals(OnOffType.ON)) {
229 value = "<BinaryState></BinaryState>" + "<Duration></Duration>" + "<EndAction></EndAction>"
230 + "<brightness></brightness>" + "<fader>600:-1:1:0:0</fader>" + "<UDN></UDN>";
231 } else if (command.equals(OnOffType.OFF)) {
232 value = "<BinaryState></BinaryState>" + "<Duration></Duration>" + "<EndAction></EndAction>"
233 + "<brightness></brightness>" + "<fader>600:-1:0:0:0</fader>" + "<UDN></UDN>";
235 setBinaryState(action, argument, value);
237 case CHANNEL_TIMER_START:
239 long ts = System.currentTimeMillis() / 1000;
240 timeStamp = String.valueOf(ts);
241 logger.info("timestamp '{}' created", timeStamp);
242 String faderSeconds = null;
243 String faderEnabled = null;
244 String fader = this.stateMap.get("fader");
246 String[] splitFader = fader.split(":");
247 if (splitFader[0] != null) {
248 faderSeconds = splitFader[0];
250 if (splitFader[0] != null) {
251 faderEnabled = splitFader[2];
254 if (faderSeconds != null && faderEnabled != null) {
255 if (OnOffType.ON.equals(command)) {
256 value = "<BinaryState></BinaryState>" + "<Duration></Duration>" + "<EndAction></EndAction>"
257 + "<brightness></brightness>" + "<fader>" + faderSeconds + ":" + timeStamp + ":"
258 + faderEnabled + ":0:0</fader>" + "<UDN></UDN>";
259 updateState(CHANNEL_STATE, OnOffType.ON);
260 } else if (OnOffType.OFF.equals(command)) {
261 value = "<BinaryState></BinaryState>" + "<Duration></Duration>" + "<EndAction></EndAction>"
262 + "<brightness></brightness>" + "<fader>" + faderSeconds + ":-1:" + faderEnabled
263 + ":0:0</fader>" + "<UDN></UDN>";
266 setBinaryState(action, argument, value);
268 case CHANNEL_NIGHT_MODE:
269 action = "ConfigureNightMode";
270 argument = "NightModeConfiguration";
271 String nightModeBrightness = String.valueOf(currentNightModeBrightness);
272 if (OnOffType.ON.equals(command)) {
273 value = "<startTime>0</startTime> \\n<nightMode>1</nightMode> \\n<endTime>23400</endTime> \\n<nightModeBrightness>"
274 + nightModeBrightness + "</nightModeBrightness> \\n";
275 } else if (OnOffType.OFF.equals(command)) {
276 value = "<startTime>0</startTime> \\n<nightMode>0</nightMode> \\n<endTime>23400</endTime> \\n<nightModeBrightness>"
277 + nightModeBrightness + "</nightModeBrightness> \\n";
279 setBinaryState(action, argument, value);
281 case CHANNEL_NIGHT_MODE_BRIGHTNESS:
282 action = "ConfigureNightMode";
283 argument = "NightModeConfiguration";
284 if (command instanceof PercentType) {
285 int newBrightness = ((PercentType) command).intValue();
286 String newNightModeBrightness = String.valueOf(newBrightness);
287 value = "<startTime>0</startTime> \\n<nightMode>" + currentNightModeState
288 + "</nightMode> \\n<endTime>23400</endTime> \\n<nightModeBrightness>"
289 + newNightModeBrightness + "</nightModeBrightness> \\n";
290 currentNightModeBrightness = newBrightness;
291 } else if (command instanceof IncreaseDecreaseType) {
293 String newNightModeBrightness = null;
294 switch (command.toString()) {
296 newBrightness = currentNightModeBrightness + DIM_STEPSIZE;
297 if (newBrightness > 100) {
300 newNightModeBrightness = String.valueOf(newBrightness);
301 currentBrightness = newBrightness;
304 newBrightness = currentNightModeBrightness - DIM_STEPSIZE;
305 if (newBrightness < 0) {
308 newNightModeBrightness = String.valueOf(newBrightness);
309 currentNightModeBrightness = newBrightness;
312 value = "<startTime>0</startTime> \\n<nightMode>" + currentNightModeState
313 + "</nightMode> \\n<endTime>23400</endTime> \\n<nightModeBrightness>"
314 + newNightModeBrightness + "</nightModeBrightness> \\n";
316 setBinaryState(action, argument, value);
323 public void onValueReceived(@Nullable String variable, @Nullable String value, @Nullable String service) {
324 logger.debug("Received pair '{}':'{}' (service '{}') for thing '{}'",
325 new Object[] { variable, value, service, this.getThing().getUID() });
326 updateStatus(ThingStatus.ONLINE);
327 if (variable != null && value != null) {
328 String oldBinaryState = this.stateMap.get("BinaryState");
329 this.stateMap.put(variable, value);
332 if (oldBinaryState == null || !oldBinaryState.equals(value)) {
333 State state = "0".equals(value) ? OnOffType.OFF : OnOffType.ON;
334 logger.debug("State '{}' for device '{}' received", state, getThing().getUID());
335 updateState(CHANNEL_BRIGHTNESS, state);
336 if (state.equals(OnOffType.OFF)) {
337 updateState(CHANNEL_TIMER_START, OnOffType.OFF);
342 logger.debug("brightness '{}' for device '{}' received", value, getThing().getUID());
343 int newBrightnessValue = Integer.valueOf(value);
344 State newBrightnessState = new PercentType(newBrightnessValue);
345 String binaryState = this.stateMap.get("BinaryState");
346 if (binaryState != null) {
347 if ("1".equals(binaryState)) {
348 updateState(CHANNEL_BRIGHTNESS, newBrightnessState);
351 currentBrightness = newBrightnessValue;
354 logger.debug("fader '{}' for device '{}' received", value, getThing().getUID());
355 String[] splitFader = value.split(":");
356 if (splitFader[0] != null) {
357 int faderSeconds = Integer.valueOf(splitFader[0]);
358 State faderMinutes = new DecimalType(faderSeconds / 60);
359 logger.debug("faderTime '{} minutes' for device '{}' received", faderMinutes,
360 getThing().getUID());
361 updateState(CHANNEL_FADER_COUNT_DOWN_TIME, faderMinutes);
363 if (splitFader[1] != null) {
364 State isTimerRunning = splitFader[1].equals("-1") ? OnOffType.OFF : OnOffType.ON;
365 logger.debug("isTimerRunning '{}' for device '{}' received", isTimerRunning,
366 getThing().getUID());
367 updateState(CHANNEL_TIMER_START, isTimerRunning);
368 if (isTimerRunning.equals(OnOffType.ON)) {
369 updateState(CHANNEL_STATE, OnOffType.ON);
372 if (splitFader[2] != null) {
373 State isFaderEnabled = splitFader[1].equals("0") ? OnOffType.OFF : OnOffType.ON;
374 logger.debug("isFaderEnabled '{}' for device '{}' received", isFaderEnabled,
375 getThing().getUID());
376 updateState(CHANNEL_FADER_ENABLED, isFaderEnabled);
380 State nightModeState = "0".equals(value) ? OnOffType.OFF : OnOffType.ON;
381 currentNightModeState = value;
382 logger.debug("nightModeState '{}' for device '{}' received", nightModeState, getThing().getUID());
383 updateState(CHANNEL_NIGHT_MODE, nightModeState);
386 State startTimeState = getDateTimeState(value);
387 logger.debug("startTimeState '{}' for device '{}' received", startTimeState, getThing().getUID());
388 if (startTimeState != null) {
389 updateState(CHANNEL_START_TIME, startTimeState);
393 State endTimeState = getDateTimeState(value);
394 logger.debug("endTimeState '{}' for device '{}' received", endTimeState, getThing().getUID());
395 if (endTimeState != null) {
396 updateState(CHANNEL_END_TIME, endTimeState);
399 case "nightModeBrightness":
400 int nightModeBrightnessValue = Integer.valueOf(value);
401 currentNightModeBrightness = nightModeBrightnessValue;
402 State nightModeBrightnessState = new PercentType(nightModeBrightnessValue);
403 logger.debug("nightModeBrightnessState '{}' for device '{}' received", nightModeBrightnessState,
404 getThing().getUID());
405 updateState(CHANNEL_NIGHT_MODE_BRIGHTNESS, nightModeBrightnessState);
412 * The {@link updateWemoState} polls the actual state of a WeMo device and
413 * calls {@link onValueReceived} to update the statemap and channels..
416 protected void updateWemoState() {
417 String wemoURL = getWemoURL(BASICACTION);
418 if (wemoURL == null) {
419 logger.debug("Failed to get actual state for device '{}': URL cannot be created", getThing().getUID());
422 String action = "GetBinaryState";
423 String variable = null;
424 String actionService = BASICACTION;
426 String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
427 String content = createStateRequestContent(action, actionService);
429 String wemoCallResponse = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
430 value = substringBetween(wemoCallResponse, "<BinaryState>", "</BinaryState>");
431 variable = "BinaryState";
432 this.onValueReceived(variable, value, actionService + "1");
433 value = substringBetween(wemoCallResponse, "<brightness>", "</brightness>");
434 variable = "brightness";
435 this.onValueReceived(variable, value, actionService + "1");
436 value = substringBetween(wemoCallResponse, "<fader>", "</fader>");
438 this.onValueReceived(variable, value, actionService + "1");
439 updateStatus(ThingStatus.ONLINE);
440 } catch (Exception e) {
441 logger.debug("Failed to get actual state for device '{}': {}", getThing().getUID(), e.getMessage());
442 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
444 action = "GetNightModeConfiguration";
447 soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
448 content = createStateRequestContent(action, actionService);
450 String wemoCallResponse = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
451 value = substringBetween(wemoCallResponse, "<startTime>", "</startTime>");
452 variable = "startTime";
453 this.onValueReceived(variable, value, actionService + "1");
454 value = substringBetween(wemoCallResponse, "<endTime>", "</endTime>");
455 variable = "endTime";
456 this.onValueReceived(variable, value, actionService + "1");
457 value = substringBetween(wemoCallResponse, "<nightMode>", "</nightMode>");
458 variable = "nightMode";
459 this.onValueReceived(variable, value, actionService + "1");
460 value = substringBetween(wemoCallResponse, "<nightModeBrightness>", "</nightModeBrightness>");
461 variable = "nightModeBrightness";
462 this.onValueReceived(variable, value, actionService + "1");
463 updateStatus(ThingStatus.ONLINE);
464 } catch (Exception e) {
465 logger.debug("Failed to get actual NightMode state for device '{}': {}", getThing().getUID(),
467 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
471 public @Nullable State getDateTimeState(String attributeValue) {
474 value = Long.parseLong(attributeValue);
475 } catch (NumberFormatException e) {
476 logger.warn("Unable to parse attributeValue '{}' for device '{}'; expected long", attributeValue,
477 getThing().getUID());
480 ZonedDateTime zoned = ZonedDateTime.ofInstant(Instant.ofEpochSecond(value), TimeZone.getDefault().toZoneId());
481 State dateTimeState = new DateTimeType(zoned);
482 return dateTimeState;
485 public void setBinaryState(String action, String argument, String value) {
486 String wemoURL = getWemoURL(BASICACTION);
487 if (wemoURL == null) {
488 logger.debug("Failed to set binary state for device '{}': URL cannot be created", getThing().getUID());
492 String soapHeader = "\"urn:Belkin:service:basicevent:1#SetBinaryState\"";
493 String content = "<?xml version=\"1.0\"?>"
494 + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
495 + "<s:Body>" + "<u:" + action + " xmlns:u=\"urn:Belkin:service:basicevent:1\">" + "<" + argument
496 + ">" + value + "</" + argument + ">" + "</u:" + action + ">" + "</s:Body>" + "</s:Envelope>";
498 wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
499 updateStatus(ThingStatus.ONLINE);
500 } catch (Exception e) {
501 logger.debug("Failed to set binaryState '{}' for device '{}': {}", value, getThing().getUID(),
503 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
507 public void setTimerStart(String action, String argument, String value) {
508 String wemoURL = getWemoURL(BASICACTION);
509 if (wemoURL == null) {
510 logger.warn("Failed to set timerStart for device '{}': URL cannot be created", getThing().getUID());
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:SetBinaryState xmlns:u=\"urn:Belkin:service:basicevent:1\">" + value
518 + "</u:SetBinaryState>" + "</s:Body>" + "</s:Envelope>";
519 wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
520 updateStatus(ThingStatus.ONLINE);
521 } catch (Exception e) {
522 logger.debug("Failed to set timerStart '{}' for device '{}': {}", value, getThing().getUID(),
524 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());