/**
* Copyright (c) 2010-2014, openHAB.org and others.
*
* 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 gnu.io.CommPort;
import gnu.io.CommPortIdentifier;
import gnu.io.NoSuchPortException;
import gnu.io.PortInUseException;
import gnu.io.SerialPort;
import gnu.io.UnsupportedCommOperationException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.openhab.binding.zwave.internal.protocol.SerialMessage.SerialMessageClass;
import org.openhab.binding.zwave.internal.protocol.SerialMessage.SerialMessagePriority;
import org.openhab.binding.zwave.internal.protocol.SerialMessage.SerialMessageType;
import org.openhab.binding.zwave.internal.protocol.NodeStage;
import org.openhab.binding.zwave.internal.protocol.commandclass.ZWaveCommandClass.CommandClass;
import org.openhab.binding.zwave.internal.protocol.commandclass.ZWaveCommandClass;
import org.openhab.binding.zwave.internal.protocol.commandclass.ZWaveCommandClassDynamicState;
import org.openhab.binding.zwave.internal.protocol.commandclass.ZWaveMultiInstanceCommandClass;
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.ZWaveInclusionEvent;
import org.openhab.binding.zwave.internal.protocol.event.ZWaveInitializationCompletedEvent;
import org.openhab.binding.zwave.internal.protocol.event.ZWaveNetworkEvent;
import org.openhab.binding.zwave.internal.protocol.event.ZWaveNodeStatusEvent;
import org.openhab.binding.zwave.internal.protocol.event.ZWaveTransactionCompletedEvent;
import org.openhab.binding.zwave.internal.protocol.event.ZWaveNetworkEvent.State;
import org.openhab.binding.zwave.internal.protocol.initialization.ZWaveNodeSerializer;
import org.openhab.binding.zwave.internal.protocol.serialmessage.AddNodeMessageClass;
import org.openhab.binding.zwave.internal.protocol.serialmessage.AssignReturnRouteMessageClass;
import org.openhab.binding.zwave.internal.protocol.serialmessage.AssignSucReturnRouteMessageClass;
import org.openhab.binding.zwave.internal.protocol.serialmessage.DeleteReturnRouteMessageClass;
import org.openhab.binding.zwave.internal.protocol.serialmessage.EnableSucMessageClass;
import org.openhab.binding.zwave.internal.protocol.serialmessage.GetControllerCapabilitiesMessageClass;
import org.openhab.binding.zwave.internal.protocol.serialmessage.GetSucNodeIdMessageClass;
import org.openhab.binding.zwave.internal.protocol.serialmessage.IdentifyNodeMessageClass;
import org.openhab.binding.zwave.internal.protocol.serialmessage.IsFailedNodeMessageClass;
import org.openhab.binding.zwave.internal.protocol.serialmessage.RemoveNodeMessageClass;
import org.openhab.binding.zwave.internal.protocol.serialmessage.RequestNodeNeighborUpdateMessageClass;
import org.openhab.binding.zwave.internal.protocol.serialmessage.RemoveFailedNodeMessageClass;
import org.openhab.binding.zwave.internal.protocol.serialmessage.RequestNodeInfoMessageClass;
import org.openhab.binding.zwave.internal.protocol.serialmessage.GetRoutingInfoMessageClass;
import org.openhab.binding.zwave.internal.protocol.serialmessage.SendDataMessageClass;
import org.openhab.binding.zwave.internal.protocol.serialmessage.SerialApiSoftResetMessageClass;
import org.openhab.binding.zwave.internal.protocol.serialmessage.SetSucNodeMessageClass;
import org.openhab.binding.zwave.internal.protocol.serialmessage.ZWaveCommandProcessor;
import org.openhab.binding.zwave.internal.protocol.serialmessage.GetVersionMessageClass;
import org.openhab.binding.zwave.internal.protocol.serialmessage.MemoryGetIdMessageClass;
import org.openhab.binding.zwave.internal.protocol.serialmessage.SerialApiGetCapabilitiesMessageClass;
import org.openhab.binding.zwave.internal.protocol.serialmessage.SerialApiGetInitDataMessageClass;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* ZWave controller class. Implements communication with the Z-Wave
* controller stick using serial messages.
* @author Victor Belov
* @author Brian Crosby
* @author Chris Jackson
* @since 1.3.0
*/
public class ZWaveController {
private static final Logger logger = LoggerFactory.getLogger(ZWaveController.class);
private static final int QUERY_STAGE_TIMEOUT = 120000;
private static final int ZWAVE_RESPONSE_TIMEOUT = 5000; // 5000 ms ZWAVE_RESPONSE TIMEOUT
private static final int ZWAVE_RECEIVE_TIMEOUT = 1000; // 1000 ms ZWAVE_RECEIVE_TIMEOUT
private static final int INITIAL_QUEUE_SIZE = 128;
private static final long WATCHDOG_TIMER_PERIOD = 10000; // 10 seconds watchdog timer
private static final int TRANSMIT_OPTION_ACK = 0x01;
private static final int TRANSMIT_OPTION_AUTO_ROUTE = 0x04;
private static final int TRANSMIT_OPTION_EXPLORE = 0x20;
private final Map<Integer, ZWaveNode> zwaveNodes = new HashMap<Integer, ZWaveNode>();
private final ArrayList<ZWaveEventListener> zwaveEventListeners = new ArrayList<ZWaveEventListener>();
private final PriorityBlockingQueue<SerialMessage> sendQueue = new PriorityBlockingQueue<SerialMessage>(INITIAL_QUEUE_SIZE, new SerialMessage.SerialMessageComparator(this));
private ZWaveSendThread sendThread;
private ZWaveReceiveThread receiveThread;
private final Semaphore transactionCompleted = new Semaphore(1);
private volatile SerialMessage lastSentMessage = null;
private long lastMessageStartTime = 0;
private long longestResponseTime = 0;
private SerialPort serialPort;
private int zWaveResponseTimeout = ZWAVE_RESPONSE_TIMEOUT;
private Timer watchdog;
private String zWaveVersion = "Unknown";
private String serialAPIVersion = "Unknown";
private int homeId = 0;
private int ownNodeId = 0;
private int manufactureId = 0;
private int deviceType = 0;
private int deviceId = 0;
private int ZWaveLibraryType = 0;
private int sentDataPointer = 1;
private boolean setSUC = false;
private ZWaveDeviceType controllerType = ZWaveDeviceType.UNKNOWN;
private int sucID = 0;
private int SOFCount = 0;
private int CANCount = 0;
private int NAKCount = 0;
private int ACKCount = 0;
private int OOFCount = 0;
private AtomicInteger timeOutCount = new AtomicInteger(0);
private boolean initializationComplete = false;
private boolean isConnected;
// Constructors
/**
* Constructor. Creates a new instance of the Z-Wave controller class.
* @param serialPortName the serial port name to use for
* communication with the Z-Wave controller stick.
* @throws SerialInterfaceException when a connection error occurs.
*/
public ZWaveController(final boolean isSUC, final String serialPortName, final Integer timeout) throws SerialInterfaceException {
logger.info("Starting Z-Wave controller");
this.setSUC = isSUC;
if(timeout != null && timeout >= 1500 && timeout <= 10000) {
zWaveResponseTimeout = timeout;
}
logger.info("Z-Wave timeout is set to {}ms.", zWaveResponseTimeout);
connect(serialPortName);
this.watchdog = new Timer(true);
this.watchdog.schedule(
new WatchDogTimerTask(serialPortName),
WATCHDOG_TIMER_PERIOD, WATCHDOG_TIMER_PERIOD);
}
// Incoming message handlers
/**
* Handles incoming Serial Messages. Serial messages can either be messages
* that are a response to our own requests, or the stick asking us information.
* @param incomingMessage the incoming message to process.
*/
private void handleIncomingMessage(SerialMessage incomingMessage) {
logger.trace("Incoming message to process");
logger.debug(incomingMessage.toString());
switch (incomingMessage.getMessageType()) {
case Request:
handleIncomingRequestMessage(incomingMessage);
break;
case Response:
handleIncomingResponseMessage(incomingMessage);
break;
default:
logger.warn("Unsupported incomingMessageType: 0x%02X", incomingMessage.getMessageType());
}
}
/**
* Handles an incoming request message.
* An incoming request message is a message initiated by a node or the controller.
* @param incomingMessage the incoming message to process.
*/
private void handleIncomingRequestMessage(SerialMessage incomingMessage) {
logger.trace("Message type = REQUEST");
ZWaveCommandProcessor processor = ZWaveCommandProcessor.getMessageDispatcher(incomingMessage.getMessageClass());
if(processor != null) {
processor.handleRequest(this, lastSentMessage, incomingMessage);
if(processor.isTransactionComplete()) {
notifyEventListeners(new ZWaveTransactionCompletedEvent(this.lastSentMessage));
transactionCompleted.release();
logger.trace("Released. Transaction completed permit count -> {}", transactionCompleted.availablePermits());
}
}
else {
logger.warn(String.format("TODO: Implement processing of Request Message = %s (0x%02X)",
incomingMessage.getMessageClass().getLabel(),
incomingMessage.getMessageClass().getKey()));
}
}
/**
* Handles a failed SendData request. This can either be because of the stick actively reporting it
* or because of a time-out of the transaction in the send thread.
* @param originalMessage the original message that was sent
*/
private void handleFailedSendDataRequest(SerialMessage originalMessage) {
new SendDataMessageClass().handleFailedSendDataRequest(this, originalMessage);
}
/**
* Handles an incoming response message.
* An incoming response message is a response, based one of our own requests.
* @param incomingMessage the response message to process.
*/
private void handleIncomingResponseMessage(SerialMessage incomingMessage) {
logger.trace("Message type = RESPONSE");
ZWaveCommandProcessor processor = ZWaveCommandProcessor.getMessageDispatcher(incomingMessage.getMessageClass());
if(processor != null) {
processor.handleResponse(this, lastSentMessage, incomingMessage);
if(processor.isTransactionComplete()) {
notifyEventListeners(new ZWaveTransactionCompletedEvent(this.lastSentMessage));
transactionCompleted.release();
logger.trace("Released. Transaction completed permit count -> {}", transactionCompleted.availablePermits());
}
}
else {
logger.warn(String.format("TODO: Implement processing of Response Message = %s (0x%02X)",
incomingMessage.getMessageClass().getLabel(),
incomingMessage.getMessageClass().getKey()));
}
switch (incomingMessage.getMessageClass()) {
case GetVersion:
this.zWaveVersion = ((GetVersionMessageClass)processor).getVersion();
this.ZWaveLibraryType = ((GetVersionMessageClass)processor).getLibraryType();
break;
case MemoryGetId:
this.ownNodeId = ((MemoryGetIdMessageClass)processor).getNodeId();
this.homeId = ((MemoryGetIdMessageClass)processor).getHomeId();
break;
case SerialApiGetInitData:
this.isConnected = true;
for(Integer nodeId : ((SerialApiGetInitDataMessageClass)processor).getNodes()) {
ZWaveNode node = null;
try {
ZWaveNodeSerializer nodeSerializer = new ZWaveNodeSerializer();
node = nodeSerializer.DeserializeNode(nodeId);
}
catch (Exception e) {
logger.error("NODE {}: Error deserialising XML file. {}", nodeId, e.toString());
node = null;
}
String name = null;
String location = null;
// Did the node deserialise ok?
if (node != null) {
// Remember the name and location - in case we decide the file was invalid
name = node.getName();
location = node.getLocation();
// Sanity check the data from the file
if (node.getManufacturer() == Integer.MAX_VALUE ||
node.getHomeId() != this.homeId ||
node.getNodeId() != nodeId) {
logger.warn("NODE {}: Config file data is invalid, ignoring config.", nodeId);
node = null;
}
else {
// The restore was ok, but we have some work to set up the links that aren't
// made as the deserialiser doesn't call the constructor
logger.debug("NODE {}: Restored from config.", nodeId);
node.setRestoredFromConfigfile(this);
// Set the controller and node references for all command classes
for (ZWaveCommandClass commandClass : node.getCommandClasses()) {
commandClass.setController(this);
commandClass.setNode(node);
// Handle event handlers
if (commandClass instanceof ZWaveEventListener) {
this.addEventListener((ZWaveEventListener)commandClass);
}
// If this is the multi-instance class, add all command classes for the endpoints
if (commandClass instanceof ZWaveMultiInstanceCommandClass) {
for (ZWaveEndpoint endPoint : ((ZWaveMultiInstanceCommandClass) commandClass)
.getEndpoints()) {
for (ZWaveCommandClass endpointCommandClass : endPoint.getCommandClasses()) {
endpointCommandClass.setController(this);
endpointCommandClass.setNode(node);
endpointCommandClass.setEndpoint(endPoint);
// Handle event handlers
if (endpointCommandClass instanceof ZWaveEventListener) {
this.addEventListener((ZWaveEventListener)endpointCommandClass);
}
}
}
}
}
}
}
// Create a new node if it wasn't deserialised ok
if(node == null) {
node = new ZWaveNode(this.homeId, nodeId, this);
// Try to maintain the name and location (user supplied data)
// even if the XML file was considered corrupt and we reload data from the device.
node.setName(name);
node.setLocation(location);
}
if(nodeId == this.ownNodeId) {
// This is the controller node.
// We already know the device type, id, manufacturer so set it here
// It won't be set later as we probably won't request the manufacturer specific data
node.setDeviceId(this.getDeviceId());
node.setDeviceType(this.getDeviceType());
node.setManufacturer(this.getManufactureId());
}
// Place nodes in the local ZWave Controller
this.zwaveNodes.put(nodeId, node);
node.advanceNodeStage(NodeStage.PROTOINFO);
}
break;
case GetSucNodeId:
// Remember the SUC ID
this.sucID = ((GetSucNodeIdMessageClass)processor).getSucNodeId();
// If we want to be the SUC, enable it here
if(this.setSUC == true && this.sucID == 0) {
// We want to be SUC
this.enqueue(new EnableSucMessageClass().doRequest(EnableSucMessageClass.SUCType.SERVER));
this.enqueue(new SetSucNodeMessageClass().doRequest(this.ownNodeId, SetSucNodeMessageClass.SUCType.SERVER));
}
else if(this.setSUC == false && this.sucID == this.ownNodeId) {
// We don't want to be SUC, but we currently are!
// Disable SERVER functionality, and set the node to 0
this.enqueue(new EnableSucMessageClass().doRequest(EnableSucMessageClass.SUCType.NONE));
this.enqueue(new SetSucNodeMessageClass().doRequest(this.ownNodeId, SetSucNodeMessageClass.SUCType.NONE));
}
this.enqueue(new GetControllerCapabilitiesMessageClass().doRequest());
break;
case SerialApiGetCapabilities:
this.serialAPIVersion = ((SerialApiGetCapabilitiesMessageClass)processor).getSerialAPIVersion();
this.manufactureId = ((SerialApiGetCapabilitiesMessageClass)processor).getManufactureId();
this.deviceId = ((SerialApiGetCapabilitiesMessageClass)processor).getDeviceId();
this.deviceType = ((SerialApiGetCapabilitiesMessageClass)processor).getDeviceType();
this.enqueue(new SerialApiGetInitDataMessageClass().doRequest());
break;
case GetControllerCapabilities:
this.controllerType = ((GetControllerCapabilitiesMessageClass)processor).getDeviceType();
break;
default:
break;
}
}
// Controller methods
/**
* Connects to the comm port and starts send and receive threads.
* @param serialPortName the port name to open
* @throws SerialInterfaceException when a connection error occurs.
*/
public void connect(final String serialPortName)
throws SerialInterfaceException {
logger.info("Connecting to serial port {}", serialPortName);
try {
CommPortIdentifier portIdentifier = CommPortIdentifier.getPortIdentifier(serialPortName);
CommPort commPort = portIdentifier.open("org.openhab.binding.zwave",2000);
this.serialPort = (SerialPort) commPort;
this.serialPort.setSerialPortParams(115200,SerialPort.DATABITS_8,SerialPort.STOPBITS_1,SerialPort.PARITY_NONE);
this.serialPort.enableReceiveThreshold(1);
this.serialPort.enableReceiveTimeout(ZWAVE_RECEIVE_TIMEOUT);
this.receiveThread = new ZWaveReceiveThread();
this.receiveThread.start();
this.sendThread = new ZWaveSendThread();
this.sendThread.start();
logger.info("Serial port is initialized");
} catch (NoSuchPortException e) {
logger.error(String.format("Port %s does not exist", serialPortName));
throw new SerialInterfaceException(String.format("Port %s does not exist", serialPortName), e);
} catch (PortInUseException e) {
logger.error(String.format("Port %s in use.", serialPortName));
throw new SerialInterfaceException(String.format("Port %s in use.", serialPortName), e);
} catch (UnsupportedCommOperationException e) {
logger.error(String.format("Unsupported comm operation on Port %s.", serialPortName));
throw new SerialInterfaceException(String.format("Unsupported comm operation on Port %s.", serialPortName), e);
}
}
/**
* Closes the connection to the Z-Wave controller.
*/
public void close() {
if (watchdog != null) {
watchdog.cancel();
watchdog = null;
}
disconnect();
// clear nodes collection and send queue
ArrayList<ZWaveEventListener> copy = new ArrayList<ZWaveEventListener>(this.zwaveEventListeners);
for (Object listener : copy.toArray()) {
if (!(listener instanceof ZWaveNode))
continue;
this.zwaveEventListeners.remove(listener);
}
this.zwaveNodes.clear();
this.sendQueue.clear();
logger.info("Stopped Z-Wave controller");
}
/**
* Disconnects from the serial interface and stops
* send and receive threads.
*/
public void disconnect() {
if (sendThread != null) {
sendThread.interrupt();
try {
sendThread.join();
} catch (InterruptedException e) {
}
sendThread = null;
}
if (receiveThread != null) {
receiveThread.interrupt();
try {
receiveThread.join();
} catch (InterruptedException e) {
}
receiveThread = null;
}
if(transactionCompleted.availablePermits() < 0)
transactionCompleted.release(transactionCompleted.availablePermits());
transactionCompleted.drainPermits();
logger.trace("Transaction completed permit count -> {}", transactionCompleted.availablePermits());
if (this.serialPort != null) {
this.serialPort.close();
this.serialPort = null;
}
logger.info("Disconnected from serial port");
}
/**
* Enqueues a message for sending on the send queue.
* @param serialMessage the serial message to enqueue.
*/
public void enqueue(SerialMessage serialMessage) {
this.sendQueue.add(serialMessage);
logger.debug("Enqueueing message. Queue length = {}", this.sendQueue.size());
}
/**
* Returns the size of the send queue.
*/
public int getSendQueueLength() {
return this.sendQueue.size();
}
/**
* Notify our own event listeners of a Z-Wave event.
* @param event the event to send.
*/
public void notifyEventListeners(ZWaveEvent event) {
logger.debug("Notifying event listeners");
ArrayList<ZWaveEventListener> copy = new ArrayList<ZWaveEventListener>(this.zwaveEventListeners);
for (ZWaveEventListener listener : copy) {
logger.trace("Notifying {}", listener.toString());
listener.ZWaveIncomingEvent(event);
}
// We also need to handle the inclusion internally within the controller
if(event instanceof ZWaveInclusionEvent) {
ZWaveInclusionEvent incEvent = (ZWaveInclusionEvent)event;
switch(incEvent.getEvent()) {
case IncludeDone:
logger.debug("NODE {}: Including node.", incEvent.getNodeId());
// First make sure this isn't an existing node
if(getNode(incEvent.getNodeId()) != null) {
logger.debug("NODE {}: Newly included node already exists - not initialising.", incEvent.getNodeId());
break;
}
// Initialise the new node
ZWaveNode node = new ZWaveNode(this.homeId, incEvent.getNodeId(), this);
this.zwaveNodes.put(incEvent.getNodeId(), node);
node.advanceNodeStage(NodeStage.PROTOINFO);
break;
case ExcludeDone:
logger.debug("NODE {}: Excluding node.", incEvent.getNodeId());
// Remove the node from the controller
if(getNode(incEvent.getNodeId()) == null) {
logger.debug("NODE {}: Excluding node that doesn't exist.", incEvent.getNodeId());
break;
}
this.zwaveNodes.remove(incEvent.getNodeId());
// Remove the XML file
ZWaveNodeSerializer nodeSerializer = new ZWaveNodeSerializer();
nodeSerializer.DeleteNode(event.getNodeId());
break;
default:
break;
}
}
if(event instanceof ZWaveNetworkEvent) {
ZWaveNetworkEvent networkEvent = (ZWaveNetworkEvent)event;
switch(networkEvent.getEvent()) {
case FailedNode:
if(getNode(networkEvent.getNodeId()) == null) {
logger.debug("NODE {}: Deleting a node that doesn't exist.", networkEvent.getNodeId());
break;
}
if (networkEvent.getState() == State.Success) {
logger.debug("NODE {}: Marking node as failed because its on the controllers failed node list.", networkEvent.getNodeId());
getNode(networkEvent.getNodeId()).setNodeStage(NodeStage.FAILED);
ZWaveEvent zEvent = new ZWaveNodeStatusEvent(networkEvent.getNodeId(), ZWaveNodeStatusEvent.State.Failed);
this.notifyEventListeners(zEvent);
break;
}
case DeleteNode:
if(getNode(networkEvent.getNodeId()) == null) {
logger.debug("NODE {}: Deleting a node that doesn't exist.", networkEvent.getNodeId());
break;
}
this.zwaveNodes.remove(networkEvent.getNodeId());
//Remove the XML file
ZWaveNodeSerializer nodeSerializer = new ZWaveNodeSerializer();
nodeSerializer.DeleteNode(event.getNodeId());
break;
default:
break;
}
}
}
/**
* Initializes communication with the Z-Wave controller stick.
*/
public void initialize() {
this.enqueue(new GetVersionMessageClass().doRequest());
this.enqueue(new MemoryGetIdMessageClass().doRequest());
this.enqueue(new SerialApiGetCapabilitiesMessageClass().doRequest());
this.enqueue(new GetSucNodeIdMessageClass().doRequest());
}
/**
* Send Identify Node message to the controller.
* @param nodeId the nodeId of the node to identify
* @throws SerialInterfaceException when timing out or getting an invalid response.
*/
public void identifyNode(int nodeId) throws SerialInterfaceException {
this.enqueue(new IdentifyNodeMessageClass().doRequest(nodeId));
}
/**
* Send Request Node info message to the controller.
* @param nodeId the nodeId of the node to identify
* @throws SerialInterfaceException when timing out or getting an invalid response.
*/
public void requestNodeInfo(int nodeId) {
this.enqueue(new RequestNodeInfoMessageClass().doRequest(nodeId));
}
/**
* Checks for dead or sleeping nodes during Node initialization.
* JwS: merged checkInitComplete and checkForDeadOrSleepingNodes to prevent possibly looping nodes multiple times.
*/
public void checkForDeadOrSleepingNodes(){
int completeCount = 0;
if (zwaveNodes.isEmpty())
return;
// There are still nodes waiting to get a ping.
// So skip the dead node checking.
for (SerialMessage serialMessage : sendQueue) {
if (serialMessage.getPriority() == SerialMessagePriority.Low)
return;
}
logger.trace("Checking for Dead or Sleeping Nodes.");
for (Map.Entry<Integer, ZWaveNode> entry : zwaveNodes.entrySet()){
if (entry.getValue().getNodeStage() == NodeStage.EMPTYNODE)
continue;
logger.debug("NODE {}: In Stage {} since {}, listening={}, FLiRS={}", entry.getKey(),
entry.getValue().getNodeStage().getLabel(), entry.getValue().getQueryStageTimeStamp().toString(),
entry.getValue().isListening(), entry.getValue().isFrequentlyListening());
if(entry.getValue().getNodeStage() == NodeStage.DONE || entry.getValue().isDead() == true
|| (!entry.getValue().isListening() && !entry.getValue().isFrequentlyListening())) {
completeCount++;
continue;
}
logger.trace("NODE {}: Checking if {} miliseconds have passed in current stage.", entry.getKey(), QUERY_STAGE_TIMEOUT);
if(Calendar.getInstance().getTimeInMillis() < (entry.getValue().getQueryStageTimeStamp().getTime() + QUERY_STAGE_TIMEOUT))
continue;
logger.warn("NODE {}: May be dead, setting stage to DEAD.", entry.getKey());
entry.getValue().setNodeStage(NodeStage.DEAD);
completeCount++;
}
// If all nodes are completed, then we say the binding is ready for business
if(this.zwaveNodes.size() == completeCount && initializationComplete == false) {
logger.debug("ZWave Initialisation Complete");
// We only want this event once!
initializationComplete = true;
ZWaveEvent zEvent = new ZWaveInitializationCompletedEvent(this.ownNodeId);
this.notifyEventListeners(zEvent);
// If there are DEAD nodes, send a Node Status event
// We do that here to avoid messing with the binding initialisation
for(ZWaveNode node : this.getNodes()) {
logger.debug("NODE {}: Checking completion state - {}.", node.getNodeId(), node.getNodeStage());
if (node.isDead()) {
logger.debug("NODE {}: DEAD node.", node.getNodeId());
zEvent = new ZWaveNodeStatusEvent(node.getNodeId(), ZWaveNodeStatusEvent.State.Dead);
this.notifyEventListeners(zEvent);
}
}
}
}
/**
* Polls a node for any dynamic information
* @param node
*/
public void pollNode(ZWaveNode node) {
for (ZWaveCommandClass zwaveCommandClass : node.getCommandClasses()) {
logger.trace("NODE {}: Inspecting command class {}", node.getNodeId(), zwaveCommandClass.getCommandClass().getLabel());
if (zwaveCommandClass instanceof ZWaveCommandClassDynamicState) {
logger.debug("NODE {}: Found dynamic state command class {}", node.getNodeId(), zwaveCommandClass.getCommandClass()
.getLabel());
ZWaveCommandClassDynamicState zdds = (ZWaveCommandClassDynamicState) zwaveCommandClass;
int instances = zwaveCommandClass.getInstances();
if (instances == 0) {
Collection<SerialMessage> dynamicQueries = zdds.getDynamicValues();
for (SerialMessage serialMessage : dynamicQueries) {
sendData(serialMessage);
}
} else {
for (int i = 1; i <= instances; i++) {
Collection<SerialMessage> dynamicQueries = zdds.getDynamicValues();
for (SerialMessage serialMessage : dynamicQueries) {
sendData(node.encapsulate(serialMessage, zwaveCommandClass, i));
}
}
}
} else if (zwaveCommandClass instanceof ZWaveMultiInstanceCommandClass) {
ZWaveMultiInstanceCommandClass multiInstanceCommandClass = (ZWaveMultiInstanceCommandClass) zwaveCommandClass;
for (ZWaveEndpoint endpoint : multiInstanceCommandClass.getEndpoints()) {
for (ZWaveCommandClass endpointCommandClass : endpoint.getCommandClasses()) {
logger.trace(String.format("NODE %d: Inspecting command class %s for endpoint %d", node.getNodeId(), endpointCommandClass
.getCommandClass().getLabel(), endpoint.getEndpointId()));
if (endpointCommandClass instanceof ZWaveCommandClassDynamicState) {
logger.debug("NODE {}: Found dynamic state command class {}", node.getNodeId(), endpointCommandClass
.getCommandClass().getLabel());
ZWaveCommandClassDynamicState zdds2 = (ZWaveCommandClassDynamicState) endpointCommandClass;
Collection<SerialMessage> dynamicQueries = zdds2.getDynamicValues();
for (SerialMessage serialMessage : dynamicQueries) {
sendData(node.encapsulate(serialMessage,
endpointCommandClass, endpoint.getEndpointId()));
}
}
}
}
}
}
}
/**
* Request the node routing information.
*
* @param nodeId The address of the node to update
*/
public void requestNodeRoutingInfo(int nodeId)
{
this.enqueue(new GetRoutingInfoMessageClass().doRequest(nodeId));
}
/**
* Request the node neighbor list to be updated for the specified node.
* Once this is complete, the requestNodeRoutingInfo will be called
* automatically to update the data in the binding.
*
* @param nodeId The address of the node to update
*/
public void requestNodeNeighborUpdate(int nodeId)
{
this.enqueue(new RequestNodeNeighborUpdateMessageClass().doRequest(nodeId));
}
/**
* Puts the controller into inclusion mode to add new nodes
*/
public void requestAddNodesStart()
{
this.enqueue(new AddNodeMessageClass().doRequestStart(true));
}
/**
* Terminates the inclusion mode
*/
public void requestAddNodesStop()
{
this.enqueue(new AddNodeMessageClass().doRequestStop());
}
/**
* Puts the controller into exclusion mode to remove new nodes
*/
public void requestRemoveNodesStart()
{
this.enqueue(new RemoveNodeMessageClass().doRequestStart(true));
}
/**
* Terminates the exclusion mode
*/
public void requestRemoveNodesStop()
{
this.enqueue(new RemoveNodeMessageClass().doRequestStop());
}
/**
* Sends a request to perform a soft reset on the controller.
* This will just reset the controller - probably similar to a power cycle.
* It doesn't reinitialise the network, or change the network configuration.
*
* NOTE: At least for some (most!) sticks, this doesn't return a response.
* Therefore, the number of retries is set to 1.
* NOTE: On some (most!) ZWave-Plus sticks, this can cause the stick to hang.
*/
public void requestSoftReset()
{
SerialMessage msg = new SerialApiSoftResetMessageClass().doRequest();
msg.attempts = 1;
this.enqueue(msg);
}
/**
* Request if the node is currently marked as failed by the controller.
* @param nodeId The address of the node to check
*/
public void requestIsFailedNode(int nodeId)
{
this.enqueue(new IsFailedNodeMessageClass().doRequest(nodeId));
}
/**
* Removes a failed node from the network.
* Note that this won't remove nodes that have not failed.
* @param nodeId The address of the node to remove
*/
public void requestRemoveFailedNode(int nodeId)
{
this.enqueue(new RemoveFailedNodeMessageClass().doRequest(nodeId));
}
/**
* Delete all return nodes from the specified node. This should be performed
* before updating the routes
*
* @param nodeId
*/
public void requestDeleteAllReturnRoutes(int nodeId)
{
this.enqueue(new DeleteReturnRouteMessageClass().doRequest(nodeId));
}
/**
* Request the controller to set the return route between two nodes
*
* @param nodeId
* Source node
* @param destinationId
* Destination node
*/
public void requestAssignReturnRoute(int nodeId, int destinationId)
{
this.enqueue(new AssignReturnRouteMessageClass().doRequest(nodeId, destinationId, getCallbackId()));
}
/**
* Request the controller to set the return route from a node to the controller
*
* @param nodeId
* Source node
*/
public void requestAssignSucReturnRoute(int nodeId)
{
this.enqueue(new AssignSucReturnRouteMessageClass().doRequest(nodeId, getCallbackId()));
}
/**
* Returns the next callback ID
* @return callback ID
*/
public int getCallbackId() {
if (++sentDataPointer > 0xFF)
sentDataPointer = 1;
logger.debug("Callback ID = {}", sentDataPointer);
return sentDataPointer;
}
/**
* Transmits the SerialMessage to a single Z-Wave Node.
* Sets the transmission options as well.
* @param serialMessage the Serial message to send.
*/
public void sendData(SerialMessage serialMessage)
{
if (serialMessage.getMessageClass() != SerialMessageClass.SendData) {
logger.error(String.format("Invalid message class %s (0x%02X) for sendData", serialMessage.getMessageClass().getLabel(), serialMessage.getMessageClass().getKey()));
return;
}
if (serialMessage.getMessageType() != SerialMessageType.Request) {
logger.error("Only request messages can be sent");
return;
}
ZWaveNode node = this.getNode(serialMessage.getMessageNode());
// Keep track of the number of packets sent to this device
node.incrementSendCount();
if (!node.isListening() && !node.isFrequentlyListening() && serialMessage.getPriority() != SerialMessagePriority.Low) {
ZWaveWakeUpCommandClass wakeUpCommandClass = (ZWaveWakeUpCommandClass)node.getCommandClass(CommandClass.WAKE_UP);
// If it's a battery operated device, check if it's awake or place in wake-up queue.
if (wakeUpCommandClass != null && !wakeUpCommandClass.processOutgoingWakeupMessage(serialMessage)) {
return;
}
}
serialMessage.setTransmitOptions(TRANSMIT_OPTION_ACK | TRANSMIT_OPTION_AUTO_ROUTE | TRANSMIT_OPTION_EXPLORE);
serialMessage.setCallbackId(getCallbackId());
this.enqueue(serialMessage);
}
/**
* Add a listener for Z-Wave events to this controller.
* @param eventListener the event listener to add.
*/
public void addEventListener(ZWaveEventListener eventListener) {
this.zwaveEventListeners.add(eventListener);
}
/**
* Remove a listener for Z-Wave events to this controller.
* @param eventListener the event listener to remove.
*/
public void removeEventListener(ZWaveEventListener eventListener) {
this.zwaveEventListeners.remove(eventListener);
}
/**
* Gets the API Version of the controller.
* @return the serialAPIVersion
*/
public String getSerialAPIVersion() {
return serialAPIVersion;
}
/**
* Gets the zWave Version of the controller.
* @return the zWaveVersion
*/
public String getZWaveVersion() {
return zWaveVersion;
}
/**
* Gets the Manufacturer ID of the controller.
* @return the manufactureId
*/
public int getManufactureId() {
return manufactureId;
}
/**
* Gets the device type of the controller;
* @return the deviceType
*/
public int getDeviceType() {
return deviceType;
}
/**
* Gets the device ID of the controller.
* @return the deviceId
*/
public int getDeviceId() {
return deviceId;
}
/**
* Gets the node ID of the controller.
* @return the deviceId
*/
public int getOwnNodeId() {
return ownNodeId;
}
/**
* Gets the device type of the controller.
* @return the device type
*/
public ZWaveDeviceType getControllerType() {
return controllerType;
}
/**
* Gets the networks SUC controller ID.
* @return the device id of the SUC, or 0 if none exists
*/
public int getSucId() {
return sucID;
}
/**
* Gets the node object using it's node ID as key.
* Returns null if the node is not found
* @param nodeId the Node ID of the node to get.
* @return node object
*/
public ZWaveNode getNode(int nodeId) {
return this.zwaveNodes.get(nodeId);
}
/**
* Gets the node list
* @return
*/
public Collection<ZWaveNode> getNodes() {
return this.zwaveNodes.values();
}
/**
* Indicates a working connection to the
* Z-Wave controller stick and initialization complete
* @return isConnected;
*/
public boolean isConnected() {
return isConnected && initializationComplete;
}
/**
* Gets the number of Start Of Frames received.
* @return the sOFCount
*/
public int getSOFCount() {
return SOFCount;
}
/**
* Gets the number of Canceled Frames received.
* @return the cANCount
*/
public int getCANCount() {
return CANCount;
}
/**
* Gets the number of Not Acknowledged Frames received.
* @return the nAKCount
*/
public int getNAKCount() {
return NAKCount;
}
/**
* Gets the number of Acknowledged Frames received.
* @return the aCKCount
*/
public int getACKCount() {
return ACKCount;
}
/**
* Returns the number of Out of Order frames received.
* @return the oOFCount
*/
public int getOOFCount() {
return OOFCount;
}
/**
* Returns the number of Time-Outs while sending.
* @return the oOFCount
*/
public int getTimeOutCount() {
return timeOutCount.get();
}
// Nested classes and enumerations
/**
* Z-Wave controller Send Thread. Takes care of sending all messages.
* It uses a semaphore to synchronize communication with the receiving thread.
* @author Jan-Willem Spuij
* @since 1.3.0
*/
private class ZWaveSendThread extends Thread {
private final Logger logger = LoggerFactory.getLogger(ZWaveSendThread.class);
/**
* Run method. Runs the actual sending process.
*/
@Override
public void run() {
logger.debug("Starting Z-Wave send thread");
try {
while (!interrupted()) {
try {
lastSentMessage = sendQueue.take();
logger.debug("Took message from queue for sending. Queue length = {}", sendQueue.size());
} catch (InterruptedException e1) {
break;
}
if (lastSentMessage == null)
continue;
// If this message is a data packet to a node
// then make sure the node is not a battery device.
// If it's a battery device, it needs to be awake, or we queue the frame until it is.
if (lastSentMessage.getMessageClass() == SerialMessageClass.SendData) {
ZWaveNode node = getNode(lastSentMessage.getMessageNode());
if (node != null && !node.isListening() && !node.isFrequentlyListening() && lastSentMessage.getPriority() != SerialMessagePriority.Low) {
ZWaveWakeUpCommandClass wakeUpCommandClass = (ZWaveWakeUpCommandClass)node.getCommandClass(CommandClass.WAKE_UP);
// If it's a battery operated device, check if it's awake or place in wake-up queue.
if (wakeUpCommandClass != null && !wakeUpCommandClass.processOutgoingWakeupMessage(lastSentMessage)) {
continue;
}
}
}
// Clear the semaphore used to acknowledge the response.
transactionCompleted.drainPermits();
// Send the message to the controller
byte[] buffer = lastSentMessage.getMessageBuffer();
logger.debug("Sending Message = " + SerialMessage.bb2hex(buffer));
lastMessageStartTime = System.currentTimeMillis();
try {
synchronized (serialPort.getOutputStream()) {
serialPort.getOutputStream().write(buffer);
serialPort.getOutputStream().flush();
}
} catch (IOException e) {
logger.error("Got I/O exception {} during sending. exiting thread.", e.getLocalizedMessage());
break;
}
// Now wait for the response...
try {
if (!transactionCompleted.tryAcquire(1, zWaveResponseTimeout, TimeUnit.MILLISECONDS)) {
timeOutCount.incrementAndGet();
if (lastSentMessage.getMessageClass() == SerialMessageClass.SendData) {
buffer = new SerialMessage(SerialMessageClass.SendDataAbort, SerialMessageType.Request, SerialMessageClass.SendData, SerialMessagePriority.High).getMessageBuffer();
logger.debug("Sending Message = " + SerialMessage.bb2hex(buffer));
try {
synchronized (serialPort.getOutputStream()) {
serialPort.getOutputStream().write(buffer);
serialPort.getOutputStream().flush();
}
} catch (IOException e) {
logger.error("Got I/O exception {} during sending. exiting thread.", e.getLocalizedMessage());
break;
}
}
if (--lastSentMessage.attempts >= 0) {
logger.error("NODE {}: Timeout while sending message. Requeueing", lastSentMessage.getMessageNode());
if (lastSentMessage.getMessageClass() == SerialMessageClass.SendData)
handleFailedSendDataRequest(lastSentMessage);
else
enqueue(lastSentMessage);
} else
{
logger.warn("NODE {}: Discarding message: {}", lastSentMessage.getMessageNode(), lastSentMessage.toString());
}
continue;
}
long responseTime = System.currentTimeMillis() - lastMessageStartTime;
if(responseTime > longestResponseTime)
longestResponseTime = responseTime;
logger.debug("Response processed after {}ms/{}ms.", responseTime, longestResponseTime);
logger.trace("Acquired. Transaction completed permit count -> {}", transactionCompleted.availablePermits());
} catch (InterruptedException e) {
break;
}
}
} catch (Exception e) {
logger.error("Got an exception during sending. exiting thread.", e);
}
logger.debug("Stopped Z-Wave send thread");
}
}
/**
* Z-Wave controller Receive Thread. Takes care of receiving all messages.
* It uses a semaphore to synchronize communication with the sending thread.
* @author Jan-Willem Spuij
* @since 1.3.0
*/
private class ZWaveReceiveThread extends Thread {
private static final int SOF = 0x01;
private static final int ACK = 0x06;
private static final int NAK = 0x15;
private static final int CAN = 0x18;
private final Logger logger = LoggerFactory.getLogger(ZWaveReceiveThread.class);
/**
* Sends 1 byte frame response.
* @param response the response code to send.
*/
private void sendResponse(int response) {
try {
synchronized (serialPort.getOutputStream()) {
serialPort.getOutputStream().write(response);
serialPort.getOutputStream().flush();
}
} catch (IOException e) {
logger.error(e.getMessage());
}
}
/**
* Processes incoming message and notifies event handlers.
* @param buffer the buffer to process.
*/
private void processIncomingMessage(byte[] buffer) {
SerialMessage serialMessage = new SerialMessage(buffer);
if (serialMessage.isValid) {
logger.trace("Message is valid, sending ACK");
sendResponse(ACK);
} else {
logger.error("Message is not valid, discarding");
return;
}
handleIncomingMessage(serialMessage);
}
/**
* Run method. Runs the actual receiving process.
*/
@Override
public void run() {
logger.debug("Starting Z-Wave receive thread");
try {
// Send a NAK to resynchronise communications
sendResponse(NAK);
while (!interrupted()) {
int nextByte;
try {
nextByte = serialPort.getInputStream().read();
if (nextByte == -1)
continue;
} catch (IOException e) {
logger.error("Got I/O exception {} during receiving. exiting thread.", e.getLocalizedMessage());
break;
}
switch (nextByte) {
case SOF:
int messageLength;
try {
messageLength = serialPort.getInputStream().read();
} catch (IOException e) {
logger.error("Got I/O exception {} during receiving. exiting thread.", e.getLocalizedMessage());
break;
}
byte[] buffer = new byte[messageLength + 2];
buffer[0] = SOF;
buffer[1] = (byte)messageLength;
int total = 0;
while (total < messageLength) {
try {
int read = serialPort.getInputStream().read(buffer, total + 2, messageLength - total);
total += (read > 0 ? read : 0);
} catch (IOException e) {
logger.error("Got I/O exception {} during receiving. exiting thread.", e.getLocalizedMessage());
return;
}
}
logger.trace("Reading message finished" );
logger.debug("Receive Message = {}", SerialMessage.bb2hex(buffer));
processIncomingMessage(buffer);
SOFCount++;
break;
case ACK:
logger.trace("Received ACK");
ACKCount++;
break;
case NAK:
logger.error("Message not acklowledged by controller (NAK), discarding");
transactionCompleted.release();
logger.trace("Released. Transaction completed permit count -> {}", transactionCompleted.availablePermits());
NAKCount++;
break;
case CAN:
logger.error("Message cancelled by controller (CAN), resending");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
break;
}
enqueue(lastSentMessage);
transactionCompleted.release();
logger.trace("Released. Transaction completed permit count -> {}", transactionCompleted.availablePermits());
CANCount++;
break;
default:
logger.warn(String.format("Out of Frame flow. Got 0x%02X. Sending NAK.", nextByte));
sendResponse(NAK);
OOFCount++;
break;
}
}
} catch (Exception e) {
logger.error("Got an exception during receiving. exiting thread.", e);
}
logger.debug("Stopped Z-Wave receive thread");
}
}
/**
* WatchDogTimerTask class. Acts as a watch dog and
* checks the serial threads to see whether they are
* still running.
* @author Jan-Willem Spuij
* @since 1.3.0
*/
private class WatchDogTimerTask extends TimerTask {
private final Logger logger = LoggerFactory.getLogger(WatchDogTimerTask.class);
private final String serialPortName;
/**
* Creates a new instance of the WatchDogTimerTask class.
* @param serialPortName the serial port name to reconnect to
* in case the serial threads have died.
*/
public WatchDogTimerTask(String serialPortName) {
this.serialPortName = serialPortName;
}
/**
* {@inheritDoc}
*/
@Override
public void run() {
logger.trace("Watchdog: Checking Serial threads");
if ((receiveThread != null && !receiveThread.isAlive()) ||
(sendThread != null && !sendThread.isAlive()))
{
logger.warn("Threads not alive, respawning");
disconnect();
try {
connect(serialPortName);
} catch (SerialInterfaceException e) {
logger.error("unable to restart Serial threads: {}", e.getLocalizedMessage());
}
}
}
}
}