2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.insteon.internal.device.feature;
15 import static org.openhab.binding.insteon.internal.InsteonBindingConstants.*;
17 import java.lang.reflect.InvocationTargetException;
18 import java.time.ZonedDateTime;
19 import java.util.HashMap;
21 import java.util.Objects;
24 import javax.measure.Unit;
25 import javax.measure.quantity.Dimensionless;
26 import javax.measure.quantity.Temperature;
27 import javax.measure.quantity.Time;
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;
74 * A command handler translates an openHAB command into a insteon message
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
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");
87 protected final Logger logger = LoggerFactory.getLogger(CommandHandler.class);
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.
96 public CommandHandler(DeviceFeature feature) {
103 * @return handler id based on command parameter
105 public String getId() {
106 return getParameterAsString("command", "default");
110 * Returns if handler can handle the openHAB command received
112 * @param cmd the openhab command received
113 * @return true if can handle
115 public abstract boolean canHandle(Command cmd);
118 * Implements what to do when an openHAB command is received
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
124 public abstract void handleCommand(InsteonChannelConfiguration config, Command cmd);
127 * Default command handler
129 public static class DefaultCommandHandler extends CommandHandler {
130 DefaultCommandHandler(DeviceFeature feature) {
135 public boolean canHandle(Command cmd) {
140 public void handleCommand(InsteonChannelConfiguration config, Command cmd) {
141 logger.warn("{}: command {}:{} is not supported", nm(), cmd.getClass().getSimpleName(), cmd);
146 * No-op command handler
148 public static class NoOpCommandHandler extends CommandHandler {
149 NoOpCommandHandler(DeviceFeature feature) {
154 public boolean canHandle(Command cmd) {
159 public void handleCommand(InsteonChannelConfiguration config, Command cmd) {
160 // do nothing, not even log
165 * Refresh command handler
167 public static class RefreshCommandHandler extends CommandHandler {
168 RefreshCommandHandler(DeviceFeature feature) {
173 public boolean canHandle(Command cmd) {
174 return cmd instanceof RefreshType;
178 public void handleCommand(InsteonChannelConfiguration config, Command cmd) {
179 feature.triggerPoll(0L);
184 * Custom abstract command handler based of parameters
186 public abstract static class CustomCommandHandler extends CommandHandler {
187 CustomCommandHandler(DeviceFeature feature) {
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);
197 logger.warn("{}: handler misconfigured, no cmd1 parameter specified", nm());
200 if (ext < 0 || ext > 2) {
201 logger.warn("{}: handler misconfigured, invalid ext parameter specified", nm());
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());
210 // determine cmd value and apply factor ratio based of parameters
211 int value = (int) Math.round(getValue(cmd) * getParameterAsInteger("factor", 1));
213 logger.debug("{}: unable to determine command value, ignoring request", nm());
217 InsteonAddress address = getInsteonDevice().getAddress();
218 boolean setCRC = getInsteonDevice().getInsteonEngine().supportsChecksum();
221 msg = Msg.makeStandardMessage(address, (byte) cmd1, (byte) cmd2);
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);
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
234 } else if (ext == 2) {
239 feature.sendRequest(msg);
240 if (logger.isDebugEnabled()) {
241 logger.debug("{}: sent {} {} request to {}", nm(), feature.getName(), HexUtils.getHexString(value),
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);
251 protected abstract double getValue(Command cmd);
255 * Custom bitmask command handler based of parameters
257 public static class CustomBitmaskCommandHandler extends CustomCommandHandler {
258 CustomBitmaskCommandHandler(DeviceFeature feature) {
263 public boolean canHandle(Command cmd) {
264 return cmd instanceof OnOffType;
268 protected double getValue(Command cmd) {
269 return getBitmask(cmd);
272 protected int getBitNumber() {
273 return getParameterAsInteger("bit", -1);
276 protected @Nullable Boolean shouldSetBit(Command cmd) {
277 return OnOffType.ON.equals(cmd) ^ getParameterAsBoolean("inverted", false);
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());
295 if (logger.isTraceEnabled()) {
296 logger.trace("{}: bitmask:{} bit:{} set:{}", nm(), BinaryUtils.getBinaryString(bitmask), bit,
299 return BinaryUtils.updateBit(bitmask, bit, shouldSetBit);
306 * Custom on/off type command handler based of parameters
308 public static class CustomOnOffCommandHandler extends CustomCommandHandler {
309 CustomOnOffCommandHandler(DeviceFeature feature) {
314 public boolean canHandle(Command cmd) {
315 return cmd instanceof OnOffType;
319 protected double getValue(Command cmd) {
320 return OnOffType.OFF.equals(cmd) ? getParameterAsInteger("off", 0x00) : getParameterAsInteger("on", 0xFF);
325 * Custom decimal type command handler based of parameters
327 public static class CustomDecimalCommandHandler extends CustomCommandHandler {
328 CustomDecimalCommandHandler(DeviceFeature feature) {
333 public boolean canHandle(Command cmd) {
334 return cmd instanceof DecimalType;
338 protected double getValue(Command cmd) {
339 return ((DecimalType) cmd).doubleValue();
344 * Custom percent type command handler based of parameters
346 public static class CustomPercentCommandHandler extends CustomCommandHandler {
347 CustomPercentCommandHandler(DeviceFeature feature) {
352 public boolean canHandle(Command cmd) {
353 return cmd instanceof PercentType;
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;
366 * Custom dimensionless quantity type command handler based of parameters
368 public static class CustomDimensionlessCommandHandler extends CustomCommandHandler {
369 CustomDimensionlessCommandHandler(DeviceFeature feature) {
374 public boolean canHandle(Command cmd) {
375 return cmd instanceof QuantityType;
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;
389 * Custom temperature quantity type command handler based of parameters
391 public static class CustomTemperatureCommandHandler extends CustomCommandHandler {
392 CustomTemperatureCommandHandler(DeviceFeature feature) {
397 public boolean canHandle(Command cmd) {
398 return cmd instanceof QuantityType;
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
411 private Unit<Temperature> getTemperatureUnit() {
412 String scale = getParameterAsString("scale", "");
415 return SIUnits.CELSIUS;
417 return ImperialUnits.FAHRENHEIT;
419 logger.debug("{}: no valid temperature scale parameter found, defaulting to: CELSIUS", nm());
420 return SIUnits.CELSIUS;
426 * Custom time quantity type command handler based of parameters
428 public static class CustomTimeCommandHandler extends CustomCommandHandler {
429 CustomTimeCommandHandler(DeviceFeature feature) {
434 public boolean canHandle(Command cmd) {
435 return cmd instanceof QuantityType;
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();
446 private Unit<Time> getTimeUnit() {
447 String scale = getParameterAsString("scale", "");
456 logger.debug("{}: no valid time scale parameter found, defaulting to: SECONDS", nm());
463 * Generic on/off abstract command handler
465 public abstract static class OnOffCommandHandler extends CommandHandler {
466 OnOffCommandHandler(DeviceFeature feature) {
471 public boolean canHandle(Command cmd) {
472 return cmd instanceof OnOffType;
476 public void handleCommand(InsteonChannelConfiguration config, Command cmd) {
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);
492 InsteonAddress address = getInsteonDevice().getAddress();
493 int componentId = feature.getGroup();
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);
500 msg = Msg.makeStandardMessage(address, (byte) cmd1, (byte) level);
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);
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);
516 protected int getCommandCode(InsteonChannelConfiguration config, Command cmd) {
517 return OnOffType.OFF.equals(cmd) ? getParameterAsInteger("off", 0x13) : getParameterAsInteger("on", 0x11);
520 protected int getLevel(InsteonChannelConfiguration config, Command cmd) {
521 return OnOffType.OFF.equals(cmd) ? 0x00 : getOnLevel(config);
524 protected int getGroup(InsteonChannelConfiguration config) {
528 private int getOnLevel(InsteonChannelConfiguration config) {
529 int level = config.getOnLevel();
531 State state = getInsteonDevice().getFeatureState(FEATURE_ON_LEVEL);
532 level = (state instanceof PercentType percent ? percent : PercentType.HUNDRED).intValue();
535 logger.trace("{}: using on level {}%", nm(), level);
536 return (int) Math.ceil(level * 255.0 / 100); // round up
541 * Dimmer on/off command handler
543 public static class DimmerOnOffCommandHandler extends OnOffCommandHandler {
544 DimmerOnOffCommandHandler(DeviceFeature feature) {
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);
558 // ramp dimmer command otherwise
559 setRampDimmer(config, cmd);
561 // update state since dimmer related channels not automatically updated by the framework
562 PercentType state = getState(config, cmd);
563 feature.updateState(state);
567 protected int getCommandCode(InsteonChannelConfiguration config, Command cmd) {
568 return OnOffType.OFF.equals(cmd) ? 0x13 : 0x11;
572 protected int getGroup(InsteonChannelConfiguration config) {
573 return config.getOnLevel() == -1 && getInsteonDevice().isDeviceSyncEnabled()
574 ? feature.getBroadcastGroup(config)
578 protected PercentType getState(InsteonChannelConfiguration config, Command cmd) {
579 if (OnOffType.OFF.equals(cmd)) {
580 return PercentType.ZERO;
582 int level = config.getOnLevel();
584 return new PercentType(level);
586 State state = getInsteonDevice().getFeatureState(FEATURE_ON_LEVEL);
587 if (state instanceof PercentType percent) {
590 return PercentType.HUNDRED;
593 private void setInstantDimmer(InsteonChannelConfiguration config, Command cmd) {
594 InstantDimmerCommandHandler handler = new InstantDimmerCommandHandler(feature);
595 handler.setParameters(parameters);
596 handler.handleCommand(config, cmd);
599 private void setRampDimmer(InsteonChannelConfiguration config, Command cmd) {
600 RampDimmerCommandHandler handler = new RampDimmerCommandHandler(feature);
601 handler.setParameters(parameters);
602 handler.handleCommand(config, cmd);
607 * Dimmer percent command handler
609 public static class DimmerPercentCommandHandler extends DimmerOnOffCommandHandler {
610 DimmerPercentCommandHandler(DeviceFeature feature) {
615 public boolean canHandle(Command cmd) {
616 return cmd instanceof PercentType;
620 protected int getCommandCode(InsteonChannelConfiguration config, Command cmd) {
621 return PercentType.ZERO.equals(cmd) ? 0x13 : 0x11;
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
631 protected int getGroup(InsteonChannelConfiguration config) {
636 protected PercentType getState(InsteonChannelConfiguration config, Command cmd) {
637 return (PercentType) cmd;
642 * Dimmer increase/decrease command handler
644 public static class DimmerIncreaseDecreaseCommandHandler extends OnOffCommandHandler {
645 DimmerIncreaseDecreaseCommandHandler(DeviceFeature feature) {
650 public boolean canHandle(Command cmd) {
651 return cmd instanceof IncreaseDecreaseType;
655 protected int getCommandCode(InsteonChannelConfiguration config, Command cmd) {
656 return IncreaseDecreaseType.INCREASE.equals(cmd) ? 0x15 : 0x16;
660 protected int getLevel(InsteonChannelConfiguration config, Command cmd) {
661 return 0x00; // not parsed
665 protected int getGroup(InsteonChannelConfiguration config) {
666 return getInsteonDevice().isDeviceSyncEnabled() ? feature.getBroadcastGroup(config) : -1;
671 * Rollershutter up/down command handler
673 public static class RollershutterUpDownCommandHandler extends OnOffCommandHandler {
674 RollershutterUpDownCommandHandler(DeviceFeature feature) {
679 public boolean canHandle(Command cmd) {
680 return cmd instanceof UpDownType;
684 protected int getCommandCode(InsteonChannelConfiguration config, Command cmd) {
685 return 0x17; // manual change start
689 protected int getLevel(InsteonChannelConfiguration config, Command cmd) {
690 return UpDownType.UP.equals(cmd) ? 0x01 : 0x00; // up or down
694 protected int getGroup(InsteonChannelConfiguration config) {
695 return getInsteonDevice().isDeviceSyncEnabled() ? feature.getBroadcastGroup(config) : -1;
700 * Rollershutter stop command handler
702 public static class RollershutterStopCommandHandler extends OnOffCommandHandler {
703 RollershutterStopCommandHandler(DeviceFeature feature) {
708 public boolean canHandle(Command cmd) {
709 return StopMoveType.STOP.equals(cmd);
713 protected int getCommandCode(InsteonChannelConfiguration config, Command cmd) {
714 return 0x18; // manual change stop
718 protected int getLevel(InsteonChannelConfiguration config, Command cmd) {
719 return 0x00; // not parsed
723 protected int getGroup(InsteonChannelConfiguration config) {
724 return getInsteonDevice().isDeviceSyncEnabled() ? feature.getBroadcastGroup(config) : -1;
729 * Instant dimmer command handler
731 public static class InstantDimmerCommandHandler extends OnOffCommandHandler {
732 InstantDimmerCommandHandler(DeviceFeature feature) {
737 public boolean canHandle(Command cmd) {
738 return cmd instanceof OnOffType || cmd instanceof PercentType;
742 protected int getCommandCode(InsteonChannelConfiguration config, Command cmd) {
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
751 return super.getLevel(config, cmd);
756 protected int getGroup(InsteonChannelConfiguration config) {
762 * Ramp dimmer command handler
764 public static class RampDimmerCommandHandler extends InstantDimmerCommandHandler {
765 RampDimmerCommandHandler(DeviceFeature feature) {
770 public void handleCommand(InsteonChannelConfiguration config, Command cmd) {
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);
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);
790 private RampRate getRampRate(InsteonChannelConfiguration config) {
791 return Objects.requireNonNullElse(config.getRampRate(), RampRate.DEFAULT);
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;
800 return level > 0 ? 0x2E : 0x2F;
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;
812 * Switch on/off command handler
814 public static class SwitchOnOffCommandHandler extends OnOffCommandHandler {
815 SwitchOnOffCommandHandler(DeviceFeature feature) {
820 protected int getCommandCode(InsteonChannelConfiguration config, Command cmd) {
821 return OnOffType.OFF.equals(cmd) ? 0x13 : 0x11;
825 protected int getLevel(InsteonChannelConfiguration config, Command cmd) {
826 return OnOffType.OFF.equals(cmd) ? 0x00 : 0xFF;
830 protected int getGroup(InsteonChannelConfiguration config) {
831 return getInsteonDevice().isDeviceSyncEnabled() ? feature.getBroadcastGroup(config) : -1;
836 * Switch percent command handler
838 public static class SwitchPercentCommandHandler extends OnOffCommandHandler {
839 SwitchPercentCommandHandler(DeviceFeature feature) {
844 public boolean canHandle(Command cmd) {
845 return cmd instanceof PercentType;
849 protected int getCommandCode(InsteonChannelConfiguration config, Command cmd) {
850 return PercentType.ZERO.equals(cmd) ? 0x13 : 0x11;
854 protected int getLevel(InsteonChannelConfiguration config, Command cmd) {
855 return PercentType.ZERO.equals(cmd) ? 0x00 : 0xFF;
860 * Switch increment command handler
862 public static class SwitchIncrementCommandHandler extends OnOffCommandHandler {
863 SwitchIncrementCommandHandler(DeviceFeature feature) {
868 public boolean canHandle(Command cmd) {
869 return IncreaseDecreaseType.INCREASE.equals(cmd) || UpDownType.UP.equals(cmd);
873 protected int getCommandCode(InsteonChannelConfiguration config, Command cmd) {
878 protected int getLevel(InsteonChannelConfiguration config, Command cmd) {
884 * Broadcast on/off command handler
886 public static class BroadcastOnOffCommandHandler extends OnOffCommandHandler {
887 BroadcastOnOffCommandHandler(DeviceFeature feature) {
892 public void handleCommand(InsteonChannelConfiguration config, Command cmd) {
893 if (getGroup(config) != -1) {
894 super.handleCommand(config, cmd);
899 protected int getCommandCode(InsteonChannelConfiguration config, Command cmd) {
900 return OnOffType.OFF.equals(cmd) ? 0x13 : 0x11;
904 protected int getLevel(InsteonChannelConfiguration config, Command cmd) {
905 return 0x00; // not parsed
909 protected int getGroup(InsteonChannelConfiguration config) {
910 return feature.getBroadcastGroup(config);
915 * Broadcast fast on/off command handler
917 public static class BroadcastFastOnOffCommandHandler extends BroadcastOnOffCommandHandler {
918 BroadcastFastOnOffCommandHandler(DeviceFeature feature) {
923 protected int getCommandCode(InsteonChannelConfiguration config, Command cmd) {
924 return OnOffType.OFF.equals(cmd) ? 0x14 : 0x12;
929 * Broadcast manual change up/down command handler
931 public static class BroadcastManualChangeUpDownCommandHandler extends BroadcastOnOffCommandHandler {
932 BroadcastManualChangeUpDownCommandHandler(DeviceFeature feature) {
937 public boolean canHandle(Command cmd) {
938 return cmd instanceof UpDownType;
942 protected int getCommandCode(InsteonChannelConfiguration config, Command cmd) {
943 return 0x17; // manual change start
947 protected int getLevel(InsteonChannelConfiguration config, Command cmd) {
948 return UpDownType.UP.equals(cmd) ? 0x01 : 0x00; // up or down
953 * Broadcast manual change stop command handler
955 public static class BroadcastManualChangeStopCommandHandler extends BroadcastOnOffCommandHandler {
956 BroadcastManualChangeStopCommandHandler(DeviceFeature feature) {
961 public boolean canHandle(Command cmd) {
962 return StopMoveType.STOP.equals(cmd);
966 protected int getCommandCode(InsteonChannelConfiguration config, Command cmd) {
967 return 0x18; // manual change stop
972 * Broadcast refresh command handler
974 public static class BroadcastRefreshCommandHandler extends RefreshCommandHandler {
975 BroadcastRefreshCommandHandler(DeviceFeature feature) {
980 public void handleCommand(InsteonChannelConfiguration config, Command cmd) {
981 int group = feature.getBroadcastGroup(config);
983 feature.pollRelatedDevices(group, 0L);
989 * Keypad button on/off command handler
991 public static class KeypadButtonOnOffCommandHandler extends CustomBitmaskCommandHandler {
992 KeypadButtonOnOffCommandHandler(DeviceFeature feature) {
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,
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);
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);
1026 protected int getBitNumber() {
1027 return feature.getGroup() - 1;
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());
1042 if (logger.isTraceEnabled()) {
1043 logger.trace("{}: bitmask:{} onMask:{} offMask:{}", nm(), BinaryUtils.getBinaryString(bitmask),
1044 BinaryUtils.getBinaryString(onMask), BinaryUtils.getBinaryString(offMask));
1046 // apply button on/off mask
1047 bitmask = bitmask & ~offMask | onMask;
1048 // update last bitmask value
1049 updateLastBitmaskValue(bitmask);
1055 protected OnOffType getOnOffCommand(Command cmd) {
1056 return (OnOffType) cmd;
1059 protected int getGroup(InsteonChannelConfiguration config) {
1060 return getInsteonDevice().isDeviceSyncEnabled() ? feature.getBroadcastGroup(config) : -1;
1063 private KeypadButtonToggleMode getToggleMode() {
1065 State state = getInsteonDevice().getFeatureState(FEATURE_TYPE_KEYPAD_BUTTON_TOGGLE_MODE,
1066 feature.getGroup());
1067 if (state != null) {
1068 return KeypadButtonToggleMode.valueOf(state.toString());
1070 } catch (IllegalArgumentException e) {
1072 return KeypadButtonToggleMode.TOGGLE;
1075 private void sendBroadcastOnOff(InsteonChannelConfiguration config, Command cmd) {
1076 BroadcastOnOffCommandHandler handler = new BroadcastOnOffCommandHandler(feature);
1077 handler.setParameters(parameters);
1078 handler.handleCommand(config, cmd);
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));
1093 * Keypad button percent command handler
1095 public static class KeypadButtonPercentCommandHandler extends KeypadButtonOnOffCommandHandler {
1096 KeypadButtonPercentCommandHandler(DeviceFeature feature) {
1101 public boolean canHandle(Command cmd) {
1102 return cmd instanceof PercentType;
1106 protected OnOffType getOnOffCommand(Command cmd) {
1107 return OnOffType.from(!PercentType.ZERO.equals(cmd));
1111 protected int getGroup(InsteonChannelConfiguration config) {
1117 * Keypad button increment command handler
1119 public static class KeypadButtonIncrementCommandHandler extends KeypadButtonOnOffCommandHandler {
1120 KeypadButtonIncrementCommandHandler(DeviceFeature feature) {
1125 public boolean canHandle(Command cmd) {
1126 return IncreaseDecreaseType.INCREASE.equals(cmd) || UpDownType.UP.equals(cmd);
1130 protected OnOffType getOnOffCommand(Command cmd) {
1131 return OnOffType.ON;
1135 protected int getGroup(InsteonChannelConfiguration config) {
1141 * Keypad button config command handler
1143 public static class KeypadButtonConfigCommandHandler extends OpFlagsCommandHandler {
1144 KeypadButtonConfigCommandHandler(DeviceFeature feature) {
1149 public boolean canHandle(Command cmd) {
1150 return cmd instanceof StringType;
1154 protected int getOpFlagCommand(Command cmd) {
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);
1165 protected boolean isStateRetrievable() {
1171 * Keypad button toggle mode command handler
1173 public static class KeypadButtonToggleModeCommandHandler extends CommandHandler {
1174 KeypadButtonToggleModeCommandHandler(DeviceFeature feature) {
1179 public boolean canHandle(Command cmd) {
1180 return cmd instanceof DecimalType || cmd instanceof StringType;
1184 public void handleCommand(InsteonChannelConfiguration config, Command cmd) {
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());
1194 int lastValue = feature.getLastMsgValueAsInteger(-1);
1195 if (lastValue == -1) {
1196 logger.debug("{}: undefined toggle mode last value for {}", nm(), feature.getName());
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);
1206 } catch (IllegalArgumentException e) {
1207 logger.warn("{}: got unexpected toggle mode command: {}, ignoring request", nm(), cmd);
1211 private void setToggleMode(int nonToggleMask, int alwaysOnOffMask) {
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);
1222 if (logger.isDebugEnabled()) {
1223 logger.debug("{}: sent keypad button non toggle mask {} request to {}", nm(),
1224 HexUtils.getHexString(nonToggleMask), address);
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);
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);
1241 * Heartbeat interval command handler
1243 public static class HeartbeatIntervalCommandHandler extends CustomCommandHandler {
1244 HeartbeatIntervalCommandHandler(DeviceFeature feature) {
1249 public boolean canHandle(Command cmd) {
1250 return cmd instanceof DecimalType || cmd instanceof QuantityType;
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);
1263 int value = (int) Math.floor(interval / increment); // round down
1264 return interval == preset ? 0x00 : Math.max(0x00, Math.min(value, 0xFF));
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();
1280 * Motion sensor 2 heartbeat interval command handler
1282 public static class MotionSensor2HeartbeatIntervalCommandHandler extends HeartbeatIntervalCommandHandler {
1283 MotionSensor2HeartbeatIntervalCommandHandler(DeviceFeature feature) {
1288 public void handleCommand(InsteonChannelConfiguration config, Command cmd) {
1290 int heartbeatInterval = (int) getValue(cmd);
1291 int lowBatteryThreshold = getInsteonDevice().getLastMsgValueAsInteger(FEATURE_LOW_BATTERY_THRESHOLD,
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);
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);
1312 * Siren on/off command handler
1314 public static class SirenOnOffCommandHandler extends SwitchOnOffCommandHandler {
1315 SirenOnOffCommandHandler(DeviceFeature feature) {
1320 protected int getLevel(InsteonChannelConfiguration config, Command cmd) {
1321 return OnOffType.OFF.equals(cmd) ? 0x00 : 0x7F; // no delay + max duration (127 seconds)
1326 * Siren armed command handler
1328 public static class SirenArmedCommandHandler extends OpFlagsCommandHandler {
1329 SirenArmedCommandHandler(DeviceFeature feature) {
1334 protected byte[] getOpFlagData(Command cmd) {
1335 return OnOffType.ON.equals(cmd) ? new byte[] { (byte) 0x01 } : new byte[0];
1339 protected boolean isStateRetrievable() {
1345 * Siren alert duration command handler
1347 public static class SirenAlertDurationCommandHandler extends CustomCommandHandler {
1348 SirenAlertDurationCommandHandler(DeviceFeature feature) {
1353 public boolean canHandle(Command cmd) {
1354 return cmd instanceof DecimalType || cmd instanceof QuantityType;
1358 protected double getValue(Command cmd) {
1359 int duration = getDuration(cmd);
1360 int value = feature.getLastMsgValueAsInteger(-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);
1366 return value & 0x80 | duration;
1371 private int getDuration(Command cmd) {
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();
1378 return duration != -1 ? Math.max(0, Math.min(duration, 127)) : -1; // allowed range 0-127 seconds
1383 * Siren alert type command handler
1385 public static class SirenAlertTypeCommandHandler extends CustomCommandHandler {
1386 SirenAlertTypeCommandHandler(DeviceFeature feature) {
1391 public boolean canHandle(Command cmd) {
1392 return cmd instanceof StringType;
1396 protected double getValue(Command cmd) {
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);
1409 * LED brightness command handler
1411 public static class LEDBrightnessCommandHandler extends CommandHandler {
1412 LEDBrightnessCommandHandler(DeviceFeature feature) {
1417 public boolean canHandle(Command cmd) {
1418 return cmd instanceof OnOffType || cmd instanceof PercentType;
1422 public void handleCommand(InsteonChannelConfiguration config, Command cmd) {
1424 int level = getLevel(cmd);
1425 int userData2 = getParameterAsInteger("d2", -1);
1426 if (userData2 != -1) {
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);
1440 logger.warn("{}: no d2 parameter specified in command handler", nm());
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);
1449 private int getLevel(Command cmd) {
1451 if (cmd instanceof PercentType percent) {
1452 level = percent.intValue();
1454 level = OnOffType.OFF.equals(cmd) ? 0 : 100;
1456 return (int) Math.round(level * 127.0 / 100);
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);
1468 * Momentary on command handler
1470 public static class MomentaryOnCommandHandler extends CommandHandler {
1471 MomentaryOnCommandHandler(DeviceFeature feature) {
1476 public boolean canHandle(Command cmd) {
1477 return OnOffType.ON.equals(cmd);
1481 public void handleCommand(InsteonChannelConfiguration config, Command cmd) {
1483 int cmd1 = getParameterAsInteger("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);
1490 logger.warn("{}: no cmd1 field specified", nm());
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);
1501 * Operating flags command handler
1503 public static class OpFlagsCommandHandler extends CommandHandler {
1504 OpFlagsCommandHandler(DeviceFeature feature) {
1509 public boolean canHandle(Command cmd) {
1510 return cmd instanceof OnOffType;
1514 public void handleCommand(InsteonChannelConfiguration config, Command cmd) {
1516 int cmd2 = getOpFlagCommand(cmd);
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);
1528 logger.warn("{}: unable to determine op flags command, ignoring request", nm());
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);
1537 protected int getOpFlagCommand(Command cmd) {
1538 return OnOffType.OFF.equals(cmd) ? getParameterAsInteger("off", -1) : getParameterAsInteger("on", -1);
1541 protected byte[] getOpFlagData(Command cmd) {
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);
1550 return Msg.makeStandardMessage(address, (byte) 0x20, (byte) cmd2);
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;
1562 * Multi-operating flags abstract command handler
1564 public abstract static class MultiOpFlagsCommandHandler extends OpFlagsCommandHandler {
1565 MultiOpFlagsCommandHandler(DeviceFeature feature) {
1570 public boolean canHandle(Command cmd) {
1571 return cmd instanceof StringType;
1575 public void handleCommand(InsteonChannelConfiguration config, Command cmd) {
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());
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);
1590 protected abstract Map<Integer, String> getOpFlagCommands(Command cmd);
1594 * Ramp rate command handler
1596 public static class RampRateCommandHandler extends CommandHandler {
1597 RampRateCommandHandler(DeviceFeature feature) {
1602 public boolean canHandle(Command cmd) {
1603 return cmd instanceof DecimalType || cmd instanceof QuantityType;
1607 public void handleCommand(InsteonChannelConfiguration config, Command cmd) {
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);
1618 logger.warn("{}: got unexpected ramp rate command {}, ignoreing request", nm(), cmd);
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);
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();
1634 return rampTime != -1 ? RampRate.fromTime(rampTime) : null;
1639 * FanLinc fan speed command handler
1641 public static class FanLincFanSpeedCommandHandler extends OnOffCommandHandler {
1642 FanLincFanSpeedCommandHandler(DeviceFeature feature) {
1647 public boolean canHandle(Command cmd) {
1648 return cmd instanceof StringType;
1652 protected int getCommandCode(InsteonChannelConfiguration config, Command cmd) {
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);
1663 protected int getLevel(InsteonChannelConfiguration config, Command cmd) {
1665 String speed = ((StringType) cmd).toString();
1666 return FanLincFanSpeed.valueOf(speed).getValue();
1667 } catch (IllegalArgumentException e) {
1674 * FanLinc fan on/off command handler
1676 public static class FanLincFanOnOffCommandHandler extends OnOffCommandHandler {
1677 FanLincFanOnOffCommandHandler(DeviceFeature feature) {
1682 protected int getCommandCode(InsteonChannelConfiguration config, Command cmd) {
1683 return OnOffType.OFF.equals(cmd) ? 0x13 : 0x11;
1688 * FanLinc fan percent command handler
1690 public static class FanLincFanPercentCommandHandler extends OnOffCommandHandler {
1691 FanLincFanPercentCommandHandler(DeviceFeature feature) {
1696 public boolean canHandle(Command cmd) {
1697 return cmd instanceof PercentType;
1701 protected int getCommandCode(InsteonChannelConfiguration config, Command cmd) {
1702 return PercentType.ZERO.equals(cmd) ? 0x13 : 0x11;
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
1713 * I/O linc momentary duration command handler
1715 public static class IOLincMomentaryDurationCommandHandler extends CommandHandler {
1716 IOLincMomentaryDurationCommandHandler(DeviceFeature feature) {
1721 public boolean canHandle(Command cmd) {
1722 return cmd instanceof DecimalType || cmd instanceof QuantityType;
1726 public void handleCommand(InsteonChannelConfiguration config, Command cmd) {
1728 double duration = getDuration(cmd);
1729 if (duration != -1) {
1730 InsteonAddress address = getInsteonDevice().getAddress();
1732 int delay = (int) Math.round(duration * 10);
1734 prescaler = (int) Math.ceil(delay / 255.0);
1735 delay = (int) Math.round(delay / (double) prescaler);
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);
1745 feature.sendRequest(delayMsg);
1746 if (logger.isDebugEnabled()) {
1747 logger.debug("{}: sent momentary duration delay {} request to {}", nm(),
1748 HexUtils.getHexString(delay), address);
1750 feature.sendRequest(prescalerMsg);
1751 if (logger.isDebugEnabled()) {
1752 logger.debug("{}: sent momentary duration prescaler {} request to {}", nm(),
1753 HexUtils.getHexString(prescaler), address);
1756 logger.warn("{}: got unexpected momentary duration command {}, ignoring request", nm(), cmd);
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);
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();
1776 * I/O linc relay mode command handler
1778 public static class IOLincRelayModeCommandHandler extends MultiOpFlagsCommandHandler {
1779 IOLincRelayModeCommandHandler(DeviceFeature feature) {
1784 protected Map<Integer, String> getOpFlagCommands(Command cmd) {
1785 Map<Integer, String> commands = new HashMap<>();
1787 String mode = ((StringType) cmd).toString();
1788 switch (IOLincRelayMode.valueOf(mode)) {
1790 commands.put(0x07, "momentary mode OFF");
1793 commands.put(0x06, "momentary mode ON");
1794 commands.put(0x13, "momentary trigger on/off OFF");
1795 commands.put(0x15, "momentary sensor follow OFF");
1798 commands.put(0x06, "momentary mode ON");
1799 commands.put(0x12, "momentary trigger on/off ON");
1800 commands.put(0x15, "momentary sensor follow OFF");
1803 commands.put(0x06, "momentary mode ON");
1804 commands.put(0x13, "momentary trigger on/off OFF");
1805 commands.put(0x14, "momentary sensor follow ON");
1808 } catch (IllegalArgumentException e) {
1809 logger.warn("{}: got unexpected relay mode command: {}, ignoring request", nm(), cmd);
1816 * Micro module operation mode command handler
1818 public static class MicroModuleOpModeCommandHandler extends MultiOpFlagsCommandHandler {
1819 MicroModuleOpModeCommandHandler(DeviceFeature feature) {
1824 protected Map<Integer, String> getOpFlagCommands(Command cmd) {
1825 Map<Integer, String> commands = new HashMap<>();
1827 String mode = ((StringType) cmd).toString();
1828 switch (MicroModuleOpMode.valueOf(mode)) {
1830 commands.put(0x20, "momentary line OFF");
1832 case SINGLE_MOMENTARY:
1833 commands.put(0x21, "momentary line ON");
1834 commands.put(0x1E, "dual line OFF");
1836 case DUAL_MOMENTARY:
1837 commands.put(0x21, "momentary line ON");
1838 commands.put(0x1E, "dual line ON");
1841 } catch (IllegalArgumentException e) {
1842 logger.warn("{}: got unexpected operation mode command: {}, ignoring request", nm(), cmd);
1849 * Sprinkler valve on/off command handler
1851 public static class SprinklerValveOnOffCommandHandler extends OnOffCommandHandler {
1852 SprinklerValveOnOffCommandHandler(DeviceFeature feature) {
1857 protected int getCommandCode(InsteonChannelConfiguration config, Command cmd) {
1858 return OnOffType.ON.equals(cmd) ? 0x40 : 0x41;
1862 protected int getLevel(InsteonChannelConfiguration config, Command cmd) {
1863 return getParameterAsInteger("valve", -1);
1868 * Sprinkler program on/off command handler
1870 public static class SprinklerProgramOnOffCommandHandler extends OnOffCommandHandler {
1871 SprinklerProgramOnOffCommandHandler(DeviceFeature feature) {
1876 public boolean canHandle(Command cmd) {
1877 return cmd instanceof PlayPauseType;
1881 protected int getCommandCode(InsteonChannelConfiguration config, Command cmd) {
1882 return PlayPauseType.PLAY.equals(cmd) ? 0x42 : 0x43;
1886 protected int getLevel(InsteonChannelConfiguration config, Command cmd) {
1887 return getParameterAsInteger("program", -1);
1892 * Sprinkler program next/previous command handler
1894 public static class SprinklerProgramNextPreviousCommandHandler extends OnOffCommandHandler {
1895 SprinklerProgramNextPreviousCommandHandler(DeviceFeature feature) {
1900 public boolean canHandle(Command cmd) {
1901 return cmd instanceof NextPreviousType;
1905 protected int getCommandCode(InsteonChannelConfiguration config, Command cmd) {
1906 return 0x44; // sprinkler control
1910 protected int getLevel(InsteonChannelConfiguration config, Command cmd) {
1911 return NextPreviousType.NEXT.equals(cmd) ? 0x05 : 0x06; // skip forward or back
1916 * Thermostat fan mode command handler
1918 public static class ThermostatFanModeCommandHandler extends CustomCommandHandler {
1919 ThermostatFanModeCommandHandler(DeviceFeature feature) {
1924 public boolean canHandle(Command cmd) {
1925 return cmd instanceof StringType;
1929 protected double getValue(Command cmd) {
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);
1941 * Thermostat system mode command handler
1943 public static class ThermostatSystemModeCommandHandler extends CustomCommandHandler {
1944 ThermostatSystemModeCommandHandler(DeviceFeature feature) {
1949 public boolean canHandle(Command cmd) {
1950 return cmd instanceof StringType;
1954 protected double getValue(Command cmd) {
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);
1966 * Venstar thermostat system mode handler
1968 public static class VenstarSystemModeCommandHandler extends CustomCommandHandler {
1969 VenstarSystemModeCommandHandler(DeviceFeature feature) {
1974 public boolean canHandle(Command cmd) {
1975 return cmd instanceof StringType;
1979 protected double getValue(Command cmd) {
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);
1991 * Thermostat temperature scale command handler
1993 public static class ThermostatTemperatureScaleCommandHandler extends CustomBitmaskCommandHandler {
1994 ThermostatTemperatureScaleCommandHandler(DeviceFeature feature) {
1999 public boolean canHandle(Command cmd) {
2000 return cmd instanceof StringType;
2004 protected @Nullable Boolean shouldSetBit(Command cmd) {
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);
2016 * Thermostat time format command handler
2018 public static class ThermostatTimeFormatCommandHandler extends CustomBitmaskCommandHandler {
2019 ThermostatTimeFormatCommandHandler(DeviceFeature feature) {
2024 public boolean canHandle(Command cmd) {
2025 return cmd instanceof StringType;
2029 protected @Nullable Boolean shouldSetBit(Command cmd) {
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);
2041 * Thermostat sync time command handler
2043 public static class ThermostatSyncTimeCommandHandler extends MomentaryOnCommandHandler {
2044 ThermostatSyncTimeCommandHandler(DeviceFeature feature) {
2049 public void handleCommand(InsteonChannelConfiguration config, Command cmd) {
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);
2067 * IM generic abstract command handler
2069 public abstract static class IMCommandHandler extends CommandHandler {
2070 IMCommandHandler(DeviceFeature feature) {
2075 public void handleCommand(InsteonChannelConfiguration config, Command cmd) {
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);
2087 protected abstract Msg getIMMessage(Command cmd) throws InvalidMessageTypeException, FieldException;
2091 * IM led on/off command handler
2093 public static class IMLEDOnOffCommandHandler extends IMCommandHandler {
2094 IMLEDOnOffCommandHandler(DeviceFeature feature) {
2099 public boolean canHandle(Command cmd) {
2100 return cmd instanceof OnOffType;
2104 public void handleCommand(InsteonChannelConfiguration config, Command cmd) {
2106 setLEDControl(config);
2108 super.handleCommand(config, cmd);
2109 // update state since not retrievable
2110 feature.updateState((State) cmd);
2114 protected Msg getIMMessage(Command cmd) throws InvalidMessageTypeException, FieldException {
2115 return Msg.makeMessage(OnOffType.OFF.equals(cmd) ? "LEDOff" : "LEDOn");
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);
2127 * IM beep command handler
2129 public static class IMBeepCommandHandler extends IMCommandHandler {
2130 IMBeepCommandHandler(DeviceFeature feature) {
2135 public boolean canHandle(Command cmd) {
2136 return OnOffType.ON.equals(cmd);
2140 protected Msg getIMMessage(Command cmd) throws InvalidMessageTypeException, FieldException {
2141 return Msg.makeMessage("Beep");
2146 * IM config command handler
2148 public static class IMConfigCommandHandler extends CustomBitmaskCommandHandler {
2149 IMConfigCommandHandler(DeviceFeature feature) {
2154 public void handleCommand(InsteonChannelConfiguration config, Command cmd) {
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());
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);
2172 * X10 generic abstract command handler
2174 public abstract static class X10CommandHandler extends CommandHandler {
2175 X10CommandHandler(DeviceFeature feature) {
2180 public void handleCommand(InsteonChannelConfiguration config, Command cmd) {
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);
2196 protected abstract int getCommandCode(Command cmd, byte houseCode);
2200 * X10 on/off command handler
2202 public static class X10OnOffCommandHandler extends X10CommandHandler {
2203 X10OnOffCommandHandler(DeviceFeature feature) {
2208 public boolean canHandle(Command cmd) {
2209 return cmd instanceof OnOffType;
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;
2220 * X10 percent command handler
2222 public static class X10PercentCommandHandler extends X10CommandHandler {
2224 private static final int[] X10_LEVEL_CODES = { 0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15 };
2226 X10PercentCommandHandler(DeviceFeature feature) {
2231 public boolean canHandle(Command cmd) {
2232 return cmd instanceof PercentType;
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;
2245 * X10 increase/decrease command handler
2247 public static class X10IncreaseDecreaseCommandHandler extends X10CommandHandler {
2248 X10IncreaseDecreaseCommandHandler(DeviceFeature feature) {
2253 public boolean canHandle(Command cmd) {
2254 return cmd instanceof IncreaseDecreaseType;
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;
2265 * Factory method to dermine if a command handler supports a given command type
2267 * @param type the handler command type
2268 * @return true if handler supports command type, otherwise false
2270 public static boolean supportsCommandType(String type) {
2271 return SUPPORTED_COMMAND_TYPES.contains(type);
2275 * Factory method for creating default command handler
2277 * @param feature the feature for which to create the handler
2278 * @return the default command handler which was created
2280 public static DefaultCommandHandler makeDefaultHandler(DeviceFeature feature) {
2281 return new DefaultCommandHandler(feature);
2285 * Factory method for creating handlers of a given name using java reflection
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
2292 public static @Nullable <T extends CommandHandler> T makeHandler(String name, Map<String, String> parameters,
2293 DeviceFeature feature) {
2295 String className = CommandHandler.class.getName() + "$" + name;
2296 @SuppressWarnings("unchecked")
2297 Class<? extends T> classRef = (Class<? extends T>) Class.forName(className);
2299 T handler = classRef.getDeclaredConstructor(DeviceFeature.class).newInstance(feature);
2300 handler.setParameters(parameters);
2302 } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | IllegalArgumentException
2303 | InvocationTargetException | NoSuchMethodException | SecurityException e) {