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