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