]> git.basschouten.com Git - openhab-addons.git/blob
a67101a79db6ba6ab959d66b44a90701e141b2ab
[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.insteon.internal.device.feature;
14
15 import static org.openhab.binding.insteon.internal.InsteonBindingConstants.*;
16
17 import java.lang.reflect.InvocationTargetException;
18 import java.time.ZonedDateTime;
19 import java.util.HashMap;
20 import java.util.Map;
21 import java.util.Objects;
22 import java.util.Set;
23
24 import javax.measure.Unit;
25 import javax.measure.quantity.Dimensionless;
26 import javax.measure.quantity.Temperature;
27 import javax.measure.quantity.Time;
28
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.openhab.binding.insteon.internal.config.InsteonChannelConfiguration;
32 import org.openhab.binding.insteon.internal.device.DeviceFeature;
33 import org.openhab.binding.insteon.internal.device.InsteonAddress;
34 import org.openhab.binding.insteon.internal.device.ProductData;
35 import org.openhab.binding.insteon.internal.device.RampRate;
36 import org.openhab.binding.insteon.internal.device.X10Address;
37 import org.openhab.binding.insteon.internal.device.X10Command;
38 import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.FanLincFanSpeed;
39 import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.IOLincRelayMode;
40 import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.KeypadButtonConfig;
41 import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.KeypadButtonToggleMode;
42 import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.MicroModuleOpMode;
43 import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.SirenAlertType;
44 import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.ThermostatFanMode;
45 import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.ThermostatSystemMode;
46 import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.ThermostatTemperatureScale;
47 import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.ThermostatTimeFormat;
48 import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.VenstarSystemMode;
49 import org.openhab.binding.insteon.internal.transport.message.FieldException;
50 import org.openhab.binding.insteon.internal.transport.message.InvalidMessageTypeException;
51 import org.openhab.binding.insteon.internal.transport.message.Msg;
52 import org.openhab.binding.insteon.internal.utils.BinaryUtils;
53 import org.openhab.binding.insteon.internal.utils.HexUtils;
54 import org.openhab.core.library.types.DecimalType;
55 import org.openhab.core.library.types.IncreaseDecreaseType;
56 import org.openhab.core.library.types.NextPreviousType;
57 import org.openhab.core.library.types.OnOffType;
58 import org.openhab.core.library.types.PercentType;
59 import org.openhab.core.library.types.PlayPauseType;
60 import org.openhab.core.library.types.QuantityType;
61 import org.openhab.core.library.types.StopMoveType;
62 import org.openhab.core.library.types.StringType;
63 import org.openhab.core.library.types.UpDownType;
64 import org.openhab.core.library.unit.ImperialUnits;
65 import org.openhab.core.library.unit.SIUnits;
66 import org.openhab.core.library.unit.Units;
67 import org.openhab.core.types.Command;
68 import org.openhab.core.types.RefreshType;
69 import org.openhab.core.types.State;
70 import org.slf4j.Logger;
71 import org.slf4j.LoggerFactory;
72
73 /**
74  * A command handler translates an openHAB command into a insteon message
75  *
76  * @author Daniel Pfrommer - Initial contribution
77  * @author Bernd Pfrommer - openHAB 1 insteonplm binding
78  * @author Rob Nielsen - Port to openHAB 2 insteon binding
79  * @author Jeremy Setton - Rewrite insteon binding
80  */
81 @NonNullByDefault
82 public abstract class CommandHandler extends BaseFeatureHandler {
83     private static final Set<String> SUPPORTED_COMMAND_TYPES = Set.of("DecimalType", "IncreaseDecreaseType",
84             "OnOffType", "NextPreviousType", "PercentType", "PlayPauseType", "QuantityType", "RefreshType",
85             "RewindFastforwardType", "StopMoveType", "StringType", "UpDownType");
86
87     protected final Logger logger = LoggerFactory.getLogger(CommandHandler.class);
88
89     /**
90      * Constructor
91      *
92      * @param feature The DeviceFeature for which this command was intended.
93      *            The openHAB commands are issued on an openhab item. The .items files bind
94      *            an openHAB item to a DeviceFeature.
95      */
96     public CommandHandler(DeviceFeature feature) {
97         super(feature);
98     }
99
100     /**
101      * Returns handler id
102      *
103      * @return handler id based on command parameter
104      */
105     public String getId() {
106         return getParameterAsString("command", "default");
107     }
108
109     /**
110      * Returns if handler can handle the openHAB command received
111      *
112      * @param cmd the openhab command received
113      * @return true if can handle
114      */
115     public abstract boolean canHandle(Command cmd);
116
117     /**
118      * Implements what to do when an openHAB command is received
119      *
120      * @param channelUID the channel uid that generated the command
121      * @param config the channel configuration that generated the command
122      * @param cmd the openhab command to handle
123      */
124     public abstract void handleCommand(InsteonChannelConfiguration config, Command cmd);
125
126     /**
127      * Default command handler
128      */
129     public static class DefaultCommandHandler extends CommandHandler {
130         DefaultCommandHandler(DeviceFeature feature) {
131             super(feature);
132         }
133
134         @Override
135         public boolean canHandle(Command cmd) {
136             return true;
137         }
138
139         @Override
140         public void handleCommand(InsteonChannelConfiguration config, Command cmd) {
141             logger.warn("{}: command {}:{} is not supported", nm(), cmd.getClass().getSimpleName(), cmd);
142         }
143     }
144
145     /**
146      * No-op command handler
147      */
148     public static class NoOpCommandHandler extends CommandHandler {
149         NoOpCommandHandler(DeviceFeature feature) {
150             super(feature);
151         }
152
153         @Override
154         public boolean canHandle(Command cmd) {
155             return true;
156         }
157
158         @Override
159         public void handleCommand(InsteonChannelConfiguration config, Command cmd) {
160             // do nothing, not even log
161         }
162     }
163
164     /**
165      * Refresh command handler
166      */
167     public static class RefreshCommandHandler extends CommandHandler {
168         RefreshCommandHandler(DeviceFeature feature) {
169             super(feature);
170         }
171
172         @Override
173         public boolean canHandle(Command cmd) {
174             return cmd instanceof RefreshType;
175         }
176
177         @Override
178         public void handleCommand(InsteonChannelConfiguration config, Command cmd) {
179             feature.triggerPoll(0L);
180         }
181     }
182
183     /**
184      * Custom abstract command handler based of parameters
185      */
186     public abstract static class CustomCommandHandler extends CommandHandler {
187         CustomCommandHandler(DeviceFeature feature) {
188             super(feature);
189         }
190
191         @Override
192         public void handleCommand(InsteonChannelConfiguration config, Command cmd) {
193             int cmd1 = getParameterAsInteger("cmd1", -1);
194             int cmd2 = getParameterAsInteger("cmd2", 0);
195             int ext = getParameterAsInteger("ext", 0);
196             if (cmd1 == -1) {
197                 logger.warn("{}: handler misconfigured, no cmd1 parameter specified", nm());
198                 return;
199             }
200             if (ext < 0 || ext > 2) {
201                 logger.warn("{}: handler misconfigured, invalid ext parameter specified", nm());
202                 return;
203             }
204             // determine data field based on parameter, default to cmd2 if is standard message
205             String field = getParameterAsString("field", ext == 0 ? "command2" : "");
206             if (field.isEmpty()) {
207                 logger.warn("{}: handler misconfigured, no field parameter specified", nm());
208                 return;
209             }
210             // determine cmd value and apply factor ratio based of parameters
211             int value = (int) Math.round(getValue(cmd) * getParameterAsInteger("factor", 1));
212             if (value == -1) {
213                 logger.debug("{}: unable to determine command value, ignoring request", nm());
214                 return;
215             }
216             try {
217                 InsteonAddress address = getInsteonDevice().getAddress();
218                 boolean setCRC = getInsteonDevice().getInsteonEngine().supportsChecksum();
219                 Msg msg;
220                 if (ext == 0) {
221                     msg = Msg.makeStandardMessage(address, (byte) cmd1, (byte) cmd2);
222                 } else {
223                     // set userData1 to d1 parameter if defined, fallback to group parameter
224                     byte[] data = { (byte) getParameterAsInteger("d1", getParameterAsInteger("group", 0)),
225                             (byte) getParameterAsInteger("d2", 0), (byte) getParameterAsInteger("d3", 0) };
226                     msg = Msg.makeExtendedMessage(address, (byte) cmd1, (byte) cmd2, data, false);
227                 }
228                 // set field to clamped byte-size value
229                 msg.setByte(field, (byte) Math.min(value, 0xFF));
230                 // set crc based on message type if supported
231                 if (setCRC) {
232                     if (ext == 1) {
233                         msg.setCRC();
234                     } else if (ext == 2) {
235                         msg.setCRC2();
236                     }
237                 }
238                 // send request
239                 feature.sendRequest(msg);
240                 if (logger.isDebugEnabled()) {
241                     logger.debug("{}: sent {} {} request to {}", nm(), feature.getName(), HexUtils.getHexString(value),
242                             address);
243                 }
244             } catch (InvalidMessageTypeException e) {
245                 logger.warn("{}: invalid message: ", nm(), e);
246             } catch (FieldException e) {
247                 logger.warn("{}: command send message creation error ", nm(), e);
248             }
249         }
250
251         protected abstract double getValue(Command cmd);
252     }
253
254     /**
255      * Custom bitmask command handler based of parameters
256      */
257     public static class CustomBitmaskCommandHandler extends CustomCommandHandler {
258         CustomBitmaskCommandHandler(DeviceFeature feature) {
259             super(feature);
260         }
261
262         @Override
263         public boolean canHandle(Command cmd) {
264             return cmd instanceof OnOffType;
265         }
266
267         @Override
268         protected double getValue(Command cmd) {
269             return getBitmask(cmd);
270         }
271
272         protected int getBitNumber() {
273             return getParameterAsInteger("bit", -1);
274         }
275
276         protected @Nullable Boolean shouldSetBit(Command cmd) {
277             return OnOffType.ON.equals(cmd) ^ getParameterAsBoolean("inverted", false);
278         }
279
280         protected int getBitmask(Command cmd) {
281             // get bit number based on parameter
282             int bit = getBitNumber();
283             // get last bitmask message value received by this feature
284             int bitmask = feature.getLastMsgValueAsInteger(-1);
285             // determine if bit should be set
286             Boolean shouldSetBit = shouldSetBit(cmd);
287             // update last bitmask value specific bit based on cmd state, if defined and bit number valid
288             if (bit < 0 || bit > 7) {
289                 logger.debug("{}: invalid bit number {} for {}", nm(), bit, feature.getName());
290             } else if (bitmask == -1) {
291                 logger.debug("{}: unable to determine last bitmask for {}", nm(), feature.getName());
292             } else if (shouldSetBit == null) {
293                 logger.debug("{}: unable to determine if bit should be set, ignoring request", nm());
294             } else {
295                 if (logger.isTraceEnabled()) {
296                     logger.trace("{}: bitmask:{} bit:{} set:{}", nm(), BinaryUtils.getBinaryString(bitmask), bit,
297                             shouldSetBit);
298                 }
299                 return BinaryUtils.updateBit(bitmask, bit, shouldSetBit);
300             }
301             return -1;
302         }
303     }
304
305     /**
306      * Custom on/off type command handler based of parameters
307      */
308     public static class CustomOnOffCommandHandler extends CustomCommandHandler {
309         CustomOnOffCommandHandler(DeviceFeature feature) {
310             super(feature);
311         }
312
313         @Override
314         public boolean canHandle(Command cmd) {
315             return cmd instanceof OnOffType;
316         }
317
318         @Override
319         protected double getValue(Command cmd) {
320             return OnOffType.OFF.equals(cmd) ? getParameterAsInteger("off", 0x00) : getParameterAsInteger("on", 0xFF);
321         }
322     }
323
324     /**
325      * Custom decimal type command handler based of parameters
326      */
327     public static class CustomDecimalCommandHandler extends CustomCommandHandler {
328         CustomDecimalCommandHandler(DeviceFeature feature) {
329             super(feature);
330         }
331
332         @Override
333         public boolean canHandle(Command cmd) {
334             return cmd instanceof DecimalType;
335         }
336
337         @Override
338         protected double getValue(Command cmd) {
339             return ((DecimalType) cmd).doubleValue();
340         }
341     }
342
343     /**
344      * Custom percent type command handler based of parameters
345      */
346     public static class CustomPercentCommandHandler extends CustomCommandHandler {
347         CustomPercentCommandHandler(DeviceFeature feature) {
348             super(feature);
349         }
350
351         @Override
352         public boolean canHandle(Command cmd) {
353             return cmd instanceof PercentType;
354         }
355
356         @Override
357         protected double getValue(Command cmd) {
358             int minValue = getParameterAsInteger("min", 0x00);
359             int maxValue = getParameterAsInteger("max", 0xFF);
360             double value = ((PercentType) cmd).doubleValue();
361             return Math.round(value * (maxValue - minValue) / 100.0) + minValue;
362         }
363     }
364
365     /**
366      * Custom dimensionless quantity type command handler based of parameters
367      */
368     public static class CustomDimensionlessCommandHandler extends CustomCommandHandler {
369         CustomDimensionlessCommandHandler(DeviceFeature feature) {
370             super(feature);
371         }
372
373         @Override
374         public boolean canHandle(Command cmd) {
375             return cmd instanceof QuantityType;
376         }
377
378         @Override
379         protected double getValue(Command cmd) {
380             int minValue = getParameterAsInteger("min", 0);
381             int maxValue = getParameterAsInteger("max", 100);
382             @SuppressWarnings("unchecked")
383             double value = ((QuantityType<Dimensionless>) cmd).doubleValue();
384             return Math.round(value * (maxValue - minValue) / 100.0) + minValue;
385         }
386     }
387
388     /**
389      * Custom temperature quantity type command handler based of parameters
390      */
391     public static class CustomTemperatureCommandHandler extends CustomCommandHandler {
392         CustomTemperatureCommandHandler(DeviceFeature feature) {
393             super(feature);
394         }
395
396         @Override
397         public boolean canHandle(Command cmd) {
398             return cmd instanceof QuantityType;
399         }
400
401         @Override
402         protected double getValue(Command cmd) {
403             @SuppressWarnings("unchecked")
404             QuantityType<Temperature> temperature = (QuantityType<Temperature>) cmd;
405             Unit<Temperature> unit = getTemperatureUnit();
406             double value = Objects.requireNonNullElse(temperature.toInvertibleUnit(unit), temperature).doubleValue();
407             double increment = SIUnits.CELSIUS.equals(unit) ? 0.5 : 1;
408             return Math.round(value / increment) * increment; // round in increment based on temperature unit
409         }
410
411         private Unit<Temperature> getTemperatureUnit() {
412             String scale = getParameterAsString("scale", "");
413             switch (scale) {
414                 case "celsius":
415                     return SIUnits.CELSIUS;
416                 case "fahrenheit":
417                     return ImperialUnits.FAHRENHEIT;
418                 default:
419                     logger.debug("{}: no valid temperature scale parameter found, defaulting to: CELSIUS", nm());
420                     return SIUnits.CELSIUS;
421             }
422         }
423     }
424
425     /**
426      * Custom time quantity type command handler based of parameters
427      */
428     public static class CustomTimeCommandHandler extends CustomCommandHandler {
429         CustomTimeCommandHandler(DeviceFeature feature) {
430             super(feature);
431         }
432
433         @Override
434         public boolean canHandle(Command cmd) {
435             return cmd instanceof QuantityType;
436         }
437
438         @Override
439         protected double getValue(Command cmd) {
440             @SuppressWarnings("unchecked")
441             QuantityType<Time> time = (QuantityType<Time>) cmd;
442             Unit<Time> unit = getTimeUnit();
443             return Objects.requireNonNullElse(time.toInvertibleUnit(unit), time).doubleValue();
444         }
445
446         private Unit<Time> getTimeUnit() {
447             String scale = getParameterAsString("scale", "");
448             switch (scale) {
449                 case "hour":
450                     return Units.HOUR;
451                 case "minute":
452                     return Units.MINUTE;
453                 case "second":
454                     return Units.SECOND;
455                 default:
456                     logger.debug("{}: no valid time scale parameter found, defaulting to: SECONDS", nm());
457                     return Units.SECOND;
458             }
459         }
460     }
461
462     /**
463      * Generic on/off abstract command handler
464      */
465     public abstract static class OnOffCommandHandler extends CommandHandler {
466         OnOffCommandHandler(DeviceFeature feature) {
467             super(feature);
468         }
469
470         @Override
471         public boolean canHandle(Command cmd) {
472             return cmd instanceof OnOffType;
473         }
474
475         @Override
476         public void handleCommand(InsteonChannelConfiguration config, Command cmd) {
477             try {
478                 int cmd1 = getCommandCode(config, cmd);
479                 int level = getLevel(config, cmd);
480                 int group = getGroup(config);
481                 // ignore request if cmd1/level not defined, send broadcast msg if group defined, otherwise direct msg
482                 if (cmd1 == -1 || level == -1) {
483                     logger.debug("{}: unable to determine cmd1 or level value, ignoring request", nm());
484                 } else if (group != -1) {
485                     Msg msg = Msg.makeBroadcastMessage(group, (byte) cmd1, (byte) level);
486                     feature.sendRequest(msg);
487                     logger.debug("{}: sent broadcast {} request to group {}", nm(), cmd, group);
488                     // poll related devices to broadcast group,
489                     // allowing each responder feature to determine its own poll delay
490                     feature.pollRelatedDevices(group, -1);
491                 } else {
492                     InsteonAddress address = getInsteonDevice().getAddress();
493                     int componentId = feature.getGroup();
494                     Msg msg;
495                     if (componentId > 1) {
496                         byte[] data = { (byte) componentId };
497                         boolean setCRC = getInsteonDevice().getInsteonEngine().supportsChecksum();
498                         msg = Msg.makeExtendedMessage(address, (byte) cmd1, (byte) level, data, setCRC);
499                     } else {
500                         msg = Msg.makeStandardMessage(address, (byte) cmd1, (byte) level);
501                     }
502                     feature.sendRequest(msg);
503                     logger.debug("{}: sent {} request to {}", nm(), cmd, address);
504                     // adjust related devices if original channel config (initial request) and device sync enabled
505                     if (config.isOriginal() && getInsteonDevice().isDeviceSyncEnabled()) {
506                         feature.adjustRelatedDevices(config, cmd);
507                     }
508                 }
509             } catch (InvalidMessageTypeException e) {
510                 logger.warn("{}: invalid message: ", nm(), e);
511             } catch (FieldException e) {
512                 logger.warn("{}: command send message creation error ", nm(), e);
513             }
514         }
515
516         protected int getCommandCode(InsteonChannelConfiguration config, Command cmd) {
517             return OnOffType.OFF.equals(cmd) ? getParameterAsInteger("off", 0x13) : getParameterAsInteger("on", 0x11);
518         }
519
520         protected int getLevel(InsteonChannelConfiguration config, Command cmd) {
521             return OnOffType.OFF.equals(cmd) ? 0x00 : getOnLevel(config);
522         }
523
524         protected int getGroup(InsteonChannelConfiguration config) {
525             return -1;
526         }
527
528         private int getOnLevel(InsteonChannelConfiguration config) {
529             int level = config.getOnLevel();
530             if (level == -1) {
531                 State state = getInsteonDevice().getFeatureState(FEATURE_ON_LEVEL);
532                 level = (state instanceof PercentType percent ? percent : PercentType.HUNDRED).intValue();
533
534             }
535             logger.trace("{}: using on level {}%", nm(), level);
536             return (int) Math.ceil(level * 255.0 / 100); // round up
537         }
538     }
539
540     /**
541      * Dimmer on/off command handler
542      */
543     public static class DimmerOnOffCommandHandler extends OnOffCommandHandler {
544         DimmerOnOffCommandHandler(DeviceFeature feature) {
545             super(feature);
546         }
547
548         @Override
549         public void handleCommand(InsteonChannelConfiguration config, Command cmd) {
550             RampRate rampRate = config.getRampRate();
551             if (rampRate == null) {
552                 // standard command if ramp rate parameter not configured
553                 super.handleCommand(config, cmd);
554             } else if (rampRate == RampRate.INSTANT) {
555                 // instant dimmer command if ramp rate parameter is instant (0.1 sec)
556                 setInstantDimmer(config, cmd);
557             } else {
558                 // ramp dimmer command otherwise
559                 setRampDimmer(config, cmd);
560             }
561             // update state since dimmer related channels not automatically updated by the framework
562             PercentType state = getState(config, cmd);
563             feature.updateState(state);
564         }
565
566         @Override
567         protected int getCommandCode(InsteonChannelConfiguration config, Command cmd) {
568             return OnOffType.OFF.equals(cmd) ? 0x13 : 0x11;
569         }
570
571         @Override
572         protected int getGroup(InsteonChannelConfiguration config) {
573             return config.getOnLevel() == -1 && getInsteonDevice().isDeviceSyncEnabled()
574                     ? feature.getBroadcastGroup(config)
575                     : -1;
576         }
577
578         protected PercentType getState(InsteonChannelConfiguration config, Command cmd) {
579             if (OnOffType.OFF.equals(cmd)) {
580                 return PercentType.ZERO;
581             }
582             int level = config.getOnLevel();
583             if (level != -1) {
584                 return new PercentType(level);
585             }
586             State state = getInsteonDevice().getFeatureState(FEATURE_ON_LEVEL);
587             if (state instanceof PercentType percent) {
588                 return percent;
589             }
590             return PercentType.HUNDRED;
591         }
592
593         private void setInstantDimmer(InsteonChannelConfiguration config, Command cmd) {
594             InstantDimmerCommandHandler handler = new InstantDimmerCommandHandler(feature);
595             handler.setParameters(parameters);
596             handler.handleCommand(config, cmd);
597         }
598
599         private void setRampDimmer(InsteonChannelConfiguration config, Command cmd) {
600             RampDimmerCommandHandler handler = new RampDimmerCommandHandler(feature);
601             handler.setParameters(parameters);
602             handler.handleCommand(config, cmd);
603         }
604     }
605
606     /**
607      * Dimmer percent command handler
608      */
609     public static class DimmerPercentCommandHandler extends DimmerOnOffCommandHandler {
610         DimmerPercentCommandHandler(DeviceFeature feature) {
611             super(feature);
612         }
613
614         @Override
615         public boolean canHandle(Command cmd) {
616             return cmd instanceof PercentType;
617         }
618
619         @Override
620         protected int getCommandCode(InsteonChannelConfiguration config, Command cmd) {
621             return PercentType.ZERO.equals(cmd) ? 0x13 : 0x11;
622         }
623
624         @Override
625         protected int getLevel(InsteonChannelConfiguration config, Command cmd) {
626             int level = ((PercentType) cmd).intValue();
627             return (int) Math.ceil(level * 255.0 / 100); // round up
628         }
629
630         @Override
631         protected int getGroup(InsteonChannelConfiguration config) {
632             return -1;
633         }
634
635         @Override
636         protected PercentType getState(InsteonChannelConfiguration config, Command cmd) {
637             return (PercentType) cmd;
638         }
639     }
640
641     /**
642      * Dimmer increase/decrease command handler
643      */
644     public static class DimmerIncreaseDecreaseCommandHandler extends OnOffCommandHandler {
645         DimmerIncreaseDecreaseCommandHandler(DeviceFeature feature) {
646             super(feature);
647         }
648
649         @Override
650         public boolean canHandle(Command cmd) {
651             return cmd instanceof IncreaseDecreaseType;
652         }
653
654         @Override
655         protected int getCommandCode(InsteonChannelConfiguration config, Command cmd) {
656             return IncreaseDecreaseType.INCREASE.equals(cmd) ? 0x15 : 0x16;
657         }
658
659         @Override
660         protected int getLevel(InsteonChannelConfiguration config, Command cmd) {
661             return 0x00; // not parsed
662         }
663
664         @Override
665         protected int getGroup(InsteonChannelConfiguration config) {
666             return getInsteonDevice().isDeviceSyncEnabled() ? feature.getBroadcastGroup(config) : -1;
667         }
668     }
669
670     /**
671      * Rollershutter up/down command handler
672      */
673     public static class RollershutterUpDownCommandHandler extends OnOffCommandHandler {
674         RollershutterUpDownCommandHandler(DeviceFeature feature) {
675             super(feature);
676         }
677
678         @Override
679         public boolean canHandle(Command cmd) {
680             return cmd instanceof UpDownType;
681         }
682
683         @Override
684         protected int getCommandCode(InsteonChannelConfiguration config, Command cmd) {
685             return 0x17; // manual change start
686         }
687
688         @Override
689         protected int getLevel(InsteonChannelConfiguration config, Command cmd) {
690             return UpDownType.UP.equals(cmd) ? 0x01 : 0x00; // up or down
691         }
692
693         @Override
694         protected int getGroup(InsteonChannelConfiguration config) {
695             return getInsteonDevice().isDeviceSyncEnabled() ? feature.getBroadcastGroup(config) : -1;
696         }
697     }
698
699     /**
700      * Rollershutter stop command handler
701      */
702     public static class RollershutterStopCommandHandler extends OnOffCommandHandler {
703         RollershutterStopCommandHandler(DeviceFeature feature) {
704             super(feature);
705         }
706
707         @Override
708         public boolean canHandle(Command cmd) {
709             return StopMoveType.STOP.equals(cmd);
710         }
711
712         @Override
713         protected int getCommandCode(InsteonChannelConfiguration config, Command cmd) {
714             return 0x18; // manual change stop
715         }
716
717         @Override
718         protected int getLevel(InsteonChannelConfiguration config, Command cmd) {
719             return 0x00; // not parsed
720         }
721
722         @Override
723         protected int getGroup(InsteonChannelConfiguration config) {
724             return getInsteonDevice().isDeviceSyncEnabled() ? feature.getBroadcastGroup(config) : -1;
725         }
726     }
727
728     /**
729      * Instant dimmer command handler
730      */
731     public static class InstantDimmerCommandHandler extends OnOffCommandHandler {
732         InstantDimmerCommandHandler(DeviceFeature feature) {
733             super(feature);
734         }
735
736         @Override
737         public boolean canHandle(Command cmd) {
738             return cmd instanceof OnOffType || cmd instanceof PercentType;
739         }
740
741         @Override
742         protected int getCommandCode(InsteonChannelConfiguration config, Command cmd) {
743             return 0x21;
744         }
745
746         @Override
747         protected int getLevel(InsteonChannelConfiguration config, Command cmd) {
748             if (cmd instanceof PercentType percent) {
749                 return (int) Math.ceil(percent.intValue() * 255.0 / 100); // round up
750             } else {
751                 return super.getLevel(config, cmd);
752             }
753         }
754
755         @Override
756         protected int getGroup(InsteonChannelConfiguration config) {
757             return -1;
758         }
759     }
760
761     /**
762      * Ramp dimmer command handler
763      */
764     public static class RampDimmerCommandHandler extends InstantDimmerCommandHandler {
765         RampDimmerCommandHandler(DeviceFeature feature) {
766             super(feature);
767         }
768
769         @Override
770         public void handleCommand(InsteonChannelConfiguration config, Command cmd) {
771             try {
772                 InsteonAddress address = getInsteonDevice().getAddress();
773                 int level = getLevel(config, cmd);
774                 RampRate rampRate = getRampRate(config);
775                 int cmd1 = getCommandCode(level);
776                 int cmd2 = getEncodedValue(level, rampRate.getValue());
777                 Msg msg = Msg.makeStandardMessage(address, (byte) cmd1, (byte) cmd2);
778                 feature.sendRequest(msg);
779                 logger.debug("{}: sent level {} with ramp time {} to {}", nm(), cmd, rampRate, address);
780                 if (config.isOriginal() && getInsteonDevice().isDeviceSyncEnabled()) {
781                     feature.adjustRelatedDevices(config, cmd);
782                 }
783             } catch (InvalidMessageTypeException e) {
784                 logger.warn("{}: invalid message: ", nm(), e);
785             } catch (FieldException e) {
786                 logger.warn("{}: command send message creation error ", nm(), e);
787             }
788         }
789
790         private RampRate getRampRate(InsteonChannelConfiguration config) {
791             return Objects.requireNonNullElse(config.getRampRate(), RampRate.DEFAULT);
792         }
793
794         private int getCommandCode(int level) {
795             ProductData productData = getInsteonDevice().getProductData();
796             // newer device with firmware >= 0x44 supports commands 0x34/0x35, while older supports 0x2E/0x2F
797             if (productData != null && productData.getFirmwareVersion() >= 0x44) {
798                 return level > 0 ? 0x34 : 0x35;
799             } else {
800                 return level > 0 ? 0x2E : 0x2F;
801             }
802         }
803
804         private int getEncodedValue(int level, int rampRate) {
805             int highByte = (int) Math.round(Math.max(0, level - 0x0F) / 16.0);
806             int lowByte = (int) Math.round(Math.max(0, rampRate - 0x01) / 2.0);
807             return highByte << 4 | lowByte;
808         }
809     }
810
811     /**
812      * Switch on/off command handler
813      */
814     public static class SwitchOnOffCommandHandler extends OnOffCommandHandler {
815         SwitchOnOffCommandHandler(DeviceFeature feature) {
816             super(feature);
817         }
818
819         @Override
820         protected int getCommandCode(InsteonChannelConfiguration config, Command cmd) {
821             return OnOffType.OFF.equals(cmd) ? 0x13 : 0x11;
822         }
823
824         @Override
825         protected int getLevel(InsteonChannelConfiguration config, Command cmd) {
826             return OnOffType.OFF.equals(cmd) ? 0x00 : 0xFF;
827         }
828
829         @Override
830         protected int getGroup(InsteonChannelConfiguration config) {
831             return getInsteonDevice().isDeviceSyncEnabled() ? feature.getBroadcastGroup(config) : -1;
832         }
833     }
834
835     /**
836      * Switch percent command handler
837      */
838     public static class SwitchPercentCommandHandler extends OnOffCommandHandler {
839         SwitchPercentCommandHandler(DeviceFeature feature) {
840             super(feature);
841         }
842
843         @Override
844         public boolean canHandle(Command cmd) {
845             return cmd instanceof PercentType;
846         }
847
848         @Override
849         protected int getCommandCode(InsteonChannelConfiguration config, Command cmd) {
850             return PercentType.ZERO.equals(cmd) ? 0x13 : 0x11;
851         }
852
853         @Override
854         protected int getLevel(InsteonChannelConfiguration config, Command cmd) {
855             return PercentType.ZERO.equals(cmd) ? 0x00 : 0xFF;
856         }
857     }
858
859     /**
860      * Switch increment command handler
861      */
862     public static class SwitchIncrementCommandHandler extends OnOffCommandHandler {
863         SwitchIncrementCommandHandler(DeviceFeature feature) {
864             super(feature);
865         }
866
867         @Override
868         public boolean canHandle(Command cmd) {
869             return IncreaseDecreaseType.INCREASE.equals(cmd) || UpDownType.UP.equals(cmd);
870         }
871
872         @Override
873         protected int getCommandCode(InsteonChannelConfiguration config, Command cmd) {
874             return 0x11;
875         }
876
877         @Override
878         protected int getLevel(InsteonChannelConfiguration config, Command cmd) {
879             return 0xFF;
880         }
881     }
882
883     /**
884      * Broadcast on/off command handler
885      */
886     public static class BroadcastOnOffCommandHandler extends OnOffCommandHandler {
887         BroadcastOnOffCommandHandler(DeviceFeature feature) {
888             super(feature);
889         }
890
891         @Override
892         public void handleCommand(InsteonChannelConfiguration config, Command cmd) {
893             if (getGroup(config) != -1) {
894                 super.handleCommand(config, cmd);
895             }
896         }
897
898         @Override
899         protected int getCommandCode(InsteonChannelConfiguration config, Command cmd) {
900             return OnOffType.OFF.equals(cmd) ? 0x13 : 0x11;
901         }
902
903         @Override
904         protected int getLevel(InsteonChannelConfiguration config, Command cmd) {
905             return 0x00; // not parsed
906         }
907
908         @Override
909         protected int getGroup(InsteonChannelConfiguration config) {
910             return feature.getBroadcastGroup(config);
911         }
912     }
913
914     /**
915      * Broadcast fast on/off command handler
916      */
917     public static class BroadcastFastOnOffCommandHandler extends BroadcastOnOffCommandHandler {
918         BroadcastFastOnOffCommandHandler(DeviceFeature feature) {
919             super(feature);
920         }
921
922         @Override
923         protected int getCommandCode(InsteonChannelConfiguration config, Command cmd) {
924             return OnOffType.OFF.equals(cmd) ? 0x14 : 0x12;
925         }
926     }
927
928     /**
929      * Broadcast manual change up/down command handler
930      */
931     public static class BroadcastManualChangeUpDownCommandHandler extends BroadcastOnOffCommandHandler {
932         BroadcastManualChangeUpDownCommandHandler(DeviceFeature feature) {
933             super(feature);
934         }
935
936         @Override
937         public boolean canHandle(Command cmd) {
938             return cmd instanceof UpDownType;
939         }
940
941         @Override
942         protected int getCommandCode(InsteonChannelConfiguration config, Command cmd) {
943             return 0x17; // manual change start
944         }
945
946         @Override
947         protected int getLevel(InsteonChannelConfiguration config, Command cmd) {
948             return UpDownType.UP.equals(cmd) ? 0x01 : 0x00; // up or down
949         }
950     }
951
952     /**
953      * Broadcast manual change stop command handler
954      */
955     public static class BroadcastManualChangeStopCommandHandler extends BroadcastOnOffCommandHandler {
956         BroadcastManualChangeStopCommandHandler(DeviceFeature feature) {
957             super(feature);
958         }
959
960         @Override
961         public boolean canHandle(Command cmd) {
962             return StopMoveType.STOP.equals(cmd);
963         }
964
965         @Override
966         protected int getCommandCode(InsteonChannelConfiguration config, Command cmd) {
967             return 0x18; // manual change stop
968         }
969     }
970
971     /**
972      * Broadcast refresh command handler
973      */
974     public static class BroadcastRefreshCommandHandler extends RefreshCommandHandler {
975         BroadcastRefreshCommandHandler(DeviceFeature feature) {
976             super(feature);
977         }
978
979         @Override
980         public void handleCommand(InsteonChannelConfiguration config, Command cmd) {
981             int group = feature.getBroadcastGroup(config);
982             if (group != -1) {
983                 feature.pollRelatedDevices(group, 0L);
984             }
985         }
986     }
987
988     /**
989      * Keypad button on/off command handler
990      */
991     public static class KeypadButtonOnOffCommandHandler extends CustomBitmaskCommandHandler {
992         KeypadButtonOnOffCommandHandler(DeviceFeature feature) {
993             super(feature);
994         }
995
996         @Override
997         public void handleCommand(InsteonChannelConfiguration config, Command cmd) {
998             OnOffType onOffCmd = getOnOffCommand(cmd);
999             int group = getGroup(config);
1000             KeypadButtonToggleMode toggleMode = getToggleMode();
1001             if (KeypadButtonToggleMode.ALWAYS_ON.equals(toggleMode) && OnOffType.OFF.equals(onOffCmd)
1002                     || KeypadButtonToggleMode.ALWAYS_OFF.equals(toggleMode) && OnOffType.ON.equals(onOffCmd)) {
1003                 // ignore command when keypad button toggle mode is always on or off
1004                 logger.debug("{}: {} toggle mode is {}, ignoring {} command", nm(), feature.getName(), toggleMode,
1005                         onOffCmd);
1006             } else if (group != -1) {
1007                 // send broadcast message if group defined
1008                 logger.debug("{}: sending broadcast message", nm());
1009                 sendBroadcastOnOff(config, onOffCmd);
1010                 // update state since button channels not automatically updated by the framework
1011                 feature.updateState(onOffCmd);
1012             } else {
1013                 // set button led bitmask otherwise
1014                 logger.debug("{}: setting button led bitmask", nm());
1015                 super.handleCommand(config, onOffCmd);
1016                 // update state since button channels not automatically updated by the framework
1017                 feature.updateState(onOffCmd);
1018                 // adjust related devices if original channel config and device sync enabled
1019                 if (config.isOriginal() && getInsteonDevice().isDeviceSyncEnabled()) {
1020                     feature.adjustRelatedDevices(config, cmd);
1021                 }
1022             }
1023         }
1024
1025         @Override
1026         protected int getBitNumber() {
1027             return feature.getGroup() - 1;
1028         }
1029
1030         @Override
1031         protected int getBitmask(Command cmd) {
1032             int bitmask = super.getBitmask(cmd);
1033             if (bitmask != -1) {
1034                 int onMask = getInsteonDevice().getLastMsgValueAsInteger(FEATURE_TYPE_KEYPAD_BUTTON_ON_MASK,
1035                         feature.getGroup(), -1);
1036                 int offMask = getInsteonDevice().getLastMsgValueAsInteger(FEATURE_TYPE_KEYPAD_BUTTON_OFF_MASK,
1037                         feature.getGroup(), -1);
1038                 if (onMask == -1 || offMask == -1) {
1039                     logger.debug("{}: undefined button on/off mask last values for {}", nm(), feature.getName());
1040                     bitmask = -1;
1041                 } else {
1042                     if (logger.isTraceEnabled()) {
1043                         logger.trace("{}: bitmask:{} onMask:{} offMask:{}", nm(), BinaryUtils.getBinaryString(bitmask),
1044                                 BinaryUtils.getBinaryString(onMask), BinaryUtils.getBinaryString(offMask));
1045                     }
1046                     // apply button on/off mask
1047                     bitmask = bitmask & ~offMask | onMask;
1048                     // update last bitmask value
1049                     updateLastBitmaskValue(bitmask);
1050                 }
1051             }
1052             return bitmask;
1053         }
1054
1055         protected OnOffType getOnOffCommand(Command cmd) {
1056             return (OnOffType) cmd;
1057         }
1058
1059         protected int getGroup(InsteonChannelConfiguration config) {
1060             return getInsteonDevice().isDeviceSyncEnabled() ? feature.getBroadcastGroup(config) : -1;
1061         }
1062
1063         private KeypadButtonToggleMode getToggleMode() {
1064             try {
1065                 State state = getInsteonDevice().getFeatureState(FEATURE_TYPE_KEYPAD_BUTTON_TOGGLE_MODE,
1066                         feature.getGroup());
1067                 if (state != null) {
1068                     return KeypadButtonToggleMode.valueOf(state.toString());
1069                 }
1070             } catch (IllegalArgumentException e) {
1071             }
1072             return KeypadButtonToggleMode.TOGGLE;
1073         }
1074
1075         private void sendBroadcastOnOff(InsteonChannelConfiguration config, Command cmd) {
1076             BroadcastOnOffCommandHandler handler = new BroadcastOnOffCommandHandler(feature);
1077             handler.setParameters(parameters);
1078             handler.handleCommand(config, cmd);
1079         }
1080
1081         private void updateLastBitmaskValue(int value) {
1082             DeviceFeature groupFeature = feature.getGroupFeature();
1083             if (groupFeature != null) {
1084                 // set button group feature last msg value
1085                 groupFeature.setLastMsgValue(value);
1086                 // set button related features last msg value
1087                 groupFeature.getConnectedFeatures().forEach(feature -> feature.setLastMsgValue(value));
1088             }
1089         }
1090     }
1091
1092     /**
1093      * Keypad button percent command handler
1094      */
1095     public static class KeypadButtonPercentCommandHandler extends KeypadButtonOnOffCommandHandler {
1096         KeypadButtonPercentCommandHandler(DeviceFeature feature) {
1097             super(feature);
1098         }
1099
1100         @Override
1101         public boolean canHandle(Command cmd) {
1102             return cmd instanceof PercentType;
1103         }
1104
1105         @Override
1106         protected OnOffType getOnOffCommand(Command cmd) {
1107             return OnOffType.from(!PercentType.ZERO.equals(cmd));
1108         }
1109
1110         @Override
1111         protected int getGroup(InsteonChannelConfiguration config) {
1112             return -1;
1113         }
1114     }
1115
1116     /**
1117      * Keypad button increment command handler
1118      */
1119     public static class KeypadButtonIncrementCommandHandler extends KeypadButtonOnOffCommandHandler {
1120         KeypadButtonIncrementCommandHandler(DeviceFeature feature) {
1121             super(feature);
1122         }
1123
1124         @Override
1125         public boolean canHandle(Command cmd) {
1126             return IncreaseDecreaseType.INCREASE.equals(cmd) || UpDownType.UP.equals(cmd);
1127         }
1128
1129         @Override
1130         protected OnOffType getOnOffCommand(Command cmd) {
1131             return OnOffType.ON;
1132         }
1133
1134         @Override
1135         protected int getGroup(InsteonChannelConfiguration config) {
1136             return -1;
1137         }
1138     }
1139
1140     /**
1141      * Keypad button config command handler
1142      */
1143     public static class KeypadButtonConfigCommandHandler extends OpFlagsCommandHandler {
1144         KeypadButtonConfigCommandHandler(DeviceFeature feature) {
1145             super(feature);
1146         }
1147
1148         @Override
1149         public boolean canHandle(Command cmd) {
1150             return cmd instanceof StringType;
1151         }
1152
1153         @Override
1154         protected int getOpFlagCommand(Command cmd) {
1155             try {
1156                 String config = ((StringType) cmd).toString();
1157                 return KeypadButtonConfig.valueOf(config).getValue();
1158             } catch (IllegalArgumentException e) {
1159                 logger.warn("{}: got unexpected button config command: {}, ignoring request", nm(), cmd);
1160                 return -1;
1161             }
1162         }
1163
1164         @Override
1165         protected boolean isStateRetrievable() {
1166             return true;
1167         }
1168     }
1169
1170     /**
1171      * Keypad button toggle mode command handler
1172      */
1173     public static class KeypadButtonToggleModeCommandHandler extends CommandHandler {
1174         KeypadButtonToggleModeCommandHandler(DeviceFeature feature) {
1175             super(feature);
1176         }
1177
1178         @Override
1179         public boolean canHandle(Command cmd) {
1180             return cmd instanceof DecimalType || cmd instanceof StringType;
1181         }
1182
1183         @Override
1184         public void handleCommand(InsteonChannelConfiguration config, Command cmd) {
1185             try {
1186                 if (cmd instanceof DecimalType decimalCmd) {
1187                     setToggleMode(decimalCmd.intValue() >> 8, decimalCmd.intValue() & 0xFF);
1188                 } else if (cmd instanceof StringType stringCmd) {
1189                     int bit = feature.getGroup() - 1;
1190                     if (bit < 0 || bit > 7) {
1191                         logger.debug("{}: invalid bit number {} for {}", nm(), bit, feature.getName());
1192                         return;
1193                     }
1194                     int lastValue = feature.getLastMsgValueAsInteger(-1);
1195                     if (lastValue == -1) {
1196                         logger.debug("{}: undefined toggle mode last value for {}", nm(), feature.getName());
1197                         return;
1198                     }
1199                     KeypadButtonToggleMode mode = KeypadButtonToggleMode.valueOf(stringCmd.toString());
1200                     int nonToggleMask = BinaryUtils.updateBit(lastValue >> 8, bit,
1201                             mode != KeypadButtonToggleMode.TOGGLE);
1202                     int alwaysOnOffMask = BinaryUtils.updateBit(lastValue & 0xFF, bit,
1203                             mode == KeypadButtonToggleMode.ALWAYS_ON);
1204                     setToggleMode(nonToggleMask, alwaysOnOffMask);
1205                 }
1206             } catch (IllegalArgumentException e) {
1207                 logger.warn("{}: got unexpected toggle mode command: {}, ignoring request", nm(), cmd);
1208             }
1209         }
1210
1211         private void setToggleMode(int nonToggleMask, int alwaysOnOffMask) {
1212             try {
1213                 InsteonAddress address = getInsteonDevice().getAddress();
1214                 boolean setCRC = getInsteonDevice().getInsteonEngine().supportsChecksum();
1215                 // define ext command message to set keypad button non toggle mask
1216                 Msg nonToggleMaskMsg = Msg.makeExtendedMessage(address, (byte) 0x2E, (byte) 0x00,
1217                         new byte[] { (byte) 0x01, (byte) 0x08, (byte) nonToggleMask }, setCRC);
1218                 // define ext command message to set keypad button always on/off mask
1219                 Msg alwaysOnOffMaskMsg = Msg.makeExtendedMessage(address, (byte) 0x2E, (byte) 0x00,
1220                         new byte[] { (byte) 0x01, (byte) 0x0B, (byte) alwaysOnOffMask }, setCRC);
1221                 // send requests
1222                 if (logger.isDebugEnabled()) {
1223                     logger.debug("{}: sent keypad button non toggle mask {} request to {}", nm(),
1224                             HexUtils.getHexString(nonToggleMask), address);
1225                 }
1226                 feature.sendRequest(nonToggleMaskMsg);
1227                 if (logger.isDebugEnabled()) {
1228                     logger.debug("{}: sent keypad button always on/off mask {} request to {}", nm(),
1229                             HexUtils.getHexString(alwaysOnOffMask), address);
1230                 }
1231                 feature.sendRequest(alwaysOnOffMaskMsg);
1232             } catch (InvalidMessageTypeException e) {
1233                 logger.warn("{}: invalid message: ", nm(), e);
1234             } catch (FieldException e) {
1235                 logger.warn("{}: command send message creation error ", nm(), e);
1236             }
1237         }
1238     }
1239
1240     /**
1241      * Heartbeat interval command handler
1242      */
1243     public static class HeartbeatIntervalCommandHandler extends CustomCommandHandler {
1244         HeartbeatIntervalCommandHandler(DeviceFeature feature) {
1245             super(feature);
1246         }
1247
1248         @Override
1249         public boolean canHandle(Command cmd) {
1250             return cmd instanceof DecimalType || cmd instanceof QuantityType;
1251         }
1252
1253         @Override
1254         protected double getValue(Command cmd) {
1255             int interval = getInterval(cmd);
1256             int increment = getParameterAsInteger("increment", -1);
1257             int preset = getParameterAsInteger("preset", 0);
1258             if (increment == -1) {
1259                 logger.warn("{}: no increment parameter specified in command handler", nm());
1260             } else if (interval == -1) {
1261                 logger.warn("{}: got unexpected heartbeat interval command: {}, ignoring request", nm(), cmd);
1262             } else {
1263                 int value = (int) Math.floor(interval / increment); // round down
1264                 return interval == preset ? 0x00 : Math.max(0x00, Math.min(value, 0xFF));
1265             }
1266             return -1;
1267         }
1268
1269         private int getInterval(Command cmd) {
1270             if (cmd instanceof DecimalType time) {
1271                 return time.intValue();
1272             } else if (cmd instanceof QuantityType<?> time) {
1273                 return Objects.requireNonNullElse(time.toInvertibleUnit(Units.MINUTE), time).intValue();
1274             }
1275             return -1;
1276         }
1277     }
1278
1279     /**
1280      * Motion sensor 2 heartbeat interval command handler
1281      */
1282     public static class MotionSensor2HeartbeatIntervalCommandHandler extends HeartbeatIntervalCommandHandler {
1283         MotionSensor2HeartbeatIntervalCommandHandler(DeviceFeature feature) {
1284             super(feature);
1285         }
1286
1287         @Override
1288         public void handleCommand(InsteonChannelConfiguration config, Command cmd) {
1289             try {
1290                 int heartbeatInterval = (int) getValue(cmd);
1291                 int lowBatteryThreshold = getInsteonDevice().getLastMsgValueAsInteger(FEATURE_LOW_BATTERY_THRESHOLD,
1292                         -1);
1293                 if (heartbeatInterval != -1 && lowBatteryThreshold != -1) {
1294                     InsteonAddress address = getInsteonDevice().getAddress();
1295                     byte[] data = { (byte) 0x00, (byte) 0x09, (byte) lowBatteryThreshold, (byte) heartbeatInterval };
1296                     Msg msg = Msg.makeExtendedMessage(address, (byte) 0x2E, (byte) 0x00, data, true);
1297                     feature.sendRequest(msg);
1298                     if (logger.isDebugEnabled()) {
1299                         logger.debug("{}: sent heartbeat interval {} request to {}", nm(),
1300                                 HexUtils.getHexString(heartbeatInterval), address);
1301                     }
1302                 }
1303             } catch (InvalidMessageTypeException e) {
1304                 logger.warn("{}: invalid message: ", nm(), e);
1305             } catch (FieldException e) {
1306                 logger.warn("{}: command send message creation error ", nm(), e);
1307             }
1308         }
1309     }
1310
1311     /**
1312      * Siren on/off command handler
1313      */
1314     public static class SirenOnOffCommandHandler extends SwitchOnOffCommandHandler {
1315         SirenOnOffCommandHandler(DeviceFeature feature) {
1316             super(feature);
1317         }
1318
1319         @Override
1320         protected int getLevel(InsteonChannelConfiguration config, Command cmd) {
1321             return OnOffType.OFF.equals(cmd) ? 0x00 : 0x7F; // no delay + max duration (127 seconds)
1322         }
1323     }
1324
1325     /**
1326      * Siren armed command handler
1327      */
1328     public static class SirenArmedCommandHandler extends OpFlagsCommandHandler {
1329         SirenArmedCommandHandler(DeviceFeature feature) {
1330             super(feature);
1331         }
1332
1333         @Override
1334         protected byte[] getOpFlagData(Command cmd) {
1335             return OnOffType.ON.equals(cmd) ? new byte[] { (byte) 0x01 } : new byte[0];
1336         }
1337
1338         @Override
1339         protected boolean isStateRetrievable() {
1340             return true;
1341         }
1342     }
1343
1344     /**
1345      * Siren alert duration command handler
1346      */
1347     public static class SirenAlertDurationCommandHandler extends CustomCommandHandler {
1348         SirenAlertDurationCommandHandler(DeviceFeature feature) {
1349             super(feature);
1350         }
1351
1352         @Override
1353         public boolean canHandle(Command cmd) {
1354             return cmd instanceof DecimalType || cmd instanceof QuantityType;
1355         }
1356
1357         @Override
1358         protected double getValue(Command cmd) {
1359             int duration = getDuration(cmd);
1360             int value = feature.getLastMsgValueAsInteger(-1);
1361             if (value == -1) {
1362                 logger.debug("{}: unable to determine last value for {}", nm(), feature.getName());
1363             } else if (duration == -1) {
1364                 logger.warn("{}: got unexpected siren alert duration cmd {}, ignoring request", nm(), cmd);
1365             } else {
1366                 return value & 0x80 | duration;
1367             }
1368             return -1;
1369         }
1370
1371         private int getDuration(Command cmd) {
1372             int duration = -1;
1373             if (cmd instanceof DecimalType time) {
1374                 duration = time.intValue();
1375             } else if (cmd instanceof QuantityType<?> time) {
1376                 duration = Objects.requireNonNullElse(time.toInvertibleUnit(Units.SECOND), time).intValue();
1377             }
1378             return duration != -1 ? Math.max(0, Math.min(duration, 127)) : -1; // allowed range 0-127 seconds
1379         }
1380     }
1381
1382     /**
1383      * Siren alert type command handler
1384      */
1385     public static class SirenAlertTypeCommandHandler extends CustomCommandHandler {
1386         SirenAlertTypeCommandHandler(DeviceFeature feature) {
1387             super(feature);
1388         }
1389
1390         @Override
1391         public boolean canHandle(Command cmd) {
1392             return cmd instanceof StringType;
1393         }
1394
1395         @Override
1396         protected double getValue(Command cmd) {
1397             try {
1398                 String type = ((StringType) cmd).toString();
1399                 return SirenAlertType.valueOf(type).getValue();
1400             } catch (IllegalArgumentException e) {
1401                 logger.warn("{}: got unexpected alert type command: {}, ignoring request", nm(), cmd);
1402                 return -1;
1403
1404             }
1405         }
1406     }
1407
1408     /**
1409      * LED brightness command handler
1410      */
1411     public static class LEDBrightnessCommandHandler extends CommandHandler {
1412         LEDBrightnessCommandHandler(DeviceFeature feature) {
1413             super(feature);
1414         }
1415
1416         @Override
1417         public boolean canHandle(Command cmd) {
1418             return cmd instanceof OnOffType || cmd instanceof PercentType;
1419         }
1420
1421         @Override
1422         public void handleCommand(InsteonChannelConfiguration config, Command cmd) {
1423             try {
1424                 int level = getLevel(cmd);
1425                 int userData2 = getParameterAsInteger("d2", -1);
1426                 if (userData2 != -1) {
1427                     // set led on/off
1428                     setLEDOnOff(config, OnOffType.from(level > 0));
1429                     // set led brightness level
1430                     InsteonAddress address = getInsteonDevice().getAddress();
1431                     byte[] data = { (byte) 0x01, (byte) userData2, (byte) level };
1432                     boolean setCRC = getInsteonDevice().getInsteonEngine().supportsChecksum();
1433                     Msg msg = Msg.makeExtendedMessage(address, (byte) 0x2E, (byte) 0x00, data, setCRC);
1434                     feature.sendRequest(msg);
1435                     if (logger.isDebugEnabled()) {
1436                         logger.debug("{}: sent led brightness level {} request to {}", nm(),
1437                                 HexUtils.getHexString(level), address);
1438                     }
1439                 } else {
1440                     logger.warn("{}: no d2 parameter specified in command handler", nm());
1441                 }
1442             } catch (InvalidMessageTypeException e) {
1443                 logger.warn("{}: invalid message: ", nm(), e);
1444             } catch (FieldException e) {
1445                 logger.warn("{}: command send message creation error ", nm(), e);
1446             }
1447         }
1448
1449         private int getLevel(Command cmd) {
1450             int level;
1451             if (cmd instanceof PercentType percent) {
1452                 level = percent.intValue();
1453             } else {
1454                 level = OnOffType.OFF.equals(cmd) ? 0 : 100;
1455             }
1456             return (int) Math.round(level * 127.0 / 100);
1457         }
1458
1459         private void setLEDOnOff(InsteonChannelConfiguration config, Command cmd) {
1460             State state = getInsteonDevice().getFeatureState(FEATURE_LED_ON_OFF);
1461             if (!((State) cmd).equals(state)) {
1462                 feature.handleCommand(config, cmd);
1463             }
1464         }
1465     }
1466
1467     /**
1468      * Momentary on command handler
1469      */
1470     public static class MomentaryOnCommandHandler extends CommandHandler {
1471         MomentaryOnCommandHandler(DeviceFeature feature) {
1472             super(feature);
1473         }
1474
1475         @Override
1476         public boolean canHandle(Command cmd) {
1477             return OnOffType.ON.equals(cmd);
1478         }
1479
1480         @Override
1481         public void handleCommand(InsteonChannelConfiguration config, Command cmd) {
1482             try {
1483                 int cmd1 = getParameterAsInteger("cmd1", -1);
1484                 if (cmd1 != -1) {
1485                     InsteonAddress address = getInsteonDevice().getAddress();
1486                     Msg msg = Msg.makeStandardMessage(address, (byte) cmd1, (byte) 0x00);
1487                     feature.sendRequest(msg);
1488                     logger.debug("{}: sent {} request to {}", nm(), feature.getName(), address);
1489                 } else {
1490                     logger.warn("{}: no cmd1 field specified", nm());
1491                 }
1492             } catch (InvalidMessageTypeException e) {
1493                 logger.warn("{}: invalid message: ", nm(), e);
1494             } catch (FieldException e) {
1495                 logger.warn("{}: command send message creation error ", nm(), e);
1496             }
1497         }
1498     }
1499
1500     /**
1501      * Operating flags command handler
1502      */
1503     public static class OpFlagsCommandHandler extends CommandHandler {
1504         OpFlagsCommandHandler(DeviceFeature feature) {
1505             super(feature);
1506         }
1507
1508         @Override
1509         public boolean canHandle(Command cmd) {
1510             return cmd instanceof OnOffType;
1511         }
1512
1513         @Override
1514         public void handleCommand(InsteonChannelConfiguration config, Command cmd) {
1515             try {
1516                 int cmd2 = getOpFlagCommand(cmd);
1517                 if (cmd2 != -1) {
1518                     byte[] data = getOpFlagData(cmd);
1519                     Msg msg = getOpFlagMessage(cmd2, data);
1520                     feature.sendRequest(msg);
1521                     logger.debug("{}: sent op flag {} {} request to {}", nm(), feature.getName(), cmd,
1522                             getInsteonDevice().getAddress());
1523                     // update state if not retrievable (e.g. stayAwake)
1524                     if (!isStateRetrievable()) {
1525                         feature.updateState((State) cmd);
1526                     }
1527                 } else {
1528                     logger.warn("{}: unable to determine op flags command, ignoring request", nm());
1529                 }
1530             } catch (InvalidMessageTypeException e) {
1531                 logger.warn("{}: invalid message: ", nm(), e);
1532             } catch (FieldException e) {
1533                 logger.warn("{}: command send message creation error ", nm(), e);
1534             }
1535         }
1536
1537         protected int getOpFlagCommand(Command cmd) {
1538             return OnOffType.OFF.equals(cmd) ? getParameterAsInteger("off", -1) : getParameterAsInteger("on", -1);
1539         }
1540
1541         protected byte[] getOpFlagData(Command cmd) {
1542             return new byte[0];
1543         }
1544
1545         protected Msg getOpFlagMessage(int cmd2, byte[] data) throws FieldException, InvalidMessageTypeException {
1546             InsteonAddress address = getInsteonDevice().getAddress();
1547             if (getInsteonDevice().getInsteonEngine().supportsChecksum()) {
1548                 return Msg.makeExtendedMessage(address, (byte) 0x20, (byte) cmd2, data, true);
1549             } else {
1550                 return Msg.makeStandardMessage(address, (byte) 0x20, (byte) cmd2);
1551             }
1552         }
1553
1554         protected boolean isStateRetrievable() {
1555             // op flag state is retrieved if a valid bit (0-7) parameter is defined
1556             int bit = getParameterAsInteger("bit", -1);
1557             return bit >= 0 && bit <= 7;
1558         }
1559     }
1560
1561     /**
1562      * Multi-operating flags abstract command handler
1563      */
1564     public abstract static class MultiOpFlagsCommandHandler extends OpFlagsCommandHandler {
1565         MultiOpFlagsCommandHandler(DeviceFeature feature) {
1566             super(feature);
1567         }
1568
1569         @Override
1570         public boolean canHandle(Command cmd) {
1571             return cmd instanceof StringType;
1572         }
1573
1574         @Override
1575         public void handleCommand(InsteonChannelConfiguration config, Command cmd) {
1576             try {
1577                 for (Map.Entry<Integer, String> entry : getOpFlagCommands(cmd).entrySet()) {
1578                     Msg msg = getOpFlagMessage(entry.getKey(), new byte[0]);
1579                     feature.sendRequest(msg);
1580                     logger.debug("{}: sent op flag {} request to {}", nm(), entry.getValue(),
1581                             getInsteonDevice().getAddress());
1582                 }
1583             } catch (InvalidMessageTypeException e) {
1584                 logger.warn("{}: invalid message: ", nm(), e);
1585             } catch (FieldException e) {
1586                 logger.warn("{}: command send message creation error ", nm(), e);
1587             }
1588         }
1589
1590         protected abstract Map<Integer, String> getOpFlagCommands(Command cmd);
1591     }
1592
1593     /**
1594      * Ramp rate command handler
1595      */
1596     public static class RampRateCommandHandler extends CommandHandler {
1597         RampRateCommandHandler(DeviceFeature feature) {
1598             super(feature);
1599         }
1600
1601         @Override
1602         public boolean canHandle(Command cmd) {
1603             return cmd instanceof DecimalType || cmd instanceof QuantityType;
1604         }
1605
1606         @Override
1607         public void handleCommand(InsteonChannelConfiguration config, Command cmd) {
1608             try {
1609                 RampRate rampRate = getRampRate(cmd);
1610                 if (rampRate != null) {
1611                     InsteonAddress address = getInsteonDevice().getAddress();
1612                     byte[] data = { (byte) feature.getGroup(), (byte) 0x05, (byte) rampRate.getValue() };
1613                     boolean setCRC = getInsteonDevice().getInsteonEngine().supportsChecksum();
1614                     Msg msg = Msg.makeExtendedMessage(address, (byte) 0x2E, (byte) 0x00, data, setCRC);
1615                     feature.sendRequest(msg);
1616                     logger.debug("{}: sent ramp time {} to {}", nm(), rampRate, address);
1617                 } else {
1618                     logger.warn("{}: got unexpected ramp rate command {}, ignoreing request", nm(), cmd);
1619                 }
1620             } catch (InvalidMessageTypeException e) {
1621                 logger.warn("{}: invalid message: ", nm(), e);
1622             } catch (FieldException e) {
1623                 logger.warn("{}: command send message creation error ", nm(), e);
1624             }
1625         }
1626
1627         private @Nullable RampRate getRampRate(Command cmd) {
1628             double rampTime = -1;
1629             if (cmd instanceof DecimalType time) {
1630                 rampTime = time.doubleValue();
1631             } else if (cmd instanceof QuantityType<?> time) {
1632                 rampTime = Objects.requireNonNullElse(time.toInvertibleUnit(Units.SECOND), time).doubleValue();
1633             }
1634             return rampTime != -1 ? RampRate.fromTime(rampTime) : null;
1635         }
1636     }
1637
1638     /**
1639      * FanLinc fan speed command handler
1640      */
1641     public static class FanLincFanSpeedCommandHandler extends OnOffCommandHandler {
1642         FanLincFanSpeedCommandHandler(DeviceFeature feature) {
1643             super(feature);
1644         }
1645
1646         @Override
1647         public boolean canHandle(Command cmd) {
1648             return cmd instanceof StringType;
1649         }
1650
1651         @Override
1652         protected int getCommandCode(InsteonChannelConfiguration config, Command cmd) {
1653             try {
1654                 String speed = ((StringType) cmd).toString();
1655                 return FanLincFanSpeed.valueOf(speed) == FanLincFanSpeed.OFF ? 0x13 : 0x11;
1656             } catch (IllegalArgumentException e) {
1657                 logger.warn("{}: got unexpected fan speed command: {}, ignoring request", nm(), cmd);
1658                 return -1;
1659             }
1660         }
1661
1662         @Override
1663         protected int getLevel(InsteonChannelConfiguration config, Command cmd) {
1664             try {
1665                 String speed = ((StringType) cmd).toString();
1666                 return FanLincFanSpeed.valueOf(speed).getValue();
1667             } catch (IllegalArgumentException e) {
1668                 return -1;
1669             }
1670         }
1671     }
1672
1673     /**
1674      * FanLinc fan on/off command handler
1675      */
1676     public static class FanLincFanOnOffCommandHandler extends OnOffCommandHandler {
1677         FanLincFanOnOffCommandHandler(DeviceFeature feature) {
1678             super(feature);
1679         }
1680
1681         @Override
1682         protected int getCommandCode(InsteonChannelConfiguration config, Command cmd) {
1683             return OnOffType.OFF.equals(cmd) ? 0x13 : 0x11;
1684         }
1685     }
1686
1687     /**
1688      * FanLinc fan percent command handler
1689      */
1690     public static class FanLincFanPercentCommandHandler extends OnOffCommandHandler {
1691         FanLincFanPercentCommandHandler(DeviceFeature feature) {
1692             super(feature);
1693         }
1694
1695         @Override
1696         public boolean canHandle(Command cmd) {
1697             return cmd instanceof PercentType;
1698         }
1699
1700         @Override
1701         protected int getCommandCode(InsteonChannelConfiguration config, Command cmd) {
1702             return PercentType.ZERO.equals(cmd) ? 0x13 : 0x11;
1703         }
1704
1705         @Override
1706         protected int getLevel(InsteonChannelConfiguration config, Command cmd) {
1707             int level = ((PercentType) cmd).intValue();
1708             return (int) Math.ceil(level * 255.0 / 100); // round up
1709         }
1710     }
1711
1712     /**
1713      * I/O linc momentary duration command handler
1714      */
1715     public static class IOLincMomentaryDurationCommandHandler extends CommandHandler {
1716         IOLincMomentaryDurationCommandHandler(DeviceFeature feature) {
1717             super(feature);
1718         }
1719
1720         @Override
1721         public boolean canHandle(Command cmd) {
1722             return cmd instanceof DecimalType || cmd instanceof QuantityType;
1723         }
1724
1725         @Override
1726         public void handleCommand(InsteonChannelConfiguration config, Command cmd) {
1727             try {
1728                 double duration = getDuration(cmd);
1729                 if (duration != -1) {
1730                     InsteonAddress address = getInsteonDevice().getAddress();
1731                     int prescaler = 1;
1732                     int delay = (int) Math.round(duration * 10);
1733                     if (delay > 255) {
1734                         prescaler = (int) Math.ceil(delay / 255.0);
1735                         delay = (int) Math.round(delay / (double) prescaler);
1736                     }
1737                     boolean setCRC = getInsteonDevice().getInsteonEngine().supportsChecksum();
1738                     // define ext command message to set momentary duration delay
1739                     Msg delayMsg = Msg.makeExtendedMessage(address, (byte) 0x2E, (byte) 0x00,
1740                             new byte[] { (byte) 0x01, (byte) 0x06, (byte) delay }, setCRC);
1741                     // define ext command message to set momentary duration prescaler
1742                     Msg prescalerMsg = Msg.makeExtendedMessage(address, (byte) 0x2E, (byte) 0x00,
1743                             new byte[] { (byte) 0x01, (byte) 0x07, (byte) prescaler }, setCRC);
1744                     // send requests
1745                     feature.sendRequest(delayMsg);
1746                     if (logger.isDebugEnabled()) {
1747                         logger.debug("{}: sent momentary duration delay {} request to {}", nm(),
1748                                 HexUtils.getHexString(delay), address);
1749                     }
1750                     feature.sendRequest(prescalerMsg);
1751                     if (logger.isDebugEnabled()) {
1752                         logger.debug("{}: sent momentary duration prescaler {} request to {}", nm(),
1753                                 HexUtils.getHexString(prescaler), address);
1754                     }
1755                 } else {
1756                     logger.warn("{}: got unexpected momentary duration command {}, ignoring request", nm(), cmd);
1757                 }
1758             } catch (InvalidMessageTypeException e) {
1759                 logger.warn("{}: invalid message: ", nm(), e);
1760             } catch (FieldException e) {
1761                 logger.warn("{}: command send message creation error ", nm(), e);
1762             }
1763         }
1764
1765         private double getDuration(Command cmd) {
1766             if (cmd instanceof DecimalType time) {
1767                 return time.doubleValue();
1768             } else if (cmd instanceof QuantityType<?> time) {
1769                 return Objects.requireNonNullElse(time.toInvertibleUnit(Units.SECOND), time).doubleValue();
1770             }
1771             return -1;
1772         }
1773     }
1774
1775     /**
1776      * I/O linc relay mode command handler
1777      */
1778     public static class IOLincRelayModeCommandHandler extends MultiOpFlagsCommandHandler {
1779         IOLincRelayModeCommandHandler(DeviceFeature feature) {
1780             super(feature);
1781         }
1782
1783         @Override
1784         protected Map<Integer, String> getOpFlagCommands(Command cmd) {
1785             Map<Integer, String> commands = new HashMap<>();
1786             try {
1787                 String mode = ((StringType) cmd).toString();
1788                 switch (IOLincRelayMode.valueOf(mode)) {
1789                     case LATCHING:
1790                         commands.put(0x07, "momentary mode OFF");
1791                         break;
1792                     case MOMENTARY_A:
1793                         commands.put(0x06, "momentary mode ON");
1794                         commands.put(0x13, "momentary trigger on/off OFF");
1795                         commands.put(0x15, "momentary sensor follow OFF");
1796                         break;
1797                     case MOMENTARY_B:
1798                         commands.put(0x06, "momentary mode ON");
1799                         commands.put(0x12, "momentary trigger on/off ON");
1800                         commands.put(0x15, "momentary sensor follow OFF");
1801                         break;
1802                     case MOMENTARY_C:
1803                         commands.put(0x06, "momentary mode ON");
1804                         commands.put(0x13, "momentary trigger on/off OFF");
1805                         commands.put(0x14, "momentary sensor follow ON");
1806                         break;
1807                 }
1808             } catch (IllegalArgumentException e) {
1809                 logger.warn("{}: got unexpected relay mode command: {}, ignoring request", nm(), cmd);
1810             }
1811             return commands;
1812         }
1813     }
1814
1815     /**
1816      * Micro module operation mode command handler
1817      */
1818     public static class MicroModuleOpModeCommandHandler extends MultiOpFlagsCommandHandler {
1819         MicroModuleOpModeCommandHandler(DeviceFeature feature) {
1820             super(feature);
1821         }
1822
1823         @Override
1824         protected Map<Integer, String> getOpFlagCommands(Command cmd) {
1825             Map<Integer, String> commands = new HashMap<>();
1826             try {
1827                 String mode = ((StringType) cmd).toString();
1828                 switch (MicroModuleOpMode.valueOf(mode)) {
1829                     case LATCHING:
1830                         commands.put(0x20, "momentary line OFF");
1831                         break;
1832                     case SINGLE_MOMENTARY:
1833                         commands.put(0x21, "momentary line ON");
1834                         commands.put(0x1E, "dual line OFF");
1835                         break;
1836                     case DUAL_MOMENTARY:
1837                         commands.put(0x21, "momentary line ON");
1838                         commands.put(0x1E, "dual line ON");
1839                         break;
1840                 }
1841             } catch (IllegalArgumentException e) {
1842                 logger.warn("{}: got unexpected operation mode command: {}, ignoring request", nm(), cmd);
1843             }
1844             return commands;
1845         }
1846     }
1847
1848     /**
1849      * Sprinkler valve on/off command handler
1850      */
1851     public static class SprinklerValveOnOffCommandHandler extends OnOffCommandHandler {
1852         SprinklerValveOnOffCommandHandler(DeviceFeature feature) {
1853             super(feature);
1854         }
1855
1856         @Override
1857         protected int getCommandCode(InsteonChannelConfiguration config, Command cmd) {
1858             return OnOffType.ON.equals(cmd) ? 0x40 : 0x41;
1859         }
1860
1861         @Override
1862         protected int getLevel(InsteonChannelConfiguration config, Command cmd) {
1863             return getParameterAsInteger("valve", -1);
1864         }
1865     }
1866
1867     /**
1868      * Sprinkler program on/off command handler
1869      */
1870     public static class SprinklerProgramOnOffCommandHandler extends OnOffCommandHandler {
1871         SprinklerProgramOnOffCommandHandler(DeviceFeature feature) {
1872             super(feature);
1873         }
1874
1875         @Override
1876         public boolean canHandle(Command cmd) {
1877             return cmd instanceof PlayPauseType;
1878         }
1879
1880         @Override
1881         protected int getCommandCode(InsteonChannelConfiguration config, Command cmd) {
1882             return PlayPauseType.PLAY.equals(cmd) ? 0x42 : 0x43;
1883         }
1884
1885         @Override
1886         protected int getLevel(InsteonChannelConfiguration config, Command cmd) {
1887             return getParameterAsInteger("program", -1);
1888         }
1889     }
1890
1891     /**
1892      * Sprinkler program next/previous command handler
1893      */
1894     public static class SprinklerProgramNextPreviousCommandHandler extends OnOffCommandHandler {
1895         SprinklerProgramNextPreviousCommandHandler(DeviceFeature feature) {
1896             super(feature);
1897         }
1898
1899         @Override
1900         public boolean canHandle(Command cmd) {
1901             return cmd instanceof NextPreviousType;
1902         }
1903
1904         @Override
1905         protected int getCommandCode(InsteonChannelConfiguration config, Command cmd) {
1906             return 0x44; // sprinkler control
1907         }
1908
1909         @Override
1910         protected int getLevel(InsteonChannelConfiguration config, Command cmd) {
1911             return NextPreviousType.NEXT.equals(cmd) ? 0x05 : 0x06; // skip forward or back
1912         }
1913     }
1914
1915     /**
1916      * Thermostat fan mode command handler
1917      */
1918     public static class ThermostatFanModeCommandHandler extends CustomCommandHandler {
1919         ThermostatFanModeCommandHandler(DeviceFeature feature) {
1920             super(feature);
1921         }
1922
1923         @Override
1924         public boolean canHandle(Command cmd) {
1925             return cmd instanceof StringType;
1926         }
1927
1928         @Override
1929         protected double getValue(Command cmd) {
1930             try {
1931                 String mode = ((StringType) cmd).toString();
1932                 return ThermostatFanMode.valueOf(mode).getValue();
1933             } catch (IllegalArgumentException e) {
1934                 logger.warn("{}: got unexpected fan mode command: {}, ignoring request", nm(), cmd);
1935                 return -1;
1936             }
1937         }
1938     }
1939
1940     /**
1941      * Thermostat system mode command handler
1942      */
1943     public static class ThermostatSystemModeCommandHandler extends CustomCommandHandler {
1944         ThermostatSystemModeCommandHandler(DeviceFeature feature) {
1945             super(feature);
1946         }
1947
1948         @Override
1949         public boolean canHandle(Command cmd) {
1950             return cmd instanceof StringType;
1951         }
1952
1953         @Override
1954         protected double getValue(Command cmd) {
1955             try {
1956                 String mode = ((StringType) cmd).toString();
1957                 return ThermostatSystemMode.valueOf(mode).getValue();
1958             } catch (IllegalArgumentException e) {
1959                 logger.warn("{}: got unexpected system mode command: {}, ignoring request", nm(), cmd);
1960                 return -1;
1961             }
1962         }
1963     }
1964
1965     /**
1966      * Venstar thermostat system mode handler
1967      */
1968     public static class VenstarSystemModeCommandHandler extends CustomCommandHandler {
1969         VenstarSystemModeCommandHandler(DeviceFeature feature) {
1970             super(feature);
1971         }
1972
1973         @Override
1974         public boolean canHandle(Command cmd) {
1975             return cmd instanceof StringType;
1976         }
1977
1978         @Override
1979         protected double getValue(Command cmd) {
1980             try {
1981                 String mode = ((StringType) cmd).toString();
1982                 return VenstarSystemMode.valueOf(mode).getValue();
1983             } catch (IllegalArgumentException e) {
1984                 logger.warn("{}: got unexpected system mode command: {}, ignoring request", nm(), cmd);
1985                 return -1;
1986             }
1987         }
1988     }
1989
1990     /**
1991      * Thermostat temperature scale command handler
1992      */
1993     public static class ThermostatTemperatureScaleCommandHandler extends CustomBitmaskCommandHandler {
1994         ThermostatTemperatureScaleCommandHandler(DeviceFeature feature) {
1995             super(feature);
1996         }
1997
1998         @Override
1999         public boolean canHandle(Command cmd) {
2000             return cmd instanceof StringType;
2001         }
2002
2003         @Override
2004         protected @Nullable Boolean shouldSetBit(Command cmd) {
2005             try {
2006                 String scale = ((StringType) cmd).toString();
2007                 return ThermostatTemperatureScale.valueOf(scale) == ThermostatTemperatureScale.CELSIUS;
2008             } catch (IllegalArgumentException e) {
2009                 logger.warn("{}: got unexpected temperature scale command: {}, ignoring request", nm(), cmd);
2010                 return null;
2011             }
2012         }
2013     }
2014
2015     /**
2016      * Thermostat time format command handler
2017      */
2018     public static class ThermostatTimeFormatCommandHandler extends CustomBitmaskCommandHandler {
2019         ThermostatTimeFormatCommandHandler(DeviceFeature feature) {
2020             super(feature);
2021         }
2022
2023         @Override
2024         public boolean canHandle(Command cmd) {
2025             return cmd instanceof StringType;
2026         }
2027
2028         @Override
2029         protected @Nullable Boolean shouldSetBit(Command cmd) {
2030             try {
2031                 String format = ((StringType) cmd).toString();
2032                 return ThermostatTimeFormat.from(format) == ThermostatTimeFormat.HR_24;
2033             } catch (IllegalArgumentException e) {
2034                 logger.warn("{}: got unexpected temperature format command: {}, ignoring request", nm(), cmd);
2035                 return null;
2036             }
2037         }
2038     }
2039
2040     /**
2041      * Thermostat sync time command handler
2042      */
2043     public static class ThermostatSyncTimeCommandHandler extends MomentaryOnCommandHandler {
2044         ThermostatSyncTimeCommandHandler(DeviceFeature feature) {
2045             super(feature);
2046         }
2047
2048         @Override
2049         public void handleCommand(InsteonChannelConfiguration config, Command cmd) {
2050             try {
2051                 InsteonAddress address = getInsteonDevice().getAddress();
2052                 ZonedDateTime time = ZonedDateTime.now();
2053                 byte[] data = { (byte) 0x02, (byte) (time.getDayOfWeek().getValue() % 7), (byte) time.getHour(),
2054                         (byte) time.getMinute(), (byte) time.getSecond() };
2055                 Msg msg = Msg.makeExtendedMessageCRC2(address, (byte) 0x2E, (byte) 0x02, data);
2056                 feature.sendRequest(msg);
2057                 logger.debug("{}: sent set time data request to {}", nm(), address);
2058             } catch (InvalidMessageTypeException e) {
2059                 logger.warn("{}: invalid message: ", nm(), e);
2060             } catch (FieldException e) {
2061                 logger.warn("{}: command send message creation error ", nm(), e);
2062             }
2063         }
2064     }
2065
2066     /**
2067      * IM generic abstract command handler
2068      */
2069     public abstract static class IMCommandHandler extends CommandHandler {
2070         IMCommandHandler(DeviceFeature feature) {
2071             super(feature);
2072         }
2073
2074         @Override
2075         public void handleCommand(InsteonChannelConfiguration config, Command cmd) {
2076             try {
2077                 Msg msg = getIMMessage(cmd);
2078                 feature.sendRequest(msg);
2079                 logger.debug("{}: sent {} request to {}", nm(), cmd, getInsteonModem().getAddress());
2080             } catch (InvalidMessageTypeException e) {
2081                 logger.warn("{}: invalid message: ", nm(), e);
2082             } catch (FieldException e) {
2083                 logger.warn("{}: command send message creation error ", nm(), e);
2084             }
2085         }
2086
2087         protected abstract Msg getIMMessage(Command cmd) throws InvalidMessageTypeException, FieldException;
2088     }
2089
2090     /**
2091      * IM led on/off command handler
2092      */
2093     public static class IMLEDOnOffCommandHandler extends IMCommandHandler {
2094         IMLEDOnOffCommandHandler(DeviceFeature feature) {
2095             super(feature);
2096         }
2097
2098         @Override
2099         public boolean canHandle(Command cmd) {
2100             return cmd instanceof OnOffType;
2101         }
2102
2103         @Override
2104         public void handleCommand(InsteonChannelConfiguration config, Command cmd) {
2105             // set led control
2106             setLEDControl(config);
2107             // set led on/off
2108             super.handleCommand(config, cmd);
2109             // update state since not retrievable
2110             feature.updateState((State) cmd);
2111         }
2112
2113         @Override
2114         protected Msg getIMMessage(Command cmd) throws InvalidMessageTypeException, FieldException {
2115             return Msg.makeMessage(OnOffType.OFF.equals(cmd) ? "LEDOff" : "LEDOn");
2116         }
2117
2118         private void setLEDControl(InsteonChannelConfiguration config) {
2119             State state = getInsteonModem().getFeatureState(FEATURE_LED_CONTROL);
2120             if (!OnOffType.ON.equals(state)) {
2121                 feature.handleCommand(config, OnOffType.ON);
2122             }
2123         }
2124     }
2125
2126     /**
2127      * IM beep command handler
2128      */
2129     public static class IMBeepCommandHandler extends IMCommandHandler {
2130         IMBeepCommandHandler(DeviceFeature feature) {
2131             super(feature);
2132         }
2133
2134         @Override
2135         public boolean canHandle(Command cmd) {
2136             return OnOffType.ON.equals(cmd);
2137         }
2138
2139         @Override
2140         protected Msg getIMMessage(Command cmd) throws InvalidMessageTypeException, FieldException {
2141             return Msg.makeMessage("Beep");
2142         }
2143     }
2144
2145     /**
2146      * IM config command handler
2147      */
2148     public static class IMConfigCommandHandler extends CustomBitmaskCommandHandler {
2149         IMConfigCommandHandler(DeviceFeature feature) {
2150             super(feature);
2151         }
2152
2153         @Override
2154         public void handleCommand(InsteonChannelConfiguration config, Command cmd) {
2155             try {
2156                 int bitmask = getBitmask(cmd);
2157                 if (bitmask != -1) {
2158                     Msg msg = Msg.makeMessage("SetIMConfig");
2159                     msg.setByte("IMConfigurationFlags", (byte) bitmask);
2160                     feature.sendRequest(msg);
2161                     logger.debug("{}: sent {} request to {}", nm(), cmd, getInsteonModem().getAddress());
2162                 }
2163             } catch (InvalidMessageTypeException e) {
2164                 logger.warn("{}: invalid message: ", nm(), e);
2165             } catch (FieldException e) {
2166                 logger.warn("{}: command send message creation error ", nm(), e);
2167             }
2168         }
2169     }
2170
2171     /**
2172      * X10 generic abstract command handler
2173      */
2174     public abstract static class X10CommandHandler extends CommandHandler {
2175         X10CommandHandler(DeviceFeature feature) {
2176             super(feature);
2177         }
2178
2179         @Override
2180         public void handleCommand(InsteonChannelConfiguration config, Command cmd) {
2181             try {
2182                 X10Address address = getX10Device().getAddress();
2183                 int cmdCode = getCommandCode(cmd, address.getHouseCode());
2184                 Msg addrMsg = Msg.makeX10AddressMessage(address);
2185                 feature.sendRequest(addrMsg);
2186                 Msg cmdMsg = Msg.makeX10CommandMessage((byte) cmdCode);
2187                 feature.sendRequest(cmdMsg);
2188                 logger.debug("{}: sent {} request to {}", nm(), cmd, address);
2189             } catch (InvalidMessageTypeException e) {
2190                 logger.warn("{}: invalid message: ", nm(), e);
2191             } catch (FieldException e) {
2192                 logger.warn("{}: command send message creation error ", nm(), e);
2193             }
2194         }
2195
2196         protected abstract int getCommandCode(Command cmd, byte houseCode);
2197     }
2198
2199     /**
2200      * X10 on/off command handler
2201      */
2202     public static class X10OnOffCommandHandler extends X10CommandHandler {
2203         X10OnOffCommandHandler(DeviceFeature feature) {
2204             super(feature);
2205         }
2206
2207         @Override
2208         public boolean canHandle(Command cmd) {
2209             return cmd instanceof OnOffType;
2210         }
2211
2212         @Override
2213         protected int getCommandCode(Command cmd, byte houseCode) {
2214             int cmdCode = OnOffType.OFF.equals(cmd) ? X10Command.OFF.code() : X10Command.ON.code();
2215             return houseCode << 4 | cmdCode;
2216         }
2217     }
2218
2219     /**
2220      * X10 percent command handler
2221      */
2222     public static class X10PercentCommandHandler extends X10CommandHandler {
2223
2224         private static final int[] X10_LEVEL_CODES = { 0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15 };
2225
2226         X10PercentCommandHandler(DeviceFeature feature) {
2227             super(feature);
2228         }
2229
2230         @Override
2231         public boolean canHandle(Command cmd) {
2232             return cmd instanceof PercentType;
2233         }
2234
2235         @Override
2236         protected int getCommandCode(Command cmd, byte houseCode) {
2237             int level = ((PercentType) cmd).intValue() * 32 / 100;
2238             int levelCode = X10_LEVEL_CODES[level % 16];
2239             int cmdCode = level >= 16 ? X10Command.PRESET_DIM_2.code() : X10Command.PRESET_DIM_1.code();
2240             return levelCode << 4 | cmdCode;
2241         }
2242     }
2243
2244     /**
2245      * X10 increase/decrease command handler
2246      */
2247     public static class X10IncreaseDecreaseCommandHandler extends X10CommandHandler {
2248         X10IncreaseDecreaseCommandHandler(DeviceFeature feature) {
2249             super(feature);
2250         }
2251
2252         @Override
2253         public boolean canHandle(Command cmd) {
2254             return cmd instanceof IncreaseDecreaseType;
2255         }
2256
2257         @Override
2258         protected int getCommandCode(Command cmd, byte houseCode) {
2259             int cmdCode = IncreaseDecreaseType.INCREASE.equals(cmd) ? X10Command.BRIGHT.code() : X10Command.DIM.code();
2260             return houseCode << 4 | cmdCode;
2261         }
2262     }
2263
2264     /**
2265      * Factory method to dermine if a command handler supports a given command type
2266      *
2267      * @param type the handler command type
2268      * @return true if handler supports command type, otherwise false
2269      */
2270     public static boolean supportsCommandType(String type) {
2271         return SUPPORTED_COMMAND_TYPES.contains(type);
2272     }
2273
2274     /**
2275      * Factory method for creating default command handler
2276      *
2277      * @param feature the feature for which to create the handler
2278      * @return the default command handler which was created
2279      */
2280     public static DefaultCommandHandler makeDefaultHandler(DeviceFeature feature) {
2281         return new DefaultCommandHandler(feature);
2282     }
2283
2284     /**
2285      * Factory method for creating handlers of a given name using java reflection
2286      *
2287      * @param name the name of the handler to create
2288      * @param parameters the parameters of the handler to create
2289      * @param feature the feature for which to create the handler
2290      * @return the handler which was created
2291      */
2292     public static @Nullable <T extends CommandHandler> T makeHandler(String name, Map<String, String> parameters,
2293             DeviceFeature feature) {
2294         try {
2295             String className = CommandHandler.class.getName() + "$" + name;
2296             @SuppressWarnings("unchecked")
2297             Class<? extends T> classRef = (Class<? extends T>) Class.forName(className);
2298             @Nullable
2299             T handler = classRef.getDeclaredConstructor(DeviceFeature.class).newInstance(feature);
2300             handler.setParameters(parameters);
2301             return handler;
2302         } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | IllegalArgumentException
2303                 | InvocationTargetException | NoSuchMethodException | SecurityException e) {
2304             return null;
2305         }
2306     }
2307 }