2 * Copyright (c) 2010-2024 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.Units;
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 public abstract class MessageHandler {
54 private static final Logger logger = LoggerFactory.getLogger(MessageHandler.class);
56 protected DeviceFeature feature;
57 protected Map<String, String> parameters = new HashMap<>();
62 * @param p state publishing object for dissemination of state changes
64 MessageHandler(DeviceFeature p) {
69 * Method that processes incoming message. The cmd1 parameter
70 * has been extracted earlier already (to make a decision which message handler to call),
71 * and is passed in as an argument so cmd1 does not have to be extracted from the message again.
73 * @param group all-link group or -1 if not specified
74 * @param cmd1 the insteon cmd1 field
75 * @param msg the received insteon message
76 * @param feature the DeviceFeature to which this message handler is attached
78 public abstract void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature feature);
81 * Method to send an extended insteon message for querying a device
83 * @param f DeviceFeature that is being currently handled
84 * @param aCmd1 cmd1 for message to be sent
85 * @param aCmd2 cmd2 for message to be sent
87 public void sendExtendedQuery(DeviceFeature f, byte aCmd1, byte aCmd2) {
88 InsteonDevice d = f.getDevice();
90 Msg m = d.makeExtendedMessage((byte) 0x1f, aCmd1, aCmd2);
92 d.enqueueMessage(m, f);
93 } catch (InvalidMessageTypeException e) {
94 logger.warn("msg exception sending query message to device {}", d.getAddress());
95 } catch (FieldException e) {
96 logger.warn("field exception sending query message to device {}", d.getAddress());
101 * Check if group matches
103 * @param group group to test for
104 * @return true if group matches or no group is specified
106 public boolean matchesGroup(int group) {
107 int g = getIntParameter("group", -1);
108 return (g == -1 || g == group);
112 * Retrieve group parameter or -1 if no group is specified
114 * @return group parameter
116 public int getGroup() {
117 return (getIntParameter("group", -1));
121 * Helper function to get an integer parameter for the handler
123 * @param key name of the int parameter (as specified in device features!)
124 * @param def default to return if parameter not found
125 * @return value of int parameter (or default if not found)
127 protected int getIntParameter(String key, int def) {
128 String val = parameters.get(key);
130 return (def); // param not found
134 ret = Utils.strToInt(val);
135 } catch (NumberFormatException e) {
136 logger.warn("malformed int parameter in message handler: {}", key);
142 * Helper function to get a String parameter for the handler
144 * @param key name of the String parameter (as specified in device features!)
145 * @param def default to return if parameter not found
146 * @return value of parameter (or default if not found)
148 protected @Nullable String getStringParameter(String key, @Nullable String def) {
149 String str = parameters.get(key);
150 return str != null ? str : def;
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 booleanValue) {
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 f 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, String> map) {
333 // ---------------- the various command handler start here -------------------
337 public static class DefaultMsgHandler extends MessageHandler {
338 DefaultMsgHandler(DeviceFeature p) {
343 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
344 logger.debug("{} ignoring unimpl message with cmd1:{}", nm(), Utils.getHexByte(cmd1));
348 public static class NoOpMsgHandler extends MessageHandler {
349 NoOpMsgHandler(DeviceFeature p) {
354 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
355 logger.trace("{} ignore msg {}: {}", nm(), Utils.getHexByte(cmd1), msg);
359 public static class LightOnDimmerHandler extends MessageHandler {
360 LightOnDimmerHandler(DeviceFeature p) {
365 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
366 if (!isMybutton(msg, f)) {
369 InsteonAddress a = f.getDevice().getAddress();
370 if (msg.isAckOfDirect()) {
371 logger.warn("{}: device {}: ignoring ack of direct.", nm(), a);
373 String mode = getStringParameter("mode", "REGULAR");
374 logger.debug("{}: device {} was turned on {}. " + "Sending poll request to get actual level", nm(), a,
376 feature.publish(PercentType.HUNDRED, StateChangeType.ALWAYS);
377 // need to poll to find out what level the dimmer is at now.
378 // it may not be at 100% because dimmers can be configured
379 // to switch to e.g. 75% when turned on.
380 Msg m = f.makePollMsg();
382 f.getDevice().enqueueDelayedMessage(m, f, 1000);
388 public static class LightOffDimmerHandler extends MessageHandler {
389 LightOffDimmerHandler(DeviceFeature p) {
394 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
395 if (isMybutton(msg, f)) {
396 String mode = getStringParameter("mode", "REGULAR");
397 logger.debug("{}: device {} was turned off {}.", nm(), f.getDevice().getAddress(), mode);
398 f.publish(PercentType.ZERO, StateChangeType.ALWAYS);
403 public static class LightOnSwitchHandler extends MessageHandler {
404 LightOnSwitchHandler(DeviceFeature p) {
409 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
410 if (isMybutton(msg, f)) {
411 String mode = getStringParameter("mode", "REGULAR");
412 logger.debug("{}: device {} was switched on {}.", nm(), f.getDevice().getAddress(), mode);
413 f.publish(OnOffType.ON, StateChangeType.ALWAYS);
415 logger.debug("ignored message: {}", isMybutton(msg, f));
420 public static class LightOffSwitchHandler extends MessageHandler {
421 LightOffSwitchHandler(DeviceFeature p) {
426 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
427 if (isMybutton(msg, f)) {
428 String mode = getStringParameter("mode", "REGULAR");
429 logger.debug("{}: device {} was switched off {}.", nm(), f.getDevice().getAddress(), mode);
430 f.publish(OnOffType.OFF, StateChangeType.ALWAYS);
436 * This message handler processes replies to Ramp ON/OFF commands.
437 * Currently, it's been tested for the 2672-222 LED Bulb. Other
438 * devices may use a different pair of commands (0x2E, 0x2F). This
439 * handler and the command handler will need to be extended to support
442 public static class RampDimmerHandler extends MessageHandler {
446 RampDimmerHandler(DeviceFeature p) {
448 // Can't process parameters here because they are set after constructor is invoked.
449 // Unfortunately, this means we can't declare the onCmd, offCmd to be final.
453 public void setParameters(Map<String, String> params) {
454 super.setParameters(params);
455 onCmd = getIntParameter("on", 0x2E);
456 offCmd = getIntParameter("off", 0x2F);
460 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
462 int level = getLevel(msg);
463 logger.debug("{}: device {} was switched on using ramp to level {}.", nm(), f.getDevice().getAddress(),
466 f.publish(OnOffType.ON, StateChangeType.ALWAYS);
468 // The publisher will convert an ON at level==0 to an OFF.
469 // However, this is not completely accurate since a ramp
470 // off at level == 0 may not turn off the dimmer completely
471 // (if I understand the Insteon docs correctly). In any
473 // it would be an odd scenario to turn ON a light at level
475 // rather than turn if OFF.
476 f.publish(new PercentType(level), StateChangeType.ALWAYS);
478 } else if (cmd1 == offCmd) {
479 logger.debug("{}: device {} was switched off using ramp.", nm(), f.getDevice().getAddress());
480 f.publish(new PercentType(0), StateChangeType.ALWAYS);
484 private int getLevel(Msg msg) {
486 byte cmd2 = msg.getByte("command2");
487 return (int) Math.round(((cmd2 >> 4) & 0x0f) * (100 / 15d));
488 } catch (FieldException e) {
489 logger.warn("Can't access command2 byte", e);
496 * A message handler that processes replies to queries.
497 * If command2 == 0xFF then the light has been turned on
498 * else if command2 == 0x00 then the light has been turned off
501 public static class SwitchRequestReplyHandler extends MessageHandler {
502 SwitchRequestReplyHandler(DeviceFeature p) {
507 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
509 InsteonAddress a = f.getDevice().getAddress();
510 int cmd2 = msg.getByte("command2") & 0xff;
511 int button = this.getIntParameter("button", -1);
513 handleNoButtons(cmd2, a, msg);
515 boolean isOn = isLEDLit(cmd2, button);
516 logger.debug("{}: dev {} button {} switched to {}", nm(), a, button, isOn ? "ON" : "OFF");
517 feature.publish(OnOffType.from(isOn), StateChangeType.CHANGED);
519 } catch (FieldException e) {
520 logger.warn("{} error parsing {}: ", nm(), msg, e);
525 * Handle the case where no buttons have been configured.
526 * In this situation, the only return values should be 0 (light off)
531 void handleNoButtons(int cmd2, InsteonAddress a, Msg msg) {
533 logger.debug("{}: set device {} to OFF", nm(), a);
534 feature.publish(OnOffType.OFF, StateChangeType.CHANGED);
535 } else if (cmd2 == 0xff) {
536 logger.debug("{}: set device {} to ON", nm(), a);
537 feature.publish(OnOffType.ON, StateChangeType.CHANGED);
539 logger.warn("{}: {} ignoring unexpected cmd2 in msg: {}", nm(), a, msg);
544 * Test if cmd byte indicates that button is lit.
545 * The cmd byte has the LED status bitwise from the left:
547 * Note that the 2487S has buttons assigned like this:
549 * They used the basis of the 8-button remote, and assigned
550 * the ON button to 1+2, the OFF button to 7+8
552 * @param cmd cmd byte as received in message
553 * @param button button to test (number in range 1..8)
554 * @return true if button is lit, false otherwise
556 private boolean isLEDLit(int cmd, int button) {
557 boolean isSet = (cmd & (0x1 << (button - 1))) != 0;
558 logger.trace("cmd: {} button {}", Integer.toBinaryString(cmd), button);
559 logger.trace("msk: {} isSet: {}", Integer.toBinaryString(0x1 << (button - 1)), isSet);
565 * Handles Dimmer replies to status requests.
566 * In the dimmers case the command2 byte represents the light level from 0-255
568 public static class DimmerRequestReplyHandler extends MessageHandler {
569 DimmerRequestReplyHandler(DeviceFeature p) {
574 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
575 InsteonDevice dev = f.getDevice();
577 int cmd2 = msg.getByte("command2") & 0xff;
579 // sometimes dimmer devices are returning 0xfe when on instead of 0xff
584 logger.debug("{}: set device {} to level 0", nm(), dev.getAddress());
585 feature.publish(PercentType.ZERO, StateChangeType.CHANGED);
586 } else if (cmd2 == 0xff) {
587 logger.debug("{}: set device {} to level 100", nm(), dev.getAddress());
588 feature.publish(PercentType.HUNDRED, StateChangeType.CHANGED);
590 int level = cmd2 * 100 / 255;
594 logger.debug("{}: set device {} to level {}", nm(), dev.getAddress(), level);
595 feature.publish(new PercentType(level), StateChangeType.CHANGED);
597 } catch (FieldException e) {
598 logger.warn("{}: error parsing {}: ", nm(), msg, e);
603 public static class DimmerStopManualChangeHandler extends MessageHandler {
604 DimmerStopManualChangeHandler(DeviceFeature p) {
609 public boolean isDuplicate(Msg msg) {
610 // Disable duplicate elimination because
611 // there are no cleanup or success messages for start/stop.
616 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
617 Msg m = f.makePollMsg();
619 f.getDevice().enqueueMessage(m, f);
624 public static class StartManualChangeHandler extends MessageHandler {
625 StartManualChangeHandler(DeviceFeature p) {
630 public boolean isDuplicate(Msg msg) {
631 // Disable duplicate elimination because
632 // there are no cleanup or success messages for start/stop.
637 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
639 int cmd2 = msg.getByte("command2") & 0xff;
640 int upDown = (cmd2 == 0) ? 0 : 2;
641 logger.debug("{}: dev {} manual state change: {}", nm(), f.getDevice().getAddress(),
642 (upDown == 0) ? "DOWN" : "UP");
643 feature.publish(new DecimalType(upDown), StateChangeType.ALWAYS);
644 } catch (FieldException e) {
645 logger.warn("{} error parsing {}: ", nm(), msg, e);
650 public static class StopManualChangeHandler extends MessageHandler {
651 StopManualChangeHandler(DeviceFeature p) {
656 public boolean isDuplicate(Msg msg) {
657 // Disable duplicate elimination because
658 // there are no cleanup or success messages for start/stop.
663 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
664 logger.debug("{}: dev {} manual state change: {}", nm(), f.getDevice().getAddress(), 0);
665 feature.publish(new DecimalType(1), StateChangeType.ALWAYS);
669 public static class InfoRequestReplyHandler extends MessageHandler {
670 InfoRequestReplyHandler(DeviceFeature p) {
675 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
676 InsteonDevice dev = f.getDevice();
677 if (!msg.isExtended()) {
678 logger.warn("{} device {} expected extended msg as info reply, got {}", nm(), dev.getAddress(), msg);
682 int cmd2 = msg.getByte("command2") & 0xff;
684 case 0x00: // this is a product data response message
685 int prodKey = msg.getInt24("userData2", "userData3", "userData4");
686 int devCat = msg.getByte("userData5");
687 int subCat = msg.getByte("userData6");
688 logger.debug("{} {} got product data: cat: {} subcat: {} key: {} ", nm(), dev.getAddress(),
689 devCat, subCat, Utils.getHexString(prodKey));
691 case 0x02: // this is a device text string response message
692 logger.debug("{} {} got text str {} ", nm(), dev.getAddress(), msg);
695 logger.warn("{} unknown cmd2 = {} in info reply message {}", nm(), cmd2, msg);
698 } catch (FieldException e) {
699 logger.warn("error parsing {}: ", msg, e);
704 public static class MotionSensorDataReplyHandler extends MessageHandler {
705 MotionSensorDataReplyHandler(DeviceFeature p) {
710 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
711 InsteonDevice dev = f.getDevice();
712 if (!msg.isExtended()) {
713 logger.trace("{} device {} ignoring non-extended msg {}", nm(), dev.getAddress(), msg);
717 int cmd2 = msg.getByte("command2") & 0xff;
720 int temperatureLevel;
722 case 0x00: // this is a product data response message
723 batteryLevel = msg.getByte("userData12") & 0xff;
724 lightLevel = msg.getByte("userData11") & 0xff;
725 logger.debug("{}: {} got light level: {}, battery level: {}", nm(), dev.getAddress(),
726 lightLevel, batteryLevel);
727 feature.publish(new DecimalType(lightLevel), StateChangeType.CHANGED,
728 InsteonDeviceHandler.FIELD, InsteonDeviceHandler.FIELD_LIGHT_LEVEL);
729 feature.publish(new DecimalType(batteryLevel), StateChangeType.CHANGED,
730 InsteonDeviceHandler.FIELD, InsteonDeviceHandler.FIELD_BATTERY_LEVEL);
732 case 0x03: // this is the 2844-222 data response message
733 batteryLevel = msg.getByte("userData6") & 0xff;
734 lightLevel = msg.getByte("userData7") & 0xff;
735 temperatureLevel = msg.getByte("userData8") & 0xff;
736 logger.debug("{}: {} got light level: {}, battery level: {}, temperature level: {}", nm(),
737 dev.getAddress(), lightLevel, batteryLevel, temperatureLevel);
738 feature.publish(new DecimalType(lightLevel), StateChangeType.CHANGED,
739 InsteonDeviceHandler.FIELD, InsteonDeviceHandler.FIELD_LIGHT_LEVEL);
740 feature.publish(new DecimalType(batteryLevel), StateChangeType.CHANGED,
741 InsteonDeviceHandler.FIELD, InsteonDeviceHandler.FIELD_BATTERY_LEVEL);
742 feature.publish(new DecimalType(temperatureLevel), StateChangeType.CHANGED,
743 InsteonDeviceHandler.FIELD, InsteonDeviceHandler.FIELD_TEMPERATURE_LEVEL);
745 // per 2844-222 dev doc: working battery level range is 0xd2 - 0x70
746 int batteryPercentage;
747 if (batteryLevel >= 0xd2) {
748 batteryPercentage = 100;
749 } else if (batteryLevel <= 0x70) {
750 batteryPercentage = 0;
752 batteryPercentage = (batteryLevel - 0x70) * 100 / (0xd2 - 0x70);
754 logger.debug("{}: {} battery percentage: {}", nm(), dev.getAddress(), batteryPercentage);
755 feature.publish(new QuantityType<>(batteryPercentage, Units.PERCENT), StateChangeType.CHANGED,
756 InsteonDeviceHandler.FIELD, InsteonDeviceHandler.FIELD_BATTERY_PERCENTAGE);
759 logger.warn("unknown cmd2 = {} in info reply message {}", cmd2, msg);
762 } catch (FieldException e) {
763 logger.warn("error parsing {}: ", msg, e);
768 public static class MotionSensor2AlternateHeartbeatHandler extends MessageHandler {
769 MotionSensor2AlternateHeartbeatHandler(DeviceFeature p) {
774 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
775 InsteonDevice dev = f.getDevice();
777 // group 0x0B (11) - alternate heartbeat group
778 InsteonAddress toAddr = msg.getAddr("toAddress");
779 if (toAddr == null) {
780 logger.warn("toAddr is null");
783 int batteryLevel = toAddr.getHighByte() & 0xff;
784 int lightLevel = toAddr.getMiddleByte() & 0xff;
785 int temperatureLevel = msg.getByte("command2") & 0xff;
787 logger.debug("{}: {} got light level: {}, battery level: {}, temperature level: {}", nm(),
788 dev.getAddress(), lightLevel, batteryLevel, temperatureLevel);
789 feature.publish(new DecimalType(lightLevel), StateChangeType.CHANGED, InsteonDeviceHandler.FIELD,
790 InsteonDeviceHandler.FIELD_LIGHT_LEVEL);
791 feature.publish(new DecimalType(batteryLevel), StateChangeType.CHANGED, InsteonDeviceHandler.FIELD,
792 InsteonDeviceHandler.FIELD_BATTERY_LEVEL);
793 feature.publish(new DecimalType(temperatureLevel), StateChangeType.CHANGED, InsteonDeviceHandler.FIELD,
794 InsteonDeviceHandler.FIELD_TEMPERATURE_LEVEL);
796 // per 2844-222 dev doc: working battery level range is 0xd2 - 0x70
797 int batteryPercentage;
798 if (batteryLevel >= 0xd2) {
799 batteryPercentage = 100;
800 } else if (batteryLevel <= 0x70) {
801 batteryPercentage = 0;
803 batteryPercentage = (batteryLevel - 0x70) * 100 / (0xd2 - 0x70);
805 logger.debug("{}: {} battery percentage: {}", nm(), dev.getAddress(), batteryPercentage);
806 feature.publish(new QuantityType<>(batteryPercentage, Units.PERCENT), StateChangeType.CHANGED,
807 InsteonDeviceHandler.FIELD, InsteonDeviceHandler.FIELD_BATTERY_PERCENTAGE);
808 } catch (FieldException e) {
809 logger.warn("error parsing {}: ", msg, e);
814 public static class HiddenDoorSensorDataReplyHandler extends MessageHandler {
815 HiddenDoorSensorDataReplyHandler(DeviceFeature p) {
820 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
821 InsteonDevice dev = f.getDevice();
822 if (!msg.isExtended()) {
823 logger.trace("{} device {} ignoring non-extended msg {}", nm(), dev.getAddress(), msg);
827 int cmd2 = msg.getByte("command2") & 0xff;
829 case 0x00: // this is a product data response message
830 int batteryLevel = msg.getByte("userData4") & 0xff;
831 int batteryWatermark = msg.getByte("userData7") & 0xff;
832 logger.debug("{}: {} got light level: {}, battery level: {}", nm(), dev.getAddress(),
833 batteryWatermark, batteryLevel);
834 feature.publish(new DecimalType(batteryWatermark), StateChangeType.CHANGED,
835 InsteonDeviceHandler.FIELD, InsteonDeviceHandler.FIELD_BATTERY_WATERMARK_LEVEL);
836 feature.publish(new DecimalType(batteryLevel), StateChangeType.CHANGED,
837 InsteonDeviceHandler.FIELD, InsteonDeviceHandler.FIELD_BATTERY_LEVEL);
840 logger.warn("unknown cmd2 = {} in info reply message {}", cmd2, msg);
843 } catch (FieldException e) {
844 logger.warn("error parsing {}: ", msg, e);
849 public static class PowerMeterUpdateHandler extends MessageHandler {
850 PowerMeterUpdateHandler(DeviceFeature p) {
855 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
856 if (msg.isExtended()) {
858 // see iMeter developer notes 2423A1dev-072013-en.pdf
859 int b7 = msg.getByte("userData7") & 0xff;
860 int b8 = msg.getByte("userData8") & 0xff;
861 int watts = (b7 << 8) | b8;
866 int b9 = msg.getByte("userData9") & 0xff;
867 int b10 = msg.getByte("userData10") & 0xff;
868 int b11 = msg.getByte("userData11") & 0xff;
869 int b12 = msg.getByte("userData12") & 0xff;
870 BigDecimal kwh = BigDecimal.ZERO;
872 int e = (b9 << 24) | (b10 << 16) | (b11 << 8) | b12;
873 kwh = new BigDecimal(e * 65535.0 / (1000 * 60 * 60 * 60)).setScale(4, RoundingMode.HALF_UP);
876 logger.debug("{}:{} watts: {} kwh: {} ", nm(), f.getDevice().getAddress(), watts, kwh);
877 feature.publish(new QuantityType<>(kwh, Units.KILOWATT_HOUR), StateChangeType.CHANGED,
878 InsteonDeviceHandler.FIELD, InsteonDeviceHandler.FIELD_KWH);
879 feature.publish(new QuantityType<>(watts, Units.WATT), StateChangeType.CHANGED,
880 InsteonDeviceHandler.FIELD, InsteonDeviceHandler.FIELD_WATTS);
881 } catch (FieldException e) {
882 logger.warn("error parsing {}: ", msg, e);
888 public static class PowerMeterResetHandler extends MessageHandler {
889 PowerMeterResetHandler(DeviceFeature p) {
894 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
895 InsteonDevice dev = f.getDevice();
896 logger.debug("{}: power meter {} was reset", nm(), dev.getAddress());
898 // poll device to get updated kilowatt hours and watts
899 Msg m = f.makePollMsg();
901 f.getDevice().enqueueMessage(m, f);
906 public static class LastTimeHandler extends MessageHandler {
907 LastTimeHandler(DeviceFeature p) {
912 public void handleMessage(int group, byte cmd1a, Msg msg, DeviceFeature f) {
913 feature.publish(new DateTimeType(), StateChangeType.ALWAYS);
917 public static class ContactRequestReplyHandler extends MessageHandler {
918 ContactRequestReplyHandler(DeviceFeature p) {
923 public void handleMessage(int group, byte cmd1a, Msg msg, DeviceFeature f) {
927 cmd = msg.getByte("Cmd");
928 cmd2 = msg.getByte("command2");
929 } catch (FieldException e) {
930 logger.debug("{} no cmd found, dropping msg {}", nm(), msg);
933 if (msg.isAckOfDirect() && (f.getQueryStatus() == DeviceFeature.QueryStatus.QUERY_PENDING) && cmd == 0x50) {
934 OpenClosedType oc = (cmd2 == 0) ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
935 logger.debug("{}: set contact {} to: {}", nm(), f.getDevice().getAddress(), oc);
936 feature.publish(oc, StateChangeType.CHANGED);
941 public static class ClosedContactHandler extends MessageHandler {
942 ClosedContactHandler(DeviceFeature p) {
947 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
948 feature.publish(OpenClosedType.CLOSED, StateChangeType.ALWAYS);
952 public static class OpenedContactHandler extends MessageHandler {
953 OpenedContactHandler(DeviceFeature p) {
958 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
959 feature.publish(OpenClosedType.OPEN, StateChangeType.ALWAYS);
963 public static class OpenedOrClosedContactHandler extends MessageHandler {
964 OpenedOrClosedContactHandler(DeviceFeature p) {
969 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
971 byte cmd2 = msg.getByte("command2");
976 feature.publish(OpenClosedType.CLOSED, StateChangeType.CHANGED);
980 feature.publish(OpenClosedType.OPEN, StateChangeType.CHANGED);
982 default: // do nothing
989 feature.publish(OpenClosedType.CLOSED, StateChangeType.CHANGED);
991 default: // do nothing
996 } catch (FieldException e) {
997 logger.debug("{} no cmd2 found, dropping msg {}", nm(), msg);
1003 public static class ClosedSleepingContactHandler extends MessageHandler {
1004 ClosedSleepingContactHandler(DeviceFeature p) {
1009 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
1010 feature.publish(OpenClosedType.CLOSED, StateChangeType.ALWAYS);
1011 if (f.getDevice().hasProductKey(InsteonDeviceHandler.MOTION_SENSOR_II_PRODUCT_KEY)) {
1012 if (!getBooleanDeviceConfig("heartbeatOnly", false)) {
1013 sendExtendedQuery(f, (byte) 0x2e, (byte) 03);
1016 sendExtendedQuery(f, (byte) 0x2e, (byte) 00);
1021 public static class OpenedSleepingContactHandler extends MessageHandler {
1022 OpenedSleepingContactHandler(DeviceFeature p) {
1027 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
1028 feature.publish(OpenClosedType.OPEN, StateChangeType.ALWAYS);
1029 if (f.getDevice().hasProductKey(InsteonDeviceHandler.MOTION_SENSOR_II_PRODUCT_KEY)) {
1030 if (!getBooleanDeviceConfig("heartbeatOnly", false)) {
1031 sendExtendedQuery(f, (byte) 0x2e, (byte) 03);
1034 sendExtendedQuery(f, (byte) 0x2e, (byte) 00);
1040 * Triggers a poll when a message comes in. Use this handler to react
1041 * to messages that notify of a status update, but don't carry the information
1042 * that you are interested in. Example: you send a command to change a setting,
1043 * get a DIRECT ack back, but the ack does not have the value of the updated setting.
1044 * Then connect this handler to the ACK, such that the device will be polled, and
1045 * the settings updated.
1047 public static class TriggerPollMsgHandler extends MessageHandler {
1048 TriggerPollMsgHandler(DeviceFeature p) {
1053 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
1054 feature.getDevice().doPoll(2000); // 2000 ms delay
1059 * Flexible handler to extract numerical data from messages.
1061 public static class NumberMsgHandler extends MessageHandler {
1062 NumberMsgHandler(DeviceFeature p) {
1067 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
1069 // first do the bit manipulations to focus on the right area
1070 int mask = getIntParameter("mask", 0xFFFF);
1071 int rawValue = extractValue(msg, group);
1072 int cooked = (rawValue & mask) >> getIntParameter("rshift", 0);
1073 // now do an arbitrary transform on the data
1074 double value = transform(cooked);
1075 // last, multiply with factor and add an offset
1076 double dvalue = getDoubleParameter("offset", 0) + value * getDoubleParameter("factor", 1.0);
1080 String scale = getStringParameter("scale", null);
1081 if ("celsius".equals(scale)) {
1082 state = new QuantityType<>(dvalue, SIUnits.CELSIUS);
1083 } else if ("fahrenheit".equals(scale)) {
1084 state = new QuantityType<>(dvalue, ImperialUnits.FAHRENHEIT);
1086 state = new DecimalType(dvalue);
1088 feature.publish(state, StateChangeType.CHANGED);
1089 } catch (FieldException e) {
1090 logger.warn("error parsing {}: ", msg, e);
1094 public int transform(int raw) {
1098 private int extractValue(Msg msg, int group) throws FieldException {
1099 String lowByte = getStringParameter("low_byte", null);
1100 if (lowByte == null) {
1101 logger.warn("{} handler misconfigured, missing low_byte!", nm());
1105 if ("group".equals(lowByte)) {
1108 value = msg.getByte(lowByte) & 0xFF;
1110 String highByte = getStringParameter("high_byte", null);
1111 if (highByte != null) {
1112 value |= (msg.getByte(highByte) & 0xFF) << 8;
1119 * Convert system mode field to number 0...4. Insteon has two different
1120 * conventions for numbering, we use the one of the status update messages
1122 public static class ThermostatSystemModeMsgHandler extends NumberMsgHandler {
1123 ThermostatSystemModeMsgHandler(DeviceFeature p) {
1128 public int transform(int raw) {
1139 return (4); // program
1143 return (4); // when in doubt assume to be in "program" mode
1148 * Handle reply to system mode change command
1150 public static class ThermostatSystemModeReplyHandler extends NumberMsgHandler {
1151 ThermostatSystemModeReplyHandler(DeviceFeature p) {
1156 public int transform(int raw) {
1167 return (4); // program
1171 return (4); // when in doubt assume to be in "program" mode
1176 * Handle reply to fan mode change command
1178 public static class ThermostatFanModeReplyHandler extends NumberMsgHandler {
1179 ThermostatFanModeReplyHandler(DeviceFeature p) {
1184 public int transform(int raw) {
1189 return (1); // always on
1193 return (0); // when in doubt assume to be auto mode
1198 * Handle reply to fanlinc fan speed change command
1200 public static class FanLincFanReplyHandler extends NumberMsgHandler {
1201 FanLincFanReplyHandler(DeviceFeature p) {
1206 public int transform(int raw) {
1213 return (2); // medium
1217 logger.warn("fanlinc got unexpected level: {}", raw);
1219 return (0); // when in doubt assume to be off
1224 * Process X10 messages that are generated when another controller
1225 * changes the state of an X10 device.
1227 public static class X10OnHandler extends MessageHandler {
1228 X10OnHandler(DeviceFeature p) {
1233 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
1234 InsteonAddress a = f.getDevice().getAddress();
1235 logger.debug("{}: set X10 device {} to ON", nm(), a);
1236 feature.publish(OnOffType.ON, StateChangeType.ALWAYS);
1240 public static class X10OffHandler extends MessageHandler {
1241 X10OffHandler(DeviceFeature p) {
1246 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
1247 InsteonAddress a = f.getDevice().getAddress();
1248 logger.debug("{}: set X10 device {} to OFF", nm(), a);
1249 feature.publish(OnOffType.OFF, StateChangeType.ALWAYS);
1253 public static class X10BrightHandler extends MessageHandler {
1254 X10BrightHandler(DeviceFeature p) {
1259 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
1260 InsteonAddress a = f.getDevice().getAddress();
1261 logger.debug("{}: ignoring brighten message for device {}", nm(), a);
1265 public static class X10DimHandler extends MessageHandler {
1266 X10DimHandler(DeviceFeature p) {
1271 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
1272 InsteonAddress a = f.getDevice().getAddress();
1273 logger.debug("{}: ignoring dim message for device {}", nm(), a);
1277 public static class X10OpenHandler extends MessageHandler {
1278 X10OpenHandler(DeviceFeature p) {
1283 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
1284 InsteonAddress a = f.getDevice().getAddress();
1285 logger.debug("{}: set X10 device {} to OPEN", nm(), a);
1286 feature.publish(OpenClosedType.OPEN, StateChangeType.ALWAYS);
1290 public static class X10ClosedHandler extends MessageHandler {
1291 X10ClosedHandler(DeviceFeature p) {
1296 public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f) {
1297 InsteonAddress a = f.getDevice().getAddress();
1298 logger.debug("{}: set X10 device {} to CLOSED", nm(), a);
1299 feature.publish(OpenClosedType.CLOSED, StateChangeType.ALWAYS);
1304 * Factory method for creating handlers of a given name using java reflection
1306 * @param name the name of the handler to create
1308 * @param f the feature for which to create the handler
1309 * @return the handler which was created
1311 public static @Nullable <T extends MessageHandler> T makeHandler(String name, Map<String, String> params,
1313 String cname = MessageHandler.class.getName() + "$" + name;
1315 Class<?> c = Class.forName(cname);
1316 @SuppressWarnings("unchecked")
1317 Class<? extends T> dc = (Class<? extends T>) c;
1319 T mh = dc.getDeclaredConstructor(DeviceFeature.class).newInstance(f);
1320 mh.setParameters(params);
1322 } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | IllegalArgumentException
1323 | InvocationTargetException | NoSuchMethodException | SecurityException e) {
1324 logger.warn("error trying to create message handler: {}", name, e);