]> git.basschouten.com Git - openhab-addons.git/blob
54ea05c63a3f1139b01228d8fc462e6f26d6b137
[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;
14
15 import java.lang.reflect.InvocationTargetException;
16 import java.math.BigDecimal;
17 import java.math.RoundingMode;
18 import java.util.HashMap;
19 import java.util.Map;
20
21 import org.eclipse.jdt.annotation.NonNullByDefault;
22 import org.eclipse.jdt.annotation.Nullable;
23 import org.openhab.binding.insteon.internal.device.DeviceFeatureListener.StateChangeType;
24 import org.openhab.binding.insteon.internal.device.GroupMessageStateMachine.GroupMessage;
25 import org.openhab.binding.insteon.internal.handler.InsteonDeviceHandler;
26 import org.openhab.binding.insteon.internal.message.FieldException;
27 import org.openhab.binding.insteon.internal.message.InvalidMessageTypeException;
28 import org.openhab.binding.insteon.internal.message.Msg;
29 import org.openhab.binding.insteon.internal.message.MsgType;
30 import org.openhab.binding.insteon.internal.utils.Utils;
31 import org.openhab.core.library.types.DateTimeType;
32 import org.openhab.core.library.types.DecimalType;
33 import org.openhab.core.library.types.OnOffType;
34 import org.openhab.core.library.types.OpenClosedType;
35 import org.openhab.core.library.types.PercentType;
36 import org.openhab.core.library.types.QuantityType;
37 import org.openhab.core.library.unit.ImperialUnits;
38 import org.openhab.core.library.unit.SIUnits;
39 import org.openhab.core.library.unit.Units;
40 import org.openhab.core.types.State;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
43
44 /**
45  * A message handler processes incoming Insteon messages and reacts by publishing
46  * corresponding messages on the openhab bus, updating device state etc.
47  *
48  * @author Daniel Pfrommer - Initial contribution
49  * @author Bernd Pfrommer - openHAB 1 insteonplm binding
50  * @author Rob Nielsen - Port to openHAB 2 insteon binding
51  */
52 @NonNullByDefault
53 public abstract class MessageHandler {
54     private static final Logger logger = LoggerFactory.getLogger(MessageHandler.class);
55
56     protected DeviceFeature feature;
57     protected Map<String, String> parameters = new HashMap<>();
58
59     /**
60      * Constructor
61      *
62      * @param p state publishing object for dissemination of state changes
63      */
64     MessageHandler(DeviceFeature p) {
65         feature = p;
66     }
67
68     /**
69      * Method that processes incoming message. The cmd1 parameter
70      * has been extracted earlier already (to make a decision which message handler to call),
71      * and is passed in as an argument so cmd1 does not have to be extracted from the message again.
72      *
73      * @param group all-link group or -1 if not specified
74      * @param cmd1 the insteon cmd1 field
75      * @param msg the received insteon message
76      * @param feature the DeviceFeature to which this message handler is attached
77      */
78     public abstract void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature feature);
79
80     /**
81      * Method to send an extended insteon message for querying a device
82      *
83      * @param f DeviceFeature that is being currently handled
84      * @param aCmd1 cmd1 for message to be sent
85      * @param aCmd2 cmd2 for message to be sent
86      */
87     public void sendExtendedQuery(DeviceFeature f, byte aCmd1, byte aCmd2) {
88         InsteonDevice d = f.getDevice();
89         try {
90             Msg m = d.makeExtendedMessage((byte) 0x1f, aCmd1, aCmd2);
91             m.setQuietTime(500L);
92             d.enqueueMessage(m, f);
93         } catch (InvalidMessageTypeException e) {
94             logger.warn("msg exception sending query message to device {}", d.getAddress());
95         } catch (FieldException e) {
96             logger.warn("field exception sending query message to device {}", d.getAddress());
97         }
98     }
99
100     /**
101      * Check if group matches
102      *
103      * @param group group to test for
104      * @return true if group matches or no group is specified
105      */
106     public boolean matchesGroup(int group) {
107         int g = getIntParameter("group", -1);
108         return (g == -1 || g == group);
109     }
110
111     /**
112      * Retrieve group parameter or -1 if no group is specified
113      *
114      * @return group parameter
115      */
116     public int getGroup() {
117         return (getIntParameter("group", -1));
118     }
119
120     /**
121      * Helper function to get an integer parameter for the handler
122      *
123      * @param key name of the int parameter (as specified in device features!)
124      * @param def default to return if parameter not found
125      * @return value of int parameter (or default if not found)
126      */
127     protected int getIntParameter(String key, int def) {
128         String val = parameters.get(key);
129         if (val == null) {
130             return (def); // param not found
131         }
132         int ret = def;
133         try {
134             ret = Utils.strToInt(val);
135         } catch (NumberFormatException e) {
136             logger.warn("malformed int parameter in message handler: {}", key);
137         }
138         return ret;
139     }
140
141     /**
142      * Helper function to get a String parameter for the handler
143      *
144      * @param key name of the String parameter (as specified in device features!)
145      * @param def default to return if parameter not found
146      * @return value of parameter (or default if not found)
147      */
148     protected @Nullable String getStringParameter(String key, @Nullable String def) {
149         String str = parameters.get(key);
150         return str != null ? str : def;
151     }
152
153     /**
154      * Helper function to get a double parameter for the handler
155      *
156      * @param key name of the parameter (as specified in device features!)
157      * @param def default to return if parameter not found
158      * @return value of parameter (or default if not found)
159      */
160     protected double getDoubleParameter(String key, double def) {
161         try {
162             String str = parameters.get(key);
163             return str != null ? Double.parseDouble(str) : def;
164         } catch (NumberFormatException e) {
165             logger.warn("malformed int parameter in message handler: {}", key);
166         }
167         return def;
168     }
169
170     protected boolean getBooleanDeviceConfig(String key, boolean def) {
171         Object o = feature.getDevice().getDeviceConfigMap().get(key);
172         if (o != null) {
173             if (o instanceof Boolean booleanValue) {
174                 return booleanValue;
175             } else {
176                 logger.warn("{} {}: The value for the '{}' key is not boolean in the device configuration parameter.",
177                         nm(), feature.getDevice().getAddress(), key);
178             }
179         }
180
181         return def;
182     }
183
184     /**
185      * Test if message refers to the button configured for given feature
186      *
187      * @param msg received message
188      * @param f device feature to test
189      * @return true if we have no button configured or the message is for this button
190      */
191     protected boolean isMybutton(Msg msg, DeviceFeature f) {
192         int myButton = getIntParameter("button", -1);
193         // if there is no button configured for this handler
194         // the message is assumed to refer to this feature
195         // no matter what button is addressed in the message
196         if (myButton == -1) {
197             return true;
198         }
199
200         int button = getButtonInfo(msg, f);
201         return button != -1 && myButton == button;
202     }
203
204     /**
205      * Test if parameter matches value
206      *
207      * @param param name of parameter to match
208      * @param msg message to search
209      * @param field field name to match
210      * @return true if parameter matches
211      * @throws FieldException if field not there
212      */
213     protected boolean testMatch(String param, Msg msg, String field) throws FieldException {
214         int mp = getIntParameter(param, -1);
215         // parameter not filtered for, declare this a match!
216         if (mp == -1) {
217             return (true);
218         }
219         byte value = msg.getByte(field);
220         return (value == mp);
221     }
222
223     /**
224      * Test if message matches the filter parameters
225      *
226      * @param msg message to be tested against
227      * @return true if message matches
228      */
229     public boolean matches(Msg msg) {
230         try {
231             int ext = getIntParameter("ext", -1);
232             if (ext != -1) {
233                 if ((msg.isExtended() && ext != 1) || (!msg.isExtended() && ext != 0)) {
234                     return (false);
235                 }
236                 if (!testMatch("match_cmd1", msg, "command1")) {
237                     return (false);
238                 }
239             }
240             if (!testMatch("match_cmd2", msg, "command2")) {
241                 return (false);
242             }
243             if (!testMatch("match_d1", msg, "userData1")) {
244                 return (false);
245             }
246             if (!testMatch("match_d2", msg, "userData2")) {
247                 return (false);
248             }
249             if (!testMatch("match_d3", msg, "userData3")) {
250                 return (false);
251             }
252         } catch (FieldException e) {
253             logger.warn("error matching message: {}", msg, e);
254             return (false);
255         }
256         return (true);
257     }
258
259     /**
260      * Determines is an incoming ALL LINK message is a duplicate
261      *
262      * @param msg the received ALL LINK message
263      * @return true if this message is a duplicate
264      */
265     protected boolean isDuplicate(Msg msg) {
266         boolean isDuplicate = false;
267         try {
268             MsgType t = MsgType.fromValue(msg.getByte("messageFlags"));
269             if (t == MsgType.ALL_LINK_BROADCAST) {
270                 int group = msg.getAddress("toAddress").getLowByte() & 0xff;
271                 byte cmd1 = msg.getByte("command1");
272                 // if the command is 0x06, then it's success message
273                 // from the original broadcaster, with which the device
274                 // confirms that it got all cleanup replies successfully.
275                 GroupMessage gm = (cmd1 == 0x06) ? GroupMessage.SUCCESS : GroupMessage.BCAST;
276                 isDuplicate = !feature.getDevice().getGroupState(group, gm, cmd1);
277             } else if (t == MsgType.ALL_LINK_CLEANUP) {
278                 // the cleanup messages are direct messages, so the
279                 // group # is not in the toAddress, but in cmd2
280                 int group = msg.getByte("command2") & 0xff;
281                 isDuplicate = !feature.getDevice().getGroupState(group, GroupMessage.CLEAN, (byte) 0);
282             }
283         } catch (IllegalArgumentException e) {
284             logger.warn("cannot parse msg: {}", msg, e);
285         } catch (FieldException e) {
286             logger.warn("cannot parse msg: {}", msg, e);
287         }
288         return (isDuplicate);
289     }
290
291     /**
292      * Extract button information from message
293      *
294      * @param msg the message to extract from
295      * @param f the device feature (needed for debug printing)
296      * @return the button number or -1 if no button found
297      */
298     protected static int getButtonInfo(Msg msg, DeviceFeature f) {
299         // the cleanup messages have the button number in the command2 field
300         // the broadcast messages have it as the lsb of the toAddress
301         try {
302             int bclean = msg.getByte("command2") & 0xff;
303             int bbcast = msg.getAddress("toAddress").getLowByte() & 0xff;
304             int button = msg.isCleanup() ? bclean : bbcast;
305             logger.trace("{} button: {} bclean: {} bbcast: {}", f.getDevice().getAddress(), button, bclean, bbcast);
306             return button;
307         } catch (FieldException e) {
308             logger.warn("field exception while parsing msg {}: ", msg, e);
309         }
310         return -1;
311     }
312
313     /**
314      * Shorthand to return class name for logging purposes
315      *
316      * @return name of the class
317      */
318     protected String nm() {
319         return (this.getClass().getSimpleName());
320     }
321
322     /**
323      * Set parameter map
324      *
325      * @param map the parameter map for this message handler
326      */
327     public void setParameters(Map<String, String> map) {
328         parameters = map;
329     }
330
331     //
332     //
333     // ---------------- the various command handler start here -------------------
334     //
335     //
336
337     public static class DefaultMsgHandler extends MessageHandler {
338         DefaultMsgHandler(DeviceFeature p) {
339             super(p);
340         }
341
342         @Override
343         public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
344             logger.debug("{} ignoring unimpl message with cmd1:{}", nm(), Utils.getHexByte(cmd1));
345         }
346     }
347
348     public static class NoOpMsgHandler extends MessageHandler {
349         NoOpMsgHandler(DeviceFeature p) {
350             super(p);
351         }
352
353         @Override
354         public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
355             logger.trace("{} ignore msg {}: {}", nm(), Utils.getHexByte(cmd1), msg);
356         }
357     }
358
359     public static class LightOnDimmerHandler extends MessageHandler {
360         LightOnDimmerHandler(DeviceFeature p) {
361             super(p);
362         }
363
364         @Override
365         public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
366             if (!isMybutton(msg, f)) {
367                 return;
368             }
369             InsteonAddress a = f.getDevice().getAddress();
370             if (msg.isAckOfDirect()) {
371                 logger.warn("{}: device {}: ignoring ack of direct.", nm(), a);
372             } else {
373                 String mode = getStringParameter("mode", "REGULAR");
374                 logger.debug("{}: device {} was turned on {}. " + "Sending poll request to get actual level", nm(), a,
375                         mode);
376                 feature.publish(PercentType.HUNDRED, StateChangeType.ALWAYS);
377                 // need to poll to find out what level the dimmer is at now.
378                 // it may not be at 100% because dimmers can be configured
379                 // to switch to e.g. 75% when turned on.
380                 Msg m = f.makePollMsg();
381                 if (m != null) {
382                     f.getDevice().enqueueDelayedMessage(m, f, 1000);
383                 }
384             }
385         }
386     }
387
388     public static class LightOffDimmerHandler extends MessageHandler {
389         LightOffDimmerHandler(DeviceFeature p) {
390             super(p);
391         }
392
393         @Override
394         public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
395             if (isMybutton(msg, f)) {
396                 String mode = getStringParameter("mode", "REGULAR");
397                 logger.debug("{}: device {} was turned off {}.", nm(), f.getDevice().getAddress(), mode);
398                 f.publish(PercentType.ZERO, StateChangeType.ALWAYS);
399             }
400         }
401     }
402
403     public static class LightOnSwitchHandler extends MessageHandler {
404         LightOnSwitchHandler(DeviceFeature p) {
405             super(p);
406         }
407
408         @Override
409         public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
410             if (isMybutton(msg, f)) {
411                 String mode = getStringParameter("mode", "REGULAR");
412                 logger.debug("{}: device {} was switched on {}.", nm(), f.getDevice().getAddress(), mode);
413                 f.publish(OnOffType.ON, StateChangeType.ALWAYS);
414             } else {
415                 logger.debug("ignored message: {}", isMybutton(msg, f));
416             }
417         }
418     }
419
420     public static class LightOffSwitchHandler extends MessageHandler {
421         LightOffSwitchHandler(DeviceFeature p) {
422             super(p);
423         }
424
425         @Override
426         public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
427             if (isMybutton(msg, f)) {
428                 String mode = getStringParameter("mode", "REGULAR");
429                 logger.debug("{}: device {} was switched off {}.", nm(), f.getDevice().getAddress(), mode);
430                 f.publish(OnOffType.OFF, StateChangeType.ALWAYS);
431             }
432         }
433     }
434
435     /**
436      * This message handler processes replies to Ramp ON/OFF commands.
437      * Currently, it's been tested for the 2672-222 LED Bulb. Other
438      * devices may use a different pair of commands (0x2E, 0x2F). This
439      * handler and the command handler will need to be extended to support
440      * those devices.
441      */
442     public static class RampDimmerHandler extends MessageHandler {
443         private int onCmd;
444         private int offCmd;
445
446         RampDimmerHandler(DeviceFeature p) {
447             super(p);
448             // Can't process parameters here because they are set after constructor is invoked.
449             // Unfortunately, this means we can't declare the onCmd, offCmd to be final.
450         }
451
452         @Override
453         public void setParameters(Map<String, String> params) {
454             super.setParameters(params);
455             onCmd = getIntParameter("on", 0x2E);
456             offCmd = getIntParameter("off", 0x2F);
457         }
458
459         @Override
460         public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
461             if (cmd1 == onCmd) {
462                 int level = getLevel(msg);
463                 logger.debug("{}: device {} was switched on using ramp to level {}.", nm(), f.getDevice().getAddress(),
464                         level);
465                 if (level == 100) {
466                     f.publish(OnOffType.ON, StateChangeType.ALWAYS);
467                 } else {
468                     // The publisher will convert an ON at level==0 to an OFF.
469                     // However, this is not completely accurate since a ramp
470                     // off at level == 0 may not turn off the dimmer completely
471                     // (if I understand the Insteon docs correctly). In any
472                     // case,
473                     // it would be an odd scenario to turn ON a light at level
474                     // == 0
475                     // rather than turn if OFF.
476                     f.publish(new PercentType(level), StateChangeType.ALWAYS);
477                 }
478             } else if (cmd1 == offCmd) {
479                 logger.debug("{}: device {} was switched off using ramp.", nm(), f.getDevice().getAddress());
480                 f.publish(new PercentType(0), StateChangeType.ALWAYS);
481             }
482         }
483
484         private int getLevel(Msg msg) {
485             try {
486                 byte cmd2 = msg.getByte("command2");
487                 return (int) Math.round(((cmd2 >> 4) & 0x0f) * (100 / 15d));
488             } catch (FieldException e) {
489                 logger.warn("Can't access command2 byte", e);
490                 return 0;
491             }
492         }
493     }
494
495     /**
496      * A message handler that processes replies to queries.
497      * If command2 == 0xFF then the light has been turned on
498      * else if command2 == 0x00 then the light has been turned off
499      */
500
501     public static class SwitchRequestReplyHandler extends MessageHandler {
502         SwitchRequestReplyHandler(DeviceFeature p) {
503             super(p);
504         }
505
506         @Override
507         public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
508             try {
509                 InsteonAddress a = f.getDevice().getAddress();
510                 int cmd2 = msg.getByte("command2") & 0xff;
511                 int button = this.getIntParameter("button", -1);
512                 if (button < 0) {
513                     handleNoButtons(cmd2, a, msg);
514                 } else {
515                     boolean isOn = isLEDLit(cmd2, button);
516                     logger.debug("{}: dev {} button {} switched to {}", nm(), a, button, isOn ? "ON" : "OFF");
517                     feature.publish(OnOffType.from(isOn), StateChangeType.CHANGED);
518                 }
519             } catch (FieldException e) {
520                 logger.warn("{} error parsing {}: ", nm(), msg, e);
521             }
522         }
523
524         /**
525          * Handle the case where no buttons have been configured.
526          * In this situation, the only return values should be 0 (light off)
527          * or 0xff (light on)
528          *
529          * @param cmd2
530          */
531         void handleNoButtons(int cmd2, InsteonAddress a, Msg msg) {
532             if (cmd2 == 0) {
533                 logger.debug("{}: set device {} to OFF", nm(), a);
534                 feature.publish(OnOffType.OFF, StateChangeType.CHANGED);
535             } else if (cmd2 == 0xff) {
536                 logger.debug("{}: set device {} to ON", nm(), a);
537                 feature.publish(OnOffType.ON, StateChangeType.CHANGED);
538             } else {
539                 logger.warn("{}: {} ignoring unexpected cmd2 in msg: {}", nm(), a, msg);
540             }
541         }
542
543         /**
544          * Test if cmd byte indicates that button is lit.
545          * The cmd byte has the LED status bitwise from the left:
546          * 87654321
547          * Note that the 2487S has buttons assigned like this:
548          * 22|6543|11
549          * They used the basis of the 8-button remote, and assigned
550          * the ON button to 1+2, the OFF button to 7+8
551          *
552          * @param cmd cmd byte as received in message
553          * @param button button to test (number in range 1..8)
554          * @return true if button is lit, false otherwise
555          */
556         private boolean isLEDLit(int cmd, int button) {
557             boolean isSet = (cmd & (0x1 << (button - 1))) != 0;
558             logger.trace("cmd: {} button {}", Integer.toBinaryString(cmd), button);
559             logger.trace("msk: {} isSet: {}", Integer.toBinaryString(0x1 << (button - 1)), isSet);
560             return (isSet);
561         }
562     }
563
564     /**
565      * Handles Dimmer replies to status requests.
566      * In the dimmers case the command2 byte represents the light level from 0-255
567      */
568     public static class DimmerRequestReplyHandler extends MessageHandler {
569         DimmerRequestReplyHandler(DeviceFeature p) {
570             super(p);
571         }
572
573         @Override
574         public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
575             InsteonDevice dev = f.getDevice();
576             try {
577                 int cmd2 = msg.getByte("command2") & 0xff;
578                 if (cmd2 == 0xfe) {
579                     // sometimes dimmer devices are returning 0xfe when on instead of 0xff
580                     cmd2 = 0xff;
581                 }
582
583                 if (cmd2 == 0) {
584                     logger.debug("{}: set device {} to level 0", nm(), dev.getAddress());
585                     feature.publish(PercentType.ZERO, StateChangeType.CHANGED);
586                 } else if (cmd2 == 0xff) {
587                     logger.debug("{}: set device {} to level 100", nm(), dev.getAddress());
588                     feature.publish(PercentType.HUNDRED, StateChangeType.CHANGED);
589                 } else {
590                     int level = cmd2 * 100 / 255;
591                     if (level == 0) {
592                         level = 1;
593                     }
594                     logger.debug("{}: set device {} to level {}", nm(), dev.getAddress(), level);
595                     feature.publish(new PercentType(level), StateChangeType.CHANGED);
596                 }
597             } catch (FieldException e) {
598                 logger.warn("{}: error parsing {}: ", nm(), msg, e);
599             }
600         }
601     }
602
603     public static class DimmerStopManualChangeHandler extends MessageHandler {
604         DimmerStopManualChangeHandler(DeviceFeature p) {
605             super(p);
606         }
607
608         @Override
609         public boolean isDuplicate(Msg msg) {
610             // Disable duplicate elimination because
611             // there are no cleanup or success messages for start/stop.
612             return (false);
613         }
614
615         @Override
616         public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
617             Msg m = f.makePollMsg();
618             if (m != null) {
619                 f.getDevice().enqueueMessage(m, f);
620             }
621         }
622     }
623
624     public static class StartManualChangeHandler extends MessageHandler {
625         StartManualChangeHandler(DeviceFeature p) {
626             super(p);
627         }
628
629         @Override
630         public boolean isDuplicate(Msg msg) {
631             // Disable duplicate elimination because
632             // there are no cleanup or success messages for start/stop.
633             return (false);
634         }
635
636         @Override
637         public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
638             try {
639                 int cmd2 = msg.getByte("command2") & 0xff;
640                 int upDown = (cmd2 == 0) ? 0 : 2;
641                 logger.debug("{}: dev {} manual state change: {}", nm(), f.getDevice().getAddress(),
642                         (upDown == 0) ? "DOWN" : "UP");
643                 feature.publish(new DecimalType(upDown), StateChangeType.ALWAYS);
644             } catch (FieldException e) {
645                 logger.warn("{} error parsing {}: ", nm(), msg, e);
646             }
647         }
648     }
649
650     public static class StopManualChangeHandler extends MessageHandler {
651         StopManualChangeHandler(DeviceFeature p) {
652             super(p);
653         }
654
655         @Override
656         public boolean isDuplicate(Msg msg) {
657             // Disable duplicate elimination because
658             // there are no cleanup or success messages for start/stop.
659             return (false);
660         }
661
662         @Override
663         public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
664             logger.debug("{}: dev {} manual state change: {}", nm(), f.getDevice().getAddress(), 0);
665             feature.publish(new DecimalType(1), StateChangeType.ALWAYS);
666         }
667     }
668
669     public static class InfoRequestReplyHandler extends MessageHandler {
670         InfoRequestReplyHandler(DeviceFeature p) {
671             super(p);
672         }
673
674         @Override
675         public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
676             InsteonDevice dev = f.getDevice();
677             if (!msg.isExtended()) {
678                 logger.warn("{} device {} expected extended msg as info reply, got {}", nm(), dev.getAddress(), msg);
679                 return;
680             }
681             try {
682                 int cmd2 = msg.getByte("command2") & 0xff;
683                 switch (cmd2) {
684                     case 0x00: // this is a product data response message
685                         int prodKey = msg.getInt24("userData2", "userData3", "userData4");
686                         int devCat = msg.getByte("userData5");
687                         int subCat = msg.getByte("userData6");
688                         logger.debug("{} {} got product data: cat: {} subcat: {} key: {} ", nm(), dev.getAddress(),
689                                 devCat, subCat, Utils.getHexString(prodKey));
690                         break;
691                     case 0x02: // this is a device text string response message
692                         logger.debug("{} {} got text str {} ", nm(), dev.getAddress(), msg);
693                         break;
694                     default:
695                         logger.warn("{} unknown cmd2 = {} in info reply message {}", nm(), cmd2, msg);
696                         break;
697                 }
698             } catch (FieldException e) {
699                 logger.warn("error parsing {}: ", msg, e);
700             }
701         }
702     }
703
704     public static class MotionSensorDataReplyHandler extends MessageHandler {
705         MotionSensorDataReplyHandler(DeviceFeature p) {
706             super(p);
707         }
708
709         @Override
710         public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
711             InsteonDevice dev = f.getDevice();
712             if (!msg.isExtended()) {
713                 logger.trace("{} device {} ignoring non-extended msg {}", nm(), dev.getAddress(), msg);
714                 return;
715             }
716             try {
717                 int cmd2 = msg.getByte("command2") & 0xff;
718                 int batteryLevel;
719                 int lightLevel;
720                 int temperatureLevel;
721                 switch (cmd2) {
722                     case 0x00: // this is a product data response message
723                         batteryLevel = msg.getByte("userData12") & 0xff;
724                         lightLevel = msg.getByte("userData11") & 0xff;
725                         logger.debug("{}: {} got light level: {}, battery level: {}", nm(), dev.getAddress(),
726                                 lightLevel, batteryLevel);
727                         feature.publish(new DecimalType(lightLevel), StateChangeType.CHANGED,
728                                 InsteonDeviceHandler.FIELD, InsteonDeviceHandler.FIELD_LIGHT_LEVEL);
729                         feature.publish(new DecimalType(batteryLevel), StateChangeType.CHANGED,
730                                 InsteonDeviceHandler.FIELD, InsteonDeviceHandler.FIELD_BATTERY_LEVEL);
731                         break;
732                     case 0x03: // this is the 2844-222 data response message
733                         batteryLevel = msg.getByte("userData6") & 0xff;
734                         lightLevel = msg.getByte("userData7") & 0xff;
735                         temperatureLevel = msg.getByte("userData8") & 0xff;
736                         logger.debug("{}: {} got light level: {}, battery level: {}, temperature level: {}", nm(),
737                                 dev.getAddress(), lightLevel, batteryLevel, temperatureLevel);
738                         feature.publish(new DecimalType(lightLevel), StateChangeType.CHANGED,
739                                 InsteonDeviceHandler.FIELD, InsteonDeviceHandler.FIELD_LIGHT_LEVEL);
740                         feature.publish(new DecimalType(batteryLevel), StateChangeType.CHANGED,
741                                 InsteonDeviceHandler.FIELD, InsteonDeviceHandler.FIELD_BATTERY_LEVEL);
742                         feature.publish(new DecimalType(temperatureLevel), StateChangeType.CHANGED,
743                                 InsteonDeviceHandler.FIELD, InsteonDeviceHandler.FIELD_TEMPERATURE_LEVEL);
744
745                         // per 2844-222 dev doc: working battery level range is 0xd2 - 0x70
746                         int batteryPercentage;
747                         if (batteryLevel >= 0xd2) {
748                             batteryPercentage = 100;
749                         } else if (batteryLevel <= 0x70) {
750                             batteryPercentage = 0;
751                         } else {
752                             batteryPercentage = (batteryLevel - 0x70) * 100 / (0xd2 - 0x70);
753                         }
754                         logger.debug("{}: {} battery percentage: {}", nm(), dev.getAddress(), batteryPercentage);
755                         feature.publish(new QuantityType<>(batteryPercentage, Units.PERCENT), StateChangeType.CHANGED,
756                                 InsteonDeviceHandler.FIELD, InsteonDeviceHandler.FIELD_BATTERY_PERCENTAGE);
757                         break;
758                     default:
759                         logger.warn("unknown cmd2 = {} in info reply message {}", cmd2, msg);
760                         break;
761                 }
762             } catch (FieldException e) {
763                 logger.warn("error parsing {}: ", msg, e);
764             }
765         }
766     }
767
768     public static class MotionSensor2AlternateHeartbeatHandler extends MessageHandler {
769         MotionSensor2AlternateHeartbeatHandler(DeviceFeature p) {
770             super(p);
771         }
772
773         @Override
774         public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
775             InsteonDevice dev = f.getDevice();
776             try {
777                 // group 0x0B (11) - alternate heartbeat group
778                 InsteonAddress toAddr = msg.getAddr("toAddress");
779                 if (toAddr == null) {
780                     logger.warn("toAddr is null");
781                     return;
782                 }
783                 int batteryLevel = toAddr.getHighByte() & 0xff;
784                 int lightLevel = toAddr.getMiddleByte() & 0xff;
785                 int temperatureLevel = msg.getByte("command2") & 0xff;
786
787                 logger.debug("{}: {} got light level: {}, battery level: {}, temperature level: {}", nm(),
788                         dev.getAddress(), lightLevel, batteryLevel, temperatureLevel);
789                 feature.publish(new DecimalType(lightLevel), StateChangeType.CHANGED, InsteonDeviceHandler.FIELD,
790                         InsteonDeviceHandler.FIELD_LIGHT_LEVEL);
791                 feature.publish(new DecimalType(batteryLevel), StateChangeType.CHANGED, InsteonDeviceHandler.FIELD,
792                         InsteonDeviceHandler.FIELD_BATTERY_LEVEL);
793                 feature.publish(new DecimalType(temperatureLevel), StateChangeType.CHANGED, InsteonDeviceHandler.FIELD,
794                         InsteonDeviceHandler.FIELD_TEMPERATURE_LEVEL);
795
796                 // per 2844-222 dev doc: working battery level range is 0xd2 - 0x70
797                 int batteryPercentage;
798                 if (batteryLevel >= 0xd2) {
799                     batteryPercentage = 100;
800                 } else if (batteryLevel <= 0x70) {
801                     batteryPercentage = 0;
802                 } else {
803                     batteryPercentage = (batteryLevel - 0x70) * 100 / (0xd2 - 0x70);
804                 }
805                 logger.debug("{}: {} battery percentage: {}", nm(), dev.getAddress(), batteryPercentage);
806                 feature.publish(new QuantityType<>(batteryPercentage, Units.PERCENT), StateChangeType.CHANGED,
807                         InsteonDeviceHandler.FIELD, InsteonDeviceHandler.FIELD_BATTERY_PERCENTAGE);
808             } catch (FieldException e) {
809                 logger.warn("error parsing {}: ", msg, e);
810             }
811         }
812     }
813
814     public static class HiddenDoorSensorDataReplyHandler extends MessageHandler {
815         HiddenDoorSensorDataReplyHandler(DeviceFeature p) {
816             super(p);
817         }
818
819         @Override
820         public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
821             InsteonDevice dev = f.getDevice();
822             if (!msg.isExtended()) {
823                 logger.trace("{} device {} ignoring non-extended msg {}", nm(), dev.getAddress(), msg);
824                 return;
825             }
826             try {
827                 int cmd2 = msg.getByte("command2") & 0xff;
828                 switch (cmd2) {
829                     case 0x00: // this is a product data response message
830                         int batteryLevel = msg.getByte("userData4") & 0xff;
831                         int batteryWatermark = msg.getByte("userData7") & 0xff;
832                         logger.debug("{}: {} got light level: {}, battery level: {}", nm(), dev.getAddress(),
833                                 batteryWatermark, batteryLevel);
834                         feature.publish(new DecimalType(batteryWatermark), StateChangeType.CHANGED,
835                                 InsteonDeviceHandler.FIELD, InsteonDeviceHandler.FIELD_BATTERY_WATERMARK_LEVEL);
836                         feature.publish(new DecimalType(batteryLevel), StateChangeType.CHANGED,
837                                 InsteonDeviceHandler.FIELD, InsteonDeviceHandler.FIELD_BATTERY_LEVEL);
838                         break;
839                     default:
840                         logger.warn("unknown cmd2 = {} in info reply message {}", cmd2, msg);
841                         break;
842                 }
843             } catch (FieldException e) {
844                 logger.warn("error parsing {}: ", msg, e);
845             }
846         }
847     }
848
849     public static class PowerMeterUpdateHandler extends MessageHandler {
850         PowerMeterUpdateHandler(DeviceFeature p) {
851             super(p);
852         }
853
854         @Override
855         public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
856             if (msg.isExtended()) {
857                 try {
858                     // see iMeter developer notes 2423A1dev-072013-en.pdf
859                     int b7 = msg.getByte("userData7") & 0xff;
860                     int b8 = msg.getByte("userData8") & 0xff;
861                     int watts = (b7 << 8) | b8;
862                     if (watts > 32767) {
863                         watts -= 65535;
864                     }
865
866                     int b9 = msg.getByte("userData9") & 0xff;
867                     int b10 = msg.getByte("userData10") & 0xff;
868                     int b11 = msg.getByte("userData11") & 0xff;
869                     int b12 = msg.getByte("userData12") & 0xff;
870                     BigDecimal kwh = BigDecimal.ZERO;
871                     if (b9 < 254) {
872                         int e = (b9 << 24) | (b10 << 16) | (b11 << 8) | b12;
873                         kwh = new BigDecimal(e * 65535.0 / (1000 * 60 * 60 * 60)).setScale(4, RoundingMode.HALF_UP);
874                     }
875
876                     logger.debug("{}:{} watts: {} kwh: {} ", nm(), f.getDevice().getAddress(), watts, kwh);
877                     feature.publish(new QuantityType<>(kwh, Units.KILOWATT_HOUR), StateChangeType.CHANGED,
878                             InsteonDeviceHandler.FIELD, InsteonDeviceHandler.FIELD_KWH);
879                     feature.publish(new QuantityType<>(watts, Units.WATT), StateChangeType.CHANGED,
880                             InsteonDeviceHandler.FIELD, InsteonDeviceHandler.FIELD_WATTS);
881                 } catch (FieldException e) {
882                     logger.warn("error parsing {}: ", msg, e);
883                 }
884             }
885         }
886     }
887
888     public static class PowerMeterResetHandler extends MessageHandler {
889         PowerMeterResetHandler(DeviceFeature p) {
890             super(p);
891         }
892
893         @Override
894         public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
895             InsteonDevice dev = f.getDevice();
896             logger.debug("{}: power meter {} was reset", nm(), dev.getAddress());
897
898             // poll device to get updated kilowatt hours and watts
899             Msg m = f.makePollMsg();
900             if (m != null) {
901                 f.getDevice().enqueueMessage(m, f);
902             }
903         }
904     }
905
906     public static class LastTimeHandler extends MessageHandler {
907         LastTimeHandler(DeviceFeature p) {
908             super(p);
909         }
910
911         @Override
912         public void handleMessage(int group, byte cmd1a, Msg msg, DeviceFeature f) {
913             feature.publish(new DateTimeType(), StateChangeType.ALWAYS);
914         }
915     }
916
917     public static class ContactRequestReplyHandler extends MessageHandler {
918         ContactRequestReplyHandler(DeviceFeature p) {
919             super(p);
920         }
921
922         @Override
923         public void handleMessage(int group, byte cmd1a, Msg msg, DeviceFeature f) {
924             byte cmd = 0x00;
925             byte cmd2 = 0x00;
926             try {
927                 cmd = msg.getByte("Cmd");
928                 cmd2 = msg.getByte("command2");
929             } catch (FieldException e) {
930                 logger.debug("{} no cmd found, dropping msg {}", nm(), msg);
931                 return;
932             }
933             if (msg.isAckOfDirect() && (f.getQueryStatus() == DeviceFeature.QueryStatus.QUERY_PENDING) && cmd == 0x50) {
934                 OpenClosedType oc = (cmd2 == 0) ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
935                 logger.debug("{}: set contact {} to: {}", nm(), f.getDevice().getAddress(), oc);
936                 feature.publish(oc, StateChangeType.CHANGED);
937             }
938         }
939     }
940
941     public static class ClosedContactHandler extends MessageHandler {
942         ClosedContactHandler(DeviceFeature p) {
943             super(p);
944         }
945
946         @Override
947         public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
948             feature.publish(OpenClosedType.CLOSED, StateChangeType.ALWAYS);
949         }
950     }
951
952     public static class OpenedContactHandler extends MessageHandler {
953         OpenedContactHandler(DeviceFeature p) {
954             super(p);
955         }
956
957         @Override
958         public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
959             feature.publish(OpenClosedType.OPEN, StateChangeType.ALWAYS);
960         }
961     }
962
963     public static class OpenedOrClosedContactHandler extends MessageHandler {
964         OpenedOrClosedContactHandler(DeviceFeature p) {
965             super(p);
966         }
967
968         @Override
969         public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
970             try {
971                 byte cmd2 = msg.getByte("command2");
972                 switch (cmd1) {
973                     case 0x11:
974                         switch (cmd2) {
975                             case 0x02:
976                                 feature.publish(OpenClosedType.CLOSED, StateChangeType.CHANGED);
977                                 break;
978                             case 0x01:
979                             case 0x04:
980                                 feature.publish(OpenClosedType.OPEN, StateChangeType.CHANGED);
981                                 break;
982                             default: // do nothing
983                                 break;
984                         }
985                         break;
986                     case 0x13:
987                         switch (cmd2) {
988                             case 0x04:
989                                 feature.publish(OpenClosedType.CLOSED, StateChangeType.CHANGED);
990                                 break;
991                             default: // do nothing
992                                 break;
993                         }
994                         break;
995                 }
996             } catch (FieldException e) {
997                 logger.debug("{} no cmd2 found, dropping msg {}", nm(), msg);
998                 return;
999             }
1000         }
1001     }
1002
1003     public static class ClosedSleepingContactHandler extends MessageHandler {
1004         ClosedSleepingContactHandler(DeviceFeature p) {
1005             super(p);
1006         }
1007
1008         @Override
1009         public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
1010             feature.publish(OpenClosedType.CLOSED, StateChangeType.ALWAYS);
1011             if (f.getDevice().hasProductKey(InsteonDeviceHandler.MOTION_SENSOR_II_PRODUCT_KEY)) {
1012                 if (!getBooleanDeviceConfig("heartbeatOnly", false)) {
1013                     sendExtendedQuery(f, (byte) 0x2e, (byte) 03);
1014                 }
1015             } else {
1016                 sendExtendedQuery(f, (byte) 0x2e, (byte) 00);
1017             }
1018         }
1019     }
1020
1021     public static class OpenedSleepingContactHandler extends MessageHandler {
1022         OpenedSleepingContactHandler(DeviceFeature p) {
1023             super(p);
1024         }
1025
1026         @Override
1027         public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
1028             feature.publish(OpenClosedType.OPEN, StateChangeType.ALWAYS);
1029             if (f.getDevice().hasProductKey(InsteonDeviceHandler.MOTION_SENSOR_II_PRODUCT_KEY)) {
1030                 if (!getBooleanDeviceConfig("heartbeatOnly", false)) {
1031                     sendExtendedQuery(f, (byte) 0x2e, (byte) 03);
1032                 }
1033             } else {
1034                 sendExtendedQuery(f, (byte) 0x2e, (byte) 00);
1035             }
1036         }
1037     }
1038
1039     /**
1040      * Triggers a poll when a message comes in. Use this handler to react
1041      * to messages that notify of a status update, but don't carry the information
1042      * that you are interested in. Example: you send a command to change a setting,
1043      * get a DIRECT ack back, but the ack does not have the value of the updated setting.
1044      * Then connect this handler to the ACK, such that the device will be polled, and
1045      * the settings updated.
1046      */
1047     public static class TriggerPollMsgHandler extends MessageHandler {
1048         TriggerPollMsgHandler(DeviceFeature p) {
1049             super(p);
1050         }
1051
1052         @Override
1053         public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
1054             feature.getDevice().doPoll(2000); // 2000 ms delay
1055         }
1056     }
1057
1058     /**
1059      * Flexible handler to extract numerical data from messages.
1060      */
1061     public static class NumberMsgHandler extends MessageHandler {
1062         NumberMsgHandler(DeviceFeature p) {
1063             super(p);
1064         }
1065
1066         @Override
1067         public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
1068             try {
1069                 // first do the bit manipulations to focus on the right area
1070                 int mask = getIntParameter("mask", 0xFFFF);
1071                 int rawValue = extractValue(msg, group);
1072                 int cooked = (rawValue & mask) >> getIntParameter("rshift", 0);
1073                 // now do an arbitrary transform on the data
1074                 double value = transform(cooked);
1075                 // last, multiply with factor and add an offset
1076                 double dvalue = getDoubleParameter("offset", 0) + value * getDoubleParameter("factor", 1.0);
1077
1078                 @Nullable
1079                 State state;
1080                 String scale = getStringParameter("scale", null);
1081                 if ("celsius".equals(scale)) {
1082                     state = new QuantityType<>(dvalue, SIUnits.CELSIUS);
1083                 } else if ("fahrenheit".equals(scale)) {
1084                     state = new QuantityType<>(dvalue, ImperialUnits.FAHRENHEIT);
1085                 } else {
1086                     state = new DecimalType(dvalue);
1087                 }
1088                 feature.publish(state, StateChangeType.CHANGED);
1089             } catch (FieldException e) {
1090                 logger.warn("error parsing {}: ", msg, e);
1091             }
1092         }
1093
1094         public int transform(int raw) {
1095             return (raw);
1096         }
1097
1098         private int extractValue(Msg msg, int group) throws FieldException {
1099             String lowByte = getStringParameter("low_byte", null);
1100             if (lowByte == null) {
1101                 logger.warn("{} handler misconfigured, missing low_byte!", nm());
1102                 return 0;
1103             }
1104             int value = 0;
1105             if ("group".equals(lowByte)) {
1106                 value = group;
1107             } else {
1108                 value = msg.getByte(lowByte) & 0xFF;
1109             }
1110             String highByte = getStringParameter("high_byte", null);
1111             if (highByte != null) {
1112                 value |= (msg.getByte(highByte) & 0xFF) << 8;
1113             }
1114             return (value);
1115         }
1116     }
1117
1118     /**
1119      * Convert system mode field to number 0...4. Insteon has two different
1120      * conventions for numbering, we use the one of the status update messages
1121      */
1122     public static class ThermostatSystemModeMsgHandler extends NumberMsgHandler {
1123         ThermostatSystemModeMsgHandler(DeviceFeature p) {
1124             super(p);
1125         }
1126
1127         @Override
1128         public int transform(int raw) {
1129             switch (raw) {
1130                 case 0:
1131                     return (0); // off
1132                 case 1:
1133                     return (3); // auto
1134                 case 2:
1135                     return (1); // heat
1136                 case 3:
1137                     return (2); // cool
1138                 case 4:
1139                     return (4); // program
1140                 default:
1141                     break;
1142             }
1143             return (4); // when in doubt assume to be in "program" mode
1144         }
1145     }
1146
1147     /**
1148      * Handle reply to system mode change command
1149      */
1150     public static class ThermostatSystemModeReplyHandler extends NumberMsgHandler {
1151         ThermostatSystemModeReplyHandler(DeviceFeature p) {
1152             super(p);
1153         }
1154
1155         @Override
1156         public int transform(int raw) {
1157             switch (raw) {
1158                 case 0x09:
1159                     return (0); // off
1160                 case 0x04:
1161                     return (1); // heat
1162                 case 0x05:
1163                     return (2); // cool
1164                 case 0x06:
1165                     return (3); // auto
1166                 case 0x0A:
1167                     return (4); // program
1168                 default:
1169                     break;
1170             }
1171             return (4); // when in doubt assume to be in "program" mode
1172         }
1173     }
1174
1175     /**
1176      * Handle reply to fan mode change command
1177      */
1178     public static class ThermostatFanModeReplyHandler extends NumberMsgHandler {
1179         ThermostatFanModeReplyHandler(DeviceFeature p) {
1180             super(p);
1181         }
1182
1183         @Override
1184         public int transform(int raw) {
1185             switch (raw) {
1186                 case 0x08:
1187                     return (0); // auto
1188                 case 0x07:
1189                     return (1); // always on
1190                 default:
1191                     break;
1192             }
1193             return (0); // when in doubt assume to be auto mode
1194         }
1195     }
1196
1197     /**
1198      * Handle reply to fanlinc fan speed change command
1199      */
1200     public static class FanLincFanReplyHandler extends NumberMsgHandler {
1201         FanLincFanReplyHandler(DeviceFeature p) {
1202             super(p);
1203         }
1204
1205         @Override
1206         public int transform(int raw) {
1207             switch (raw) {
1208                 case 0x00:
1209                     return (0); // off
1210                 case 0x55:
1211                     return (1); // low
1212                 case 0xAA:
1213                     return (2); // medium
1214                 case 0xFF:
1215                     return (3); // high
1216                 default:
1217                     logger.warn("fanlinc got unexpected level: {}", raw);
1218             }
1219             return (0); // when in doubt assume to be off
1220         }
1221     }
1222
1223     /**
1224      * Process X10 messages that are generated when another controller
1225      * changes the state of an X10 device.
1226      */
1227     public static class X10OnHandler extends MessageHandler {
1228         X10OnHandler(DeviceFeature p) {
1229             super(p);
1230         }
1231
1232         @Override
1233         public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
1234             InsteonAddress a = f.getDevice().getAddress();
1235             logger.debug("{}: set X10 device {} to ON", nm(), a);
1236             feature.publish(OnOffType.ON, StateChangeType.ALWAYS);
1237         }
1238     }
1239
1240     public static class X10OffHandler extends MessageHandler {
1241         X10OffHandler(DeviceFeature p) {
1242             super(p);
1243         }
1244
1245         @Override
1246         public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
1247             InsteonAddress a = f.getDevice().getAddress();
1248             logger.debug("{}: set X10 device {} to OFF", nm(), a);
1249             feature.publish(OnOffType.OFF, StateChangeType.ALWAYS);
1250         }
1251     }
1252
1253     public static class X10BrightHandler extends MessageHandler {
1254         X10BrightHandler(DeviceFeature p) {
1255             super(p);
1256         }
1257
1258         @Override
1259         public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
1260             InsteonAddress a = f.getDevice().getAddress();
1261             logger.debug("{}: ignoring brighten message for device {}", nm(), a);
1262         }
1263     }
1264
1265     public static class X10DimHandler extends MessageHandler {
1266         X10DimHandler(DeviceFeature p) {
1267             super(p);
1268         }
1269
1270         @Override
1271         public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
1272             InsteonAddress a = f.getDevice().getAddress();
1273             logger.debug("{}: ignoring dim message for device {}", nm(), a);
1274         }
1275     }
1276
1277     public static class X10OpenHandler extends MessageHandler {
1278         X10OpenHandler(DeviceFeature p) {
1279             super(p);
1280         }
1281
1282         @Override
1283         public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
1284             InsteonAddress a = f.getDevice().getAddress();
1285             logger.debug("{}: set X10 device {} to OPEN", nm(), a);
1286             feature.publish(OpenClosedType.OPEN, StateChangeType.ALWAYS);
1287         }
1288     }
1289
1290     public static class X10ClosedHandler extends MessageHandler {
1291         X10ClosedHandler(DeviceFeature p) {
1292             super(p);
1293         }
1294
1295         @Override
1296         public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
1297             InsteonAddress a = f.getDevice().getAddress();
1298             logger.debug("{}: set X10 device {} to CLOSED", nm(), a);
1299             feature.publish(OpenClosedType.CLOSED, StateChangeType.ALWAYS);
1300         }
1301     }
1302
1303     /**
1304      * Factory method for creating handlers of a given name using java reflection
1305      *
1306      * @param name the name of the handler to create
1307      * @param params
1308      * @param f the feature for which to create the handler
1309      * @return the handler which was created
1310      */
1311     public static @Nullable <T extends MessageHandler> T makeHandler(String name, Map<String, String> params,
1312             DeviceFeature f) {
1313         String cname = MessageHandler.class.getName() + "$" + name;
1314         try {
1315             Class<?> c = Class.forName(cname);
1316             @SuppressWarnings("unchecked")
1317             Class<? extends T> dc = (Class<? extends T>) c;
1318             @Nullable
1319             T mh = dc.getDeclaredConstructor(DeviceFeature.class).newInstance(f);
1320             mh.setParameters(params);
1321             return mh;
1322         } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | IllegalArgumentException
1323                 | InvocationTargetException | NoSuchMethodException | SecurityException e) {
1324             logger.warn("error trying to create message handler: {}", name, e);
1325         }
1326         return null;
1327     }
1328 }