2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.insteon.internal.device;
15 import java.lang.reflect.InvocationTargetException;
16 import java.math.BigDecimal;
17 import java.math.RoundingMode;
18 import java.util.HashMap;
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;
45 * A message handler processes incoming Insteon messages and reacts by publishing
46 * corresponding messages on the openhab bus, updating device state etc.
48 * @author Daniel Pfrommer - Initial contribution
49 * @author Bernd Pfrommer - openHAB 1 insteonplm binding
50 * @author Rob Nielsen - Port to openHAB 2 insteon binding
53 @SuppressWarnings("null")
54 public abstract class MessageHandler {
55 private static final Logger logger = LoggerFactory.getLogger(MessageHandler.class);
57 protected DeviceFeature feature;
58 protected Map<String, @Nullable String> parameters = new HashMap<>();
63 * @param p state publishing object for dissemination of state changes
65 MessageHandler(DeviceFeature p) {
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.
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
79 public abstract void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature feature);
82 * Method to send an extended insteon message for querying a device
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
88 public void sendExtendedQuery(DeviceFeature f, byte aCmd1, byte aCmd2) {
89 InsteonDevice d = f.getDevice();
91 Msg m = d.makeExtendedMessage((byte) 0x1f, aCmd1, aCmd2);
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());
102 * Check if group matches
104 * @param group group to test for
105 * @return true if group matches or no group is specified
107 public boolean matchesGroup(int group) {
108 int g = getIntParameter("group", -1);
109 return (g == -1 || g == group);
113 * Retrieve group parameter or -1 if no group is specified
115 * @return group parameter
117 public int getGroup() {
118 return (getIntParameter("group", -1));
122 * Helper function to get an integer parameter for the handler
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)
128 protected int getIntParameter(String key, int def) {
129 String val = parameters.get(key);
131 return (def); // param not found
135 ret = Utils.strToInt(val);
136 } catch (NumberFormatException e) {
137 logger.warn("malformed int parameter in message handler: {}", key);
143 * Helper function to get a String parameter for the handler
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)
149 protected @Nullable String getStringParameter(String key, @Nullable String def) {
150 return (parameters.get(key) == null ? def : parameters.get(key));
154 * Helper function to get a double parameter for the handler
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)
160 protected double getDoubleParameter(String key, double def) {
162 if (parameters.get(key) != null) {
163 return Double.parseDouble(parameters.get(key));
165 } catch (NumberFormatException e) {
166 logger.warn("malformed int parameter in message handler: {}", key);
171 protected boolean getBooleanDeviceConfig(String key, boolean def) {
172 Object o = feature.getDevice().getDeviceConfigMap().get(key);
174 if (o instanceof Boolean) {
177 logger.warn("{} {}: The value for the '{}' key is not boolean in the device configuration parameter.",
178 nm(), feature.getDevice().getAddress(), key);
186 * Test if message refers to the button configured for given feature
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
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) {
201 int button = getButtonInfo(msg, f);
202 return button != -1 && myButton == button;
206 * Test if parameter matches value
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
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!
220 byte value = msg.getByte(field);
221 return (value == mp);
225 * Test if message matches the filter parameters
227 * @param msg message to be tested against
228 * @return true if message matches
230 public boolean matches(Msg msg) {
232 int ext = getIntParameter("ext", -1);
234 if ((msg.isExtended() && ext != 1) || (!msg.isExtended() && ext != 0)) {
237 if (!testMatch("match_cmd1", msg, "command1")) {
241 if (!testMatch("match_cmd2", msg, "command2")) {
244 if (!testMatch("match_d1", msg, "userData1")) {
247 if (!testMatch("match_d2", msg, "userData2")) {
250 if (!testMatch("match_d3", msg, "userData3")) {
253 } catch (FieldException e) {
254 logger.warn("error matching message: {}", msg, e);
261 * Determines is an incoming ALL LINK message is a duplicate
263 * @param msg the received ALL LINK message
264 * @return true if this message is a duplicate
266 protected boolean isDuplicate(Msg msg) {
267 boolean isDuplicate = false;
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);
284 } catch (IllegalArgumentException e) {
285 logger.warn("cannot parse msg: {}", msg, e);
286 } catch (FieldException e) {
287 logger.warn("cannot parse msg: {}", msg, e);
289 return (isDuplicate);
293 * Extract button information from message
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
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
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);
308 } catch (FieldException e) {
309 logger.warn("field exception while parsing msg {}: ", msg, e);
315 * Shorthand to return class name for logging purposes
317 * @return name of the class
319 protected String nm() {
320 return (this.getClass().getSimpleName());
326 * @param map the parameter map for this message handler
328 public void setParameters(Map<String, @Nullable String> map) {
334 // ---------------- the various command handler start here -------------------
339 public static class DefaultMsgHandler extends MessageHandler {
340 DefaultMsgHandler(DeviceFeature p) {
345 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
346 logger.debug("{} ignoring unimpl message with cmd1:{}", nm(), Utils.getHexByte(cmd1));
351 public static class NoOpMsgHandler extends MessageHandler {
352 NoOpMsgHandler(DeviceFeature p) {
357 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
358 logger.trace("{} ignore msg {}: {}", nm(), Utils.getHexByte(cmd1), msg);
363 public static class LightOnDimmerHandler extends MessageHandler {
364 LightOnDimmerHandler(DeviceFeature p) {
369 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
370 if (!isMybutton(msg, f)) {
373 InsteonAddress a = f.getDevice().getAddress();
374 if (msg.isAckOfDirect()) {
375 logger.warn("{}: device {}: ignoring ack of direct.", nm(), a);
377 String mode = getStringParameter("mode", "REGULAR");
378 logger.debug("{}: device {} was turned on {}. " + "Sending poll request to get actual level", nm(), a,
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();
386 f.getDevice().enqueueDelayedMessage(m, f, 1000);
393 public static class LightOffDimmerHandler extends MessageHandler {
394 LightOffDimmerHandler(DeviceFeature p) {
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);
409 public static class LightOnSwitchHandler extends MessageHandler {
410 LightOnSwitchHandler(DeviceFeature p) {
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);
421 logger.debug("ignored message: {}", isMybutton(msg, f));
427 public static class LightOffSwitchHandler extends MessageHandler {
428 LightOffSwitchHandler(DeviceFeature p) {
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);
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
450 public static class RampDimmerHandler extends MessageHandler {
454 RampDimmerHandler(DeviceFeature 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.
461 public void setParameters(Map<String, @Nullable String> params) {
462 super.setParameters(params);
463 onCmd = getIntParameter("on", 0x2E);
464 offCmd = getIntParameter("off", 0x2F);
468 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
470 int level = getLevel(msg);
471 logger.debug("{}: device {} was switched on using ramp to level {}.", nm(), f.getDevice().getAddress(),
474 f.publish(OnOffType.ON, StateChangeType.ALWAYS);
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
481 // it would be an odd scenario to turn ON a light at level
483 // rather than turn if OFF.
484 f.publish(new PercentType(level), StateChangeType.ALWAYS);
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);
492 private int getLevel(Msg msg) {
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);
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
510 public static class SwitchRequestReplyHandler extends MessageHandler {
511 SwitchRequestReplyHandler(DeviceFeature p) {
516 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
518 InsteonAddress a = f.getDevice().getAddress();
519 int cmd2 = msg.getByte("command2") & 0xff;
520 int button = this.getIntParameter("button", -1);
522 handleNoButtons(cmd2, a, msg);
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);
528 } catch (FieldException e) {
529 logger.warn("{} error parsing {}: ", nm(), msg, e);
534 * Handle the case where no buttons have been configured.
535 * In this situation, the only return values should be 0 (light off)
540 void handleNoButtons(int cmd2, InsteonAddress a, Msg msg) {
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);
548 logger.warn("{}: {} ignoring unexpected cmd2 in msg: {}", nm(), a, msg);
553 * Test if cmd byte indicates that button is lit.
554 * The cmd byte has the LED status bitwise from the left:
556 * Note that the 2487S has buttons assigned like this:
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
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
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);
574 * Handles Dimmer replies to status requests.
575 * In the dimmers case the command2 byte represents the light level from 0-255
578 public static class DimmerRequestReplyHandler extends MessageHandler {
579 DimmerRequestReplyHandler(DeviceFeature p) {
584 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
585 InsteonDevice dev = f.getDevice();
587 int cmd2 = msg.getByte("command2") & 0xff;
589 // sometimes dimmer devices are returning 0xfe when on instead of 0xff
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);
600 int level = cmd2 * 100 / 255;
604 logger.debug("{}: set device {} to level {}", nm(), dev.getAddress(), level);
605 feature.publish(new PercentType(level), StateChangeType.CHANGED);
607 } catch (FieldException e) {
608 logger.warn("{}: error parsing {}: ", nm(), msg, e);
614 public static class DimmerStopManualChangeHandler extends MessageHandler {
615 DimmerStopManualChangeHandler(DeviceFeature p) {
620 public boolean isDuplicate(Msg msg) {
621 // Disable duplicate elimination because
622 // there are no cleanup or success messages for start/stop.
627 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
628 Msg m = f.makePollMsg();
630 f.getDevice().enqueueMessage(m, f);
636 public static class StartManualChangeHandler extends MessageHandler {
637 StartManualChangeHandler(DeviceFeature p) {
642 public boolean isDuplicate(Msg msg) {
643 // Disable duplicate elimination because
644 // there are no cleanup or success messages for start/stop.
649 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
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);
663 public static class StopManualChangeHandler extends MessageHandler {
664 StopManualChangeHandler(DeviceFeature p) {
669 public boolean isDuplicate(Msg msg) {
670 // Disable duplicate elimination because
671 // there are no cleanup or success messages for start/stop.
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);
683 public static class InfoRequestReplyHandler extends MessageHandler {
684 InfoRequestReplyHandler(DeviceFeature p) {
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);
696 int cmd2 = msg.getByte("command2") & 0xff;
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));
705 case 0x02: // this is a device text string response message
706 logger.debug("{} {} got text str {} ", nm(), dev.getAddress(), msg);
709 logger.warn("{} unknown cmd2 = {} in info reply message {}", nm(), cmd2, msg);
712 } catch (FieldException e) {
713 logger.warn("error parsing {}: ", msg, e);
719 public static class MotionSensorDataReplyHandler extends MessageHandler {
720 MotionSensorDataReplyHandler(DeviceFeature p) {
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);
732 int cmd2 = msg.getByte("command2") & 0xff;
735 int temperatureLevel;
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);
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);
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;
767 batteryPercentage = (batteryLevel - 0x70) * 100 / (0xd2 - 0x70);
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);
775 logger.warn("unknown cmd2 = {} in info reply message {}", cmd2, msg);
778 } catch (FieldException e) {
779 logger.warn("error parsing {}: ", msg, e);
785 public static class MotionSensor2AlternateHeartbeatHandler extends MessageHandler {
786 MotionSensor2AlternateHeartbeatHandler(DeviceFeature p) {
791 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
792 InsteonDevice dev = f.getDevice();
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;
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);
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;
816 batteryPercentage = (batteryLevel - 0x70) * 100 / (0xd2 - 0x70);
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);
828 public static class HiddenDoorSensorDataReplyHandler extends MessageHandler {
829 HiddenDoorSensorDataReplyHandler(DeviceFeature p) {
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);
841 int cmd2 = msg.getByte("command2") & 0xff;
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);
854 logger.warn("unknown cmd2 = {} in info reply message {}", cmd2, msg);
857 } catch (FieldException e) {
858 logger.warn("error parsing {}: ", msg, e);
864 public static class PowerMeterUpdateHandler extends MessageHandler {
865 PowerMeterUpdateHandler(DeviceFeature p) {
870 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
871 if (msg.isExtended()) {
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;
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;
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);
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);
904 public static class PowerMeterResetHandler extends MessageHandler {
905 PowerMeterResetHandler(DeviceFeature p) {
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());
914 // poll device to get updated kilowatt hours and watts
915 Msg m = f.makePollMsg();
917 f.getDevice().enqueueMessage(m, f);
923 public static class LastTimeHandler extends MessageHandler {
924 LastTimeHandler(DeviceFeature p) {
929 public void handleMessage(int group, byte cmd1a, Msg msg, DeviceFeature f) {
930 feature.publish(new DateTimeType(), StateChangeType.ALWAYS);
935 public static class ContactRequestReplyHandler extends MessageHandler {
936 ContactRequestReplyHandler(DeviceFeature p) {
941 public void handleMessage(int group, byte cmd1a, Msg msg, DeviceFeature f) {
945 cmd = msg.getByte("Cmd");
946 cmd2 = msg.getByte("command2");
947 } catch (FieldException e) {
948 logger.debug("{} no cmd found, dropping msg {}", nm(), msg);
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);
960 public static class ClosedContactHandler extends MessageHandler {
961 ClosedContactHandler(DeviceFeature p) {
966 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
967 feature.publish(OpenClosedType.CLOSED, StateChangeType.ALWAYS);
972 public static class OpenedContactHandler extends MessageHandler {
973 OpenedContactHandler(DeviceFeature p) {
978 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
979 feature.publish(OpenClosedType.OPEN, StateChangeType.ALWAYS);
984 public static class OpenedOrClosedContactHandler extends MessageHandler {
985 OpenedOrClosedContactHandler(DeviceFeature p) {
990 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
992 byte cmd2 = msg.getByte("command2");
997 feature.publish(OpenClosedType.CLOSED, StateChangeType.CHANGED);
1001 feature.publish(OpenClosedType.OPEN, StateChangeType.CHANGED);
1003 default: // do nothing
1010 feature.publish(OpenClosedType.CLOSED, StateChangeType.CHANGED);
1012 default: // do nothing
1017 } catch (FieldException e) {
1018 logger.debug("{} no cmd2 found, dropping msg {}", nm(), msg);
1025 public static class ClosedSleepingContactHandler extends MessageHandler {
1026 ClosedSleepingContactHandler(DeviceFeature p) {
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);
1038 sendExtendedQuery(f, (byte) 0x2e, (byte) 00);
1044 public static class OpenedSleepingContactHandler extends MessageHandler {
1045 OpenedSleepingContactHandler(DeviceFeature p) {
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);
1057 sendExtendedQuery(f, (byte) 0x2e, (byte) 00);
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.
1071 public static class TriggerPollMsgHandler extends MessageHandler {
1072 TriggerPollMsgHandler(DeviceFeature p) {
1077 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
1078 feature.getDevice().doPoll(2000); // 2000 ms delay
1083 * Flexible handler to extract numerical data from messages.
1086 public static class NumberMsgHandler extends MessageHandler {
1087 NumberMsgHandler(DeviceFeature p) {
1092 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
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);
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);
1111 state = new DecimalType(dvalue);
1113 feature.publish(state, StateChangeType.CHANGED);
1114 } catch (FieldException e) {
1115 logger.warn("error parsing {}: ", msg, e);
1119 public int transform(int raw) {
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());
1130 if (lowByte.equals("group")) {
1133 value = msg.getByte(lowByte) & 0xFF;
1135 String highByte = getStringParameter("high_byte", "");
1136 if (!highByte.equals("")) {
1137 value |= (msg.getByte(highByte) & 0xFF) << 8;
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
1148 public static class ThermostatSystemModeMsgHandler extends NumberMsgHandler {
1149 ThermostatSystemModeMsgHandler(DeviceFeature p) {
1154 public int transform(int raw) {
1165 return (4); // program
1169 return (4); // when in doubt assume to be in "program" mode
1174 * Handle reply to system mode change command
1177 public static class ThermostatSystemModeReplyHandler extends NumberMsgHandler {
1178 ThermostatSystemModeReplyHandler(DeviceFeature p) {
1183 public int transform(int raw) {
1194 return (4); // program
1198 return (4); // when in doubt assume to be in "program" mode
1203 * Handle reply to fan mode change command
1206 public static class ThermostatFanModeReplyHandler extends NumberMsgHandler {
1207 ThermostatFanModeReplyHandler(DeviceFeature p) {
1212 public int transform(int raw) {
1217 return (1); // always on
1221 return (0); // when in doubt assume to be auto mode
1226 * Handle reply to fanlinc fan speed change command
1229 public static class FanLincFanReplyHandler extends NumberMsgHandler {
1230 FanLincFanReplyHandler(DeviceFeature p) {
1235 public int transform(int raw) {
1242 return (2); // medium
1246 logger.warn("fanlinc got unexpected level: {}", raw);
1248 return (0); // when in doubt assume to be off
1253 * Process X10 messages that are generated when another controller
1254 * changes the state of an X10 device.
1257 public static class X10OnHandler extends MessageHandler {
1258 X10OnHandler(DeviceFeature p) {
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);
1271 public static class X10OffHandler extends MessageHandler {
1272 X10OffHandler(DeviceFeature p) {
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);
1285 public static class X10BrightHandler extends MessageHandler {
1286 X10BrightHandler(DeviceFeature p) {
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);
1298 public static class X10DimHandler extends MessageHandler {
1299 X10DimHandler(DeviceFeature p) {
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);
1311 public static class X10OpenHandler extends MessageHandler {
1312 X10OpenHandler(DeviceFeature p) {
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);
1325 public static class X10ClosedHandler extends MessageHandler {
1326 X10ClosedHandler(DeviceFeature p) {
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);
1339 * Factory method for creating handlers of a given name using java reflection
1341 * @param name the name of the handler to create
1343 * @param f the feature for which to create the handler
1344 * @return the handler which was created
1346 public static @Nullable <T extends MessageHandler> T makeHandler(String name, Map<String, @Nullable String> params,
1348 String cname = MessageHandler.class.getName() + "$" + name;
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);
1356 } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | IllegalArgumentException
1357 | InvocationTargetException | NoSuchMethodException | SecurityException e) {
1358 logger.warn("error trying to create message handler: {}", name, e);