/** * 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.zwave.internal.protocol; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.openhab.binding.zwave.internal.HexToIntegerConverter; import org.openhab.binding.zwave.internal.protocol.SerialMessage.SerialMessageClass; import org.openhab.binding.zwave.internal.protocol.ZWaveDeviceClass.Basic; import org.openhab.binding.zwave.internal.protocol.ZWaveDeviceClass.Generic; import org.openhab.binding.zwave.internal.protocol.ZWaveDeviceClass.Specific; import org.openhab.binding.zwave.internal.protocol.commandclass.ZWaveAssociationCommandClass; import org.openhab.binding.zwave.internal.protocol.commandclass.ZWaveCommandClass; import org.openhab.binding.zwave.internal.protocol.commandclass.ZWaveCommandClass.CommandClass; import org.openhab.binding.zwave.internal.protocol.commandclass.ZWaveMultiInstanceCommandClass; import org.openhab.binding.zwave.internal.protocol.commandclass.ZWaveNodeNamingCommandClass; import org.openhab.binding.zwave.internal.protocol.commandclass.ZWaveSecurityCommandClass; import org.openhab.binding.zwave.internal.protocol.commandclass.ZWaveVersionCommandClass; import org.openhab.binding.zwave.internal.protocol.commandclass.ZWaveWakeUpCommandClass; import org.openhab.binding.zwave.internal.protocol.event.ZWaveEvent; import org.openhab.binding.zwave.internal.protocol.event.ZWaveNodeStatusEvent; import org.openhab.binding.zwave.internal.protocol.initialization.ZWaveNodeInitStage; import org.openhab.binding.zwave.internal.protocol.initialization.ZWaveNodeStageAdvancer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.thoughtworks.xstream.annotations.XStreamAlias; import com.thoughtworks.xstream.annotations.XStreamConverter; import com.thoughtworks.xstream.annotations.XStreamOmitField; /** * Z-Wave node class. Represents a node in the Z-Wave network. * * @author Brian Crosby * @author Chris Jackson * @since 1.3.0 */ @XStreamAlias("node") public class ZWaveNode { @XStreamOmitField private static final Logger logger = LoggerFactory.getLogger(ZWaveNode.class); private final ZWaveDeviceClass deviceClass; @XStreamOmitField private ZWaveController controller; @XStreamOmitField private ZWaveNodeStageAdvancer nodeStageAdvancer; @XStreamOmitField private ZWaveNodeState nodeState; @XStreamConverter(HexToIntegerConverter.class) private int homeId = Integer.MAX_VALUE; private int nodeId = Integer.MAX_VALUE; private int version = Integer.MAX_VALUE; private String name; private String location; @XStreamConverter(HexToIntegerConverter.class) private int manufacturer = Integer.MAX_VALUE; @XStreamConverter(HexToIntegerConverter.class) private int deviceId = Integer.MAX_VALUE; @XStreamConverter(HexToIntegerConverter.class) private int deviceType = Integer.MAX_VALUE; private boolean listening; // i.e. sleeping private boolean frequentlyListening; private boolean routing; private String healState; @SuppressWarnings("unused") private boolean security; @SuppressWarnings("unused") private boolean beaming; @SuppressWarnings("unused") private int maxBaudRate; // Keep the NIF - just used for information and debug in the XML @SuppressWarnings("unused") private List<Integer> nodeInformationFrame = null; private Map<CommandClass, ZWaveCommandClass> supportedCommandClasses = new HashMap<CommandClass, ZWaveCommandClass>(); private Set<CommandClass> securedCommandClasses = new HashSet<CommandClass>(); private List<Integer> nodeNeighbors = new ArrayList<Integer>(); private Date lastSent = null; private Date lastReceived = null; @XStreamOmitField private boolean applicationUpdateReceived = false; @XStreamOmitField private int resendCount = 0; @XStreamOmitField private int receiveCount = 0; @XStreamOmitField private int sendCount = 0; @XStreamOmitField private int deadCount = 0; @XStreamOmitField private Date deadTime; @XStreamOmitField private int retryCount = 0; /** * Constructor. Creates a new instance of the ZWaveNode class. * * @param homeId the home ID to use. * @param nodeId the node ID to use. * @param controller the wave controller instance */ public ZWaveNode(int homeId, int nodeId, ZWaveController controller) { nodeState = ZWaveNodeState.ALIVE; this.homeId = homeId; this.nodeId = nodeId; this.controller = controller; this.nodeStageAdvancer = new ZWaveNodeStageAdvancer(this, controller); this.deviceClass = new ZWaveDeviceClass(Basic.NOT_KNOWN, Generic.NOT_KNOWN, Specific.NOT_USED); } /** * Configures the node after it's been restored from file. * NOTE: XStream doesn't run any default constructor. So, any initialisation * made in a constructor, or statically, won't be performed!!! * Set defaults here if it's important!!! * * @param controller the wave controller instance */ public void setRestoredFromConfigfile(ZWaveController controller) { nodeState = ZWaveNodeState.ALIVE; this.controller = controller; // Create the initialisation advancer and tell it we've loaded from file this.nodeStageAdvancer = new ZWaveNodeStageAdvancer(this, controller); this.nodeStageAdvancer.setRestoredFromConfigfile(); nodeStageAdvancer.setCurrentStage(ZWaveNodeInitStage.EMPTYNODE); } /** * Gets the node ID. * * @return the node id */ public int getNodeId() { return nodeId; } /** * Gets whether the node is listening. * * @return boolean indicating whether the node is listening or not. */ public boolean isListening() { return listening; } /** * Sets whether the node is listening. * * @param listening */ public void setListening(boolean listening) { this.listening = listening; } /** * Gets whether the node is frequently listening. * Frequently listening is responding to a beam signal. Apart from * increased latency, nothing else is noticeable from the serial api * side. * * @return boolean indicating whether the node is frequently * listening or not. */ public boolean isFrequentlyListening() { return frequentlyListening; } /** * Sets whether the node is frequently listening. * Frequently listening is responding to a beam signal. Apart from * increased latency, nothing else is noticeable from the serial api * side. * * @param frequentlyListening indicating whether the node is frequently * listening or not. */ public void setFrequentlyListening(boolean frequentlyListening) { this.frequentlyListening = frequentlyListening; } /** * Gets the Heal State of the node. * * @return String indicating the node Heal State. */ public String getHealState() { return healState; } /** * Sets the Heal State of the node. * * @param healState */ public void setHealState(String healState) { this.healState = healState; } /** * Gets whether the node is dead. * * @return */ public boolean isDead() { if (nodeState == ZWaveNodeState.DEAD || nodeState == ZWaveNodeState.FAILED) { return true; } else { return false; } } /** * Sets the node to be 'undead'. */ public void setNodeState(ZWaveNodeState state) { // Make sure we only handle real state changes if (state == nodeState) { return; } switch (state) { case ALIVE: logger.debug("NODE {}: Node has risen from the DEAD. Init stage is {}:{}.", nodeId, this.getNodeInitializationStage().toString()); // Reset the resend counter this.resendCount = 0; break; case DEAD: // If the node is failed, then we don't allow transitions to DEAD // The only valid state change from FAILED is to ALIVE if (nodeState == ZWaveNodeState.FAILED) { return; } case FAILED: this.deadCount++; this.deadTime = Calendar.getInstance().getTime(); logger.debug("NODE {}: Node is DEAD.", this.nodeId); break; } // Don't alert state changes while we're still initialising if (nodeStageAdvancer.isInitializationComplete() == true) { ZWaveEvent zEvent = new ZWaveNodeStatusEvent(this.getNodeId(), ZWaveNodeState.DEAD); controller.notifyEventListeners(zEvent); } else { logger.debug("NODE {}: Initialisation incomplete, not signalling state change.", this.nodeId); } nodeState = state; } /** * Gets the home ID * * @return the homeId */ public Integer getHomeId() { return homeId; } /** * Gets the node name. * If Node Naming Command Class is supported get name from device, * else return the name stored in binding * * @return the name */ public String getName() { ZWaveNodeNamingCommandClass commandClass = (ZWaveNodeNamingCommandClass) getCommandClass( CommandClass.NODE_NAMING); if (commandClass == null) { return this.name; } return commandClass.getName(); } /** * Sets the node name. * If Node Naming Command Class is supported set name in the device, * else set it in locally in the binding * * @param name the name to set */ public void setName(String name) { ZWaveNodeNamingCommandClass commandClass = (ZWaveNodeNamingCommandClass) getCommandClass( CommandClass.NODE_NAMING); if (commandClass == null) { this.name = name; return; } SerialMessage m = commandClass.setNameMessage(name); this.controller.sendData(m); m = commandClass.getNameMessage(); this.controller.sendData(m); } /** * Gets the node location. * If Node Naming Command Class is supported get location from device, * else return the location stored in binding * * @return the location */ public String getLocation() { ZWaveNodeNamingCommandClass commandClass = (ZWaveNodeNamingCommandClass) getCommandClass( CommandClass.NODE_NAMING); if (commandClass == null) { return this.location; } return commandClass.getLocation(); } /** * Sets the node location. * If Node Naming Command Class is supported set location in the device, * else set it in locally in the binding * * @param location the location to set */ public void setLocation(String location) { ZWaveNodeNamingCommandClass commandClass = (ZWaveNodeNamingCommandClass) getCommandClass( CommandClass.NODE_NAMING); if (commandClass == null) { this.location = location; return; } SerialMessage m = commandClass.setLocationMessage(location); this.controller.sendData(m); m = commandClass.getLocationMessage(); this.controller.sendData(m); } /** * Gets the manufacturer of the node. * * @return the manufacturer */ public int getManufacturer() { return manufacturer; } /** * Sets the manufacturer of the node. * * @param tempMan the manufacturer to set */ public void setManufacturer(int tempMan) { this.manufacturer = tempMan; } /** * Gets the device id of the node. * * @return the deviceId */ public int getDeviceId() { return deviceId; } /** * Sets the device id of the node. * * @param tempDeviceId the device to set */ public void setDeviceId(int tempDeviceId) { this.deviceId = tempDeviceId; } /** * Gets the device type of the node. * * @return the deviceType */ public int getDeviceType() { return deviceType; } /** * Sets the device type of the node. * * @param tempDeviceType the deviceType to set */ public void setDeviceType(int tempDeviceType) { this.deviceType = tempDeviceType; } /** * Get the date/time the node was last updated (ie a frame was received from it). * * @return the lastUpdated time */ public Date getLastReceived() { return lastReceived; } /** * Get the date/time we last sent a frame to the node. * * @return the lastSent */ public Date getLastSent() { return lastSent; } /** * Gets the node state. * * @return the nodeState */ public ZWaveNodeState getNodeState() { return this.nodeState; } /** * Gets the node stage. * * @return the nodeStage */ public ZWaveNodeInitStage getNodeInitializationStage() { return this.nodeStageAdvancer.getCurrentStage(); } /** * Gets the initialization state * * @return true if initialization has been completed */ public boolean isInitializationComplete() { return this.nodeStageAdvancer.isInitializationComplete(); } /** * Sets the node stage. * * @param nodeStage the nodeStage to set */ public void setNodeStage(ZWaveNodeInitStage nodeStage) { nodeStageAdvancer.setCurrentStage(nodeStage); } /** * Gets the node version * * @return the version */ public int getVersion() { return version; } /** * Sets the node version. * * @param version the version to set */ public void setVersion(int version) { this.version = version; } /** * Gets the node application firmware version * * @return the version */ public String getApplicationVersion() { ZWaveVersionCommandClass versionCmdClass = (ZWaveVersionCommandClass) this .getCommandClass(CommandClass.VERSION); if (versionCmdClass == null) { return "0.0"; } String appVersion = versionCmdClass.getApplicationVersion(); if (appVersion == null) { logger.trace("NODE {}: App version requested but version is unknown", this.getNodeId()); return "0.0"; } return appVersion; } /** * Gets whether the node is routing messages. * * @return the routing */ public boolean isRouting() { return routing; } /** * Sets whether the node is routing messages. * * @param routing the routing to set */ public void setRouting(boolean routing) { this.routing = routing; } /** * Gets the time stamp the node was last queried. * * @return the queryStageTimeStamp */ public Date getQueryStageTimeStamp() { return this.nodeStageAdvancer.getQueryStageTimeStamp(); } /** * Increments the resend counter. * On three increments the node stage is set to DEAD and no * more messages will be sent. * This is only used for SendData messages. */ public void incrementResendCount() { if (++resendCount >= 3) { setNodeState(ZWaveNodeState.DEAD); } this.retryCount++; } /** * Resets the resend counter and possibly resets the * node stage to DONE when previous initialization was * complete. * Note that if the node is DEAD, then the nodeStage stays DEAD */ public void resetResendCount() { this.resendCount = 0; if (this.nodeStageAdvancer.isInitializationComplete() && this.isDead() == false) { nodeStageAdvancer.setCurrentStage(ZWaveNodeInitStage.DONE); } } /** * Returns the device class of the node. * * @return the deviceClass */ public ZWaveDeviceClass getDeviceClass() { return deviceClass; } /** * Returns the Command classes this node implements. * * @return the command classes. */ public Collection<ZWaveCommandClass> getCommandClasses() { return supportedCommandClasses.values(); } /** * Returns a commandClass object this node implements. * Returns null if command class is not supported by this node. * * @param commandClass The command class to get. * @return the command class. */ public ZWaveCommandClass getCommandClass(CommandClass commandClass) { return supportedCommandClasses.get(commandClass); } /** * Returns whether a node supports this command class. * * @param commandClass the command class to check * @return true if the command class is supported, false otherwise. */ public boolean supportsCommandClass(CommandClass commandClass) { return supportedCommandClasses.containsKey(commandClass); } /** * Adds a command class to the list of supported command classes by this node. * Does nothing if command class is already added. * * @param commandClass the command class instance to add. */ public void addCommandClass(ZWaveCommandClass commandClass) { CommandClass key = commandClass.getCommandClass(); if (!supportedCommandClasses.containsKey(key)) { logger.debug("NODE {}: Adding command class {} to the list of supported command classes.", nodeId, commandClass.getCommandClass().getLabel()); supportedCommandClasses.put(key, commandClass); if (commandClass instanceof ZWaveEventListener) { this.controller.addEventListener((ZWaveEventListener) commandClass); } } } /** * Removes a command class from the node. * This is used to remove classes that a node may report it supports * but it doesn't respond to. * * @param commandClass The command class key */ public void removeCommandClass(CommandClass commandClass) { supportedCommandClasses.remove(commandClass); } /** * Resolves a command class for this node. First endpoint is checked. * If endpoint == 0 or (endpoint != 1 and version of the multi instance * command == 1) then return a supported command class on the node itself. * If endpoint != 1 and version of the multi instance command == 2 then * first try command classes of endpoints. If not found the return a * supported command class on the node itself. * Returns null if a command class is not found. * * @param commandClass The command class to resolve. * @param endpointId the endpoint / instance to resolve this command class for. * @return the command class. */ public ZWaveCommandClass resolveCommandClass(CommandClass commandClass, int endpointId) { if (commandClass == null) { return null; } if (endpointId == 0) { return getCommandClass(commandClass); } ZWaveMultiInstanceCommandClass multiInstanceCommandClass = (ZWaveMultiInstanceCommandClass) supportedCommandClasses .get(CommandClass.MULTI_INSTANCE); if (multiInstanceCommandClass == null) { return null; } else if (multiInstanceCommandClass.getVersion() == 2) { ZWaveEndpoint endpoint = multiInstanceCommandClass.getEndpoint(endpointId); if (endpoint != null) { ZWaveCommandClass result = endpoint.getCommandClass(commandClass); if (result != null) { return result; } } } else if (multiInstanceCommandClass.getVersion() == 1) { ZWaveCommandClass result = getCommandClass(commandClass); if (result != null && endpointId <= result.getInstances()) { return result; } } else { logger.warn("NODE {}: Unsupported multi instance command version: {}.", nodeId, multiInstanceCommandClass.getVersion()); } return null; } /** * Initialise the node */ public void initialiseNode() { this.nodeStageAdvancer.startInitialisation(); } /** * Encapsulates a serial message for sending to a * multi-instance instance/ multi-channel endpoint on * a node. * * @param serialMessage the serial message to encapsulate * @param commandClass the command class used to generate the message. * @param endpointId the instance / endpoint to encapsulate the message for * @param node the destination node. * @return SerialMessage on success, null on failure. */ public SerialMessage encapsulate(SerialMessage serialMessage, ZWaveCommandClass commandClass, int endpointId) { ZWaveMultiInstanceCommandClass multiInstanceCommandClass; if (serialMessage == null) { return null; } // no encapsulation necessary. if (endpointId == 0) { return serialMessage; } multiInstanceCommandClass = (ZWaveMultiInstanceCommandClass) this.getCommandClass(CommandClass.MULTI_INSTANCE); if (multiInstanceCommandClass != null) { logger.debug("NODE {}: Encapsulating message, instance / endpoint {}", this.getNodeId(), endpointId); switch (multiInstanceCommandClass.getVersion()) { case 2: if (commandClass.getEndpoint() != null) { serialMessage = multiInstanceCommandClass.getMultiChannelEncapMessage(serialMessage, commandClass.getEndpoint()); return serialMessage; } break; case 1: default: if (commandClass.getInstances() >= endpointId) { serialMessage = multiInstanceCommandClass.getMultiInstanceEncapMessage(serialMessage, endpointId); return serialMessage; } break; } } logger.warn("NODE {}: Encapsulating message, instance / endpoint {} failed, will discard message.", this.getNodeId(), endpointId); return null; } /** * Return a list with the nodes neighbors * * @return list of node IDs */ public List<Integer> getNeighbors() { return nodeNeighbors; } /** * Clear the neighbor list */ public void clearNeighbors() { nodeNeighbors.clear(); } /** * Updates a nodes routing information * Generation of routes uses associations * * @param nodeId */ public ArrayList<Integer> getRoutingList() { logger.debug("NODE {}: Update return routes", nodeId); // Create a list of nodes this device is configured to talk to ArrayList<Integer> routedNodes = new ArrayList<Integer>(); // Only update routes if this is a routing node if (isRouting() == false) { logger.debug("NODE {}: Node is not a routing node. No routes can be set.", nodeId); return null; } // Get the number of association groups reported by this node ZWaveAssociationCommandClass associationCmdClass = (ZWaveAssociationCommandClass) getCommandClass( CommandClass.ASSOCIATION); if (associationCmdClass == null) { logger.debug("NODE {}: Node has no association class. No routes can be set.", nodeId); return null; } int groups = associationCmdClass.getGroupCount(); if (groups != 0) { // Loop through each association group and add the node ID to the list for (int group = 1; group <= groups; group++) { for (Integer associationNodeId : associationCmdClass.getGroupMembers(group)) { routedNodes.add(associationNodeId); } } } // Add the wakeup destination node to the list for battery devices ZWaveWakeUpCommandClass wakeupCmdClass = (ZWaveWakeUpCommandClass) getCommandClass(CommandClass.WAKE_UP); if (wakeupCmdClass != null) { Integer wakeupNodeId = wakeupCmdClass.getTargetNodeId(); routedNodes.add(wakeupNodeId); } // Are there any nodes to which we need to set routes? if (routedNodes.size() == 0) { logger.debug("NODE {}: No return routes required.", nodeId); return null; } return routedNodes; } /** * Add a node ID to the neighbor list * * @param nodeId the node to add */ public void addNeighbor(Integer nodeId) { nodeNeighbors.add(nodeId); } /** * Gets the number of times the node has been determined as DEAD * * @return dead count */ public int getDeadCount() { return deadCount; } /** * Gets the number of times the node has been determined as DEAD * * @return dead count */ public Date getDeadTime() { return deadTime; } /** * Gets the number of packets that have been resent to the node * * @return retry count */ public int getRetryCount() { return retryCount; } /** * Increments the sent packet counter and records the last sent time * This is simply used for statistical purposes to assess the health * of a node. */ public void incrementSendCount() { sendCount++; this.lastSent = Calendar.getInstance().getTime(); } /** * Increments the received packet counter and records the last received time * This is simply used for statistical purposes to assess the health * of a node. */ public void incrementReceiveCount() { receiveCount++; this.lastReceived = Calendar.getInstance().getTime(); } /** * Gets the number of packets sent to the node * * @return send count */ public int getSendCount() { return sendCount; } /** * Gets the applicationUpdateReceived flag. * This is set to indicate that we have received the required information from the device * * @return true if information received */ public boolean getApplicationUpdateReceived() { return applicationUpdateReceived; } /** * Sets the applicationUpdateReceived flag. * This is set to indicate that we have received the required information from the device * * @param received true if received */ public void setApplicationUpdateReceived(boolean received) { applicationUpdateReceived = received; } public void updateNIF(List<Integer> nif) { nodeInformationFrame = nif; } public void setSecurity(boolean security) { this.security = security; } public void setBeaming(boolean beaming) { this.beaming = beaming; } public void setMaxBaud(int maxBaudRate) { this.maxBaudRate = maxBaudRate; } // TODO: DB do we really need this? public ZWaveController getController() { return controller; } /** * Invoked by {@link ZWaveSecurityCommandClass} when a * {@link ZWaveSecurityCommandClass#SECURITY_SUPPORTED_REPORT} is received. * * @param data the class id for each class which must be encrypted in transmission */ public void setSecuredClasses(byte[] data) { logger.info("NODE {}: Setting secured command classes for node with {}", this.getNodeId(), SerialMessage.bb2hex(data)); boolean afterMark = false; securedCommandClasses.clear(); // reset the existing list for (final byte aByte : data) { // TODO: DB support extended commandClass format by checking for 0xF1 - 0xFF if (ZWaveSecurityCommandClass.bytesAreEqual(aByte, ZWaveSecurityCommandClass.COMMAND_CLASS_MARK)) { /** * Marks the end of the list of supported command classes. The remaining classes are those * that can be controlled by the device. These classes are created without values. * Messages received cause notification events instead. */ afterMark = true; continue; } // Check if this is a commandClass that is already registered with the node final CommandClass commandClass = CommandClass.getCommandClass((aByte & 0xFF)); if (commandClass == null) { // Not supported by OpenHab logger.error( "NODE {}: setSecuredClasses requested secure " + "class NOT supported by OpenHab: {} afterMark={}", this.getNodeId(), commandClass, afterMark); } else { // Sometimes security will be transmitted as a secure class, but it // can't be set that way since it's the one doing the encryption work So ignore that. if (commandClass == CommandClass.SECURITY) { continue; } else if (afterMark) { // Nothing to do, we don't track devices that control other devices logger.info("NODE {}: is after mark for commandClass {}", this.getNodeId(), commandClass); break; } else { if (!this.supportsCommandClass(commandClass)) { logger.info( "NODE {}: Adding secured command class to supported that wasn't in original list {}", this.getNodeId(), commandClass.getLabel()); final ZWaveCommandClass classInstance = ZWaveCommandClass.getInstance((aByte & 0xFF), this, controller); if (classInstance != null) { addCommandClass(classInstance); } } securedCommandClasses.add(commandClass); logger.info("NODE {}: (Secured) {}", this.getNodeId(), commandClass.getLabel()); } } } if (logger.isInfoEnabled()) { // show which classes are still insecure after the update final StringBuilder buf = new StringBuilder( "NODE " + this.getNodeId() + ": After update, INSECURE command classes are: "); for (final ZWaveCommandClass zwCommandClass : this.getCommandClasses()) { if (!securedCommandClasses.contains(zwCommandClass.getCommandClass())) { buf.append(zwCommandClass.getCommandClass() + ", "); } } logger.info(buf.toString().substring(0, buf.toString().length() - 1)); } } public boolean doesMessageRequireSecurityEncapsulation(SerialMessage serialMessage) { boolean result = false; if (serialMessage.getMessageClass() != SerialMessageClass.SendData) { result = false; } else if (!supportedCommandClasses.containsKey(CommandClass.SECURITY)) { // Does this node support security at all? result = false; } else { final int commandClassCode = (byte) serialMessage.getMessagePayloadByte(2) & 0xFF; final CommandClass commandClassOfMessage = CommandClass.getCommandClass(commandClassCode); if (commandClassOfMessage == null) { // not sure how we would ever get here logger.warn(String.format("NODE %s: CommandClass not found for 0x%02X so treating as INSECURE %s", getNodeId(), commandClassCode, serialMessage)); result = false; } else if (CommandClass.SECURITY == commandClassOfMessage) { // CommandClass.SECURITY is a special case because only <b>some</b> commands get encrypted final Byte messageCode = Byte.valueOf((byte) (serialMessage.getMessagePayloadByte(3) & 0xFF)); result = ZWaveSecurityCommandClass.doesCommandRequireSecurityEncapsulation(messageCode); } else if (commandClassOfMessage == CommandClass.NO_OPERATION) { // TODO: DB // On controller startup, PING seems to fail whenever it's encrypted, so don't // TODO: DB try again result = false; } else { result = securedCommandClasses.contains(commandClassOfMessage); if (!result) { // Certain messages must always be sent securely per the zwave spec if (commandClassOfMessage == CommandClass.DOOR_LOCK || commandClassOfMessage == CommandClass.USER_CODE) { // TODO: DB what else? logger.warn("NODE {}: CommandClass {} is not marked as secure but should be, forcing secure", getNodeId(), commandClassOfMessage); result = true; } } } if (result) { logger.trace("NODE {}: Message {} requires security encapsulation", getNodeId(), serialMessage); } } return result; } }