/** * Copyright (c) 2010-2016 by the respective copyright holders. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ package org.openhab.binding.insteonplm.internal.device; import java.io.IOException; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.GregorianCalendar; import java.util.HashMap; import org.openhab.binding.insteonplm.internal.device.DeviceFeatureListener.StateChangeType; import org.openhab.binding.insteonplm.internal.device.GroupMessageStateMachine.GroupMessage; import org.openhab.binding.insteonplm.internal.message.FieldException; import org.openhab.binding.insteonplm.internal.message.Msg; import org.openhab.binding.insteonplm.internal.message.MsgType; import org.openhab.binding.insteonplm.internal.utils.Utils; import org.openhab.core.library.types.DateTimeType; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.OpenClosedType; import org.openhab.core.library.types.PercentType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A message handler processes incoming Insteon messages and reacts by publishing * corresponding messages on the openhab bus, updating device state etc. * * @author Daniel Pfrommer * @author Bernd Pfrommer * @since 1.5.0 */ public abstract class MessageHandler { private static final Logger logger = LoggerFactory.getLogger(MessageHandler.class); DeviceFeature m_feature = null; HashMap<String, String> m_parameters = new HashMap<String, String>(); HashMap<Integer, GroupMessageStateMachine> m_groupState = new HashMap<Integer, GroupMessageStateMachine>(); /** * Constructor * * @param p state publishing object for dissemination of state changes */ MessageHandler(DeviceFeature p) { m_feature = p; } /** * Method that processes incoming message. The cmd1 parameter * has been extracted earlier already (to make a decision which message handler to call), * and is passed in as an argument so cmd1 does not have to be extracted from the message again. * * @param group all-link group or -1 if not specified * @param cmd1 the insteon cmd1 field * @param msg the received insteon message * @param feature the DeviceFeature to which this message handler is attached * @param fromPort the device (/dev/ttyUSB0) from which the message has been received */ public abstract void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature feature, String fromPort); /** * Method to send an extended insteon message for querying a device * * @param f DeviceFeature that is being currently handled * @param aCmd1 cmd1 for message to be sent * @param aCmd2 cmd2 for message to be sent */ public void sendExtendedQuery(DeviceFeature f, byte aCmd1, byte aCmd2) { InsteonDevice d = f.getDevice(); try { Msg m = d.makeExtendedMessage((byte) 0x1f, aCmd1, aCmd2); m.setQuietTime(500L); d.enqueueMessage(m, f); } catch (IOException e) { logger.warn("i/o problem sending query message to device {}", d.getAddress()); } catch (FieldException e) { logger.warn("field exception sending query message to device {}", d.getAddress()); } } /** * Check if group matches * * @param group group to test for * @return true if group matches or no group is specified */ public boolean matchesGroup(int group) { int g = getIntParameter("group", -1); return (g == -1 || g == group); } /** * Retrieve group parameter or -1 if no group is specified * * @return group parameter */ public int getGroup() { return (getIntParameter("group", -1)); } /** * Helper function to get an integer parameter for the handler * * @param key name of the int parameter (as specified in device features!) * @param def default to return if parameter not found * @return value of int parameter (or default if not found) */ protected int getIntParameter(String key, int def) { String val = m_parameters.get(key); if (val == null) { return (def); // param not found } int ret = def; try { ret = Utils.strToInt(val); } catch (NumberFormatException e) { logger.error("malformed int parameter in message handler: {}", key); } return ret; } /** * Helper function to get a String parameter for the handler * * @param key name of the String parameter (as specified in device features!) * @param def default to return if parameter not found * @return value of parameter (or default if not found) */ protected String getStringParameter(String key, String def) { return (m_parameters.get(key) == null ? def : m_parameters.get(key)); } /** * Helper function to get a double parameter for the handler * * @param key name of the parameter (as specified in device features!) * @param def default to return if parameter not found * @return value of parameter (or default if not found) */ protected double getDoubleParameter(String key, double def) { try { if (m_parameters.get(key) != null) { return Double.parseDouble(m_parameters.get(key)); } } catch (NumberFormatException e) { logger.error("malformed int parameter in message handler: {}", key); } return def; } /** * Test if message refers to the button configured for given feature * * @param msg received message * @param f device feature to test * @return true if we have no button configured or the message is for this button */ protected boolean isMybutton(Msg msg, DeviceFeature f) { int myButton = getIntParameter("button", -1); // if there is no button configured for this handler // the message is assumed to refer to this feature // no matter what button is addressed in the message if (myButton == -1) { return true; } int button = getButtonInfo(msg, f); return button != -1 && myButton == button; } /** * Test if parameter matches value * * @param param name of parameter to match * @param msg message to search * @param field field name to match * @return true if parameter matches * @throws FieldException if field not there */ protected boolean testMatch(String param, Msg msg, String field) throws FieldException { int mp = getIntParameter(param, -1); // parameter not filtered for, declare this a match! if (mp == -1) { return (true); } byte value = msg.getByte(field); return (value == mp); } /** * Test if message matches the filter parameters * * @param msg message to be tested against * @return true if message matches */ public boolean matches(Msg msg) { try { int ext = getIntParameter("ext", -1); if (ext != -1) { if ((msg.isExtended() && ext != 1) || (!msg.isExtended() && ext != 0)) { return (false); } if (!testMatch("match_cmd1", msg, "command1")) { return (false); } } if (!testMatch("match_cmd2", msg, "command2")) { return (false); } if (!testMatch("match_d1", msg, "userData1")) { return (false); } if (!testMatch("match_d2", msg, "userData2")) { return (false); } if (!testMatch("match_d3", msg, "userData3")) { return (false); } } catch (FieldException e) { logger.error("error matching message: {}", msg, e); return (false); } return (true); } /** * Determines is an incoming ALL LINK message is a duplicate * * @param msg the received ALL LINK message * @return true if this message is a duplicate */ protected boolean isDuplicate(Msg msg) { boolean isDuplicate = false; try { MsgType t = MsgType.s_fromValue(msg.getByte("messageFlags")); int hops = msg.getHopsLeft(); if (t == MsgType.ALL_LINK_BROADCAST) { int group = msg.getAddress("toAddress").getLowByte() & 0xff; byte cmd1 = msg.getByte("command1"); // if the command is 0x06, then it's success message // from the original broadcaster, with which the device // confirms that it got all cleanup replies successfully. GroupMessage gm = (cmd1 == 0x06) ? GroupMessage.SUCCESS : GroupMessage.BCAST; isDuplicate = !updateGroupState(group, hops, gm); } else if (t == MsgType.ALL_LINK_CLEANUP) { // the cleanup messages are direct messages, so the // group # is not in the toAddress, but in cmd2 int group = msg.getByte("command2") & 0xff; isDuplicate = !updateGroupState(group, hops, GroupMessage.CLEAN); } } catch (IllegalArgumentException e) { logger.error("cannot parse msg: {}", msg, e); } catch (FieldException e) { logger.error("cannot parse msg: {}", msg, e); } return (isDuplicate); } /** * Advance the state of the state machine that suppresses duplicates * * @param group the insteon group of the broadcast message * @param hops number of hops left * @param a what type of group message came in (action etc) * @return true if this is message is NOT a duplicate */ private boolean updateGroupState(int group, int hops, GroupMessage a) { GroupMessageStateMachine m = m_groupState.get(new Integer(group)); if (m == null) { m = new GroupMessageStateMachine(); m_groupState.put(new Integer(group), m); } logger.trace("updating group state for {} to {}", group, a); return (m.action(a, hops)); } /** * Extract button information from message * * @param msg the message to extract from * @param the device feature (needed for debug printing) * @return the button number or -1 if no button found */ static protected int getButtonInfo(Msg msg, DeviceFeature f) { // the cleanup messages have the button number in the command2 field // the broadcast messages have it as the lsb of the toAddress try { int bclean = msg.getByte("command2") & 0xff; int bbcast = msg.getAddress("toAddress").getLowByte() & 0xff; int button = msg.isCleanup() ? bclean : bbcast; logger.trace("{} button: {} bclean: {} bbcast: {}", f.getDevice().getAddress(), button, bclean, bbcast); return button; } catch (FieldException e) { logger.error("field exception while parsing msg {}: ", msg, e); } return -1; } /** * Shorthand to return class name for logging purposes * * @return name of the class */ protected String nm() { return (this.getClass().getSimpleName()); } /** * Set parameter map * * @param hm the parameter map for this message handler */ public void setParameters(HashMap<String, String> hm) { m_parameters = hm; } // // // ---------------- the various command handler start here ------------------- // // public static class DefaultMsgHandler extends MessageHandler { DefaultMsgHandler(DeviceFeature p) { super(p); } @Override public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f, String fromPort) { logger.debug("{} ignoring unimpl message with cmd1:{}", nm(), Utils.getHexByte(cmd1)); } } public static class NoOpMsgHandler extends MessageHandler { NoOpMsgHandler(DeviceFeature p) { super(p); } @Override public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f, String fromPort) { logger.trace("{} ignore msg {}: {}", nm(), Utils.getHexByte(cmd1), msg); } } public static class LightOnDimmerHandler extends MessageHandler { LightOnDimmerHandler(DeviceFeature p) { super(p); } @Override public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f, String fromPort) { if (!isMybutton(msg, f)) { return; } InsteonAddress a = f.getDevice().getAddress(); if (msg.isAckOfDirect()) { logger.error("{}: device {}: ignoring ack of direct.", nm(), a); } else { String mode = getStringParameter("mode", "REGULAR"); logger.info("{}: device {} was turned on {}. " + "Sending poll request to get actual level", nm(), a, mode); m_feature.publish(PercentType.HUNDRED, StateChangeType.ALWAYS); // need to poll to find out what level the dimmer is at now. // it may not be at 100% because dimmers can be configured // to switch to e.g. 75% when turned on. Msg m = f.makePollMsg(); if (m != null) { f.getDevice().enqueueDelayedMessage(m, f, 1000); } } } } public static class LightOffDimmerHandler extends MessageHandler { LightOffDimmerHandler(DeviceFeature p) { super(p); } @Override public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f, String fromPort) { if (isMybutton(msg, f)) { String mode = getStringParameter("mode", "REGULAR"); logger.info("{}: device {} was turned off {}.", nm(), f.getDevice().getAddress(), mode); f.publish(PercentType.ZERO, StateChangeType.ALWAYS); } } } public static class LightOnSwitchHandler extends MessageHandler { LightOnSwitchHandler(DeviceFeature p) { super(p); } @Override public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f, String fromPort) { if (isMybutton(msg, f)) { String mode = getStringParameter("mode", "REGULAR"); logger.info("{}: device {} was switched on {}.", nm(), f.getDevice().getAddress(), mode); f.publish(OnOffType.ON, StateChangeType.ALWAYS); } else { logger.debug("ignored message: {}", isMybutton(msg, f)); } } } public static class LightOffSwitchHandler extends MessageHandler { LightOffSwitchHandler(DeviceFeature p) { super(p); } @Override public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f, String fromPort) { if (isMybutton(msg, f)) { String mode = getStringParameter("mode", "REGULAR"); logger.info("{}: device {} was switched off {}.", nm(), f.getDevice().getAddress(), mode); f.publish(OnOffType.OFF, StateChangeType.ALWAYS); } } } /** * This message handler processes replies to Ramp ON/OFF commands. * Currently, it's been tested for the 2672-222 LED Bulb. Other * devices may use a different pair of commands (0x2E, 0x2F). This * handler and the command handler will need to be extended to support * those devices. */ public static class RampDimmerHandler extends MessageHandler { private int onCmd; private int offCmd; RampDimmerHandler(DeviceFeature p) { super(p); // Can't process parameters here because they are set after constructor is invoked. // Unfortunately, this means we can't declare the onCmd, offCmd to be final. } @Override public void setParameters(HashMap<String, String> params) { super.setParameters(params); onCmd = getIntParameter("on", 0x2E); offCmd = getIntParameter("off", 0x2F); } @Override public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f, String fromPort) { if (cmd1 == onCmd) { int level = getLevel(msg); logger.info("{}: device {} was switched on using ramp to level {}.", nm(), f.getDevice().getAddress(), level); if (level == 100) { f.publish(OnOffType.ON, StateChangeType.ALWAYS); } else { // The publisher will convert an ON at level==0 to an OFF. // However, this is not completely accurate since a ramp // off at level == 0 may not turn off the dimmer completely // (if I understand the Insteon docs correctly). In any // case, // it would be an odd scenario to turn ON a light at level // == 0 // rather than turn if OFF. f.publish(new PercentType(level), StateChangeType.ALWAYS); } } else if (cmd1 == offCmd) { logger.info("{}: device {} was switched off using ramp.", nm(), f.getDevice().getAddress()); f.publish(new PercentType(0), StateChangeType.ALWAYS); } } private int getLevel(Msg msg) { try { byte cmd2 = msg.getByte("command2"); return (int) Math.round(((cmd2 >> 4) & 0x0f) * (100 / 15d)); } catch (FieldException e) { logger.error("Can't access command2 byte", e); return 0; } } } /** * A message handler that processes replies to queries. * If command2 == 0xFF then the light has been turned on * else if command2 == 0x00 then the light has been turned off */ public static class SwitchRequestReplyHandler extends MessageHandler { SwitchRequestReplyHandler(DeviceFeature p) { super(p); } @Override public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f, String fromPort) { try { InsteonAddress a = f.getDevice().getAddress(); int cmd2 = msg.getByte("command2") & 0xff; int button = this.getIntParameter("button", -1); if (button < 0) { handleNoButtons(cmd2, a, msg); } else { boolean isOn = isLEDLit(cmd2, button); logger.info("{}: dev {} button {} switched to {}", nm(), a, button, isOn ? "ON" : "OFF"); m_feature.publish(isOn ? OnOffType.ON : OnOffType.OFF, StateChangeType.CHANGED); } } catch (FieldException e) { logger.error("{} error parsing {}: ", nm(), msg, e); } } /** * Handle the case where no buttons have been configured. * In this situation, the only return values should be 0 (light off) * or 0xff (light on) * * @param cmd2 */ void handleNoButtons(int cmd2, InsteonAddress a, Msg msg) { if (cmd2 == 0) { logger.info("{}: set device {} to OFF", nm(), a); m_feature.publish(OnOffType.OFF, StateChangeType.CHANGED); } else if (cmd2 == 0xff) { logger.info("{}: set device {} to ON", nm(), a); m_feature.publish(OnOffType.ON, StateChangeType.CHANGED); } else { logger.warn("{}: {} ignoring unexpected cmd2 in msg: {}", nm(), a, msg); } } /** * Test if cmd byte indicates that button is lit. * The cmd byte has the LED status bitwise from the left: * 87654321 * Note that the 2487S has buttons assigned like this: * 22|6543|11 * They used the basis of the 8-button remote, and assigned * the ON button to 1+2, the OFF button to 7+8 * * @param cmd cmd byte as received in message * @param button button to test (number in range 1..8) * @return true if button is lit, false otherwise */ private boolean isLEDLit(int cmd, int button) { boolean isSet = (cmd & (0x1 << (button - 1))) != 0; logger.trace("cmd: {} button {}", Integer.toBinaryString(cmd), button); logger.trace("msk: {} isSet: {}", Integer.toBinaryString(0x1 << (button - 1)), isSet); return (isSet); } } /** * Handles Dimmer replies to status requests. * In the dimmers case the command2 byte represents the light level from 0-255 */ public static class DimmerRequestReplyHandler extends MessageHandler { DimmerRequestReplyHandler(DeviceFeature p) { super(p); } @Override public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f, String fromPort) { InsteonDevice dev = f.getDevice(); try { int cmd2 = msg.getByte("command2") & 0xff; if (cmd2 == 0xfe) { // sometimes dimmer devices are returning 0xfe when on instead of 0xff cmd2 = 0xff; } if (cmd2 == 0) { logger.info("{}: set device {} to level 0", nm(), dev.getAddress()); m_feature.publish(PercentType.ZERO, StateChangeType.CHANGED); } else if (cmd2 == 0xff) { logger.info("{}: set device {} to level 100", nm(), dev.getAddress()); m_feature.publish(PercentType.HUNDRED, StateChangeType.CHANGED); } else { int level = cmd2 * 100 / 255; if (level == 0) { level = 1; } logger.info("{}: set device {} to level {}", nm(), dev.getAddress(), level); m_feature.publish(new PercentType(level), StateChangeType.CHANGED); } } catch (FieldException e) { logger.error("{}: error parsing {}: ", nm(), msg, e); } } } public static class DimmerStopManualChangeHandler extends MessageHandler { DimmerStopManualChangeHandler(DeviceFeature p) { super(p); } @Override public boolean isDuplicate(Msg msg) { // Disable duplicate elimination because // there are no cleanup or success messages for start/stop. return (false); } @Override public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f, String fromPort) { Msg m = f.makePollMsg(); if (m != null) { f.getDevice().enqueueMessage(m, f); } } } public static class StartManualChangeHandler extends MessageHandler { StartManualChangeHandler(DeviceFeature p) { super(p); } @Override public boolean isDuplicate(Msg msg) { // Disable duplicate elimination because // there are no cleanup or success messages for start/stop. return (false); } @Override public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f, String fromPort) { try { int cmd2 = msg.getByte("command2") & 0xff; int upDown = (cmd2 == 0) ? 0 : 2; logger.info("{}: dev {} manual state change: {}", nm(), f.getDevice().getAddress(), (upDown == 0) ? "DOWN" : "UP"); m_feature.publish(new DecimalType(upDown), StateChangeType.ALWAYS); } catch (FieldException e) { logger.error("{} error parsing {}: ", nm(), msg, e); } } } public static class StopManualChangeHandler extends MessageHandler { StopManualChangeHandler(DeviceFeature p) { super(p); } @Override public boolean isDuplicate(Msg msg) { // Disable duplicate elimination because // there are no cleanup or success messages for start/stop. return (false); } @Override public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f, String fromPort) { logger.info("{}: dev {} manual state change: {}", nm(), f.getDevice().getAddress(), 0); m_feature.publish(new DecimalType(1), StateChangeType.ALWAYS); } } public static class InfoRequestReplyHandler extends MessageHandler { InfoRequestReplyHandler(DeviceFeature p) { super(p); } @Override public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f, String fromPort) { InsteonDevice dev = f.getDevice(); if (!msg.isExtended()) { logger.warn("{} device {} expected extended msg as info reply, got {}", nm(), dev.getAddress(), msg); return; } try { int cmd2 = msg.getByte("command2") & 0xff; switch (cmd2) { case 0x00: // this is a product data response message int prodKey = msg.getInt24("userData2", "userData3", "userData4"); int devCat = msg.getByte("userData5"); int subCat = msg.getByte("userData6"); logger.info("{} {} got product data: cat: {} subcat: {} key: {} ", nm(), dev.getAddress(), devCat, subCat, Utils.getHexString(prodKey)); break; case 0x02: // this is a device text string response message logger.info("{} {} got text str {} ", nm(), dev.getAddress(), msg); break; default: logger.warn("{} unknown cmd2 = {} in info reply message {}", nm(), cmd2, msg); break; } } catch (FieldException e) { logger.error("error parsing {}: ", msg, e); } } } public static class MotionSensorDataReplyHandler extends MessageHandler { MotionSensorDataReplyHandler(DeviceFeature p) { super(p); } @Override public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f, String fromPort) { InsteonDevice dev = f.getDevice(); if (!msg.isExtended()) { logger.trace("{} device {} ignoring non-extended msg {}", nm(), dev.getAddress(), msg); return; } try { int cmd2 = msg.getByte("command2") & 0xff; switch (cmd2) { case 0x00: // this is a product data response message int batteryLevel = msg.getByte("userData12") & 0xff; int lightLevel = msg.getByte("userData11") & 0xff; logger.debug("{}: {} got light level: {}, battery level: {}", nm(), dev.getAddress(), lightLevel, batteryLevel); m_feature.publish(new DecimalType(lightLevel), StateChangeType.CHANGED, "field", "light_level"); m_feature.publish(new DecimalType(batteryLevel), StateChangeType.CHANGED, "field", "battery_level"); break; default: logger.warn("unknown cmd2 = {} in info reply message {}", cmd2, msg); break; } } catch (FieldException e) { logger.error("error parsing {}: ", msg, e); } } } public static class HiddenDoorSensorDataReplyHandler extends MessageHandler { HiddenDoorSensorDataReplyHandler(DeviceFeature p) { super(p); } @Override public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f, String fromPort) { InsteonDevice dev = f.getDevice(); if (!msg.isExtended()) { logger.trace("{} device {} ignoring non-extended msg {}", nm(), dev.getAddress(), msg); return; } try { int cmd2 = msg.getByte("command2") & 0xff; switch (cmd2) { case 0x00: // this is a product data response message int batteryLevel = msg.getByte("userData4") & 0xff; int batteryWatermark = msg.getByte("userData7") & 0xff; logger.debug("{}: {} got light level: {}, battery level: {}", nm(), dev.getAddress(), batteryWatermark, batteryLevel); m_feature.publish(new DecimalType(batteryWatermark), StateChangeType.CHANGED, "field", "battery_watermark_level"); m_feature.publish(new DecimalType(batteryLevel), StateChangeType.CHANGED, "field", "battery_level"); break; default: logger.warn("unknown cmd2 = {} in info reply message {}", cmd2, msg); break; } } catch (FieldException e) { logger.error("error parsing {}: ", msg, e); } } } public static class PowerMeterUpdateHandler extends MessageHandler { PowerMeterUpdateHandler(DeviceFeature p) { super(p); } @Override public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f, String fromPort) { if (msg.isExtended()) { try { // see iMeter developer notes 2423A1dev-072013-en.pdf int b7 = msg.getByte("userData7") & 0xff; int b8 = msg.getByte("userData8") & 0xff; int watts = (b7 << 8) | b8; if (watts > 32767) { watts -= 65535; } int b9 = msg.getByte("userData9") & 0xff; int b10 = msg.getByte("userData10") & 0xff; int b11 = msg.getByte("userData11") & 0xff; int b12 = msg.getByte("userData12") & 0xff; BigDecimal kwh = BigDecimal.ZERO; if (b9 < 254) { int e = (b9 << 24) | (b10 << 16) | (b11 << 8) | b12; kwh = new BigDecimal(e * 65535.0 / (1000 * 60 * 60 * 60)).setScale(4, RoundingMode.HALF_UP); } logger.debug("{}:{} watts: {} kwh: {} ", nm(), f.getDevice().getAddress(), watts, kwh); m_feature.publish(new DecimalType(kwh), StateChangeType.CHANGED, "field", "kwh"); m_feature.publish(new DecimalType(watts), StateChangeType.CHANGED, "field", "watts"); } catch (FieldException e) { logger.error("error parsing {}: ", msg, e); } } } } public static class PowerMeterResetHandler extends MessageHandler { PowerMeterResetHandler(DeviceFeature p) { super(p); } @Override public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f, String fromPort) { InsteonDevice dev = f.getDevice(); logger.info("{}: power meter {} was reset", nm(), dev.getAddress()); // poll device to get updated kilowatt hours and watts Msg m = f.makePollMsg(); if (m != null) { f.getDevice().enqueueMessage(m, f); } } } public static class LastTimeHandler extends MessageHandler { LastTimeHandler(DeviceFeature p) { super(p); } @Override public void handleMessage(int group, byte cmd1a, Msg msg, DeviceFeature f, String fromPort) { GregorianCalendar calendar = new GregorianCalendar(); calendar.setTimeInMillis(System.currentTimeMillis()); DateTimeType t = new DateTimeType(calendar); m_feature.publish(t, StateChangeType.ALWAYS); } } public static class ContactRequestReplyHandler extends MessageHandler { ContactRequestReplyHandler(DeviceFeature p) { super(p); } @Override public void handleMessage(int group, byte cmd1a, Msg msg, DeviceFeature f, String fromPort) { byte cmd = 0x00; byte cmd2 = 0x00; try { cmd = msg.getByte("Cmd"); cmd2 = msg.getByte("command2"); } catch (FieldException e) { logger.debug("{} no cmd found, dropping msg {}", nm(), msg); return; } if (msg.isAckOfDirect() && (f.getQueryStatus() == DeviceFeature.QueryStatus.QUERY_PENDING) && cmd == 0x50) { OpenClosedType oc = (cmd2 == 0) ? OpenClosedType.OPEN : OpenClosedType.CLOSED; logger.info("{}: set contact {} to: {}", nm(), f.getDevice().getAddress(), oc); m_feature.publish(oc, StateChangeType.CHANGED); } } } public static class ClosedContactHandler extends MessageHandler { ClosedContactHandler(DeviceFeature p) { super(p); } @Override public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f, String fromPort) { m_feature.publish(OpenClosedType.CLOSED, StateChangeType.ALWAYS); } } public static class OpenedContactHandler extends MessageHandler { OpenedContactHandler(DeviceFeature p) { super(p); } @Override public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f, String fromPort) { m_feature.publish(OpenClosedType.OPEN, StateChangeType.ALWAYS); } } public static class OpenedOrClosedContactHandler extends MessageHandler { OpenedOrClosedContactHandler(DeviceFeature p) { super(p); } @Override public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f, String fromPort) { try { byte cmd2 = msg.getByte("command2"); switch (cmd1) { case 0x11: switch (cmd2) { case 0x02: m_feature.publish(OpenClosedType.CLOSED, StateChangeType.CHANGED); break; case 0x01: case 0x04: m_feature.publish(OpenClosedType.OPEN, StateChangeType.CHANGED); break; default: // do nothing break; } break; case 0x13: switch (cmd2) { case 0x04: m_feature.publish(OpenClosedType.CLOSED, StateChangeType.CHANGED); break; default: // do nothing break; } break; } } catch (FieldException e) { logger.debug("{} no cmd2 found, dropping msg {}", nm(), msg); return; } } } public static class ClosedSleepingContactHandler extends MessageHandler { ClosedSleepingContactHandler(DeviceFeature p) { super(p); } @Override public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f, String fromPort) { m_feature.publish(OpenClosedType.CLOSED, StateChangeType.ALWAYS); sendExtendedQuery(f, (byte) 0x2e, (byte) 00); } } public static class OpenedSleepingContactHandler extends MessageHandler { OpenedSleepingContactHandler(DeviceFeature p) { super(p); } @Override public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f, String fromPort) { m_feature.publish(OpenClosedType.OPEN, StateChangeType.ALWAYS); sendExtendedQuery(f, (byte) 0x2e, (byte) 00); } } /** * Triggers a poll when a message comes in. Use this handler to react * to messages that notify of a status update, but don't carry the information * that you are interested in. Example: you send a command to change a setting, * get a DIRECT ack back, but the ack does not have the value of the updated setting. * Then connect this handler to the ACK, such that the device will be polled, and * the settings updated. */ public static class TriggerPollMsgHandler extends MessageHandler { TriggerPollMsgHandler(DeviceFeature p) { super(p); } @Override public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f, String fromPort) { m_feature.getDevice().doPoll(2000); // 2000 ms delay } } /** * Flexible handler to extract numerical data from messages. */ public static class NumberMsgHandler extends MessageHandler { NumberMsgHandler(DeviceFeature p) { super(p); } @Override public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f, String fromPort) { try { // first do the bit manipulations to focus on the right area int mask = getIntParameter("mask", 0xFFFF); int rawValue = extractValue(msg, group); int cooked = (rawValue & mask) >> getIntParameter("rshift", 0); // now do an arbitrary transform on the data double value = transform(cooked); // last, multiply with factor and add an offset double dvalue = getDoubleParameter("offset", 0) + value * getDoubleParameter("factor", 1.0); m_feature.publish(new DecimalType(dvalue), StateChangeType.CHANGED); } catch (FieldException e) { logger.error("error parsing {}: ", msg, e); } } public int transform(int raw) { return (raw); } private int extractValue(Msg msg, int group) throws FieldException { String lowByte = getStringParameter("low_byte", ""); if (lowByte.equals("")) { logger.error("{} handler misconfigured, missing low_byte!", nm()); return 0; } int value = 0; if (lowByte.equals("group")) { value = group; } else { value = msg.getByte(lowByte) & 0xFF; } String highByte = getStringParameter("high_byte", ""); if (!highByte.equals("")) { value |= (msg.getByte(highByte) & 0xFF) << 8; } return (value); } } /** * Convert system mode field to number 0...4. Insteon has two different * conventions for numbering, we use the one of the status update messages */ public static class ThermostatSystemModeMsgHandler extends NumberMsgHandler { ThermostatSystemModeMsgHandler(DeviceFeature p) { super(p); } @Override public int transform(int raw) { switch (raw) { case 0: return (0); // off case 1: return (3); // auto case 2: return (1); // heat case 3: return (2); // cool case 4: return (4); // program default: break; } return (4); // when in doubt assume to be in "program" mode } } /** * Handle reply to system mode change command */ public static class ThermostatSystemModeReplyHandler extends NumberMsgHandler { ThermostatSystemModeReplyHandler(DeviceFeature p) { super(p); } @Override public int transform(int raw) { switch (raw) { case 0x09: return (0); // off case 0x04: return (1); // heat case 0x05: return (2); // cool case 0x06: return (3); // auto case 0x0A: return (4); // program default: break; } return (4); // when in doubt assume to be in "program" mode } } /** * Handle reply to fan mode change command */ public static class ThermostatFanModeReplyHandler extends NumberMsgHandler { ThermostatFanModeReplyHandler(DeviceFeature p) { super(p); } @Override public int transform(int raw) { switch (raw) { case 0x08: return (0); // auto case 0x07: return (1); // always on default: break; } return (0); // when in doubt assume to be auto mode } } /** * Handle reply to fanlinc fan speed change command */ public static class FanLincFanReplyHandler extends NumberMsgHandler { FanLincFanReplyHandler(DeviceFeature p) { super(p); } @Override public int transform(int raw) { switch (raw) { case 0x00: return (0); // off case 0x55: return (1); // low case 0xAA: return (2); // medium case 0xFF: return (3); // high default: logger.warn("fanlinc got unexpected level: {}", raw); } return (0); // when in doubt assume to be off } } /** * Process X10 messages that are generated when another controller * changes the state of an X10 device. */ public static class X10OnHandler extends MessageHandler { X10OnHandler(DeviceFeature p) { super(p); } @Override public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f, String fromPort) { InsteonAddress a = f.getDevice().getAddress(); logger.info("{}: set X10 device {} to ON", nm(), a); m_feature.publish(OnOffType.ON, StateChangeType.ALWAYS); } } public static class X10OffHandler extends MessageHandler { X10OffHandler(DeviceFeature p) { super(p); } @Override public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f, String fromPort) { InsteonAddress a = f.getDevice().getAddress(); logger.info("{}: set X10 device {} to OFF", nm(), a); m_feature.publish(OnOffType.OFF, StateChangeType.ALWAYS); } } public static class X10BrightHandler extends MessageHandler { X10BrightHandler(DeviceFeature p) { super(p); } @Override public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f, String fromPort) { InsteonAddress a = f.getDevice().getAddress(); logger.debug("{}: ignoring brighten message for device {}", nm(), a); } } public static class X10DimHandler extends MessageHandler { X10DimHandler(DeviceFeature p) { super(p); } @Override public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f, String fromPort) { InsteonAddress a = f.getDevice().getAddress(); logger.debug("{}: ignoring dim message for device {}", nm(), a); } } public static class X10OpenHandler extends MessageHandler { X10OpenHandler(DeviceFeature p) { super(p); } @Override public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f, String fromPort) { InsteonAddress a = f.getDevice().getAddress(); logger.info("{}: set X10 device {} to OPEN", nm(), a); m_feature.publish(OpenClosedType.OPEN, StateChangeType.ALWAYS); } } public static class X10ClosedHandler extends MessageHandler { X10ClosedHandler(DeviceFeature p) { super(p); } @Override public void handleMessage(int group, byte cmd1, Msg msg, DeviceFeature f, String fromPort) { InsteonAddress a = f.getDevice().getAddress(); logger.info("{}: set X10 device {} to CLOSED", nm(), a); m_feature.publish(OpenClosedType.CLOSED, StateChangeType.ALWAYS); } } /** * Factory method for creating handlers of a given name using java reflection * * @param name the name of the handler to create * @param params * @param f the feature for which to create the handler * @return the handler which was created */ public static <T extends MessageHandler> T s_makeHandler(String name, HashMap<String, String> params, DeviceFeature f) { String cname = MessageHandler.class.getName() + "$" + name; try { Class<?> c = Class.forName(cname); @SuppressWarnings("unchecked") Class<? extends T> dc = (Class<? extends T>) c; T mh = dc.getDeclaredConstructor(DeviceFeature.class).newInstance(f); mh.setParameters(params); return mh; } catch (Exception e) { logger.error("error trying to create message handler: {}", name, e); } return null; } }