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