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