]> git.basschouten.com Git - openhab-addons.git/blob
d5d0072d3406da3dd3c625a578f37b354a7b1a3e
[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.util.Arrays;
17 import java.util.HashMap;
18 import java.util.Map;
19 import java.util.Timer;
20 import java.util.TimerTask;
21
22 import org.eclipse.jdt.annotation.NonNullByDefault;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.openhab.binding.insteon.internal.config.InsteonChannelConfiguration;
25 import org.openhab.binding.insteon.internal.device.DeviceFeatureListener.StateChangeType;
26 import org.openhab.binding.insteon.internal.handler.InsteonDeviceHandler;
27 import org.openhab.binding.insteon.internal.message.FieldException;
28 import org.openhab.binding.insteon.internal.message.InvalidMessageTypeException;
29 import org.openhab.binding.insteon.internal.message.Msg;
30 import org.openhab.binding.insteon.internal.utils.Utils;
31 import org.openhab.core.library.types.DecimalType;
32 import org.openhab.core.library.types.IncreaseDecreaseType;
33 import org.openhab.core.library.types.OnOffType;
34 import org.openhab.core.library.types.PercentType;
35 import org.openhab.core.types.Command;
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
38
39 /**
40  * A command handler translates an openHAB command into a insteon message
41  *
42  * @author Daniel Pfrommer - Initial contribution
43  * @author Bernd Pfrommer - openHAB 1 insteonplm binding
44  * @author Rob Nielsen - Port to openHAB 2 insteon binding
45  */
46 @NonNullByDefault
47 @SuppressWarnings("null")
48 public abstract class CommandHandler {
49     private static final Logger logger = LoggerFactory.getLogger(CommandHandler.class);
50     DeviceFeature feature; // related DeviceFeature
51     @Nullable
52     Map<String, String> parameters = new HashMap<>();
53
54     /**
55      * Constructor
56      *
57      * @param feature The DeviceFeature for which this command was intended.
58      *            The openHAB commands are issued on an openhab item. The .items files bind
59      *            an openHAB item to a DeviceFeature.
60      */
61     CommandHandler(DeviceFeature feature) {
62         this.feature = feature;
63     }
64
65     /**
66      * Implements what to do when an openHAB command is received
67      *
68      * @param config the configuration for the item that generated the command
69      * @param cmd the openhab command issued
70      * @param device the Insteon device to which this command applies
71      */
72     public abstract void handleCommand(InsteonChannelConfiguration conf, Command cmd, InsteonDevice device);
73
74     /**
75      * Returns parameter as integer
76      *
77      * @param key key of parameter
78      * @param def default
79      * @return value of parameter
80      */
81     protected int getIntParameter(String key, int def) {
82         String val = parameters.get(key);
83         if (val == null) {
84             return (def); // param not found
85         }
86         int ret = def;
87         try {
88             ret = Utils.strToInt(val);
89         } catch (NumberFormatException e) {
90             logger.warn("malformed int parameter in command handler: {}", key);
91         }
92         return ret;
93     }
94
95     /**
96      * Returns parameter as String
97      *
98      * @param key key of parameter
99      * @param def default
100      * @return value of parameter
101      */
102     protected @Nullable String getStringParameter(String key, String def) {
103         return (parameters.get(key) == null ? def : parameters.get(key));
104     }
105
106     /**
107      * Shorthand to return class name for logging purposes
108      *
109      * @return name of the class
110      */
111     protected String nm() {
112         return (this.getClass().getSimpleName());
113     }
114
115     protected int getMaxLightLevel(InsteonChannelConfiguration conf, int defaultLevel) {
116         Map<String, String> params = conf.getParameters();
117         if (conf.getFeature().contains("dimmer") && params.containsKey("dimmermax")) {
118             String item = conf.getChannelName();
119             String dimmerMax = params.get("dimmermax");
120             try {
121                 int i = Integer.parseInt(dimmerMax);
122                 if (i > 1 && i <= 99) {
123                     int level = (int) Math.ceil((i * 255.0) / 100); // round up
124                     if (level < defaultLevel) {
125                         logger.debug("item {}: using dimmermax value of {}", item, dimmerMax);
126                         return level;
127                     }
128                 } else {
129                     logger.warn("item {}: dimmermax must be between 1-99 inclusive: {}", item, dimmerMax);
130                 }
131             } catch (NumberFormatException e) {
132                 logger.warn("item {}: invalid int value for dimmermax: {}", item, dimmerMax);
133             }
134         }
135
136         return defaultLevel;
137     }
138
139     void setParameters(Map<String, String> map) {
140         parameters = map;
141     }
142
143     /**
144      * Helper function to extract the group parameter from the binding config,
145      *
146      * @param c the binding configuration to test
147      * @return the value of the "group" parameter, or -1 if none
148      */
149     protected static int getGroup(InsteonChannelConfiguration c) {
150         String v = c.getParameters().get("group");
151         int iv = -1;
152         try {
153             iv = (v == null) ? -1 : Utils.strToInt(v);
154         } catch (NumberFormatException e) {
155             logger.warn("malformed int parameter in for item {}", c.getChannelName());
156         }
157         return iv;
158     }
159
160     @NonNullByDefault
161     public static class WarnCommandHandler extends CommandHandler {
162         WarnCommandHandler(DeviceFeature f) {
163             super(f);
164         }
165
166         @Override
167         public void handleCommand(InsteonChannelConfiguration conf, Command cmd, InsteonDevice dev) {
168             logger.warn("{}: command {} is not implemented yet!", nm(), cmd);
169         }
170     }
171
172     @NonNullByDefault
173     public static class NoOpCommandHandler extends CommandHandler {
174         NoOpCommandHandler(DeviceFeature f) {
175             super(f);
176         }
177
178         @Override
179         public void handleCommand(InsteonChannelConfiguration conf, Command cmd, InsteonDevice dev) {
180             // do nothing, not even log
181         }
182     }
183
184     @NonNullByDefault
185     public static class LightOnOffCommandHandler extends CommandHandler {
186         LightOnOffCommandHandler(DeviceFeature f) {
187             super(f);
188         }
189
190         @Override
191         public void handleCommand(InsteonChannelConfiguration conf, Command cmd, InsteonDevice dev) {
192             try {
193                 int ext = getIntParameter("ext", 0);
194                 int direc = 0x00;
195                 int level = 0x00;
196                 Msg m = null;
197                 if (cmd == OnOffType.ON) {
198                     level = getMaxLightLevel(conf, 0xff);
199                     direc = 0x11;
200                     logger.debug("{}: sent msg to switch {} to {}", nm(), dev.getAddress(),
201                             level == 0xff ? "on" : level);
202                 } else if (cmd == OnOffType.OFF) {
203                     direc = 0x13;
204                     logger.debug("{}: sent msg to switch {} off", nm(), dev.getAddress());
205                 }
206                 if (ext == 1 || ext == 2) {
207                     byte[] data = new byte[] { (byte) getIntParameter("d1", 0), (byte) getIntParameter("d2", 0),
208                             (byte) getIntParameter("d3", 0) };
209                     m = dev.makeExtendedMessage((byte) 0x0f, (byte) direc, (byte) level, data);
210                     logger.debug("{}: was an extended message for device {}", nm(), dev.getAddress());
211                     if (ext == 1) {
212                         m.setCRC();
213                     } else if (ext == 2) {
214                         m.setCRC2();
215                     }
216                 } else {
217                     m = dev.makeStandardMessage((byte) 0x0f, (byte) direc, (byte) level, getGroup(conf));
218                 }
219                 logger.debug("Sending message to {}", dev.getAddress());
220                 dev.enqueueMessage(m, feature);
221                 // expect to get a direct ack after this!
222             } catch (InvalidMessageTypeException e) {
223                 logger.warn("{}: invalid message: ", nm(), e);
224             } catch (FieldException e) {
225                 logger.warn("{}: command send message creation error ", nm(), e);
226             }
227         }
228     }
229
230     @NonNullByDefault
231     public static class FastOnOffCommandHandler extends CommandHandler {
232         FastOnOffCommandHandler(DeviceFeature f) {
233             super(f);
234         }
235
236         @Override
237         public void handleCommand(InsteonChannelConfiguration conf, Command cmd, InsteonDevice dev) {
238             try {
239                 if (cmd == OnOffType.ON) {
240                     int level = getMaxLightLevel(conf, 0xff);
241                     Msg m = dev.makeStandardMessage((byte) 0x0f, (byte) 0x12, (byte) level, getGroup(conf));
242                     dev.enqueueMessage(m, feature);
243                     logger.debug("{}: sent fast on to switch {} level {}", nm(), dev.getAddress(),
244                             level == 0xff ? "on" : level);
245                 } else if (cmd == OnOffType.OFF) {
246                     Msg m = dev.makeStandardMessage((byte) 0x0f, (byte) 0x14, (byte) 0x00, getGroup(conf));
247                     dev.enqueueMessage(m, feature);
248                     logger.debug("{}: sent fast off to switch {}", nm(), dev.getAddress());
249                 }
250                 // expect to get a direct ack after this!
251             } catch (InvalidMessageTypeException e) {
252                 logger.warn("{}: invalid message: ", nm(), e);
253             } catch (FieldException e) {
254                 logger.warn("{}: command send message creation error ", nm(), e);
255             }
256         }
257     }
258
259     @NonNullByDefault
260     public static class RampOnOffCommandHandler extends RampCommandHandler {
261         RampOnOffCommandHandler(DeviceFeature f) {
262             super(f);
263         }
264
265         @Override
266         public void handleCommand(InsteonChannelConfiguration conf, Command cmd, InsteonDevice dev) {
267             try {
268                 if (cmd == OnOffType.ON) {
269                     double ramptime = getRampTime(conf, 0);
270                     int ramplevel = getRampLevel(conf, 100);
271                     byte cmd2 = encode(ramptime, ramplevel);
272                     Msg m = dev.makeStandardMessage((byte) 0x0f, getOnCmd(), cmd2, getGroup(conf));
273                     dev.enqueueMessage(m, feature);
274                     logger.debug("{}: sent ramp on to switch {} time {} level {} cmd1 {}", nm(), dev.getAddress(),
275                             ramptime, ramplevel, getOnCmd());
276                 } else if (cmd == OnOffType.OFF) {
277                     double ramptime = getRampTime(conf, 0);
278                     int ramplevel = getRampLevel(conf, 0 /* ignored */);
279                     byte cmd2 = encode(ramptime, ramplevel);
280                     Msg m = dev.makeStandardMessage((byte) 0x0f, getOffCmd(), cmd2, getGroup(conf));
281                     dev.enqueueMessage(m, feature);
282                     logger.debug("{}: sent ramp off to switch {} time {} cmd1 {}", nm(), dev.getAddress(), ramptime,
283                             getOffCmd());
284                 }
285                 // expect to get a direct ack after this!
286             } catch (InvalidMessageTypeException e) {
287                 logger.warn("{}: invalid message: ", nm(), e);
288             } catch (FieldException e) {
289                 logger.warn("{}: command send message creation error ", nm(), e);
290             }
291         }
292
293         private int getRampLevel(InsteonChannelConfiguration conf, int defaultValue) {
294             String str = conf.getParameters().get("ramplevel");
295             return str != null ? Integer.parseInt(str) : defaultValue;
296         }
297     }
298
299     @NonNullByDefault
300     public static class ManualChangeCommandHandler extends CommandHandler {
301         ManualChangeCommandHandler(DeviceFeature f) {
302             super(f);
303         }
304
305         @Override
306         public void handleCommand(InsteonChannelConfiguration conf, Command cmd, InsteonDevice dev) {
307             try {
308                 if (cmd instanceof DecimalType) {
309                     int v = ((DecimalType) cmd).intValue();
310                     int cmd1 = (v != 1) ? 0x17 : 0x18; // start or stop
311                     int cmd2 = (v == 2) ? 0x01 : 0; // up or down
312                     Msg m = dev.makeStandardMessage((byte) 0x0f, (byte) cmd1, (byte) cmd2, getGroup(conf));
313                     dev.enqueueMessage(m, feature);
314                     logger.debug("{}: cmd {} sent manual change {} {} to {}", nm(), v,
315                             (cmd1 == 0x17) ? "START" : "STOP", (cmd2 == 0x01) ? "UP" : "DOWN", dev.getAddress());
316                 } else {
317                     logger.warn("{}: invalid command type: {}", nm(), cmd);
318                 }
319             } catch (InvalidMessageTypeException e) {
320                 logger.warn("{}: invalid message: ", nm(), e);
321             } catch (FieldException e) {
322                 logger.warn("{}: command send message creation error ", nm(), e);
323             }
324         }
325     }
326
327     /**
328      * Sends ALLLink broadcast commands to group
329      */
330     @NonNullByDefault
331     public static class GroupBroadcastCommandHandler extends CommandHandler {
332         GroupBroadcastCommandHandler(DeviceFeature f) {
333             super(f);
334         }
335
336         @Override
337         public void handleCommand(InsteonChannelConfiguration conf, Command cmd, InsteonDevice dev) {
338             try {
339                 if (cmd == OnOffType.ON || cmd == OnOffType.OFF) {
340                     byte cmd1 = (byte) ((cmd == OnOffType.ON) ? 0x11 : 0x13);
341                     byte value = (byte) ((cmd == OnOffType.ON) ? 0xFF : 0x00);
342                     int group = getGroup(conf);
343                     if (group == -1) {
344                         logger.warn("no group=xx specified in item {}", conf.getChannelName());
345                         return;
346                     }
347                     logger.debug("{}: sending {} broadcast to group {}", nm(), (cmd1 == 0x11) ? "ON" : "OFF",
348                             getGroup(conf));
349                     Msg m = dev.makeStandardMessage((byte) 0x0f, cmd1, value, group);
350                     dev.enqueueMessage(m, feature);
351                     feature.pollRelatedDevices();
352                 }
353             } catch (InvalidMessageTypeException e) {
354                 logger.warn("{}: invalid message: ", nm(), e);
355             } catch (FieldException e) {
356                 logger.warn("{}: command send message creation error ", nm(), e);
357             }
358         }
359     }
360
361     @NonNullByDefault
362     public static class LEDOnOffCommandHandler extends CommandHandler {
363         LEDOnOffCommandHandler(DeviceFeature f) {
364             super(f);
365         }
366
367         @Override
368         public void handleCommand(InsteonChannelConfiguration conf, Command cmd, InsteonDevice dev) {
369             try {
370                 if (cmd == OnOffType.ON) {
371                     Msg m = dev.makeExtendedMessage((byte) 0x1f, (byte) 0x20, (byte) 0x09,
372                             new byte[] { (byte) 0x00, (byte) 0x00, (byte) 0x00 });
373                     dev.enqueueMessage(m, feature);
374                     logger.debug("{}: sent msg to switch {} on", nm(), dev.getAddress());
375                 } else if (cmd == OnOffType.OFF) {
376                     Msg m = dev.makeExtendedMessage((byte) 0x1f, (byte) 0x20, (byte) 0x08,
377                             new byte[] { (byte) 0x00, (byte) 0x00, (byte) 0x00 });
378                     dev.enqueueMessage(m, feature);
379                     logger.debug("{}: sent msg to switch {} off", nm(), dev.getAddress());
380                 }
381             } catch (InvalidMessageTypeException e) {
382                 logger.warn("{}: invalid message: ", nm(), e);
383             } catch (FieldException e) {
384                 logger.warn("{}: command send message creation error ", nm(), e);
385             }
386         }
387     }
388
389     @NonNullByDefault
390     public static class X10OnOffCommandHandler extends CommandHandler {
391         X10OnOffCommandHandler(DeviceFeature f) {
392             super(f);
393         }
394
395         @Override
396         public void handleCommand(InsteonChannelConfiguration conf, Command cmd, InsteonDevice dev) {
397             try {
398                 byte houseCode = dev.getX10HouseCode();
399                 byte houseUnitCode = (byte) (houseCode << 4 | dev.getX10UnitCode());
400                 if (cmd == OnOffType.ON || cmd == OnOffType.OFF) {
401                     byte houseCommandCode = (byte) (houseCode << 4
402                             | (cmd == OnOffType.ON ? X10.Command.ON.code() : X10.Command.OFF.code()));
403                     Msg munit = dev.makeX10Message(houseUnitCode, (byte) 0x00); // send unit code
404                     dev.enqueueMessage(munit, feature);
405                     Msg mcmd = dev.makeX10Message(houseCommandCode, (byte) 0x80); // send command code
406                     dev.enqueueMessage(mcmd, feature);
407                     String onOff = cmd == OnOffType.ON ? "ON" : "OFF";
408                     logger.debug("{}: sent msg to switch {} {}", nm(), dev.getAddress(), onOff);
409                 }
410             } catch (InvalidMessageTypeException e) {
411                 logger.warn("{}: invalid message: ", nm(), e);
412             } catch (FieldException e) {
413                 logger.warn("{}: command send message creation error ", nm(), e);
414             }
415         }
416     }
417
418     @NonNullByDefault
419     public static class X10PercentCommandHandler extends CommandHandler {
420         X10PercentCommandHandler(DeviceFeature f) {
421             super(f);
422         }
423
424         @Override
425         public void handleCommand(InsteonChannelConfiguration conf, Command cmd, InsteonDevice dev) {
426             try {
427                 //
428                 // I did not have hardware that would respond to the PRESET_DIM codes.
429                 // This code path needs testing.
430                 //
431                 byte houseCode = dev.getX10HouseCode();
432                 byte houseUnitCode = (byte) (houseCode << 4 | dev.getX10UnitCode());
433                 Msg munit = dev.makeX10Message(houseUnitCode, (byte) 0x00); // send unit code
434                 dev.enqueueMessage(munit, feature);
435                 PercentType pc = (PercentType) cmd;
436                 logger.debug("{}: changing level of {} to {}", nm(), dev.getAddress(), pc.intValue());
437                 int level = (pc.intValue() * 32) / 100;
438                 byte cmdCode = (level >= 16) ? X10.Command.PRESET_DIM_2.code() : X10.Command.PRESET_DIM_1.code();
439                 level = level % 16;
440                 if (level <= 0) {
441                     level = 0;
442                 }
443                 houseCode = (byte) x10CodeForLevel[level];
444                 cmdCode |= (houseCode << 4);
445                 Msg mcmd = dev.makeX10Message(cmdCode, (byte) 0x80); // send command code
446                 dev.enqueueMessage(mcmd, feature);
447             } catch (InvalidMessageTypeException e) {
448                 logger.warn("{}: invalid message: ", nm(), e);
449             } catch (FieldException e) {
450                 logger.warn("{}: command send message creation error ", nm(), e);
451             }
452         }
453
454         private final int[] x10CodeForLevel = { 0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15 };
455     }
456
457     @NonNullByDefault
458     public static class X10IncreaseDecreaseCommandHandler extends CommandHandler {
459         X10IncreaseDecreaseCommandHandler(DeviceFeature f) {
460             super(f);
461         }
462
463         @Override
464         public void handleCommand(InsteonChannelConfiguration conf, Command cmd, InsteonDevice dev) {
465             try {
466                 byte houseCode = dev.getX10HouseCode();
467                 byte houseUnitCode = (byte) (houseCode << 4 | dev.getX10UnitCode());
468                 if (cmd == IncreaseDecreaseType.INCREASE || cmd == IncreaseDecreaseType.DECREASE) {
469                     byte houseCommandCode = (byte) (houseCode << 4
470                             | (cmd == IncreaseDecreaseType.INCREASE ? X10.Command.BRIGHT.code()
471                                     : X10.Command.DIM.code()));
472                     Msg munit = dev.makeX10Message(houseUnitCode, (byte) 0x00); // send unit code
473                     dev.enqueueMessage(munit, feature);
474                     Msg mcmd = dev.makeX10Message(houseCommandCode, (byte) 0x80); // send command code
475                     dev.enqueueMessage(mcmd, feature);
476                     String bd = cmd == IncreaseDecreaseType.INCREASE ? "BRIGHTEN" : "DIM";
477                     logger.debug("{}: sent msg to switch {} {}", nm(), dev.getAddress(), bd);
478                 }
479             } catch (InvalidMessageTypeException e) {
480                 logger.warn("{}: invalid message: ", nm(), e);
481             } catch (FieldException e) {
482                 logger.warn("{}: command send message creation error ", nm(), e);
483             }
484         }
485     }
486
487     @NonNullByDefault
488     public static class IOLincOnOffCommandHandler extends CommandHandler {
489         IOLincOnOffCommandHandler(DeviceFeature f) {
490             super(f);
491         }
492
493         @Override
494         public void handleCommand(InsteonChannelConfiguration conf, Command cmd, InsteonDevice dev) {
495             try {
496                 if (cmd == OnOffType.ON) {
497                     Msg m = dev.makeStandardMessage((byte) 0x0f, (byte) 0x11, (byte) 0xff);
498                     dev.enqueueMessage(m, feature);
499                     logger.debug("{}: sent msg to switch {} on", nm(), dev.getAddress());
500                 } else if (cmd == OnOffType.OFF) {
501                     Msg m = dev.makeStandardMessage((byte) 0x0f, (byte) 0x13, (byte) 0x00);
502                     dev.enqueueMessage(m, feature);
503                     logger.debug("{}: sent msg to switch {} off", nm(), dev.getAddress());
504                 }
505                 // This used to be configurable, but was made static to make
506                 // the architecture of the binding cleaner.
507                 int delay = 2000;
508                 delay = Math.max(1000, delay);
509                 delay = Math.min(10000, delay);
510                 Timer timer = new Timer();
511                 timer.schedule(new TimerTask() {
512                     @Override
513                     public void run() {
514                         Msg m = feature.makePollMsg();
515                         InsteonDevice dev = feature.getDevice();
516                         if (m != null) {
517                             dev.enqueueMessage(m, feature);
518                         }
519                     }
520                 }, delay);
521             } catch (InvalidMessageTypeException e) {
522                 logger.warn("{}: invalid message: ", nm(), e);
523             } catch (FieldException e) {
524                 logger.warn("{}: command send message creation error: ", nm(), e);
525             }
526         }
527     }
528
529     @NonNullByDefault
530     public static class IncreaseDecreaseCommandHandler extends CommandHandler {
531         IncreaseDecreaseCommandHandler(DeviceFeature f) {
532             super(f);
533         }
534
535         @Override
536         public void handleCommand(InsteonChannelConfiguration conf, Command cmd, InsteonDevice dev) {
537             try {
538                 if (cmd == IncreaseDecreaseType.INCREASE) {
539                     Msg m = dev.makeStandardMessage((byte) 0x0f, (byte) 0x15, (byte) 0x00);
540                     dev.enqueueMessage(m, feature);
541                     logger.debug("{}: sent msg to brighten {}", nm(), dev.getAddress());
542                 } else if (cmd == IncreaseDecreaseType.DECREASE) {
543                     Msg m = dev.makeStandardMessage((byte) 0x0f, (byte) 0x16, (byte) 0x00);
544                     dev.enqueueMessage(m, feature);
545                     logger.debug("{}: sent msg to dimm {}", nm(), dev.getAddress());
546                 }
547             } catch (InvalidMessageTypeException e) {
548                 logger.warn("{}: invalid message: ", nm(), e);
549             } catch (FieldException e) {
550                 logger.warn("{}: command send message creation error ", nm(), e);
551             }
552         }
553     }
554
555     @NonNullByDefault
556     public static class PercentHandler extends CommandHandler {
557         PercentHandler(DeviceFeature f) {
558             super(f);
559         }
560
561         @Override
562         public void handleCommand(InsteonChannelConfiguration conf, Command cmd, InsteonDevice dev) {
563             try {
564                 PercentType pc = (PercentType) cmd;
565                 logger.debug("changing level of {} to {}", dev.getAddress(), pc.intValue());
566                 int level = (int) Math.ceil((pc.intValue() * 255.0) / 100); // round up
567                 if (level > 0) { // make light on message with given level
568                     level = getMaxLightLevel(conf, level);
569                     Msg m = dev.makeStandardMessage((byte) 0x0f, (byte) 0x11, (byte) level);
570                     dev.enqueueMessage(m, feature);
571                     logger.debug("{}: sent msg to set {} to {}", nm(), dev.getAddress(), level);
572                 } else { // switch off
573                     Msg m = dev.makeStandardMessage((byte) 0x0f, (byte) 0x13, (byte) 0x00);
574                     dev.enqueueMessage(m, feature);
575                     logger.debug("{}: sent msg to set {} to zero by switching off", nm(), dev.getAddress());
576                 }
577             } catch (InvalidMessageTypeException e) {
578                 logger.warn("{}: invalid message: ", nm(), e);
579             } catch (FieldException e) {
580                 logger.warn("{}: command send message creation error ", nm(), e);
581             }
582         }
583     }
584
585     @NonNullByDefault
586     private abstract static class RampCommandHandler extends CommandHandler {
587         private static double[] halfRateRampTimes = new double[] { 0.1, 0.3, 2, 6.5, 19, 23.5, 28, 32, 38.5, 47, 90,
588                 150, 210, 270, 360, 480 };
589
590         private byte onCmd;
591         private byte offCmd;
592
593         RampCommandHandler(DeviceFeature f) {
594             super(f);
595             // Can't process parameters here because they are set after constructor is invoked.
596             // Unfortunately, this means we can't declare the onCmd, offCmd to be final.
597         }
598
599         @Override
600         void setParameters(Map<String, String> params) {
601             super.setParameters(params);
602             onCmd = (byte) getIntParameter("on", 0x2E);
603             offCmd = (byte) getIntParameter("off", 0x2F);
604         }
605
606         protected final byte getOnCmd() {
607             return onCmd;
608         }
609
610         protected final byte getOffCmd() {
611             return offCmd;
612         }
613
614         protected byte encode(double ramptimeSeconds, int ramplevel) throws FieldException {
615             if (ramplevel < 0 || ramplevel > 100) {
616                 throw new FieldException("ramplevel must be in the range 0-100 (inclusive)");
617             }
618
619             if (ramptimeSeconds < 0) {
620                 throw new FieldException("ramptime must be greater than 0");
621             }
622
623             int ramptime;
624             int insertionPoint = Arrays.binarySearch(halfRateRampTimes, ramptimeSeconds);
625             if (insertionPoint > 0) {
626                 ramptime = 15 - insertionPoint;
627             } else {
628                 insertionPoint = -insertionPoint - 1;
629                 if (insertionPoint == 0) {
630                     ramptime = 15;
631                 } else {
632                     double d1 = Math.abs(halfRateRampTimes[insertionPoint - 1] - ramptimeSeconds);
633                     double d2 = Math.abs(halfRateRampTimes[insertionPoint] - ramptimeSeconds);
634                     ramptime = 15 - (d1 > d2 ? insertionPoint : insertionPoint - 1);
635                     logger.debug("ramp encoding: time {} insert {} d1 {} d2 {} ramp {}", ramptimeSeconds,
636                             insertionPoint, d1, d2, ramptime);
637                 }
638             }
639
640             int r = (int) Math.round(ramplevel / (100.0 / 15.0));
641             return (byte) (((r & 0x0f) << 4) | (ramptime & 0xf));
642         }
643
644         protected double getRampTime(InsteonChannelConfiguration conf, double defaultValue) {
645             String str = conf.getParameters().get("ramptime");
646             return str != null ? Double.parseDouble(str) : defaultValue;
647         }
648     }
649
650     @NonNullByDefault
651     public static class RampPercentHandler extends RampCommandHandler {
652
653         RampPercentHandler(DeviceFeature f) {
654             super(f);
655         }
656
657         @Override
658         public void handleCommand(InsteonChannelConfiguration conf, Command cmd, InsteonDevice dev) {
659             try {
660                 PercentType pc = (PercentType) cmd;
661                 double ramptime = getRampTime(conf, 0);
662                 int level = pc.intValue();
663                 if (level > 0) { // make light on message with given level
664                     level = getMaxLightLevel(conf, level);
665                     byte cmd2 = encode(ramptime, level);
666                     Msg m = dev.makeStandardMessage((byte) 0x0f, getOnCmd(), cmd2);
667                     dev.enqueueMessage(m, feature);
668                     logger.debug("{}: sent msg to set {} to {} with {} second ramp time.", nm(), dev.getAddress(),
669                             level, ramptime);
670                 } else { // switch off
671                     Msg m = dev.makeStandardMessage((byte) 0x0f, getOffCmd(), (byte) 0x00);
672                     dev.enqueueMessage(m, feature);
673                     logger.debug("{}: sent msg to set {} to zero by switching off with {} ramp time.", nm(),
674                             dev.getAddress(), ramptime);
675                 }
676             } catch (InvalidMessageTypeException e) {
677                 logger.warn("{}: invalid message: ", nm(), e);
678             } catch (FieldException e) {
679                 logger.warn("{}: command send message creation error ", nm(), e);
680             }
681         }
682     }
683
684     @NonNullByDefault
685     public static class PowerMeterCommandHandler extends CommandHandler {
686         PowerMeterCommandHandler(DeviceFeature f) {
687             super(f);
688         }
689
690         @Override
691         public void handleCommand(InsteonChannelConfiguration conf, Command cmd, InsteonDevice dev) {
692             String cmdParam = conf.getParameters().get(InsteonDeviceHandler.CMD);
693             if (cmdParam == null) {
694                 logger.warn("{} ignoring cmd {} because no cmd= is configured!", nm(), cmd);
695                 return;
696             }
697             try {
698                 if (cmd == OnOffType.ON) {
699                     if (cmdParam.equals(InsteonDeviceHandler.CMD_RESET)) {
700                         Msg m = dev.makeStandardMessage((byte) 0x0f, (byte) 0x80, (byte) 0x00);
701                         dev.enqueueMessage(m, feature);
702                         logger.debug("{}: sent reset msg to power meter {}", nm(), dev.getAddress());
703                         feature.publish(OnOffType.OFF, StateChangeType.ALWAYS, InsteonDeviceHandler.CMD,
704                                 InsteonDeviceHandler.CMD_RESET);
705                     } else if (cmdParam.equals(InsteonDeviceHandler.CMD_UPDATE)) {
706                         Msg m = dev.makeStandardMessage((byte) 0x0f, (byte) 0x82, (byte) 0x00);
707                         dev.enqueueMessage(m, feature);
708                         logger.debug("{}: sent update msg to power meter {}", nm(), dev.getAddress());
709                         feature.publish(OnOffType.OFF, StateChangeType.ALWAYS, InsteonDeviceHandler.CMD,
710                                 InsteonDeviceHandler.CMD_UPDATE);
711                     } else {
712                         logger.warn("{}: ignoring unknown cmd {} for power meter {}", nm(), cmdParam, dev.getAddress());
713                     }
714                 } else if (cmd == OnOffType.OFF) {
715                     logger.debug("{}: ignoring off request for power meter {}", nm(), dev.getAddress());
716                 }
717             } catch (InvalidMessageTypeException e) {
718                 logger.warn("{}: invalid message: ", nm(), e);
719             } catch (FieldException e) {
720                 logger.warn("{}: command send message creation error ", nm(), e);
721             }
722         }
723     }
724
725     /**
726      * Command handler that sends a command with a numerical value to a device.
727      * The handler is very parameterizable so it can be reused for different devices.
728      * First used for setting thermostat parameters.
729      */
730
731     @NonNullByDefault
732     public static class NumberCommandHandler extends CommandHandler {
733         NumberCommandHandler(DeviceFeature f) {
734             super(f);
735         }
736
737         public int transform(int cmd) {
738             return (cmd);
739         }
740
741         @Override
742         public void handleCommand(InsteonChannelConfiguration conf, Command cmd, InsteonDevice dev) {
743             try {
744                 int dc = transform(((DecimalType) cmd).intValue());
745                 int intFactor = getIntParameter("factor", 1);
746                 //
747                 // determine what level should be, and what field it should be in
748                 //
749                 int ilevel = dc * intFactor;
750                 byte level = (byte) (ilevel > 255 ? 0xFF : ((ilevel < 0) ? 0 : ilevel));
751                 String vfield = getStringParameter("value", "");
752                 if (vfield == "") {
753                     logger.warn("{} has no value field specified", nm());
754                 }
755                 //
756                 // figure out what cmd1, cmd2, d1, d2, d3 are supposed to be
757                 // to form a proper message
758                 //
759                 int cmd1 = getIntParameter("cmd1", -1);
760                 if (cmd1 < 0) {
761                     logger.warn("{} has no cmd1 specified!", nm());
762                     return;
763                 }
764                 int cmd2 = getIntParameter("cmd2", 0);
765                 int ext = getIntParameter("ext", 0);
766                 Msg m = null;
767                 if (ext == 1 || ext == 2) {
768                     byte[] data = new byte[] { (byte) getIntParameter("d1", 0), (byte) getIntParameter("d2", 0),
769                             (byte) getIntParameter("d3", 0) };
770                     m = dev.makeExtendedMessage((byte) 0x0f, (byte) cmd1, (byte) cmd2, data);
771                     m.setByte(vfield, level);
772                     if (ext == 1) {
773                         m.setCRC();
774                     } else if (ext == 2) {
775                         m.setCRC2();
776                     }
777                 } else {
778                     m = dev.makeStandardMessage((byte) 0x0f, (byte) cmd1, (byte) cmd2);
779                     m.setByte(vfield, level);
780                 }
781                 dev.enqueueMessage(m, feature);
782                 logger.debug("{}: sent msg to change level to {}", nm(), ((DecimalType) cmd).intValue());
783                 m = null;
784             } catch (InvalidMessageTypeException e) {
785                 logger.warn("{}: invalid message: ", nm(), e);
786             } catch (FieldException e) {
787                 logger.warn("{}: command send message creation error ", nm(), e);
788             }
789         }
790     }
791
792     /**
793      * Handler to set the thermostat system mode
794      */
795     @NonNullByDefault
796     public static class ThermostatSystemModeCommandHandler extends NumberCommandHandler {
797         ThermostatSystemModeCommandHandler(DeviceFeature f) {
798             super(f);
799         }
800
801         @Override
802         public int transform(int cmd) {
803             switch (cmd) {
804                 case 0:
805                     return (0x09); // off
806                 case 1:
807                     return (0x04); // heat
808                 case 2:
809                     return (0x05); // cool
810                 case 3:
811                     return (0x06); // auto (aka manual auto)
812                 case 4:
813                     return (0x0A); // program (aka auto)
814                 default:
815                     break;
816             }
817             return (0x0A); // when in doubt go to program
818         }
819     }
820
821     /**
822      * Handler to set the thermostat fan mode
823      */
824     @NonNullByDefault
825     public static class ThermostatFanModeCommandHandler extends NumberCommandHandler {
826         ThermostatFanModeCommandHandler(DeviceFeature f) {
827             super(f);
828         }
829
830         @Override
831         public int transform(int cmd) {
832             switch (cmd) {
833                 case 0:
834                     return (0x08); // fan mode auto
835                 case 1:
836                     return (0x07); // fan always on
837                 default:
838                     break;
839             }
840             return (0x08); // when in doubt go auto mode
841         }
842     }
843
844     /**
845      * Handler to set the fanlinc fan mode
846      */
847     @NonNullByDefault
848     public static class FanLincFanCommandHandler extends NumberCommandHandler {
849         FanLincFanCommandHandler(DeviceFeature f) {
850             super(f);
851         }
852
853         @Override
854         public int transform(int cmd) {
855             switch (cmd) {
856                 case 0:
857                     return (0x00); // fan off
858                 case 1:
859                     return (0x55); // fan low
860                 case 2:
861                     return (0xAA); // fan medium
862                 case 3:
863                     return (0xFF); // fan high
864                 default:
865                     break;
866             }
867             return (0x00); // all other modes are "off"
868         }
869     }
870
871     /**
872      * Factory method for creating handlers of a given name using java reflection
873      *
874      * @param name the name of the handler to create
875      * @param params
876      * @param f the feature for which to create the handler
877      * @return the handler which was created
878      */
879     @Nullable
880     public static <T extends CommandHandler> T makeHandler(String name, Map<String, String> params, DeviceFeature f) {
881         String cname = CommandHandler.class.getName() + "$" + name;
882         try {
883             Class<?> c = Class.forName(cname);
884             @SuppressWarnings("unchecked")
885             Class<? extends T> dc = (Class<? extends T>) c;
886             T ch = dc.getDeclaredConstructor(DeviceFeature.class).newInstance(f);
887             ch.setParameters(params);
888             return ch;
889         } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | IllegalArgumentException
890                 | InvocationTargetException | NoSuchMethodException | SecurityException e) {
891             logger.warn("error trying to create message handler: {}", name, e);
892         }
893         return null;
894     }
895 }