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 String str = parameters.get(key);
163 return str != null ? Double.parseDouble(str) : def;
164 } catch (NumberFormatException e) {
165 logger.warn("malformed int parameter in message handler: {}", key);
170 protected boolean getBooleanDeviceConfig(String key, boolean def) {
171 Object o = feature.getDevice().getDeviceConfigMap().get(key);
173 if (o instanceof Boolean) {
176 logger.warn("{} {}: The value for the '{}' key is not boolean in the device configuration parameter.",
177 nm(), feature.getDevice().getAddress(), key);
185 * Test if message refers to the button configured for given feature
187 * @param msg received message
188 * @param f device feature to test
189 * @return true if we have no button configured or the message is for this button
191 protected boolean isMybutton(Msg msg, DeviceFeature f) {
192 int myButton = getIntParameter("button", -1);
193 // if there is no button configured for this handler
194 // the message is assumed to refer to this feature
195 // no matter what button is addressed in the message
196 if (myButton == -1) {
200 int button = getButtonInfo(msg, f);
201 return button != -1 && myButton == button;
205 * Test if parameter matches value
207 * @param param name of parameter to match
208 * @param msg message to search
209 * @param field field name to match
210 * @return true if parameter matches
211 * @throws FieldException if field not there
213 protected boolean testMatch(String param, Msg msg, String field) throws FieldException {
214 int mp = getIntParameter(param, -1);
215 // parameter not filtered for, declare this a match!
219 byte value = msg.getByte(field);
220 return (value == mp);
224 * Test if message matches the filter parameters
226 * @param msg message to be tested against
227 * @return true if message matches
229 public boolean matches(Msg msg) {
231 int ext = getIntParameter("ext", -1);
233 if ((msg.isExtended() && ext != 1) || (!msg.isExtended() && ext != 0)) {
236 if (!testMatch("match_cmd1", msg, "command1")) {
240 if (!testMatch("match_cmd2", msg, "command2")) {
243 if (!testMatch("match_d1", msg, "userData1")) {
246 if (!testMatch("match_d2", msg, "userData2")) {
249 if (!testMatch("match_d3", msg, "userData3")) {
252 } catch (FieldException e) {
253 logger.warn("error matching message: {}", msg, e);
260 * Determines is an incoming ALL LINK message is a duplicate
262 * @param msg the received ALL LINK message
263 * @return true if this message is a duplicate
265 protected boolean isDuplicate(Msg msg) {
266 boolean isDuplicate = false;
268 MsgType t = MsgType.fromValue(msg.getByte("messageFlags"));
269 if (t == MsgType.ALL_LINK_BROADCAST) {
270 int group = msg.getAddress("toAddress").getLowByte() & 0xff;
271 byte cmd1 = msg.getByte("command1");
272 // if the command is 0x06, then it's success message
273 // from the original broadcaster, with which the device
274 // confirms that it got all cleanup replies successfully.
275 GroupMessage gm = (cmd1 == 0x06) ? GroupMessage.SUCCESS : GroupMessage.BCAST;
276 isDuplicate = !feature.getDevice().getGroupState(group, gm, cmd1);
277 } else if (t == MsgType.ALL_LINK_CLEANUP) {
278 // the cleanup messages are direct messages, so the
279 // group # is not in the toAddress, but in cmd2
280 int group = msg.getByte("command2") & 0xff;
281 isDuplicate = !feature.getDevice().getGroupState(group, GroupMessage.CLEAN, (byte) 0);
283 } catch (IllegalArgumentException e) {
284 logger.warn("cannot parse msg: {}", msg, e);
285 } catch (FieldException e) {
286 logger.warn("cannot parse msg: {}", msg, e);
288 return (isDuplicate);
292 * Extract button information from message
294 * @param msg the message to extract from
295 * @param the device feature (needed for debug printing)
296 * @return the button number or -1 if no button found
298 protected static int getButtonInfo(Msg msg, DeviceFeature f) {
299 // the cleanup messages have the button number in the command2 field
300 // the broadcast messages have it as the lsb of the toAddress
302 int bclean = msg.getByte("command2") & 0xff;
303 int bbcast = msg.getAddress("toAddress").getLowByte() & 0xff;
304 int button = msg.isCleanup() ? bclean : bbcast;
305 logger.trace("{} button: {} bclean: {} bbcast: {}", f.getDevice().getAddress(), button, bclean, bbcast);
307 } catch (FieldException e) {
308 logger.warn("field exception while parsing msg {}: ", msg, e);
314 * Shorthand to return class name for logging purposes
316 * @return name of the class
318 protected String nm() {
319 return (this.getClass().getSimpleName());
325 * @param map the parameter map for this message handler
327 public void setParameters(Map<String, @Nullable String> map) {
333 // ---------------- the various command handler start here -------------------
338 public static class DefaultMsgHandler extends MessageHandler {
339 DefaultMsgHandler(DeviceFeature p) {
344 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
345 logger.debug("{} ignoring unimpl message with cmd1:{}", nm(), Utils.getHexByte(cmd1));
350 public static class NoOpMsgHandler extends MessageHandler {
351 NoOpMsgHandler(DeviceFeature p) {
356 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
357 logger.trace("{} ignore msg {}: {}", nm(), Utils.getHexByte(cmd1), msg);
362 public static class LightOnDimmerHandler extends MessageHandler {
363 LightOnDimmerHandler(DeviceFeature p) {
368 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
369 if (!isMybutton(msg, f)) {
372 InsteonAddress a = f.getDevice().getAddress();
373 if (msg.isAckOfDirect()) {
374 logger.warn("{}: device {}: ignoring ack of direct.", nm(), a);
376 String mode = getStringParameter("mode", "REGULAR");
377 logger.debug("{}: device {} was turned on {}. " + "Sending poll request to get actual level", nm(), a,
379 feature.publish(PercentType.HUNDRED, StateChangeType.ALWAYS);
380 // need to poll to find out what level the dimmer is at now.
381 // it may not be at 100% because dimmers can be configured
382 // to switch to e.g. 75% when turned on.
383 Msg m = f.makePollMsg();
385 f.getDevice().enqueueDelayedMessage(m, f, 1000);
392 public static class LightOffDimmerHandler extends MessageHandler {
393 LightOffDimmerHandler(DeviceFeature p) {
398 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
399 if (isMybutton(msg, f)) {
400 String mode = getStringParameter("mode", "REGULAR");
401 logger.debug("{}: device {} was turned off {}.", nm(), f.getDevice().getAddress(), mode);
402 f.publish(PercentType.ZERO, StateChangeType.ALWAYS);
408 public static class LightOnSwitchHandler extends MessageHandler {
409 LightOnSwitchHandler(DeviceFeature p) {
414 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
415 if (isMybutton(msg, f)) {
416 String mode = getStringParameter("mode", "REGULAR");
417 logger.debug("{}: device {} was switched on {}.", nm(), f.getDevice().getAddress(), mode);
418 f.publish(OnOffType.ON, StateChangeType.ALWAYS);
420 logger.debug("ignored message: {}", isMybutton(msg, f));
426 public static class LightOffSwitchHandler extends MessageHandler {
427 LightOffSwitchHandler(DeviceFeature p) {
432 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
433 if (isMybutton(msg, f)) {
434 String mode = getStringParameter("mode", "REGULAR");
435 logger.debug("{}: device {} was switched off {}.", nm(), f.getDevice().getAddress(), mode);
436 f.publish(OnOffType.OFF, StateChangeType.ALWAYS);
442 * This message handler processes replies to Ramp ON/OFF commands.
443 * Currently, it's been tested for the 2672-222 LED Bulb. Other
444 * devices may use a different pair of commands (0x2E, 0x2F). This
445 * handler and the command handler will need to be extended to support
449 public static class RampDimmerHandler extends MessageHandler {
453 RampDimmerHandler(DeviceFeature p) {
455 // Can't process parameters here because they are set after constructor is invoked.
456 // Unfortunately, this means we can't declare the onCmd, offCmd to be final.
460 public void setParameters(Map<String, @Nullable String> params) {
461 super.setParameters(params);
462 onCmd = getIntParameter("on", 0x2E);
463 offCmd = getIntParameter("off", 0x2F);
467 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
469 int level = getLevel(msg);
470 logger.debug("{}: device {} was switched on using ramp to level {}.", nm(), f.getDevice().getAddress(),
473 f.publish(OnOffType.ON, StateChangeType.ALWAYS);
475 // The publisher will convert an ON at level==0 to an OFF.
476 // However, this is not completely accurate since a ramp
477 // off at level == 0 may not turn off the dimmer completely
478 // (if I understand the Insteon docs correctly). In any
480 // it would be an odd scenario to turn ON a light at level
482 // rather than turn if OFF.
483 f.publish(new PercentType(level), StateChangeType.ALWAYS);
485 } else if (cmd1 == offCmd) {
486 logger.debug("{}: device {} was switched off using ramp.", nm(), f.getDevice().getAddress());
487 f.publish(new PercentType(0), StateChangeType.ALWAYS);
491 private int getLevel(Msg msg) {
493 byte cmd2 = msg.getByte("command2");
494 return (int) Math.round(((cmd2 >> 4) & 0x0f) * (100 / 15d));
495 } catch (FieldException e) {
496 logger.warn("Can't access command2 byte", e);
503 * A message handler that processes replies to queries.
504 * If command2 == 0xFF then the light has been turned on
505 * else if command2 == 0x00 then the light has been turned off
509 public static class SwitchRequestReplyHandler extends MessageHandler {
510 SwitchRequestReplyHandler(DeviceFeature p) {
515 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
517 InsteonAddress a = f.getDevice().getAddress();
518 int cmd2 = msg.getByte("command2") & 0xff;
519 int button = this.getIntParameter("button", -1);
521 handleNoButtons(cmd2, a, msg);
523 boolean isOn = isLEDLit(cmd2, button);
524 logger.debug("{}: dev {} button {} switched to {}", nm(), a, button, isOn ? "ON" : "OFF");
525 feature.publish(isOn ? OnOffType.ON : OnOffType.OFF, StateChangeType.CHANGED);
527 } catch (FieldException e) {
528 logger.warn("{} error parsing {}: ", nm(), msg, e);
533 * Handle the case where no buttons have been configured.
534 * In this situation, the only return values should be 0 (light off)
539 void handleNoButtons(int cmd2, InsteonAddress a, Msg msg) {
541 logger.debug("{}: set device {} to OFF", nm(), a);
542 feature.publish(OnOffType.OFF, StateChangeType.CHANGED);
543 } else if (cmd2 == 0xff) {
544 logger.debug("{}: set device {} to ON", nm(), a);
545 feature.publish(OnOffType.ON, StateChangeType.CHANGED);
547 logger.warn("{}: {} ignoring unexpected cmd2 in msg: {}", nm(), a, msg);
552 * Test if cmd byte indicates that button is lit.
553 * The cmd byte has the LED status bitwise from the left:
555 * Note that the 2487S has buttons assigned like this:
557 * They used the basis of the 8-button remote, and assigned
558 * the ON button to 1+2, the OFF button to 7+8
560 * @param cmd cmd byte as received in message
561 * @param button button to test (number in range 1..8)
562 * @return true if button is lit, false otherwise
564 private boolean isLEDLit(int cmd, int button) {
565 boolean isSet = (cmd & (0x1 << (button - 1))) != 0;
566 logger.trace("cmd: {} button {}", Integer.toBinaryString(cmd), button);
567 logger.trace("msk: {} isSet: {}", Integer.toBinaryString(0x1 << (button - 1)), isSet);
573 * Handles Dimmer replies to status requests.
574 * In the dimmers case the command2 byte represents the light level from 0-255
577 public static class DimmerRequestReplyHandler extends MessageHandler {
578 DimmerRequestReplyHandler(DeviceFeature p) {
583 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
584 InsteonDevice dev = f.getDevice();
586 int cmd2 = msg.getByte("command2") & 0xff;
588 // sometimes dimmer devices are returning 0xfe when on instead of 0xff
593 logger.debug("{}: set device {} to level 0", nm(), dev.getAddress());
594 feature.publish(PercentType.ZERO, StateChangeType.CHANGED);
595 } else if (cmd2 == 0xff) {
596 logger.debug("{}: set device {} to level 100", nm(), dev.getAddress());
597 feature.publish(PercentType.HUNDRED, StateChangeType.CHANGED);
599 int level = cmd2 * 100 / 255;
603 logger.debug("{}: set device {} to level {}", nm(), dev.getAddress(), level);
604 feature.publish(new PercentType(level), StateChangeType.CHANGED);
606 } catch (FieldException e) {
607 logger.warn("{}: error parsing {}: ", nm(), msg, e);
613 public static class DimmerStopManualChangeHandler extends MessageHandler {
614 DimmerStopManualChangeHandler(DeviceFeature p) {
619 public boolean isDuplicate(Msg msg) {
620 // Disable duplicate elimination because
621 // there are no cleanup or success messages for start/stop.
626 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
627 Msg m = f.makePollMsg();
629 f.getDevice().enqueueMessage(m, f);
635 public static class StartManualChangeHandler extends MessageHandler {
636 StartManualChangeHandler(DeviceFeature p) {
641 public boolean isDuplicate(Msg msg) {
642 // Disable duplicate elimination because
643 // there are no cleanup or success messages for start/stop.
648 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
650 int cmd2 = msg.getByte("command2") & 0xff;
651 int upDown = (cmd2 == 0) ? 0 : 2;
652 logger.debug("{}: dev {} manual state change: {}", nm(), f.getDevice().getAddress(),
653 (upDown == 0) ? "DOWN" : "UP");
654 feature.publish(new DecimalType(upDown), StateChangeType.ALWAYS);
655 } catch (FieldException e) {
656 logger.warn("{} error parsing {}: ", nm(), msg, e);
662 public static class StopManualChangeHandler extends MessageHandler {
663 StopManualChangeHandler(DeviceFeature p) {
668 public boolean isDuplicate(Msg msg) {
669 // Disable duplicate elimination because
670 // there are no cleanup or success messages for start/stop.
675 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
676 logger.debug("{}: dev {} manual state change: {}", nm(), f.getDevice().getAddress(), 0);
677 feature.publish(new DecimalType(1), StateChangeType.ALWAYS);
682 public static class InfoRequestReplyHandler extends MessageHandler {
683 InfoRequestReplyHandler(DeviceFeature p) {
688 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
689 InsteonDevice dev = f.getDevice();
690 if (!msg.isExtended()) {
691 logger.warn("{} device {} expected extended msg as info reply, got {}", nm(), dev.getAddress(), msg);
695 int cmd2 = msg.getByte("command2") & 0xff;
697 case 0x00: // this is a product data response message
698 int prodKey = msg.getInt24("userData2", "userData3", "userData4");
699 int devCat = msg.getByte("userData5");
700 int subCat = msg.getByte("userData6");
701 logger.debug("{} {} got product data: cat: {} subcat: {} key: {} ", nm(), dev.getAddress(),
702 devCat, subCat, Utils.getHexString(prodKey));
704 case 0x02: // this is a device text string response message
705 logger.debug("{} {} got text str {} ", nm(), dev.getAddress(), msg);
708 logger.warn("{} unknown cmd2 = {} in info reply message {}", nm(), cmd2, msg);
711 } catch (FieldException e) {
712 logger.warn("error parsing {}: ", msg, e);
718 public static class MotionSensorDataReplyHandler extends MessageHandler {
719 MotionSensorDataReplyHandler(DeviceFeature p) {
724 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
725 InsteonDevice dev = f.getDevice();
726 if (!msg.isExtended()) {
727 logger.trace("{} device {} ignoring non-extended msg {}", nm(), dev.getAddress(), msg);
731 int cmd2 = msg.getByte("command2") & 0xff;
734 int temperatureLevel;
736 case 0x00: // this is a product data response message
737 batteryLevel = msg.getByte("userData12") & 0xff;
738 lightLevel = msg.getByte("userData11") & 0xff;
739 logger.debug("{}: {} got light level: {}, battery level: {}", nm(), dev.getAddress(),
740 lightLevel, batteryLevel);
741 feature.publish(new DecimalType(lightLevel), StateChangeType.CHANGED,
742 InsteonDeviceHandler.FIELD, InsteonDeviceHandler.FIELD_LIGHT_LEVEL);
743 feature.publish(new DecimalType(batteryLevel), StateChangeType.CHANGED,
744 InsteonDeviceHandler.FIELD, InsteonDeviceHandler.FIELD_BATTERY_LEVEL);
746 case 0x03: // this is the 2844-222 data response message
747 batteryLevel = msg.getByte("userData6") & 0xff;
748 lightLevel = msg.getByte("userData7") & 0xff;
749 temperatureLevel = msg.getByte("userData8") & 0xff;
750 logger.debug("{}: {} got light level: {}, battery level: {}, temperature level: {}", nm(),
751 dev.getAddress(), lightLevel, batteryLevel, temperatureLevel);
752 feature.publish(new DecimalType(lightLevel), StateChangeType.CHANGED,
753 InsteonDeviceHandler.FIELD, InsteonDeviceHandler.FIELD_LIGHT_LEVEL);
754 feature.publish(new DecimalType(batteryLevel), StateChangeType.CHANGED,
755 InsteonDeviceHandler.FIELD, InsteonDeviceHandler.FIELD_BATTERY_LEVEL);
756 feature.publish(new DecimalType(temperatureLevel), StateChangeType.CHANGED,
757 InsteonDeviceHandler.FIELD, InsteonDeviceHandler.FIELD_TEMPERATURE_LEVEL);
759 // per 2844-222 dev doc: working battery level range is 0xd2 - 0x70
760 int batteryPercentage;
761 if (batteryLevel >= 0xd2) {
762 batteryPercentage = 100;
763 } else if (batteryLevel <= 0x70) {
764 batteryPercentage = 0;
766 batteryPercentage = (batteryLevel - 0x70) * 100 / (0xd2 - 0x70);
768 logger.debug("{}: {} battery percentage: {}", nm(), dev.getAddress(), batteryPercentage);
769 feature.publish(new QuantityType<>(batteryPercentage, SmartHomeUnits.PERCENT),
770 StateChangeType.CHANGED, InsteonDeviceHandler.FIELD,
771 InsteonDeviceHandler.FIELD_BATTERY_PERCENTAGE);
774 logger.warn("unknown cmd2 = {} in info reply message {}", cmd2, msg);
777 } catch (FieldException e) {
778 logger.warn("error parsing {}: ", msg, e);
784 public static class MotionSensor2AlternateHeartbeatHandler extends MessageHandler {
785 MotionSensor2AlternateHeartbeatHandler(DeviceFeature p) {
790 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
791 InsteonDevice dev = f.getDevice();
793 // group 0x0B (11) - alternate heartbeat group
794 InsteonAddress toAddr = msg.getAddr("toAddress");
795 int batteryLevel = toAddr.getHighByte() & 0xff;
796 int lightLevel = toAddr.getMiddleByte() & 0xff;
797 int temperatureLevel = msg.getByte("command2") & 0xff;
799 logger.debug("{}: {} got light level: {}, battery level: {}, temperature level: {}", nm(),
800 dev.getAddress(), lightLevel, batteryLevel, temperatureLevel);
801 feature.publish(new DecimalType(lightLevel), StateChangeType.CHANGED, InsteonDeviceHandler.FIELD,
802 InsteonDeviceHandler.FIELD_LIGHT_LEVEL);
803 feature.publish(new DecimalType(batteryLevel), StateChangeType.CHANGED, InsteonDeviceHandler.FIELD,
804 InsteonDeviceHandler.FIELD_BATTERY_LEVEL);
805 feature.publish(new DecimalType(temperatureLevel), StateChangeType.CHANGED, InsteonDeviceHandler.FIELD,
806 InsteonDeviceHandler.FIELD_TEMPERATURE_LEVEL);
808 // per 2844-222 dev doc: working battery level range is 0xd2 - 0x70
809 int batteryPercentage;
810 if (batteryLevel >= 0xd2) {
811 batteryPercentage = 100;
812 } else if (batteryLevel <= 0x70) {
813 batteryPercentage = 0;
815 batteryPercentage = (batteryLevel - 0x70) * 100 / (0xd2 - 0x70);
817 logger.debug("{}: {} battery percentage: {}", nm(), dev.getAddress(), batteryPercentage);
818 feature.publish(new QuantityType<>(batteryPercentage, SmartHomeUnits.PERCENT), StateChangeType.CHANGED,
819 InsteonDeviceHandler.FIELD, InsteonDeviceHandler.FIELD_BATTERY_PERCENTAGE);
820 } catch (FieldException e) {
821 logger.warn("error parsing {}: ", msg, e);
827 public static class HiddenDoorSensorDataReplyHandler extends MessageHandler {
828 HiddenDoorSensorDataReplyHandler(DeviceFeature p) {
833 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
834 InsteonDevice dev = f.getDevice();
835 if (!msg.isExtended()) {
836 logger.trace("{} device {} ignoring non-extended msg {}", nm(), dev.getAddress(), msg);
840 int cmd2 = msg.getByte("command2") & 0xff;
842 case 0x00: // this is a product data response message
843 int batteryLevel = msg.getByte("userData4") & 0xff;
844 int batteryWatermark = msg.getByte("userData7") & 0xff;
845 logger.debug("{}: {} got light level: {}, battery level: {}", nm(), dev.getAddress(),
846 batteryWatermark, batteryLevel);
847 feature.publish(new DecimalType(batteryWatermark), StateChangeType.CHANGED,
848 InsteonDeviceHandler.FIELD, InsteonDeviceHandler.FIELD_BATTERY_WATERMARK_LEVEL);
849 feature.publish(new DecimalType(batteryLevel), StateChangeType.CHANGED,
850 InsteonDeviceHandler.FIELD, InsteonDeviceHandler.FIELD_BATTERY_LEVEL);
853 logger.warn("unknown cmd2 = {} in info reply message {}", cmd2, msg);
856 } catch (FieldException e) {
857 logger.warn("error parsing {}: ", msg, e);
863 public static class PowerMeterUpdateHandler extends MessageHandler {
864 PowerMeterUpdateHandler(DeviceFeature p) {
869 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
870 if (msg.isExtended()) {
872 // see iMeter developer notes 2423A1dev-072013-en.pdf
873 int b7 = msg.getByte("userData7") & 0xff;
874 int b8 = msg.getByte("userData8") & 0xff;
875 int watts = (b7 << 8) | b8;
880 int b9 = msg.getByte("userData9") & 0xff;
881 int b10 = msg.getByte("userData10") & 0xff;
882 int b11 = msg.getByte("userData11") & 0xff;
883 int b12 = msg.getByte("userData12") & 0xff;
884 BigDecimal kwh = BigDecimal.ZERO;
886 int e = (b9 << 24) | (b10 << 16) | (b11 << 8) | b12;
887 kwh = new BigDecimal(e * 65535.0 / (1000 * 60 * 60 * 60)).setScale(4, RoundingMode.HALF_UP);
890 logger.debug("{}:{} watts: {} kwh: {} ", nm(), f.getDevice().getAddress(), watts, kwh);
891 feature.publish(new QuantityType<>(kwh, SmartHomeUnits.KILOWATT_HOUR), StateChangeType.CHANGED,
892 InsteonDeviceHandler.FIELD, InsteonDeviceHandler.FIELD_KWH);
893 feature.publish(new QuantityType<>(watts, SmartHomeUnits.WATT), StateChangeType.CHANGED,
894 InsteonDeviceHandler.FIELD, InsteonDeviceHandler.FIELD_WATTS);
895 } catch (FieldException e) {
896 logger.warn("error parsing {}: ", msg, e);
903 public static class PowerMeterResetHandler extends MessageHandler {
904 PowerMeterResetHandler(DeviceFeature p) {
909 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
910 InsteonDevice dev = f.getDevice();
911 logger.debug("{}: power meter {} was reset", nm(), dev.getAddress());
913 // poll device to get updated kilowatt hours and watts
914 Msg m = f.makePollMsg();
916 f.getDevice().enqueueMessage(m, f);
922 public static class LastTimeHandler extends MessageHandler {
923 LastTimeHandler(DeviceFeature p) {
928 public void handleMessage(int group, byte cmd1a, Msg msg, DeviceFeature f) {
929 feature.publish(new DateTimeType(), StateChangeType.ALWAYS);
934 public static class ContactRequestReplyHandler extends MessageHandler {
935 ContactRequestReplyHandler(DeviceFeature p) {
940 public void handleMessage(int group, byte cmd1a, Msg msg, DeviceFeature f) {
944 cmd = msg.getByte("Cmd");
945 cmd2 = msg.getByte("command2");
946 } catch (FieldException e) {
947 logger.debug("{} no cmd found, dropping msg {}", nm(), msg);
950 if (msg.isAckOfDirect() && (f.getQueryStatus() == DeviceFeature.QueryStatus.QUERY_PENDING) && cmd == 0x50) {
951 OpenClosedType oc = (cmd2 == 0) ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
952 logger.debug("{}: set contact {} to: {}", nm(), f.getDevice().getAddress(), oc);
953 feature.publish(oc, StateChangeType.CHANGED);
959 public static class ClosedContactHandler extends MessageHandler {
960 ClosedContactHandler(DeviceFeature p) {
965 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
966 feature.publish(OpenClosedType.CLOSED, StateChangeType.ALWAYS);
971 public static class OpenedContactHandler extends MessageHandler {
972 OpenedContactHandler(DeviceFeature p) {
977 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
978 feature.publish(OpenClosedType.OPEN, StateChangeType.ALWAYS);
983 public static class OpenedOrClosedContactHandler extends MessageHandler {
984 OpenedOrClosedContactHandler(DeviceFeature p) {
989 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
991 byte cmd2 = msg.getByte("command2");
996 feature.publish(OpenClosedType.CLOSED, StateChangeType.CHANGED);
1000 feature.publish(OpenClosedType.OPEN, StateChangeType.CHANGED);
1002 default: // do nothing
1009 feature.publish(OpenClosedType.CLOSED, StateChangeType.CHANGED);
1011 default: // do nothing
1016 } catch (FieldException e) {
1017 logger.debug("{} no cmd2 found, dropping msg {}", nm(), msg);
1024 public static class ClosedSleepingContactHandler extends MessageHandler {
1025 ClosedSleepingContactHandler(DeviceFeature p) {
1030 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
1031 feature.publish(OpenClosedType.CLOSED, StateChangeType.ALWAYS);
1032 if (f.getDevice().hasProductKey(InsteonDeviceHandler.MOTION_SENSOR_II_PRODUCT_KEY)) {
1033 if (!getBooleanDeviceConfig("heartbeatOnly", false)) {
1034 sendExtendedQuery(f, (byte) 0x2e, (byte) 03);
1037 sendExtendedQuery(f, (byte) 0x2e, (byte) 00);
1043 public static class OpenedSleepingContactHandler extends MessageHandler {
1044 OpenedSleepingContactHandler(DeviceFeature p) {
1049 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
1050 feature.publish(OpenClosedType.OPEN, StateChangeType.ALWAYS);
1051 if (f.getDevice().hasProductKey(InsteonDeviceHandler.MOTION_SENSOR_II_PRODUCT_KEY)) {
1052 if (!getBooleanDeviceConfig("heartbeatOnly", false)) {
1053 sendExtendedQuery(f, (byte) 0x2e, (byte) 03);
1056 sendExtendedQuery(f, (byte) 0x2e, (byte) 00);
1062 * Triggers a poll when a message comes in. Use this handler to react
1063 * to messages that notify of a status update, but don't carry the information
1064 * that you are interested in. Example: you send a command to change a setting,
1065 * get a DIRECT ack back, but the ack does not have the value of the updated setting.
1066 * Then connect this handler to the ACK, such that the device will be polled, and
1067 * the settings updated.
1070 public static class TriggerPollMsgHandler extends MessageHandler {
1071 TriggerPollMsgHandler(DeviceFeature p) {
1076 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
1077 feature.getDevice().doPoll(2000); // 2000 ms delay
1082 * Flexible handler to extract numerical data from messages.
1085 public static class NumberMsgHandler extends MessageHandler {
1086 NumberMsgHandler(DeviceFeature p) {
1091 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
1093 // first do the bit manipulations to focus on the right area
1094 int mask = getIntParameter("mask", 0xFFFF);
1095 int rawValue = extractValue(msg, group);
1096 int cooked = (rawValue & mask) >> getIntParameter("rshift", 0);
1097 // now do an arbitrary transform on the data
1098 double value = transform(cooked);
1099 // last, multiply with factor and add an offset
1100 double dvalue = getDoubleParameter("offset", 0) + value * getDoubleParameter("factor", 1.0);
1104 String scale = getStringParameter("scale", null);
1105 if (scale != null && scale.equals("celsius")) {
1106 state = new QuantityType<>(dvalue, SIUnits.CELSIUS);
1107 } else if (scale != null && scale.equals("fahrenheit")) {
1108 state = new QuantityType<>(dvalue, ImperialUnits.FAHRENHEIT);
1110 state = new DecimalType(dvalue);
1112 feature.publish(state, StateChangeType.CHANGED);
1113 } catch (FieldException e) {
1114 logger.warn("error parsing {}: ", msg, e);
1118 public int transform(int raw) {
1122 private int extractValue(Msg msg, int group) throws FieldException {
1123 String lowByte = getStringParameter("low_byte", "");
1124 if (lowByte.equals("")) {
1125 logger.warn("{} handler misconfigured, missing low_byte!", nm());
1129 if (lowByte.equals("group")) {
1132 value = msg.getByte(lowByte) & 0xFF;
1134 String highByte = getStringParameter("high_byte", "");
1135 if (!highByte.equals("")) {
1136 value |= (msg.getByte(highByte) & 0xFF) << 8;
1143 * Convert system mode field to number 0...4. Insteon has two different
1144 * conventions for numbering, we use the one of the status update messages
1147 public static class ThermostatSystemModeMsgHandler extends NumberMsgHandler {
1148 ThermostatSystemModeMsgHandler(DeviceFeature p) {
1153 public int transform(int raw) {
1164 return (4); // program
1168 return (4); // when in doubt assume to be in "program" mode
1173 * Handle reply to system mode change command
1176 public static class ThermostatSystemModeReplyHandler extends NumberMsgHandler {
1177 ThermostatSystemModeReplyHandler(DeviceFeature p) {
1182 public int transform(int raw) {
1193 return (4); // program
1197 return (4); // when in doubt assume to be in "program" mode
1202 * Handle reply to fan mode change command
1205 public static class ThermostatFanModeReplyHandler extends NumberMsgHandler {
1206 ThermostatFanModeReplyHandler(DeviceFeature p) {
1211 public int transform(int raw) {
1216 return (1); // always on
1220 return (0); // when in doubt assume to be auto mode
1225 * Handle reply to fanlinc fan speed change command
1228 public static class FanLincFanReplyHandler extends NumberMsgHandler {
1229 FanLincFanReplyHandler(DeviceFeature p) {
1234 public int transform(int raw) {
1241 return (2); // medium
1245 logger.warn("fanlinc got unexpected level: {}", raw);
1247 return (0); // when in doubt assume to be off
1252 * Process X10 messages that are generated when another controller
1253 * changes the state of an X10 device.
1256 public static class X10OnHandler extends MessageHandler {
1257 X10OnHandler(DeviceFeature p) {
1262 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
1263 InsteonAddress a = f.getDevice().getAddress();
1264 logger.debug("{}: set X10 device {} to ON", nm(), a);
1265 feature.publish(OnOffType.ON, StateChangeType.ALWAYS);
1270 public static class X10OffHandler extends MessageHandler {
1271 X10OffHandler(DeviceFeature p) {
1276 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
1277 InsteonAddress a = f.getDevice().getAddress();
1278 logger.debug("{}: set X10 device {} to OFF", nm(), a);
1279 feature.publish(OnOffType.OFF, StateChangeType.ALWAYS);
1284 public static class X10BrightHandler extends MessageHandler {
1285 X10BrightHandler(DeviceFeature p) {
1290 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
1291 InsteonAddress a = f.getDevice().getAddress();
1292 logger.debug("{}: ignoring brighten message for device {}", nm(), a);
1297 public static class X10DimHandler extends MessageHandler {
1298 X10DimHandler(DeviceFeature p) {
1303 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
1304 InsteonAddress a = f.getDevice().getAddress();
1305 logger.debug("{}: ignoring dim message for device {}", nm(), a);
1310 public static class X10OpenHandler extends MessageHandler {
1311 X10OpenHandler(DeviceFeature p) {
1316 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
1317 InsteonAddress a = f.getDevice().getAddress();
1318 logger.debug("{}: set X10 device {} to OPEN", nm(), a);
1319 feature.publish(OpenClosedType.OPEN, StateChangeType.ALWAYS);
1324 public static class X10ClosedHandler extends MessageHandler {
1325 X10ClosedHandler(DeviceFeature p) {
1330 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
1331 InsteonAddress a = f.getDevice().getAddress();
1332 logger.debug("{}: set X10 device {} to CLOSED", nm(), a);
1333 feature.publish(OpenClosedType.CLOSED, StateChangeType.ALWAYS);
1338 * Factory method for creating handlers of a given name using java reflection
1340 * @param name the name of the handler to create
1342 * @param f the feature for which to create the handler
1343 * @return the handler which was created
1345 public static @Nullable <T extends MessageHandler> T makeHandler(String name, Map<String, @Nullable String> params,
1347 String cname = MessageHandler.class.getName() + "$" + name;
1349 Class<?> c = Class.forName(cname);
1350 @SuppressWarnings("unchecked")
1351 Class<? extends T> dc = (Class<? extends T>) c;
1352 T mh = dc.getDeclaredConstructor(DeviceFeature.class).newInstance(f);
1353 mh.setParameters(params);
1355 } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | IllegalArgumentException
1356 | InvocationTargetException | NoSuchMethodException | SecurityException e) {
1357 logger.warn("error trying to create message handler: {}", name, e);