]> git.basschouten.com Git - openhab-addons.git/blob
336791740865f659c8eeef057d3a7be43961df40
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.hue.internal.handler;
14
15 import static org.openhab.binding.hue.internal.HueBindingConstants.*;
16 import static org.openhab.core.thing.Thing.*;
17
18 import java.math.BigDecimal;
19 import java.util.Collection;
20 import java.util.List;
21 import java.util.Map;
22 import java.util.Objects;
23 import java.util.Set;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
26
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.openhab.binding.hue.internal.FullLight;
30 import org.openhab.binding.hue.internal.State;
31 import org.openhab.binding.hue.internal.State.ColorMode;
32 import org.openhab.binding.hue.internal.StateUpdate;
33 import org.openhab.binding.hue.internal.action.LightActions;
34 import org.openhab.binding.hue.internal.dto.Capabilities;
35 import org.openhab.binding.hue.internal.dto.ColorTemperature;
36 import org.openhab.core.library.types.DecimalType;
37 import org.openhab.core.library.types.HSBType;
38 import org.openhab.core.library.types.IncreaseDecreaseType;
39 import org.openhab.core.library.types.OnOffType;
40 import org.openhab.core.library.types.PercentType;
41 import org.openhab.core.library.types.StringType;
42 import org.openhab.core.thing.Bridge;
43 import org.openhab.core.thing.ChannelUID;
44 import org.openhab.core.thing.Thing;
45 import org.openhab.core.thing.ThingStatus;
46 import org.openhab.core.thing.ThingStatusDetail;
47 import org.openhab.core.thing.ThingStatusInfo;
48 import org.openhab.core.thing.ThingTypeUID;
49 import org.openhab.core.thing.binding.BaseThingHandler;
50 import org.openhab.core.thing.binding.ThingHandler;
51 import org.openhab.core.thing.binding.ThingHandlerService;
52 import org.openhab.core.types.Command;
53 import org.openhab.core.types.StateDescription;
54 import org.openhab.core.types.StateDescriptionFragmentBuilder;
55 import org.openhab.core.types.UnDefType;
56 import org.slf4j.Logger;
57 import org.slf4j.LoggerFactory;
58
59 /**
60  * {@link HueLightHandler} is the handler for a hue light. It uses the {@link HueClient} to execute the actual
61  * command.
62  *
63  * @author Dennis Nobel - Initial contribution
64  * @author Oliver Libutzki - Adjustments
65  * @author Kai Kreuzer - stabilized code
66  * @author Andre Fuechsel - implemented switch off when brightness == 0, changed to support generic thing types, changed
67  *         the initialization of properties
68  * @author Thomas Höfer - added thing properties
69  * @author Jochen Hiller - fixed status updates for reachable=true/false
70  * @author Markus Mazurczak - added code for command handling of OSRAM PAR16 50
71  *         bulbs
72  * @author Yordan Zhelev - added alert and effect functions
73  * @author Denis Dudnik - switched to internally integrated source of Jue library
74  * @author Christoph Weitkamp - Added support for bulbs using CIE XY colormode only
75  * @author Jochen Leopold - Added support for custom fade times
76  */
77 @NonNullByDefault
78 public class HueLightHandler extends BaseThingHandler implements LightStatusListener {
79
80     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_COLOR_LIGHT,
81             THING_TYPE_COLOR_TEMPERATURE_LIGHT, THING_TYPE_DIMMABLE_LIGHT, THING_TYPE_EXTENDED_COLOR_LIGHT,
82             THING_TYPE_ON_OFF_LIGHT, THING_TYPE_ON_OFF_PLUG, THING_TYPE_DIMMABLE_PLUG);
83
84     private static final Map<String, List<String>> VENDOR_MODEL_MAP = Map.of( //
85             "Philips", List.of("LCT001", "LCT002", "LCT003", "LCT007", "LLC001", "LLC006", "LLC007", "LLC010", //
86                     "LLC011", "LLC012", "LLC013", "LLC020", "LST001", "LST002", "LWB004", "LWB006", "LWB007", //
87                     "LWL001"),
88             "OSRAM", List.of("Classic_A60_RGBW", "PAR16_50_TW", "Surface_Light_TW", "Plug_01"));
89
90     private static final String OSRAM_PAR16_50_TW_MODEL_ID = "PAR16_50_TW";
91
92     private final Logger logger = LoggerFactory.getLogger(HueLightHandler.class);
93     private final HueStateDescriptionOptionProvider stateDescriptionOptionProvider;
94
95     private @NonNullByDefault({}) String lightId;
96
97     private @Nullable FullLight lastFullLight;
98     private long endBypassTime = 0L;
99
100     private @Nullable Integer lastSentColorTemp;
101     private @Nullable Integer lastSentBrightness;
102
103     // Flag to indicate whether the bulb is of type Osram par16 50 TW or not
104     private boolean isOsramPar16 = false;
105
106     private boolean propertiesInitializedSuccessfully = false;
107     private boolean capabilitiesInitializedSuccessfully = false;
108     private ColorTemperature colorTemperatureCapabilties = new ColorTemperature();
109     private long defaultFadeTime = 400;
110
111     private @Nullable HueClient hueClient;
112
113     private @Nullable ScheduledFuture<?> scheduledFuture;
114
115     public HueLightHandler(Thing hueLight, HueStateDescriptionOptionProvider stateDescriptionOptionProvider) {
116         super(hueLight);
117         this.stateDescriptionOptionProvider = stateDescriptionOptionProvider;
118     }
119
120     @Override
121     public void initialize() {
122         logger.debug("Initializing hue light handler.");
123         Bridge bridge = getBridge();
124         initializeThing((bridge == null) ? null : bridge.getStatus());
125     }
126
127     @Override
128     public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
129         logger.debug("bridgeStatusChanged {}", bridgeStatusInfo);
130         initializeThing(bridgeStatusInfo.getStatus());
131     }
132
133     private void initializeThing(@Nullable ThingStatus bridgeStatus) {
134         logger.debug("initializeThing thing {} bridge status {}", getThing().getUID(), bridgeStatus);
135         final String configLightId = (String) getConfig().get(LIGHT_ID);
136         if (configLightId != null) {
137             BigDecimal time = (BigDecimal) getConfig().get(FADETIME);
138             if (time != null) {
139                 defaultFadeTime = time.longValueExact();
140             }
141
142             lightId = configLightId;
143             // note: this call implicitly registers our handler as a listener on the bridge
144             HueClient bridgeHandler = getHueClient();
145             if (bridgeHandler != null) {
146                 if (bridgeStatus == ThingStatus.ONLINE) {
147                     FullLight fullLight = bridgeHandler.getLightById(lightId);
148                     initializeProperties(fullLight);
149                     initializeCapabilities(fullLight);
150                     updateStatus(ThingStatus.ONLINE);
151                 } else {
152                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
153                 }
154             } else {
155                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
156             }
157         } else {
158             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
159                     "@text/offline.conf-error-no-light-id");
160         }
161     }
162
163     private synchronized void initializeProperties(@Nullable FullLight fullLight) {
164         if (!propertiesInitializedSuccessfully && fullLight != null) {
165             Map<String, String> properties = editProperties();
166             String softwareVersion = fullLight.getSoftwareVersion();
167             if (softwareVersion != null) {
168                 properties.put(PROPERTY_FIRMWARE_VERSION, softwareVersion);
169             }
170             String modelId = fullLight.getNormalizedModelID();
171             if (modelId != null) {
172                 properties.put(PROPERTY_MODEL_ID, modelId);
173                 String vendor = getVendor(modelId);
174                 if (vendor != null) {
175                     properties.put(PROPERTY_VENDOR, vendor);
176                 }
177             } else {
178                 properties.put(PROPERTY_VENDOR, fullLight.getManufacturerName());
179             }
180             properties.put(PRODUCT_NAME, fullLight.getProductName());
181             String uniqueID = fullLight.getUniqueID();
182             if (uniqueID != null) {
183                 properties.put(UNIQUE_ID, uniqueID);
184             }
185             updateProperties(properties);
186             isOsramPar16 = OSRAM_PAR16_50_TW_MODEL_ID.equals(modelId);
187             propertiesInitializedSuccessfully = true;
188         }
189     }
190
191     private void initializeCapabilities(@Nullable FullLight fullLight) {
192         if (!capabilitiesInitializedSuccessfully && fullLight != null) {
193             Capabilities capabilities = fullLight.capabilities;
194             if (capabilities != null) {
195                 ColorTemperature ct = capabilities.control.ct;
196                 if (ct != null) {
197                     colorTemperatureCapabilties = ct;
198
199                     // minimum and maximum are inverted due to mired/Kelvin conversion!
200                     StateDescription stateDescription = StateDescriptionFragmentBuilder.create()
201                             .withMinimum(new BigDecimal(LightStateConverter.miredToKelvin(ct.max))) //
202                             .withMaximum(new BigDecimal(LightStateConverter.miredToKelvin(ct.min))) //
203                             .withStep(new BigDecimal(100)) //
204                             .withPattern("%.0f K") //
205                             .build().toStateDescription();
206                     if (stateDescription != null) {
207                         stateDescriptionOptionProvider.setDescription(
208                                 new ChannelUID(thing.getUID(), CHANNEL_COLORTEMPERATURE_ABS), stateDescription);
209                     } else {
210                         logger.warn("Failed to create state description in thing {}", thing.getUID());
211                     }
212                 }
213             }
214             capabilitiesInitializedSuccessfully = true;
215         }
216     }
217
218     private @Nullable String getVendor(String modelId) {
219         for (String vendor : VENDOR_MODEL_MAP.keySet()) {
220             if (VENDOR_MODEL_MAP.get(vendor).contains(modelId)) {
221                 return vendor;
222             }
223         }
224         return null;
225     }
226
227     @Override
228     public void dispose() {
229         logger.debug("Hue light handler disposes. Unregistering listener.");
230         cancelScheduledFuture();
231         if (lightId != null) {
232             HueClient bridgeHandler = getHueClient();
233             if (bridgeHandler != null) {
234                 bridgeHandler.unregisterLightStatusListener(this);
235                 hueClient = null;
236             }
237             lightId = null;
238         }
239     }
240
241     @Override
242     public void handleCommand(ChannelUID channelUID, Command command) {
243         handleCommand(channelUID.getId(), command, defaultFadeTime);
244     }
245
246     public void handleCommand(String channel, Command command, long fadeTime) {
247         HueClient bridgeHandler = getHueClient();
248         if (bridgeHandler == null) {
249             logger.warn("hue bridge handler not found. Cannot handle command without bridge.");
250             return;
251         }
252
253         final FullLight light = lastFullLight == null ? bridgeHandler.getLightById(lightId) : lastFullLight;
254         if (light == null) {
255             logger.debug("hue light not known on bridge. Cannot handle command.");
256             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
257                     "@text/offline.conf-error-wrong-light-id");
258             return;
259         }
260
261         Integer lastColorTemp;
262         StateUpdate lightState = null;
263         switch (channel) {
264             case CHANNEL_COLORTEMPERATURE:
265                 if (command instanceof PercentType) {
266                     lightState = LightStateConverter.toColorTemperatureLightStateFromPercentType((PercentType) command,
267                             colorTemperatureCapabilties);
268                     lightState.setTransitionTime(fadeTime);
269                 } else if (command instanceof OnOffType) {
270                     lightState = LightStateConverter.toOnOffLightState((OnOffType) command);
271                     if (isOsramPar16) {
272                         lightState = addOsramSpecificCommands(lightState, (OnOffType) command);
273                     }
274                 } else if (command instanceof IncreaseDecreaseType) {
275                     lightState = convertColorTempChangeToStateUpdate((IncreaseDecreaseType) command, light);
276                     if (lightState != null) {
277                         lightState.setTransitionTime(fadeTime);
278                     }
279                 }
280                 break;
281             case CHANNEL_COLORTEMPERATURE_ABS:
282                 if (command instanceof DecimalType) {
283                     lightState = LightStateConverter.toColorTemperatureLightState((DecimalType) command,
284                             colorTemperatureCapabilties);
285                     lightState.setTransitionTime(fadeTime);
286                 }
287                 break;
288             case CHANNEL_BRIGHTNESS:
289                 if (command instanceof PercentType) {
290                     lightState = LightStateConverter.toBrightnessLightState((PercentType) command);
291                     lightState.setTransitionTime(fadeTime);
292                 } else if (command instanceof OnOffType) {
293                     lightState = LightStateConverter.toOnOffLightState((OnOffType) command);
294                     if (isOsramPar16) {
295                         lightState = addOsramSpecificCommands(lightState, (OnOffType) command);
296                     }
297                 } else if (command instanceof IncreaseDecreaseType) {
298                     lightState = convertBrightnessChangeToStateUpdate((IncreaseDecreaseType) command, light);
299                     if (lightState != null) {
300                         lightState.setTransitionTime(fadeTime);
301                     }
302                 }
303                 lastColorTemp = lastSentColorTemp;
304                 if (lightState != null && lastColorTemp != null) {
305                     // make sure that the light also has the latest color temp
306                     // this might not have been yet set in the light, if it was off
307                     lightState.setColorTemperature(lastColorTemp, colorTemperatureCapabilties);
308                     lightState.setTransitionTime(fadeTime);
309                 }
310                 break;
311             case CHANNEL_SWITCH:
312                 logger.trace("CHANNEL_SWITCH handling command {}", command);
313                 if (command instanceof OnOffType) {
314                     lightState = LightStateConverter.toOnOffLightState((OnOffType) command);
315                     if (isOsramPar16) {
316                         lightState = addOsramSpecificCommands(lightState, (OnOffType) command);
317                     }
318                 }
319                 lastColorTemp = lastSentColorTemp;
320                 if (lightState != null && lastColorTemp != null) {
321                     // make sure that the light also has the latest color temp
322                     // this might not have been yet set in the light, if it was off
323                     lightState.setColorTemperature(lastColorTemp, colorTemperatureCapabilties);
324                     lightState.setTransitionTime(fadeTime);
325                 }
326                 break;
327             case CHANNEL_COLOR:
328                 if (command instanceof HSBType) {
329                     HSBType hsbCommand = (HSBType) command;
330                     if (hsbCommand.getBrightness().intValue() == 0) {
331                         lightState = LightStateConverter.toOnOffLightState(OnOffType.OFF);
332                     } else {
333                         lightState = LightStateConverter.toColorLightState(hsbCommand, light.getState());
334                         lightState.setTransitionTime(fadeTime);
335                     }
336                 } else if (command instanceof PercentType) {
337                     lightState = LightStateConverter.toBrightnessLightState((PercentType) command);
338                     lightState.setTransitionTime(fadeTime);
339                 } else if (command instanceof OnOffType) {
340                     lightState = LightStateConverter.toOnOffLightState((OnOffType) command);
341                 } else if (command instanceof IncreaseDecreaseType) {
342                     lightState = convertBrightnessChangeToStateUpdate((IncreaseDecreaseType) command, light);
343                     if (lightState != null) {
344                         lightState.setTransitionTime(fadeTime);
345                     }
346                 }
347                 break;
348             case CHANNEL_ALERT:
349                 if (command instanceof StringType) {
350                     lightState = LightStateConverter.toAlertState((StringType) command);
351                     if (lightState == null) {
352                         // Unsupported StringType is passed. Log a warning
353                         // message and return.
354                         logger.warn("Unsupported String command: {}. Supported commands are: {}, {}, {} ", command,
355                                 LightStateConverter.ALERT_MODE_NONE, LightStateConverter.ALERT_MODE_SELECT,
356                                 LightStateConverter.ALERT_MODE_LONG_SELECT);
357                         return;
358                     } else {
359                         scheduleAlertStateRestore(command);
360                     }
361                 }
362                 break;
363             case CHANNEL_EFFECT:
364                 if (command instanceof OnOffType) {
365                     lightState = LightStateConverter.toOnOffEffectState((OnOffType) command);
366                 }
367                 break;
368         }
369         if (lightState != null) {
370             // Cache values which we have sent
371             Integer tmpBrightness = lightState.getBrightness();
372             if (tmpBrightness != null) {
373                 lastSentBrightness = tmpBrightness;
374             }
375             Integer tmpColorTemp = lightState.getColorTemperature();
376             if (tmpColorTemp != null) {
377                 lastSentColorTemp = tmpColorTemp;
378             }
379             bridgeHandler.updateLightState(this, light, lightState, fadeTime);
380         } else {
381             logger.warn("Command sent to an unknown channel id: {}:{}", getThing().getUID(), channel);
382         }
383     }
384
385     /*
386      * Applies additional {@link StateUpdate} commands as a workaround for Osram
387      * Lightify PAR16 TW firmware bug. Also see
388      * http://www.everyhue.com/vanilla/discussion/1756/solved-lightify-turning-off
389      */
390     private StateUpdate addOsramSpecificCommands(StateUpdate lightState, OnOffType actionType) {
391         if (actionType.equals(OnOffType.ON)) {
392             lightState.setBrightness(254);
393         } else {
394             lightState.setTransitionTime(0);
395         }
396         return lightState;
397     }
398
399     private @Nullable StateUpdate convertColorTempChangeToStateUpdate(IncreaseDecreaseType command, FullLight light) {
400         StateUpdate stateUpdate = null;
401         Integer currentColorTemp = getCurrentColorTemp(light.getState());
402         if (currentColorTemp != null) {
403             int newColorTemp = LightStateConverter.toAdjustedColorTemp(command, currentColorTemp,
404                     colorTemperatureCapabilties);
405             stateUpdate = new StateUpdate().setColorTemperature(newColorTemp, colorTemperatureCapabilties);
406         }
407         return stateUpdate;
408     }
409
410     private @Nullable Integer getCurrentColorTemp(@Nullable State lightState) {
411         Integer colorTemp = lastSentColorTemp;
412         if (colorTemp == null && lightState != null) {
413             colorTemp = lightState.getColorTemperature();
414         }
415         return colorTemp;
416     }
417
418     private @Nullable StateUpdate convertBrightnessChangeToStateUpdate(IncreaseDecreaseType command, FullLight light) {
419         StateUpdate stateUpdate = null;
420         Integer currentBrightness = getCurrentBrightness(light.getState());
421         if (currentBrightness != null) {
422             int newBrightness = LightStateConverter.toAdjustedBrightness(command, currentBrightness);
423             stateUpdate = createBrightnessStateUpdate(currentBrightness, newBrightness);
424         }
425         return stateUpdate;
426     }
427
428     private @Nullable Integer getCurrentBrightness(@Nullable State lightState) {
429         Integer brightness = lastSentBrightness;
430         if (brightness == null && lightState != null) {
431             if (!lightState.isOn()) {
432                 brightness = 0;
433             } else {
434                 brightness = lightState.getBrightness();
435             }
436         }
437         return brightness;
438     }
439
440     private StateUpdate createBrightnessStateUpdate(int currentBrightness, int newBrightness) {
441         StateUpdate lightUpdate = new StateUpdate();
442         if (newBrightness == 0) {
443             lightUpdate.turnOff();
444         } else {
445             lightUpdate.setBrightness(newBrightness);
446             if (currentBrightness == 0) {
447                 lightUpdate.turnOn();
448             }
449         }
450         return lightUpdate;
451     }
452
453     protected synchronized @Nullable HueClient getHueClient() {
454         if (hueClient == null) {
455             Bridge bridge = getBridge();
456             if (bridge == null) {
457                 return null;
458             }
459             ThingHandler handler = bridge.getHandler();
460             if (handler instanceof HueClient) {
461                 HueClient bridgeHandler = (HueClient) handler;
462                 hueClient = bridgeHandler;
463                 bridgeHandler.registerLightStatusListener(this);
464             } else {
465                 return null;
466             }
467         }
468         return hueClient;
469     }
470
471     @Override
472     public void setPollBypass(long bypassTime) {
473         endBypassTime = System.currentTimeMillis() + bypassTime;
474     }
475
476     @Override
477     public void unsetPollBypass() {
478         endBypassTime = 0L;
479     }
480
481     @Override
482     public boolean onLightStateChanged(FullLight fullLight) {
483         logger.trace("onLightStateChanged() was called");
484
485         if (System.currentTimeMillis() <= endBypassTime) {
486             logger.debug("Bypass light update after command ({}).", lightId);
487             return false;
488         }
489
490         State state = fullLight.getState();
491
492         final FullLight lastState = lastFullLight;
493         if (lastState == null || !Objects.equals(lastState.getState(), state)) {
494             lastFullLight = fullLight;
495         } else {
496             return true;
497         }
498
499         logger.trace("New state for light {}", lightId);
500
501         initializeProperties(fullLight);
502
503         lastSentColorTemp = null;
504         lastSentBrightness = null;
505
506         // update status (ONLINE, OFFLINE)
507         if (state.isReachable()) {
508             updateStatus(ThingStatus.ONLINE);
509         } else {
510             // we assume OFFLINE without any error (NONE), as this is an
511             // expected state (when bulb powered off)
512             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/offline.light-not-reachable");
513         }
514
515         logger.debug("onLightStateChanged Light {}: on {} bri {} hue {} sat {} temp {} mode {} XY {}",
516                 fullLight.getName(), state.isOn(), state.getBrightness(), state.getHue(), state.getSaturation(),
517                 state.getColorTemperature(), state.getColorMode(), state.getXY());
518
519         HSBType hsbType = LightStateConverter.toHSBType(state);
520         if (!state.isOn()) {
521             hsbType = new HSBType(hsbType.getHue(), hsbType.getSaturation(), PercentType.ZERO);
522         }
523         updateState(CHANNEL_COLOR, hsbType);
524
525         ColorMode colorMode = state.getColorMode();
526         if (ColorMode.CT.equals(colorMode)) {
527             updateState(CHANNEL_COLORTEMPERATURE,
528                     LightStateConverter.toColorTemperaturePercentType(state, colorTemperatureCapabilties));
529             updateState(CHANNEL_COLORTEMPERATURE_ABS, LightStateConverter.toColorTemperature(state));
530         } else {
531             updateState(CHANNEL_COLORTEMPERATURE, UnDefType.UNDEF);
532             updateState(CHANNEL_COLORTEMPERATURE_ABS, UnDefType.UNDEF);
533         }
534
535         PercentType brightnessPercentType = LightStateConverter.toBrightnessPercentType(state);
536         if (!state.isOn()) {
537             brightnessPercentType = PercentType.ZERO;
538         }
539         updateState(CHANNEL_BRIGHTNESS, brightnessPercentType);
540
541         updateState(CHANNEL_SWITCH, OnOffType.from(state.isOn()));
542
543         StringType stringType = LightStateConverter.toAlertStringType(state);
544         if (!"NULL".equals(stringType.toString())) {
545             updateState(CHANNEL_ALERT, stringType);
546             scheduleAlertStateRestore(stringType);
547         }
548
549         return true;
550     }
551
552     @Override
553     public void channelLinked(ChannelUID channelUID) {
554         HueClient handler = getHueClient();
555         if (handler != null) {
556             FullLight light = handler.getLightById(lightId);
557             if (light != null) {
558                 onLightStateChanged(light);
559             }
560         }
561     }
562
563     @Override
564     public void onLightRemoved() {
565         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/offline.light-removed");
566     }
567
568     @Override
569     public void onLightGone() {
570         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, "@text/offline.light-not-reachable");
571     }
572
573     @Override
574     public void onLightAdded(FullLight light) {
575         onLightStateChanged(light);
576     }
577
578     /**
579      * Schedules restoration of the alert item state to {@link LightStateConverter#ALERT_MODE_NONE} after a given time.
580      * <br>
581      * Based on the initial command:
582      * <ul>
583      * <li>For {@link LightStateConverter#ALERT_MODE_SELECT} restoration will be triggered after <strong>2
584      * seconds</strong>.
585      * <li>For {@link LightStateConverter#ALERT_MODE_LONG_SELECT} restoration will be triggered after <strong>15
586      * seconds</strong>.
587      * </ul>
588      * This method also cancels any previously scheduled restoration.
589      *
590      * @param command The {@link Command} sent to the item
591      */
592     private void scheduleAlertStateRestore(Command command) {
593         cancelScheduledFuture();
594         int delay = getAlertDuration(command);
595
596         if (delay > 0) {
597             scheduledFuture = scheduler.schedule(() -> {
598                 updateState(CHANNEL_ALERT, new StringType(LightStateConverter.ALERT_MODE_NONE));
599             }, delay, TimeUnit.MILLISECONDS);
600         }
601     }
602
603     /**
604      * This method will cancel previously scheduled alert item state
605      * restoration.
606      */
607     private void cancelScheduledFuture() {
608         ScheduledFuture<?> scheduledJob = scheduledFuture;
609         if (scheduledJob != null) {
610             scheduledJob.cancel(true);
611             scheduledFuture = null;
612         }
613     }
614
615     /**
616      * This method returns the time in <strong>milliseconds</strong> after
617      * which, the state of the alert item has to be restored to {@link LightStateConverter#ALERT_MODE_NONE}.
618      *
619      * @param command The initial command sent to the alert item.
620      * @return Based on the initial command will return:
621      *         <ul>
622      *         <li><strong>2000</strong> for {@link LightStateConverter#ALERT_MODE_SELECT}.
623      *         <li><strong>15000</strong> for {@link LightStateConverter#ALERT_MODE_LONG_SELECT}.
624      *         <li><strong>-1</strong> for any command different from the previous two.
625      *         </ul>
626      */
627     private int getAlertDuration(Command command) {
628         int delay;
629         switch (command.toString()) {
630             case LightStateConverter.ALERT_MODE_LONG_SELECT:
631                 delay = 15000;
632                 break;
633             case LightStateConverter.ALERT_MODE_SELECT:
634                 delay = 2000;
635                 break;
636             default:
637                 delay = -1;
638                 break;
639         }
640
641         return delay;
642     }
643
644     @Override
645     public Collection<Class<? extends ThingHandlerService>> getServices() {
646         return List.of(LightActions.class);
647     }
648
649     @Override
650     public String getLightId() {
651         return lightId;
652     }
653 }