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