]> git.basschouten.com Git - openhab-addons.git/blob
0cddd0ae67508d341eb5d537631989b05a7b5c06
[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.math.BigDecimal;
19 import java.math.RoundingMode;
20 import java.time.Instant;
21 import java.time.ZoneId;
22 import java.time.ZonedDateTime;
23 import java.util.Map;
24 import java.util.Set;
25
26 import javax.measure.Unit;
27 import javax.measure.quantity.Dimensionless;
28 import javax.measure.quantity.Energy;
29 import javax.measure.quantity.Power;
30 import javax.measure.quantity.Temperature;
31 import javax.measure.quantity.Time;
32
33 import org.eclipse.jdt.annotation.NonNullByDefault;
34 import org.eclipse.jdt.annotation.Nullable;
35 import org.openhab.binding.insteon.internal.device.DeviceFeature;
36 import org.openhab.binding.insteon.internal.device.DeviceType;
37 import org.openhab.binding.insteon.internal.device.DeviceTypeRegistry;
38 import org.openhab.binding.insteon.internal.device.InsteonEngine;
39 import org.openhab.binding.insteon.internal.device.RampRate;
40 import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.ButtonEvent;
41 import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.FanLincFanSpeed;
42 import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.IMButtonEvent;
43 import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.IOLincRelayMode;
44 import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.KeypadButtonConfig;
45 import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.KeypadButtonToggleMode;
46 import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.MicroModuleOpMode;
47 import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.SirenAlertType;
48 import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.ThermostatFanMode;
49 import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.ThermostatSystemMode;
50 import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.ThermostatSystemState;
51 import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.ThermostatTemperatureScale;
52 import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.ThermostatTimeFormat;
53 import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.VenstarSystemMode;
54 import org.openhab.binding.insteon.internal.transport.message.FieldException;
55 import org.openhab.binding.insteon.internal.transport.message.GroupMessageStateMachine.GroupMessageType;
56 import org.openhab.binding.insteon.internal.transport.message.Msg;
57 import org.openhab.binding.insteon.internal.utils.BinaryUtils;
58 import org.openhab.binding.insteon.internal.utils.HexUtils;
59 import org.openhab.binding.insteon.internal.utils.ParameterParser;
60 import org.openhab.core.library.types.DateTimeType;
61 import org.openhab.core.library.types.DecimalType;
62 import org.openhab.core.library.types.OnOffType;
63 import org.openhab.core.library.types.OpenClosedType;
64 import org.openhab.core.library.types.PercentType;
65 import org.openhab.core.library.types.QuantityType;
66 import org.openhab.core.library.types.StringType;
67 import org.openhab.core.library.unit.ImperialUnits;
68 import org.openhab.core.library.unit.SIUnits;
69 import org.openhab.core.library.unit.Units;
70 import org.openhab.core.types.State;
71 import org.openhab.core.types.UnDefType;
72 import org.slf4j.Logger;
73 import org.slf4j.LoggerFactory;
74
75 /**
76  * A message handler processes incoming Insteon messages
77  *
78  * @author Daniel Pfrommer - Initial contribution
79  * @author Bernd Pfrommer - openHAB 1 insteonplm binding
80  * @author Rob Nielsen - Port to openHAB 2 insteon binding
81  * @author Jeremy Setton - Rewrite insteon binding
82  */
83 @NonNullByDefault
84 public abstract class MessageHandler extends BaseFeatureHandler {
85     private static final Set<Integer> SUPPORTED_GROUP_COMMANDS = Set.of(0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
86             0x2E);
87
88     protected final Logger logger = LoggerFactory.getLogger(MessageHandler.class);
89
90     public MessageHandler(DeviceFeature feature) {
91         super(feature);
92     }
93
94     /**
95      * Returns handler id
96      *
97      * @return handler id based on command and group parameters
98      */
99     public String getId() {
100         int command = getParameterAsInteger("command", -1);
101         int group = getGroup();
102         return MessageHandler.generateId(command, group);
103     }
104
105     /**
106      * Returns handler group
107      *
108      * @return handler group based on feature or handler group parameter, if supports group, otherwise -1
109      */
110     public int getGroup() {
111         int command = getParameterAsInteger("command", -1);
112         // return -1 if handler doesn't support groups
113         if (!MessageHandler.supportsGroup(command)) {
114             return -1;
115         }
116         int group = ParameterParser.getParameterAsOrDefault(parameters.get("group"), Integer.class, -1);
117         // return handler group parameter if non-standard
118         if (group > 1) {
119             return group;
120         }
121         // return feature group parameter if defined, otherwise handler group parameter
122         return feature.getParameterAsInteger("group", group);
123     }
124
125     /**
126      * Returns if can handle a given message
127      *
128      * @param msg the message to be handled
129      * @return true if handler not duplicate, valid and matches filter parameters
130      */
131     public boolean canHandle(Msg msg) {
132         if (isDuplicate(msg)) {
133             logger.trace("{}:{} ignoring msg as duplicate", getDevice().getAddress(), feature.getName());
134             return false;
135         } else if (!isValid(msg)) {
136             logger.trace("{}:{} ignoring msg as not valid", getDevice().getAddress(), feature.getName());
137             return false;
138         } else if (!matchesFilters(msg)) {
139             logger.trace("{}:{} ignoring msg as unmatch filters", getDevice().getAddress(), feature.getName());
140             return false;
141         }
142         return true;
143     }
144
145     /**
146      * Returns if an incoming message is a duplicate
147      *
148      * @param msg the received message
149      * @return true if the broadcast message is a duplicate
150      */
151     protected boolean isDuplicate(Msg msg) {
152         try {
153             if (msg.isAllLinkBroadcastOrCleanup()) {
154                 byte cmd1 = msg.getByte("command1");
155                 long timestamp = msg.getTimestamp();
156                 int group = msg.getGroup();
157                 GroupMessageType type = msg.isAllLinkBroadcast() ? GroupMessageType.BCAST : GroupMessageType.CLEAN;
158                 if (msg.isAllLinkSuccessReport()) {
159                     cmd1 = msg.getInsteonAddress("toAddress").getHighByte();
160                     type = GroupMessageType.SUCCESS;
161                 }
162                 return getInsteonDevice().isDuplicateGroupMsg(cmd1, timestamp, group, type);
163             } else if (msg.isBroadcast()) {
164                 byte cmd1 = msg.getByte("command1");
165                 long timestamp = msg.getTimestamp();
166                 return getInsteonDevice().isDuplicateBroadcastMsg(cmd1, timestamp);
167             }
168         } catch (IllegalArgumentException e) {
169             logger.warn("cannot parse msg: {}", msg, e);
170         } catch (FieldException e) {
171             logger.warn("cannot parse msg: {}", msg, e);
172         }
173         return false;
174     }
175
176     /**
177      * Returns if an incoming DIRECT message is valid
178      *
179      * @param msg the received DIRECT message
180      * @return true if this message is valid
181      */
182     protected boolean isValid(Msg msg) {
183         if (msg.isDirect()) {
184             int ext = getParameterAsInteger("ext", -1);
185             // extended message crc is only included in incoming message when using the newer 2-byte method
186             if (ext == 2) {
187                 return msg.hasValidCRC2();
188             }
189         }
190         return true;
191     }
192
193     /**
194      * Returns if message matches the filter parameters
195      *
196      * @param msg message to check
197      * @return true if message matches
198      */
199     protected boolean matchesFilters(Msg msg) {
200         try {
201             int ext = getParameterAsInteger("ext", -1);
202             if (ext != -1) {
203                 if ((!msg.isExtended() && ext != 0) || (msg.isExtended() && ext != 1 && ext != 2)) {
204                     return false;
205                 }
206                 if (!matchesParameter(msg, "command1", "cmd1")) {
207                     return false;
208                 }
209             }
210             if (!matchesParameter(msg, "command2", "cmd2")) {
211                 return false;
212             }
213             if (!matchesParameter(msg, "userData1", "d1")) {
214                 return false;
215             }
216             if (!matchesParameter(msg, "userData2", "d2")) {
217                 return false;
218             }
219             if (!matchesParameter(msg, "userData3", "d3")) {
220                 return false;
221             }
222         } catch (FieldException e) {
223             logger.warn("error matching message: {}", msg, e);
224             return false;
225         }
226         return true;
227     }
228
229     /**
230      * Returns if parameter matches value
231      *
232      * @param msg message to check
233      * @param field field name to match
234      * @param param name of parameter to match
235      * @return true if parameter matches
236      * @throws FieldException if field not there
237      */
238     private boolean matchesParameter(Msg msg, String field, String param) throws FieldException {
239         int mp = getParameterAsInteger(param, -1);
240         // parameter not filtered for, declare this a match!
241         if (mp == -1) {
242             return true;
243         }
244         byte value = msg.getByte(field);
245         return value == mp;
246     }
247
248     /**
249      * Handles incoming message. The cmd1 parameter
250      * has been extracted earlier already (to make a decision which message handler to call),
251      * and is passed in as an argument so cmd1 does not have to be extracted from the message again.
252      *
253      * @param cmd1 the insteon cmd1 field
254      * @param msg the received insteon message
255      */
256     public abstract void handleMessage(byte cmd1, Msg msg);
257
258     /**
259      * Default message handler
260      */
261     public static class DefaultMsgHandler extends MessageHandler {
262         DefaultMsgHandler(DeviceFeature feature) {
263             super(feature);
264         }
265
266         @Override
267         public void handleMessage(byte cmd1, Msg msg) {
268             if (logger.isDebugEnabled()) {
269                 logger.debug("{}: ignoring unimpl message with cmd1 {}", nm(), HexUtils.getHexString(cmd1));
270             }
271         }
272     }
273
274     /**
275      * No-op message handler
276      */
277     public static class NoOpMsgHandler extends MessageHandler {
278         NoOpMsgHandler(DeviceFeature feature) {
279             super(feature);
280         }
281
282         @Override
283         public void handleMessage(byte cmd1, Msg msg) {
284             if (logger.isTraceEnabled()) {
285                 logger.trace("{}: ignoring message with cmd1 {}", nm(), HexUtils.getHexString(cmd1));
286             }
287         }
288     }
289
290     /**
291      * Trigger poll message handler
292      */
293     public static class TriggerPollMsgHandler extends MessageHandler {
294         TriggerPollMsgHandler(DeviceFeature feature) {
295             super(feature);
296         }
297
298         @Override
299         public void handleMessage(byte cmd1, Msg msg) {
300             // trigger poll with delay based on parameter, defaulting to 0 ms
301             long delay = getParameterAsLong("delay", 0L);
302             feature.triggerPoll(delay);
303         }
304     }
305
306     /**
307      * Custom state abstract message handler based of parameters
308      */
309     public abstract static class CustomMsgHandler extends MessageHandler {
310         CustomMsgHandler(DeviceFeature feature) {
311             super(feature);
312         }
313
314         @Override
315         public void handleMessage(byte cmd1, Msg msg) {
316             try {
317                 // extract raw value from message
318                 int raw = getRawValue(msg);
319                 // apply mask and right shift bit manipulation
320                 int cooked = (raw & getParameterAsInteger("mask", 0xFF)) >> getParameterAsInteger("rshift", 0);
321                 // multiply with factor and add offset
322                 double value = cooked * getParameterAsDouble("factor", 1.0) + getParameterAsDouble("offset", 0.0);
323                 // get state to update
324                 State state = getState(cmd1, value);
325                 // store extracted cooked message value
326                 feature.setLastMsgValue(value);
327                 // update state if defined
328                 if (state != null) {
329                     logger.debug("{}: device {} {} is {}", nm(), getInsteonDevice().getAddress(), feature.getName(),
330                             state);
331                     feature.updateState(state);
332                 }
333             } catch (FieldException e) {
334                 logger.warn("{}: error parsing msg {}", nm(), msg, e);
335             }
336         }
337
338         private int getRawValue(Msg msg) throws FieldException {
339             // determine data field name based on parameter, default to cmd2 if is standard message
340             String field = getParameterAsString("field", !msg.isExtended() ? "command2" : "");
341             if (field.isEmpty()) {
342                 throw new FieldException("handler misconfigured, no field parameter specified!");
343             }
344             if (field.startsWith("address") && !msg.isBroadcast() && !msg.isAllLinkBroadcast()) {
345                 throw new FieldException("not broadcast msg, cannot use address bytes!");
346             }
347             // return raw value based on field name
348             switch (field) {
349                 case "group":
350                     return msg.getGroup();
351                 case "addressHighByte":
352                     // return broadcast address high byte value
353                     return msg.getInsteonAddress("toAddress").getHighByte() & 0xFF;
354                 case "addressMiddleByte":
355                     // return broadcast address middle byte value
356                     return msg.getInsteonAddress("toAddress").getMiddleByte() & 0xFF;
357                 case "addressLowByte":
358                     // return broadcast address low byte value
359                     return msg.getInsteonAddress("toAddress").getLowByte() & 0xFF;
360                 default:
361                     // return integer value starting from field name up to 4-bytes in size based on parameter
362                     return msg.getInt(field, getParameterAsInteger("num_bytes", 1));
363             }
364         }
365
366         protected abstract @Nullable State getState(byte cmd1, double value);
367     }
368
369     /**
370      * Custom bitmask message handler based of parameters
371      */
372     public static class CustomBitmaskMsgHandler extends CustomMsgHandler {
373         CustomBitmaskMsgHandler(DeviceFeature feature) {
374             super(feature);
375         }
376
377         @Override
378         protected @Nullable State getState(byte cmd1, double value) {
379             State state = null;
380             // get bit number based on parameter
381             int bit = getBitNumber();
382             // get bit state from bitmask value, if bit defined
383             if (bit != -1) {
384                 boolean isSet = BinaryUtils.isBitSet((int) value, bit);
385                 state = getBitState(isSet);
386             } else {
387                 logger.debug("{}: invalid bit number defined for {}", nm(), feature.getName());
388             }
389             return state;
390         }
391
392         protected int getBitNumber() {
393             int bit = getParameterAsInteger("bit", -1);
394             // return bit if valid (0-7), otherwise -1
395             return bit >= 0 && bit <= 7 ? bit : -1;
396         }
397
398         protected State getBitState(boolean isSet) {
399             return OnOffType.from(isSet ^ getParameterAsBoolean("inverted", false));
400         }
401     }
402
403     /**
404      * Custom cache message handler based of parameters
405      */
406     public static class CustomCacheMsgHandler extends CustomMsgHandler {
407         CustomCacheMsgHandler(DeviceFeature feature) {
408             super(feature);
409         }
410
411         @Override
412         protected @Nullable State getState(byte cmd1, double value) {
413             // only cache extracted message value
414             // mostly used for hidden features which are used by others
415             return null;
416         }
417     }
418
419     /**
420      * Custom decimal type message handler based of parameters
421      */
422     public static class CustomDecimalMsgHandler extends CustomMsgHandler {
423         CustomDecimalMsgHandler(DeviceFeature feature) {
424             super(feature);
425         }
426
427         @Override
428         protected @Nullable State getState(byte cmd1, double value) {
429             return new DecimalType(value);
430         }
431     }
432
433     /**
434      * Custom on/off type message handler based of parameters
435      */
436     public static class CustomOnOffMsgHandler extends CustomMsgHandler {
437         CustomOnOffMsgHandler(DeviceFeature feature) {
438             super(feature);
439         }
440
441         @Override
442         protected @Nullable State getState(byte cmd1, double value) {
443             int onLevel = getParameterAsInteger("on", 0xFF);
444             int offLevel = getParameterAsInteger("off", 0x00);
445             return value == onLevel ? OnOffType.ON : value == offLevel ? OnOffType.OFF : null;
446         }
447     }
448
449     /**
450      * Custom percent type message handler based of parameters
451      */
452     public static class CustomPercentMsgHandler extends CustomMsgHandler {
453         CustomPercentMsgHandler(DeviceFeature feature) {
454             super(feature);
455         }
456
457         @Override
458         protected @Nullable State getState(byte cmd1, double value) {
459             int minValue = getParameterAsInteger("min", 0x00);
460             int maxValue = getParameterAsInteger("max", 0xFF);
461             double clampValue = Math.max(minValue, Math.min(maxValue, value));
462             int level = (int) Math.round((clampValue - minValue) / (maxValue - minValue) * 100);
463             return new PercentType(level);
464         }
465     }
466
467     /**
468      * Custom dimensionless quantity type message handler based of parameters
469      */
470     public static class CustomDimensionlessMsgHandler extends CustomMsgHandler {
471         CustomDimensionlessMsgHandler(DeviceFeature feature) {
472             super(feature);
473         }
474
475         @Override
476         protected @Nullable State getState(byte cmd1, double value) {
477             int minValue = getParameterAsInteger("min", 0);
478             int maxValue = getParameterAsInteger("max", 100);
479             double clampValue = Math.max(minValue, Math.min(maxValue, value));
480             int level = (int) Math.round((clampValue - minValue) * 100 / (maxValue - minValue));
481             return new QuantityType<Dimensionless>(level, Units.PERCENT);
482         }
483     }
484
485     /**
486      * Custom temperature quantity type message handler based of parameters
487      */
488     public static class CustomTemperatureMsgHandler extends CustomMsgHandler {
489         CustomTemperatureMsgHandler(DeviceFeature feature) {
490             super(feature);
491         }
492
493         @Override
494         protected @Nullable State getState(byte cmd1, double value) {
495             Unit<Temperature> unit = getTemperatureUnit();
496             return new QuantityType<Temperature>(value, unit);
497         }
498
499         protected Unit<Temperature> getTemperatureUnit() {
500             String scale = getParameterAsString("scale", "");
501             switch (scale) {
502                 case "celsius":
503                     return SIUnits.CELSIUS;
504                 case "fahrenheit":
505                     return ImperialUnits.FAHRENHEIT;
506                 default:
507                     logger.debug("{}: no valid temperature scale parameter found, defaulting to: CELSIUS", nm());
508                     return SIUnits.CELSIUS;
509             }
510         }
511     }
512
513     /**
514      * Custom time quantity type message handler based of parameters
515      */
516     public static class CustomTimeMsgHandler extends CustomMsgHandler {
517         CustomTimeMsgHandler(DeviceFeature feature) {
518             super(feature);
519         }
520
521         @Override
522         protected @Nullable State getState(byte cmd1, double value) {
523             Unit<Time> unit = getTimeUnit();
524             return new QuantityType<Time>(value, unit);
525         }
526
527         protected Unit<Time> getTimeUnit() {
528             String scale = getParameterAsString("scale", "");
529             switch (scale) {
530                 case "hour":
531                     return Units.HOUR;
532                 case "minute":
533                     return Units.MINUTE;
534                 case "second":
535                     return Units.SECOND;
536                 default:
537                     logger.debug("{}: no valid time scale parameter found, defaulting to: SECONDS", nm());
538                     return Units.SECOND;
539             }
540         }
541     }
542
543     /**
544      * Database delta reply message handler
545      */
546     public static class DatabaseDeltaReplyHandler extends MessageHandler {
547         DatabaseDeltaReplyHandler(DeviceFeature feature) {
548             super(feature);
549         }
550
551         @Override
552         public void handleMessage(byte cmd1, Msg msg) {
553             try {
554                 int delta = msg.getInt("command2");
555                 // update link db delta
556                 getInsteonDevice().getLinkDB().updateDatabaseDelta(delta);
557             } catch (FieldException e) {
558                 logger.warn("{}: error parsing msg: {}", nm(), msg, e);
559             }
560         }
561     }
562
563     /**
564      * Insteon engine reply message handler
565      */
566     public static class InsteonEngineReplyHandler extends MessageHandler {
567         InsteonEngineReplyHandler(DeviceFeature feature) {
568             super(feature);
569         }
570
571         @Override
572         public void handleMessage(byte cmd1, Msg msg) {
573             try {
574                 int version = msg.getInt("command2");
575                 InsteonEngine engine = InsteonEngine.valueOf(version);
576                 // set device insteon engine
577                 getInsteonDevice().setInsteonEngine(engine);
578                 // continue device polling
579                 getInsteonDevice().doPoll(0L);
580             } catch (FieldException e) {
581                 logger.warn("{}: error parsing msg: {}", nm(), msg, e);
582             }
583         }
584     }
585
586     /**
587      * Ping reply message handler
588      */
589     public static class PingReplyHandler extends MessageHandler {
590         PingReplyHandler(DeviceFeature feature) {
591             super(feature);
592         }
593
594         @Override
595         public void handleMessage(byte cmd1, Msg msg) {
596             logger.debug("{}: successfully pinged device {}", nm(), getInsteonDevice().getAddress());
597         }
598     }
599
600     /**
601      * Heartbeat monitor message handler
602      */
603     public static class HeartbeatMonitorMsgHandler extends MessageHandler {
604         HeartbeatMonitorMsgHandler(DeviceFeature feature) {
605             super(feature);
606         }
607
608         @Override
609         public void handleMessage(byte cmd1, Msg msg) {
610             // reset device heartbeat monitor on all link broadcast or cleanup message not replayed
611             if (msg.isAllLinkBroadcastOrCleanup() && !msg.isReplayed()) {
612                 getInsteonDevice().resetHeartbeatMonitor();
613             }
614         }
615     }
616
617     /**
618      * Last time message handler
619      */
620     public static class LastTimeMsgHandler extends MessageHandler {
621         LastTimeMsgHandler(DeviceFeature feature) {
622             super(feature);
623         }
624
625         @Override
626         public void handleMessage(byte cmd1, Msg msg) {
627             Instant instant = Instant.ofEpochMilli(msg.getTimestamp());
628             ZonedDateTime timestamp = ZonedDateTime.ofInstant(instant, ZoneId.systemDefault());
629             ZonedDateTime lastTimestamp = getLastTimestamp();
630             // set last time if not defined yet or message timestamp is greater than last value
631             if (lastTimestamp == null || timestamp.compareTo(lastTimestamp) > 0) {
632                 feature.updateState(new DateTimeType(timestamp));
633             }
634         }
635
636         private @Nullable ZonedDateTime getLastTimestamp() {
637             State state = feature.getState();
638             return state instanceof DateTimeType datetime ? datetime.getZonedDateTime() : null;
639         }
640     }
641
642     /**
643      * Button event message handler
644      */
645     public static class ButtonEventMsgHandler extends MessageHandler {
646         ButtonEventMsgHandler(DeviceFeature feature) {
647             super(feature);
648         }
649
650         @Override
651         protected boolean isDuplicate(Msg msg) {
652             // Disable duplicate elimination based on parameter because
653             // some button events such as hold or release have no cleanup or success messages.
654             return getParameterAsBoolean("duplicate", super.isDuplicate(msg));
655         }
656
657         @Override
658         public void handleMessage(byte cmd1, Msg msg) {
659             try {
660                 byte cmd2 = msg.getByte("command2");
661                 ButtonEvent event = ButtonEvent.valueOf(cmd1, cmd2);
662                 logger.debug("{}: device {} {} received event {}", nm(), getInsteonDevice().getAddress(),
663                         feature.getName(), event);
664                 feature.triggerEvent(event.toString());
665                 feature.pollRelatedDevices(0L);
666             } catch (FieldException e) {
667                 logger.warn("{}: error parsing msg: {}", nm(), msg, e);
668             } catch (IllegalArgumentException e) {
669                 logger.warn("{}: got unexpected button event: {}", nm(), HexUtils.getHexString(cmd1));
670             }
671         }
672     }
673
674     /**
675      * Status request reply message handler
676      */
677     public static class StatusRequestReplyHandler extends CustomMsgHandler {
678         StatusRequestReplyHandler(DeviceFeature feature) {
679             super(feature);
680         }
681
682         @Override
683         public void handleMessage(byte cmd1, Msg msg) {
684             // update link db delta if is my request status reply message (0x19)
685             if (feature.getQueryCommand() == 0x19) {
686                 getInsteonDevice().getLinkDB().updateDatabaseDelta(cmd1 & 0xFF);
687             }
688             super.handleMessage(cmd1, msg);
689         }
690
691         @Override
692         protected @Nullable State getState(byte cmd1, double value) {
693             return null;
694         }
695     }
696
697     /**
698      * On/Off abstract message handler
699      */
700     public abstract static class OnOffMsgHandler extends MessageHandler {
701         OnOffMsgHandler(DeviceFeature feature) {
702             super(feature);
703         }
704
705         @Override
706         public void handleMessage(byte cmd1, Msg msg) {
707             String mode = getParameterAsString("mode", "REGULAR");
708             State state = getState(mode);
709             if (state != null) {
710                 logger.debug("{}: device {} is {} ({})", nm(), getInsteonDevice().getAddress(), state, mode);
711                 feature.updateState(state);
712             }
713         }
714
715         protected abstract @Nullable State getState(String mode);
716     }
717
718     /**
719      * Dimmer on message handler
720      */
721     public static class DimmerOnMsgHandler extends OnOffMsgHandler {
722         DimmerOnMsgHandler(DeviceFeature feature) {
723             super(feature);
724         }
725
726         @Override
727         protected @Nullable State getState(String mode) {
728             switch (mode) {
729                 case "FAST":
730                     // set to 100% for fast on change
731                     return PercentType.HUNDRED;
732                 default:
733                     // set to device on level if the current state not at that level already, defaulting to 100%
734                     // this is due to subsequent dimmer on button press cycling between on level and 100%
735                     State onLevel = getInsteonDevice().getFeatureState(FEATURE_ON_LEVEL);
736                     State state = feature.getState();
737                     return onLevel instanceof PercentType && !state.equals(onLevel) ? onLevel : PercentType.HUNDRED;
738             }
739         }
740     }
741
742     /**
743      * Dimmer off message handler
744      */
745     public static class DimmerOffMsgHandler extends OnOffMsgHandler {
746         DimmerOffMsgHandler(DeviceFeature feature) {
747             super(feature);
748         }
749
750         @Override
751         protected @Nullable State getState(String mode) {
752             return PercentType.ZERO;
753         }
754     }
755
756     /**
757      * Dimmer request reply message handler
758      */
759     public static class DimmerRequestReplyHandler extends StatusRequestReplyHandler {
760         DimmerRequestReplyHandler(DeviceFeature feature) {
761             super(feature);
762         }
763
764         @Override
765         public void handleMessage(byte cmd1, Msg msg) {
766             int queryCmd = feature.getQueryCommand();
767             // 1) trigger poll if is my bright/dim or manual change stop command reply
768             // 2) handle fast on/off message if is my fast on/off command reply
769             // 3) handle ramp dimmer message if is my ramp rate on/off command reply
770             // 4) handle my standard/instant on/off command reply ignoring manual change start messages
771             if (queryCmd == 0x15 || queryCmd == 0x16 || queryCmd == 0x18) {
772                 feature.triggerPoll(0L);
773             } else if (queryCmd == 0x12 || queryCmd == 0x14) {
774                 handleFastOnOffMessage(cmd1, msg);
775             } else if (queryCmd == 0x2E || queryCmd == 0x2F || queryCmd == 0x34 || queryCmd == 0x35) {
776                 handleRampDimmerMessage(cmd1, msg);
777             } else if (queryCmd != 0x17) {
778                 super.handleMessage(cmd1, msg);
779             }
780         }
781
782         @Override
783         protected @Nullable State getState(byte cmd1, double value) {
784             int level = (int) Math.round(value * 100 / 255.0);
785             return new PercentType(level);
786         }
787
788         private void handleFastOnOffMessage(byte cmd1, Msg msg) {
789             FastOnOffMsgHandler handler = new FastOnOffMsgHandler(feature);
790             handler.setParameters(parameters);
791             handler.handleMessage(cmd1, msg);
792         }
793
794         private void handleRampDimmerMessage(byte cmd1, Msg msg) {
795             RampDimmerMsgHandler handler = new RampDimmerMsgHandler(feature);
796             handler.setParameters(parameters);
797             handler.handleMessage(cmd1, msg);
798         }
799     }
800
801     /**
802      * Fast on/off message handler
803      */
804     public static class FastOnOffMsgHandler extends CustomMsgHandler {
805         FastOnOffMsgHandler(DeviceFeature feature) {
806             super(feature);
807         }
808
809         @Override
810         protected @Nullable State getState(byte cmd1, double value) {
811             switch (cmd1) {
812                 case 0x14:
813                     return PercentType.ZERO;
814                 case 0x12:
815                     return PercentType.HUNDRED;
816                 default:
817                     logger.warn("{}: got unexpected command value: {}", nm(), HexUtils.getHexString(cmd1));
818                     return null;
819             }
820         }
821     }
822
823     /**
824      * Ramp dimmer message handler
825      */
826     public static class RampDimmerMsgHandler extends CustomMsgHandler {
827         RampDimmerMsgHandler(DeviceFeature feature) {
828             super(feature);
829         }
830
831         @Override
832         protected @Nullable State getState(byte cmd1, double value) {
833             switch (cmd1) {
834                 case 0x2F:
835                 case 0x35:
836                     return PercentType.ZERO;
837                 case 0x2E:
838                 case 0x34:
839                     int highByte = ((int) value) >> 4;
840                     int level = (int) Math.round((highByte * 16 + 0x0F) * 100 / 255.0);
841                     return new PercentType(level);
842                 default:
843                     logger.warn("{}: got unexpected command value: {}", nm(), HexUtils.getHexString(cmd1));
844                     return null;
845             }
846         }
847     }
848
849     /**
850      * Switch on message handler
851      */
852     public static class SwitchOnMsgHandler extends OnOffMsgHandler {
853         SwitchOnMsgHandler(DeviceFeature feature) {
854             super(feature);
855         }
856
857         @Override
858         protected @Nullable State getState(String mode) {
859             return OnOffType.ON;
860         }
861     }
862
863     /**
864      * Switch off message handler
865      */
866     public static class SwitchOffMsgHandler extends OnOffMsgHandler {
867         SwitchOffMsgHandler(DeviceFeature feature) {
868             super(feature);
869         }
870
871         @Override
872         protected @Nullable State getState(String mode) {
873             return OnOffType.OFF;
874         }
875     }
876
877     /**
878      * Switch request reply message handler
879      */
880     public static class SwitchRequestReplyHandler extends StatusRequestReplyHandler {
881         SwitchRequestReplyHandler(DeviceFeature feature) {
882             super(feature);
883         }
884
885         @Override
886         protected @Nullable State getState(byte cmd1, double value) {
887             int level = (int) value;
888             State state = null;
889             if (level == 0x00 || level == 0xFF) {
890                 state = OnOffType.from(level == 0xFF);
891             } else {
892                 logger.warn("{}: ignoring unexpected level received {}", nm(), HexUtils.getHexString(level));
893             }
894             return state;
895         }
896     }
897
898     /**
899      * Keypad button on message handler
900      */
901     public static class KeypadButtonOnMsgHandler extends SwitchOnMsgHandler {
902         KeypadButtonOnMsgHandler(DeviceFeature feature) {
903             super(feature);
904         }
905
906         @Override
907         public void handleMessage(byte cmd1, Msg msg) {
908             super.handleMessage(cmd1, msg);
909             // trigger poll to account for button group changes
910             feature.triggerPoll(0L);
911         }
912     }
913
914     /**
915      * Keypad button off message handler
916      */
917     public static class KeypadButtonOffMsgHandler extends SwitchOffMsgHandler {
918         KeypadButtonOffMsgHandler(DeviceFeature feature) {
919             super(feature);
920         }
921
922         @Override
923         public void handleMessage(byte cmd1, Msg msg) {
924             super.handleMessage(cmd1, msg);
925             // trigger poll to account for button group changes
926             feature.triggerPoll(0L);
927         }
928     }
929
930     /**
931      * Keypad button reply message handler
932      */
933     public static class KeypadButtonReplyHandler extends CustomBitmaskMsgHandler {
934         KeypadButtonReplyHandler(DeviceFeature feature) {
935             super(feature);
936         }
937
938         @Override
939         public void handleMessage(byte cmd1, Msg msg) {
940             // trigger poll if is my command reply message (0x2E)
941             if (feature.getQueryCommand() == 0x2E) {
942                 feature.triggerPoll(0L);
943             } else {
944                 super.handleMessage(cmd1, msg);
945             }
946         }
947
948         @Override
949         protected int getBitNumber() {
950             int bit = feature.getGroup() - 1;
951             // return bit if representing keypad button 2-8, otherwise -1
952             return bit >= 1 && bit <= 7 ? bit : -1;
953         }
954     }
955
956     /**
957      * Keypad button toggle mode message handler
958      */
959     public static class KeypadButtonToggleModeMsgHandler extends MessageHandler {
960         KeypadButtonToggleModeMsgHandler(DeviceFeature feature) {
961             super(feature);
962         }
963
964         @Override
965         public void handleMessage(byte cmd1, Msg msg) {
966             try {
967                 int bit = feature.getGroup() - 1;
968                 if (bit < 0 || bit > 7) {
969                     logger.debug("{}: invalid bit number defined for {}", nm(), feature.getName());
970                 } else {
971                     int value = msg.getByte("userData10") << 8 | msg.getByte("userData13");
972                     KeypadButtonToggleMode mode = KeypadButtonToggleMode.valueOf(value, bit);
973                     logger.debug("{}: device {} {} is {}", nm(), getInsteonDevice().getAddress(), feature.getName(),
974                             mode);
975                     feature.setLastMsgValue(value);
976                     feature.updateState(new StringType(mode.toString()));
977                 }
978             } catch (FieldException e) {
979                 logger.warn("{}: error parsing msg: {}", nm(), msg, e);
980             }
981         }
982     }
983
984     /**
985      * Operating flags reply message handler
986      */
987     public static class OpFlagsReplyHandler extends CustomBitmaskMsgHandler {
988         OpFlagsReplyHandler(DeviceFeature feature) {
989             super(feature);
990         }
991
992         @Override
993         public void handleMessage(byte cmd1, Msg msg) {
994             // trigger poll if is my command reply message (0x20)
995             if (feature.getQueryCommand() == 0x20) {
996                 feature.triggerPoll(0L);
997             } else {
998                 super.handleMessage(cmd1, msg);
999             }
1000         }
1001     }
1002
1003     /**
1004      * Link operating flags reply message handler
1005      */
1006     public static class LinkOpFlagsReplyHandler extends OpFlagsReplyHandler {
1007         LinkOpFlagsReplyHandler(DeviceFeature feature) {
1008             super(feature);
1009         }
1010
1011         @Override
1012         public void handleMessage(byte cmd1, Msg msg) {
1013             super.handleMessage(cmd1, msg);
1014             // update default links
1015             getInsteonDevice().updateDefaultLinks();
1016         }
1017     }
1018
1019     /**
1020      * Heartbeat on/off operating flag reply message handler
1021      */
1022     public static class HeartbeatOnOffReplyHandler extends OpFlagsReplyHandler {
1023         HeartbeatOnOffReplyHandler(DeviceFeature feature) {
1024             super(feature);
1025         }
1026
1027         @Override
1028         public void handleMessage(byte cmd1, Msg msg) {
1029             super.handleMessage(cmd1, msg);
1030             // reset device heartbeat monitor
1031             getInsteonDevice().resetHeartbeatMonitor();
1032         }
1033     }
1034
1035     /**
1036      * Keypad button config operating flag reply message handler
1037      */
1038     public static class KeypadButtonConfigReplyHandler extends OpFlagsReplyHandler {
1039         KeypadButtonConfigReplyHandler(DeviceFeature feature) {
1040             super(feature);
1041         }
1042
1043         @Override
1044         protected State getBitState(boolean is8Button) {
1045             KeypadButtonConfig config = KeypadButtonConfig.from(is8Button);
1046             // update device type based on button count
1047             updateDeviceType(config.getCount());
1048             // return button config state
1049             return new StringType(config.toString());
1050         }
1051
1052         private void updateDeviceType(int buttonCount) {
1053             DeviceType deviceType = getInsteonDevice().getType();
1054             if (deviceType == null) {
1055                 logger.warn("{}: unknown device type for {}", nm(), getInsteonDevice().getAddress());
1056             } else {
1057                 String name = deviceType.getName().replaceAll(".$", String.valueOf(buttonCount));
1058                 DeviceType newType = DeviceTypeRegistry.getInstance().getDeviceType(name);
1059                 if (newType == null) {
1060                     logger.warn("{}: unknown device type {}", nm(), name);
1061                 } else {
1062                     getInsteonDevice().updateType(newType);
1063                 }
1064             }
1065         }
1066     }
1067
1068     /**
1069      * LED brightness message handler
1070      */
1071     public static class LEDBrightnessMsgHandler extends CustomMsgHandler {
1072         LEDBrightnessMsgHandler(DeviceFeature feature) {
1073             super(feature);
1074         }
1075
1076         @Override
1077         protected @Nullable State getState(byte cmd1, double value) {
1078             int level = (int) Math.round(value * 100 / 127.0);
1079             State state = getInsteonDevice().getFeatureState(FEATURE_LED_ON_OFF);
1080             return OnOffType.OFF.equals(state) ? PercentType.ZERO : new PercentType(level);
1081         }
1082     }
1083
1084     /**
1085      * Ramp rate message handler
1086      */
1087     public static class RampRateMsgHandler extends CustomMsgHandler {
1088         RampRateMsgHandler(DeviceFeature feature) {
1089             super(feature);
1090         }
1091
1092         @Override
1093         protected @Nullable State getState(byte cmd1, double value) {
1094             RampRate rampRate = RampRate.valueOf((int) value);
1095             return new QuantityType<Time>(rampRate.getTimeInSeconds(), Units.SECOND);
1096         }
1097     }
1098
1099     /**
1100      * Sensor abstract message handler
1101      */
1102     public abstract static class SensorMsgHandler extends CustomMsgHandler {
1103         SensorMsgHandler(DeviceFeature feature) {
1104             super(feature);
1105         }
1106
1107         @Override
1108         public void handleMessage(byte cmd1, Msg msg) {
1109             super.handleMessage(cmd1, msg);
1110             // poll battery powered sensor device while awake
1111             if (getInsteonDevice().isBatteryPowered()) {
1112                 // no delay for all link cleanup, all link success report or replayed messages
1113                 // otherise, 1500ms for all link broadcast message allowing cleanup msg to be be processed beforehand
1114                 long delay = msg.isAllLinkCleanup() || msg.isAllLinkSuccessReport() || msg.isReplayed() ? 0L : 1500L;
1115                 getInsteonDevice().doPoll(delay);
1116             }
1117             // poll related devices
1118             feature.pollRelatedDevices(0L);
1119         }
1120     }
1121
1122     /**
1123      * Contact open message handler
1124      */
1125     public static class ContactOpenMsgHandler extends SensorMsgHandler {
1126         ContactOpenMsgHandler(DeviceFeature feature) {
1127             super(feature);
1128         }
1129
1130         @Override
1131         protected @Nullable State getState(byte cmd1, double value) {
1132             return OpenClosedType.OPEN;
1133         }
1134     }
1135
1136     /**
1137      * Contact closed message handler
1138      */
1139     public static class ContactClosedMsgHandler extends SensorMsgHandler {
1140         ContactClosedMsgHandler(DeviceFeature feature) {
1141             super(feature);
1142         }
1143
1144         @Override
1145         protected @Nullable State getState(byte cmd1, double value) {
1146             return OpenClosedType.CLOSED;
1147         }
1148     }
1149
1150     /**
1151      * Contact request reply message handler
1152      */
1153     public static class ContactRequestReplyHandler extends StatusRequestReplyHandler {
1154         ContactRequestReplyHandler(DeviceFeature feature) {
1155             super(feature);
1156         }
1157
1158         @Override
1159         protected @Nullable State getState(byte cmd1, double value) {
1160             return value == 0 ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
1161         }
1162     }
1163
1164     /**
1165      * Wireless sensor open message handler
1166      */
1167     public static class WirelessSensorOpenMsgHandler extends SensorMsgHandler {
1168         WirelessSensorOpenMsgHandler(DeviceFeature feature) {
1169             super(feature);
1170         }
1171
1172         @Override
1173         protected @Nullable State getState(byte cmd1, double value) {
1174             return OpenClosedType.OPEN;
1175         }
1176     }
1177
1178     /**
1179      * Wireless sensor closed message handler
1180      */
1181     public static class WirelessSensorClosedMsgHandler extends SensorMsgHandler {
1182         WirelessSensorClosedMsgHandler(DeviceFeature feature) {
1183             super(feature);
1184         }
1185
1186         @Override
1187         protected @Nullable State getState(byte cmd1, double value) {
1188             return OpenClosedType.CLOSED;
1189         }
1190     }
1191
1192     /**
1193      * Wireless sensor on message handler
1194      */
1195     public static class WirelessSensorOnMsgHandler extends SensorMsgHandler {
1196         WirelessSensorOnMsgHandler(DeviceFeature feature) {
1197             super(feature);
1198         }
1199
1200         @Override
1201         protected @Nullable State getState(byte cmd1, double value) {
1202             return OnOffType.ON;
1203         }
1204     }
1205
1206     /**
1207      * Wireless sensor off message handler
1208      */
1209     public static class WirelessSensorOffMsgHandler extends SensorMsgHandler {
1210         WirelessSensorOffMsgHandler(DeviceFeature feature) {
1211             super(feature);
1212         }
1213
1214         @Override
1215         protected @Nullable State getState(byte cmd1, double value) {
1216             return OnOffType.OFF;
1217         }
1218     }
1219
1220     /**
1221      * Motion sensor 2 battery powered reply message handler
1222      */
1223     public static class MotionSensor2BatteryPoweredReplyHandler extends CustomMsgHandler {
1224         MotionSensor2BatteryPoweredReplyHandler(DeviceFeature feature) {
1225             super(feature);
1226         }
1227
1228         @Override
1229         protected @Nullable State getState(byte cmd1, double value) {
1230             // stage flag bit 1 = USB Powered
1231             boolean isBatteryPowered = !BinaryUtils.isBitSet((int) value, 1);
1232             // update device based on battery powered flag
1233             updateDeviceFlag(isBatteryPowered);
1234             // return battery powered state
1235             return OnOffType.from(isBatteryPowered);
1236         }
1237
1238         private void updateDeviceFlag(boolean isBatteryPowered) {
1239             // update device batteryPowered flag
1240             getInsteonDevice().setFlag("batteryPowered", isBatteryPowered);
1241             // stop device polling if battery powered, otherwise start it
1242             if (isBatteryPowered) {
1243                 getInsteonDevice().stopPolling();
1244             } else {
1245                 getInsteonDevice().startPolling();
1246             }
1247         }
1248     }
1249
1250     /**
1251      * Motion sensor 2 temperature message handler
1252      */
1253     public static class MotionSensor2TemperatureMsgHandler extends CustomMsgHandler {
1254         MotionSensor2TemperatureMsgHandler(DeviceFeature feature) {
1255             super(feature);
1256         }
1257
1258         @Override
1259         protected @Nullable State getState(byte cmd1, double value) {
1260             boolean isBatteryPowered = getInsteonDevice().isBatteryPowered();
1261             // temperature (°F) = 0.73 * value - 20.53 (battery powered); 0.72 * value - 24.61 (usb powered)
1262             double temperature = isBatteryPowered ? 0.73 * value - 20.53 : 0.72 * value - 24.61;
1263             return new QuantityType<Temperature>(temperature, ImperialUnits.FAHRENHEIT);
1264         }
1265     }
1266
1267     /**
1268      * Heartbeat interval message handler
1269      */
1270     public static class HeartbeatIntervalMsgHandler extends CustomMsgHandler {
1271         HeartbeatIntervalMsgHandler(DeviceFeature feature) {
1272             super(feature);
1273         }
1274
1275         @Override
1276         public void handleMessage(byte cmd1, Msg msg) {
1277             super.handleMessage(cmd1, msg);
1278             // reset device heartbeat monitor
1279             getInsteonDevice().resetHeartbeatMonitor();
1280         }
1281
1282         @Override
1283         protected @Nullable State getState(byte cmd1, double value) {
1284             int interval = getInterval((int) value);
1285             return interval > 0 ? new QuantityType<Time>(interval, Units.MINUTE) : null;
1286         }
1287
1288         private int getInterval(int value) {
1289             int preset = getParameterAsInteger("preset", 0);
1290             int increment = getParameterAsInteger("increment", 0);
1291             return value == 0x00 ? preset : value * increment;
1292         }
1293     }
1294
1295     /**
1296      * FanLinc fan mode reply message handler
1297      */
1298     public static class FanLincFanReplyHandler extends CustomMsgHandler {
1299         FanLincFanReplyHandler(DeviceFeature feature) {
1300             super(feature);
1301         }
1302
1303         @Override
1304         protected @Nullable State getState(byte cmd1, double value) {
1305             try {
1306                 FanLincFanSpeed speed = FanLincFanSpeed.valueOf((int) value);
1307                 return new StringType(speed.toString());
1308             } catch (IllegalArgumentException e) {
1309                 logger.warn("{}: got unexpected fan speed reply value: {}", nm(), HexUtils.getHexString((int) value));
1310                 return UnDefType.UNDEF;
1311             }
1312         }
1313     }
1314
1315     /**
1316      * I/O linc momentary duration message handler
1317      */
1318     public static class IOLincMomentaryDurationMsgHandler extends CustomMsgHandler {
1319         IOLincMomentaryDurationMsgHandler(DeviceFeature feature) {
1320             super(feature);
1321         }
1322
1323         @Override
1324         protected @Nullable State getState(byte cmd1, double value) {
1325             int duration = getDuration((int) value);
1326             return new QuantityType<Time>(duration, Units.SECOND);
1327         }
1328
1329         private int getDuration(int value) {
1330             int prescaler = value >> 8; // high byte
1331             int delay = value & 0xFF; // low byte
1332             if (delay == 0) {
1333                 delay = 255;
1334             }
1335             return delay * prescaler / 10;
1336         }
1337     }
1338
1339     /**
1340      * I/O linc relay mode reply message handler
1341      */
1342     public static class IOLincRelayModeReplyHandler extends CustomMsgHandler {
1343         IOLincRelayModeReplyHandler(DeviceFeature feature) {
1344             super(feature);
1345         }
1346
1347         @Override
1348         public void handleMessage(byte cmd1, Msg msg) {
1349             // trigger poll if is my command reply message (0x20)
1350             if (feature.getQueryCommand() == 0x20) {
1351                 feature.triggerPoll(5000L); // 5000ms delay to allow all op flag commands to be processed
1352             } else {
1353                 super.handleMessage(cmd1, msg);
1354             }
1355         }
1356
1357         @Override
1358         protected @Nullable State getState(byte cmd1, double value) {
1359             IOLincRelayMode mode = IOLincRelayMode.valueOf((int) value);
1360             return new StringType(mode.toString());
1361         }
1362     }
1363
1364     /**
1365      * Micro module operation mode reply message handler
1366      */
1367     public static class MicroModuleOpModeReplyHandler extends CustomMsgHandler {
1368         MicroModuleOpModeReplyHandler(DeviceFeature feature) {
1369             super(feature);
1370         }
1371
1372         @Override
1373         public void handleMessage(byte cmd1, Msg msg) {
1374             // trigger poll if is my command reply message (0x20)
1375             if (feature.getQueryCommand() == 0x20) {
1376                 feature.triggerPoll(2000L); // 2000ms delay to allow all op flag commands to be processed
1377             } else {
1378                 super.handleMessage(cmd1, msg);
1379             }
1380         }
1381
1382         @Override
1383         protected @Nullable State getState(byte cmd1, double value) {
1384             MicroModuleOpMode mode = MicroModuleOpMode.valueOf((int) value);
1385             return new StringType(mode.toString());
1386         }
1387     }
1388
1389     /**
1390      * Outlet switch reply message handler
1391      *
1392      * 0x00 = Both Outlets Off
1393      * 0x01 = Only Top Outlet On
1394      * 0x02 = Only Bottom Outlet On
1395      * 0x03 = Both Outlets On
1396      */
1397     public static class OutletSwitchReplyHandler extends CustomMsgHandler {
1398         OutletSwitchReplyHandler(DeviceFeature feature) {
1399             super(feature);
1400         }
1401
1402         @Override
1403         protected @Nullable State getState(byte cmd1, double value) {
1404             return OnOffType.from(value == feature.getGroup() || value == 0x03);
1405         }
1406     }
1407
1408     /**
1409      * Power meter energy message handler
1410      */
1411     public static class PowerMeterEnergyMsgHandler extends CustomMsgHandler {
1412         PowerMeterEnergyMsgHandler(DeviceFeature feature) {
1413             super(feature);
1414         }
1415
1416         @Override
1417         protected @Nullable State getState(byte cmd1, double value) {
1418             BigDecimal energy = getEnergy((int) value);
1419             return new QuantityType<Energy>(energy, Units.KILOWATT_HOUR);
1420         }
1421
1422         private BigDecimal getEnergy(int value) {
1423             return (value >> 24) < 254
1424                     ? new BigDecimal(value * 65535.0 / (1000 * 60 * 60 * 60)).setScale(4, RoundingMode.HALF_UP)
1425                     : BigDecimal.ZERO;
1426         }
1427     }
1428
1429     /**
1430      * Power meter power message handler
1431      */
1432     public static class PowerMeterPowerMsgHandler extends CustomMsgHandler {
1433         PowerMeterPowerMsgHandler(DeviceFeature feature) {
1434             super(feature);
1435         }
1436
1437         @Override
1438         protected @Nullable State getState(byte cmd1, double value) {
1439             int power = getPower((int) value);
1440             return new QuantityType<Power>(power, Units.WATT);
1441         }
1442
1443         private int getPower(int power) {
1444             return power > 32767 ? power - 65535 : power;
1445         }
1446     }
1447
1448     /**
1449      * Siren request reply message handler
1450      */
1451     public static class SirenRequesteplyHandler extends StatusRequestReplyHandler {
1452         SirenRequesteplyHandler(DeviceFeature feature) {
1453             super(feature);
1454         }
1455
1456         @Override
1457         protected @Nullable State getState(byte cmd1, double value) {
1458             int level = (int) value;
1459             return OnOffType.from(level != 0x00);
1460         }
1461     }
1462
1463     /**
1464      * Siren armed reply message handler
1465      */
1466     public static class SirenArmedReplyHandler extends CustomMsgHandler {
1467         SirenArmedReplyHandler(DeviceFeature feature) {
1468             super(feature);
1469         }
1470
1471         @Override
1472         protected @Nullable State getState(byte cmd1, double value) {
1473             boolean isArmed = BinaryUtils.isBitSet((int) value, 6) || BinaryUtils.isBitSet((int) value, 7);
1474             return OnOffType.from(isArmed);
1475         }
1476     }
1477
1478     /**
1479      * Siren alert type message handler
1480      */
1481     public static class SirenAlertTypeMsgHandler extends CustomMsgHandler {
1482         SirenAlertTypeMsgHandler(DeviceFeature feature) {
1483             super(feature);
1484         }
1485
1486         @Override
1487         protected @Nullable State getState(byte cmd1, double value) {
1488             try {
1489                 SirenAlertType type = SirenAlertType.valueOf((int) value);
1490                 return new StringType(type.toString());
1491             } catch (IllegalArgumentException e) {
1492                 logger.warn("{}: got unexpected alert type value: {}", nm(), (int) value);
1493                 return UnDefType.UNDEF;
1494             }
1495         }
1496     }
1497
1498     /**
1499      * Sprinkler valve message handler
1500      */
1501     public static class SprinklerValveMsgHandler extends CustomMsgHandler {
1502         SprinklerValveMsgHandler(DeviceFeature feature) {
1503             super(feature);
1504         }
1505
1506         @Override
1507         protected @Nullable State getState(byte cmd1, double value) {
1508             int valve = getParameterAsInteger("valve", -1);
1509             if (valve < 0 || valve > 8) {
1510                 logger.debug("{}: invalid valve number defined for {}", nm(), feature.getName());
1511                 return UnDefType.UNDEF;
1512             }
1513             boolean isValveOn = BinaryUtils.isBitSet((int) value, 7) && (((int) value) & 0x07) == valve
1514                     || BinaryUtils.isBitSet((int) value, 6) && valve == 7;
1515             return OnOffType.from(isValveOn);
1516         }
1517     }
1518
1519     /**
1520      * Sprinkler program message handler
1521      */
1522     public static class SprinklerProgramMsgHandler extends CustomMsgHandler {
1523         SprinklerProgramMsgHandler(DeviceFeature feature) {
1524             super(feature);
1525         }
1526
1527         @Override
1528         protected @Nullable State getState(byte cmd1, double value) {
1529             int program = getParameterAsInteger("program", -1);
1530             if (program < 0 || program > 4) {
1531                 logger.debug("{}: invalid program number defined for {}", nm(), feature.getName());
1532                 return UnDefType.UNDEF;
1533             }
1534             boolean isProgramOn = BinaryUtils.isBitSet((int) value, 5) && (((int) value) & 0x18) >> 3 == program;
1535             return OnOffType.from(isProgramOn);
1536         }
1537     }
1538
1539     /**
1540      * Thermostat fan mode message handler
1541      */
1542     public static class ThermostatFanModeMsgHandler extends CustomMsgHandler {
1543         ThermostatFanModeMsgHandler(DeviceFeature feature) {
1544             super(feature);
1545         }
1546
1547         @Override
1548         protected @Nullable State getState(byte cmd1, double value) {
1549             try {
1550                 ThermostatFanMode mode = ThermostatFanMode.fromStatus((int) value);
1551                 return new StringType(mode.toString());
1552             } catch (IllegalArgumentException e) {
1553                 logger.warn("{}: got unexpected fan mode status: {}", nm(), HexUtils.getHexString((int) value));
1554                 return UnDefType.UNDEF;
1555             }
1556         }
1557     }
1558
1559     /**
1560      * Thermostat fan mode reply message handler
1561      */
1562     public static class ThermostatFanModeReplyHandler extends CustomMsgHandler {
1563         ThermostatFanModeReplyHandler(DeviceFeature feature) {
1564             super(feature);
1565         }
1566
1567         @Override
1568         protected @Nullable State getState(byte cmd1, double value) {
1569             try {
1570                 ThermostatFanMode mode = ThermostatFanMode.valueOf((int) value);
1571                 return new StringType(mode.toString());
1572             } catch (IllegalArgumentException e) {
1573                 logger.warn("{}: got unexpected fan mode reply: {}", nm(), HexUtils.getHexString((int) value));
1574                 return UnDefType.UNDEF;
1575             }
1576         }
1577     }
1578
1579     /**
1580      * Thermostat humidifier dehumidifying message handler
1581      */
1582     public static class ThermostatHumidifierDehumidifyingMsgHandler extends CustomMsgHandler {
1583         ThermostatHumidifierDehumidifyingMsgHandler(DeviceFeature feature) {
1584             super(feature);
1585         }
1586
1587         @Override
1588         protected @Nullable State getState(byte cmd1, double value) {
1589             return new StringType(ThermostatSystemState.DEHUMIDIFYING.toString());
1590         }
1591     }
1592
1593     /**
1594      * Thermostat humidifier humidifying message handler
1595      */
1596     public static class ThermostatHumidifierHumidifyingMsgHandler extends CustomMsgHandler {
1597         ThermostatHumidifierHumidifyingMsgHandler(DeviceFeature feature) {
1598             super(feature);
1599         }
1600
1601         @Override
1602         protected @Nullable State getState(byte cmd1, double value) {
1603             return new StringType(ThermostatSystemState.HUMIDIFYING.toString());
1604         }
1605     }
1606
1607     /**
1608      * Thermostat humidifier off message handler
1609      */
1610     public static class ThermostatHumidifierOffMsgHandler extends CustomMsgHandler {
1611         ThermostatHumidifierOffMsgHandler(DeviceFeature feature) {
1612             super(feature);
1613         }
1614
1615         @Override
1616         protected @Nullable State getState(byte cmd1, double value) {
1617             return new StringType(ThermostatSystemState.OFF.toString());
1618         }
1619     }
1620
1621     /**
1622      * Termostat system mode message handler
1623      */
1624     public static class ThermostatSystemModeMsgHandler extends CustomMsgHandler {
1625         ThermostatSystemModeMsgHandler(DeviceFeature feature) {
1626             super(feature);
1627         }
1628
1629         @Override
1630         protected @Nullable State getState(byte cmd1, double value) {
1631             try {
1632                 ThermostatSystemMode mode = ThermostatSystemMode.fromStatus((int) value);
1633                 return new StringType(mode.toString());
1634             } catch (IllegalArgumentException e) {
1635                 logger.warn("{}: got unexpected system mode status: {}", nm(), HexUtils.getHexString((int) value));
1636                 return UnDefType.UNDEF;
1637             }
1638         }
1639     }
1640
1641     /**
1642      * Thermostat system mode reply message handler
1643      */
1644     public static class ThermostatSystemModeReplyHandler extends CustomMsgHandler {
1645         ThermostatSystemModeReplyHandler(DeviceFeature feature) {
1646             super(feature);
1647         }
1648
1649         @Override
1650         protected @Nullable State getState(byte cmd1, double value) {
1651             try {
1652                 ThermostatSystemMode mode = ThermostatSystemMode.valueOf((int) value);
1653                 return new StringType(mode.toString());
1654             } catch (IllegalArgumentException e) {
1655                 logger.warn("{}: got unexpected system mode reply: {}", nm(), HexUtils.getHexString((int) value));
1656                 return UnDefType.UNDEF;
1657             }
1658         }
1659     }
1660
1661     /**
1662      * Thermostat system cooling message handler
1663      */
1664     public static class ThermostatSystemCoolingMsgHandler extends CustomMsgHandler {
1665         ThermostatSystemCoolingMsgHandler(DeviceFeature feature) {
1666             super(feature);
1667         }
1668
1669         @Override
1670         protected @Nullable State getState(byte cmd1, double value) {
1671             return new StringType(ThermostatSystemState.COOLING.toString());
1672         }
1673     }
1674
1675     /**
1676      * Thermostat system heating message handler
1677      */
1678     public static class ThermostatSystemHeatingMsgHandler extends CustomMsgHandler {
1679         ThermostatSystemHeatingMsgHandler(DeviceFeature feature) {
1680             super(feature);
1681         }
1682
1683         @Override
1684         protected @Nullable State getState(byte cmd1, double value) {
1685             return new StringType(ThermostatSystemState.HEATING.toString());
1686         }
1687     }
1688
1689     /**
1690      * Thermostat system off message handler
1691      */
1692     public static class ThermostatSystemOffMsgHandler extends CustomMsgHandler {
1693         ThermostatSystemOffMsgHandler(DeviceFeature feature) {
1694             super(feature);
1695         }
1696
1697         @Override
1698         protected @Nullable State getState(byte cmd1, double value) {
1699             return new StringType(ThermostatSystemState.OFF.toString());
1700         }
1701     }
1702
1703     /**
1704      * Thermostat temperature scale message handler
1705      */
1706     public static class ThermostatTemperatureScaleMsgHandler extends CustomBitmaskMsgHandler {
1707         ThermostatTemperatureScaleMsgHandler(DeviceFeature feature) {
1708             super(feature);
1709         }
1710
1711         @Override
1712         protected State getBitState(boolean isCelsius) {
1713             ThermostatTemperatureScale format = ThermostatTemperatureScale.from(isCelsius);
1714             return new StringType(format.toString());
1715         }
1716     }
1717
1718     /**
1719      * Thermostat time format message handler
1720      */
1721     public static class ThermostatTimeFormatMsgHandler extends CustomBitmaskMsgHandler {
1722         ThermostatTimeFormatMsgHandler(DeviceFeature feature) {
1723             super(feature);
1724         }
1725
1726         @Override
1727         protected State getBitState(boolean is24Hr) {
1728             ThermostatTimeFormat format = ThermostatTimeFormat.from(is24Hr);
1729             return new StringType(format.toString());
1730         }
1731     }
1732
1733     /**
1734      * Venstar thermostat system mode message handler
1735      */
1736     public static class VenstarSystemModeMsgHandler extends CustomMsgHandler {
1737         VenstarSystemModeMsgHandler(DeviceFeature feature) {
1738             super(feature);
1739         }
1740
1741         @Override
1742         protected @Nullable State getState(byte cmd1, double value) {
1743             try {
1744                 VenstarSystemMode mode = VenstarSystemMode.fromStatus((int) value);
1745                 return new StringType(mode.toString());
1746             } catch (IllegalArgumentException e) {
1747                 logger.warn("{}: got unexpected system mode status: {}", nm(), HexUtils.getHexString((int) value));
1748                 return UnDefType.UNDEF;
1749             }
1750         }
1751     }
1752
1753     /**
1754      * Venstar thermostat system mode message handler
1755      */
1756     public static class VenstarSystemModeReplyHandler extends CustomMsgHandler {
1757         VenstarSystemModeReplyHandler(DeviceFeature feature) {
1758             super(feature);
1759         }
1760
1761         @Override
1762         protected @Nullable State getState(byte cmd1, double value) {
1763             try {
1764                 VenstarSystemMode mode = VenstarSystemMode.valueOf((int) value);
1765                 return new StringType(mode.toString());
1766             } catch (IllegalArgumentException e) {
1767                 logger.warn("{}: got unexpected system mode reply: {}", nm(), HexUtils.getHexString((int) value));
1768                 return UnDefType.UNDEF;
1769             }
1770         }
1771     }
1772
1773     /**
1774      * Venstar thermostat temperature message handler
1775      */
1776     public static class VenstarTemperatureMsgHandler extends CustomTemperatureMsgHandler {
1777         VenstarTemperatureMsgHandler(DeviceFeature feature) {
1778             super(feature);
1779         }
1780
1781         @Override
1782         protected Unit<Temperature> getTemperatureUnit() {
1783             try {
1784                 // use temperature scale current state to determine temperature unit, defaulting to fahrenheit
1785                 State state = getInsteonDevice().getFeatureState(FEATURE_TEMPERATURE_SCALE);
1786                 if (state != null
1787                         && ThermostatTemperatureScale.valueOf(state.toString()) == ThermostatTemperatureScale.CELSIUS) {
1788                     return SIUnits.CELSIUS;
1789                 }
1790             } catch (IllegalArgumentException e) {
1791                 logger.debug("{}: unable to determine temperature unit, defaulting to: FAHRENHEIT", nm());
1792             }
1793             return ImperialUnits.FAHRENHEIT;
1794         }
1795     }
1796
1797     /**
1798      * IM button event message handler
1799      */
1800     public static class IMButtonEventMsgHandler extends MessageHandler {
1801         IMButtonEventMsgHandler(DeviceFeature feature) {
1802             super(feature);
1803         }
1804
1805         @Override
1806         public void handleMessage(byte cmd1, Msg msg) {
1807             try {
1808                 int cmd = msg.getInt("buttonEvent");
1809                 int button = getParameterAsInteger("button", 1);
1810                 int mask = (button - 1) << 4;
1811                 IMButtonEvent event = IMButtonEvent.valueOf(cmd ^ mask);
1812                 logger.debug("{}: IM {} received event {}", nm(), feature.getName(), event);
1813                 feature.triggerEvent(event.toString());
1814             } catch (FieldException e) {
1815                 logger.warn("{}: error parsing msg {}", nm(), msg, e);
1816             } catch (IllegalArgumentException e) {
1817                 logger.warn("{}: got unexpected button event", nm(), e);
1818             }
1819         }
1820     }
1821
1822     /**
1823      * IM config message handler
1824      */
1825     public static class IMConfigMsgHandler extends MessageHandler {
1826         IMConfigMsgHandler(DeviceFeature feature) {
1827             super(feature);
1828         }
1829
1830         @Override
1831         public void handleMessage(byte cmd1, Msg msg) {
1832             try {
1833                 int flags = msg.getInt("IMConfigurationFlags");
1834                 int bit = getParameterAsInteger("bit", -1);
1835                 if (bit < 3 || bit > 7) {
1836                     logger.debug("{}: invalid bit number defined for {}", nm(), feature.getName());
1837                     return;
1838                 }
1839                 boolean isSet = BinaryUtils.isBitSet(flags, bit);
1840                 State state = OnOffType.from(isSet ^ getParameterAsBoolean("inverted", false));
1841                 logger.debug("{}: IM {} is {}", nm(), feature.getName(), state);
1842                 feature.setLastMsgValue(flags);
1843                 feature.updateState(state);
1844             } catch (FieldException e) {
1845                 logger.warn("{}: error parsing msg {}", nm(), msg, e);
1846             }
1847         }
1848     }
1849
1850     /**
1851      * Process X10 messages that are generated when another controller
1852      * changes the state of an X10 device.
1853      */
1854     public static class X10OnHandler extends MessageHandler {
1855         X10OnHandler(DeviceFeature feature) {
1856             super(feature);
1857         }
1858
1859         @Override
1860         public void handleMessage(byte cmd1, Msg msg) {
1861             logger.debug("{}: device {} is ON", nm(), getX10Device().getAddress());
1862             feature.updateState(OnOffType.ON);
1863         }
1864     }
1865
1866     public static class X10OffHandler extends MessageHandler {
1867         X10OffHandler(DeviceFeature feature) {
1868             super(feature);
1869         }
1870
1871         @Override
1872         public void handleMessage(byte cmd1, Msg msg) {
1873             logger.debug("{}: device {} is OFF", nm(), getX10Device().getAddress());
1874             feature.updateState(OnOffType.OFF);
1875         }
1876     }
1877
1878     public static class X10BrightHandler extends MessageHandler {
1879         X10BrightHandler(DeviceFeature feature) {
1880             super(feature);
1881         }
1882
1883         @Override
1884         public void handleMessage(byte cmd1, Msg msg) {
1885             logger.debug("{}: ignoring brighten message for device {}", nm(), getX10Device().getAddress());
1886         }
1887     }
1888
1889     public static class X10DimHandler extends MessageHandler {
1890         X10DimHandler(DeviceFeature feature) {
1891             super(feature);
1892         }
1893
1894         @Override
1895         public void handleMessage(byte cmd1, Msg msg) {
1896             logger.debug("{}: ignoring dim message for device {}", nm(), getX10Device().getAddress());
1897         }
1898     }
1899
1900     public static class X10OpenHandler extends MessageHandler {
1901         X10OpenHandler(DeviceFeature feature) {
1902             super(feature);
1903         }
1904
1905         @Override
1906         public void handleMessage(byte cmd1, Msg msg) {
1907             logger.debug("{}: device {} is OPEN", nm(), getX10Device().getAddress());
1908             feature.updateState(OpenClosedType.OPEN);
1909         }
1910     }
1911
1912     public static class X10ClosedHandler extends MessageHandler {
1913         X10ClosedHandler(DeviceFeature feature) {
1914             super(feature);
1915         }
1916
1917         @Override
1918         public void handleMessage(byte cmd1, Msg msg) {
1919             logger.debug("{}: device {} is CLOSED", nm(), getX10Device().getAddress());
1920             feature.updateState(OpenClosedType.CLOSED);
1921         }
1922     }
1923
1924     /**
1925      * Factory method for dermining if a message handler command supports group
1926      *
1927      * @param command the handler command
1928      * @return true if handler supports group, otherwise false
1929      */
1930     public static boolean supportsGroup(int command) {
1931         return SUPPORTED_GROUP_COMMANDS.contains(command);
1932     }
1933
1934     /**
1935      * Factory method for generating a message handler id
1936      *
1937      * @param command the handler command
1938      * @param group the handler group
1939      * @return the generated handler id
1940      */
1941     public static String generateId(int command, int group) {
1942         if (command == -1) {
1943             return "default";
1944         }
1945         String id = HexUtils.getHexString(command);
1946         if (group != -1) {
1947             id += ":" + group;
1948         }
1949         return id;
1950     }
1951
1952     /**
1953      * Factory method for creating a default message handler
1954      *
1955      * @param feature the feature for which to create the handler
1956      * @return the default message handler which was created
1957      */
1958     public static DefaultMsgHandler makeDefaultHandler(DeviceFeature feature) {
1959         return new DefaultMsgHandler(feature);
1960     }
1961
1962     /**
1963      * Factory method for creating a message handler for a given name using java reflection
1964      *
1965      * @param name the name of the handler to create
1966      * @param parameters the parameters of the handler to create
1967      * @param feature the feature for which to create the handler
1968      * @return the handler which was created
1969      */
1970     public static @Nullable <T extends MessageHandler> T makeHandler(String name, Map<String, String> parameters,
1971             DeviceFeature feature) {
1972         try {
1973             String className = MessageHandler.class.getName() + "$" + name;
1974             @SuppressWarnings("unchecked")
1975             Class<? extends T> classRef = (Class<? extends T>) Class.forName(className);
1976             @Nullable
1977             T handler = classRef.getDeclaredConstructor(DeviceFeature.class).newInstance(feature);
1978             handler.setParameters(parameters);
1979             return handler;
1980         } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | IllegalArgumentException
1981                 | InvocationTargetException | NoSuchMethodException | SecurityException e) {
1982             return null;
1983         }
1984     }
1985 }