/**
* 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.commandclass;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.openhab.binding.zwave.internal.protocol.AssociationGroup;
import org.openhab.binding.zwave.internal.protocol.SerialMessage;
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.ZWaveController;
import org.openhab.binding.zwave.internal.protocol.ZWaveEndpoint;
import org.openhab.binding.zwave.internal.protocol.ZWaveNode;
import org.openhab.binding.zwave.internal.protocol.event.ZWaveEvent;
import org.openhab.binding.zwave.internal.protocol.event.ZWaveNetworkEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.thoughtworks.xstream.annotations.XStreamAlias;
import com.thoughtworks.xstream.annotations.XStreamOmitField;
/**
* Handles the Association command class. This allows reading and writing of
* node association parameters
*
* @author Chris Jackson
* @since 1.4.0
*/
@XStreamAlias("associationCommandClass")
public class ZWaveAssociationCommandClass extends ZWaveCommandClass implements ZWaveCommandClassInitialization {
@XStreamOmitField
private static final Logger logger = LoggerFactory.getLogger(ZWaveAssociationCommandClass.class);
private static final int ASSOCIATIONCMD_SET = 0x01;
private static final int ASSOCIATIONCMD_GET = 0x02;
private static final int ASSOCIATIONCMD_REPORT = 0x03;
private static final int ASSOCIATIONCMD_REMOVE = 0x04;
private static final int ASSOCIATIONCMD_GROUPINGSGET = 0x05;
private static final int ASSOCIATIONCMD_GROUPINGSREPORT = 0x06;
// Stores the list of association groups
private Map<Integer, AssociationGroup> configAssociations = new HashMap<Integer, AssociationGroup>();
@XStreamOmitField
private int updateAssociationsNode = 0;
@XStreamOmitField
private AssociationGroup pendingAssociation = null;
// This will be set when we query a node for the number of groups it supports
private int maxGroups = 0;
@XStreamOmitField
private boolean initialiseDone = false;
/**
* Creates a new instance of the ZWaveAssociationCommandClass class.
*
* @param node
* the node this command class belongs to
* @param controller
* the controller to use
* @param endpoint
* the endpoint this Command class belongs to
*/
public ZWaveAssociationCommandClass(ZWaveNode node, ZWaveController controller, ZWaveEndpoint endpoint) {
super(node, controller, endpoint);
}
/**
* {@inheritDoc}
*/
@Override
public CommandClass getCommandClass() {
return CommandClass.ASSOCIATION;
}
/**
* {@inheritDoc}
*/
@Override
public void handleApplicationCommandRequest(SerialMessage serialMessage, int offset, int endpoint) {
logger.debug("NODE {}: Received Association Request", this.getNode().getNodeId());
int command = serialMessage.getMessagePayloadByte(offset);
switch (command) {
case ASSOCIATIONCMD_SET:
logger.trace("Process Association Set");
processAssociationReport(serialMessage, offset);
break;
case ASSOCIATIONCMD_GET:
logger.trace("Process Association Get");
return;
case ASSOCIATIONCMD_REPORT:
logger.trace("Process Association Report");
processAssociationReport(serialMessage, offset);
break;
case ASSOCIATIONCMD_REMOVE:
logger.trace("Process Association Remove");
return;
case ASSOCIATIONCMD_GROUPINGSGET:
logger.trace("Process Association GroupingsGet");
return;
case ASSOCIATIONCMD_GROUPINGSREPORT:
logger.trace("Process Association GroupingsReport");
processGroupingsReport(serialMessage, offset);
return;
default:
logger.warn(String.format("NODE %d: Unsupported Command 0x%02X for command class %s (0x%02X).",
this.getNode().getNodeId(), command, this.getCommandClass().getLabel(),
this.getCommandClass().getKey()));
}
}
/**
* Processes a CONFIGURATIONCMD_REPORT / CONFIGURATIONCMD_SET message.
*
* @param serialMessage
* the incoming message to process.
* @param offset
* the offset position from which to start message processing.
*/
protected void processAssociationReport(SerialMessage serialMessage, int offset) {
// Extract the group index
int group = serialMessage.getMessagePayloadByte(offset + 1);
// The max associations supported (0 if the requested group is not supported)
int maxAssociations = serialMessage.getMessagePayloadByte(offset + 2);
// Number of outstanding requests (if the group is large, it may come in multiple frames)
int following = serialMessage.getMessagePayloadByte(offset + 3);
if (maxAssociations == 0) {
// Unsupported association group. Nothing to do!
if (updateAssociationsNode == group) {
logger.debug("NODE {}: All association groups acquired.", this.getNode().getNodeId());
updateAssociationsNode = 0;
// This is used for network management, so send a network event
this.getController()
.notifyEventListeners(new ZWaveNetworkEvent(ZWaveNetworkEvent.Type.AssociationUpdate,
this.getNode().getNodeId(), ZWaveNetworkEvent.State.Success));
}
return;
}
logger.debug("NODE {}: association group {} has max associations " + maxAssociations,
this.getNode().getNodeId(), group);
// Are we waiting to synchronise the start of a new group?
if (pendingAssociation == null) {
pendingAssociation = new AssociationGroup(group);
}
if (serialMessage.getMessagePayload().length > (offset + 4)) {
logger.debug("NODE {}: association group {} includes the following nodes:", this.getNode().getNodeId(),
group);
int numAssociations = serialMessage.getMessagePayload().length - (offset + 4);
for (int cnt = 0; cnt < numAssociations; cnt++) {
int node = serialMessage.getMessagePayloadByte(offset + 4 + cnt);
logger.debug("Node {}", node);
// Add the node to the group
pendingAssociation.addNode(node);
}
}
// If this is the end of the group, update the list then let the listeners know
if (following == 0) {
// Clear the current information for this group
configAssociations.remove(group);
// Update the group in the list
configAssociations.put(group, pendingAssociation);
pendingAssociation = null;
// Send an event to the users
ZWaveAssociationEvent zEvent = new ZWaveAssociationEvent(this.getNode().getNodeId(), group);
List<Integer> members = getGroupMembers(group);
if (members != null) {
for (int node : members) {
zEvent.addMember(node);
}
}
this.getController().notifyEventListeners(zEvent);
}
// Is this the end of the list
if (following == 0 && group == updateAssociationsNode) {
// This is the end of this group and the current 'get all groups' node
// so we need to request the next group
if (updateAssociationsNode < maxGroups) {
updateAssociationsNode++;
SerialMessage outputMessage = getAssociationMessage(updateAssociationsNode);
if (outputMessage != null) {
this.getController().sendData(outputMessage);
}
} else {
logger.debug("NODE {}: All association groups acquired.", this.getNode().getNodeId());
// we have reached our maxNodes, notify listeners we are done.
updateAssociationsNode = 0;
// This is used for network management, so send a network event
this.getController()
.notifyEventListeners(new ZWaveNetworkEvent(ZWaveNetworkEvent.Type.AssociationUpdate,
this.getNode().getNodeId(), ZWaveNetworkEvent.State.Success));
}
}
}
/**
* Processes a ASSOCIATIONCMD_GROUPINGSREPORT message.
*
* @param serialMessage
* the incoming message to process.
* @param offset
* the offset position from which to start message processing.
*/
protected void processGroupingsReport(SerialMessage serialMessage, int offset) {
maxGroups = serialMessage.getMessagePayloadByte(offset + 1);
logger.debug("NODE {}: processGroupingsReport number of groups {}", getNode(), maxGroups);
initialiseDone = true;
// Start the process to query these nodes
// updateAssociationsNode = 1;
// configAssociations.clear();
// SerialMessage sm = getAssociationMessage(updateAssociationsNode);
// if(sm != null) {
// this.getController().sendData(sm);
// }
}
/**
* Gets a SerialMessage with the ASSOCIATIONCMD_SET command
*
* @param group
* the association group
* @param node
* the node to add to the specified group
* @return the serial message
*/
public SerialMessage setAssociationMessage(int group, int node) {
logger.debug("NODE {}: Creating new message for application command ASSOCIATIONCMD_SET",
this.getNode().getNodeId());
SerialMessage result = new SerialMessage(this.getNode().getNodeId(), SerialMessageClass.SendData,
SerialMessageType.Request, SerialMessageClass.SendData, SerialMessagePriority.Config);
byte[] newPayload = { (byte) this.getNode().getNodeId(), 4, (byte) getCommandClass().getKey(),
(byte) ASSOCIATIONCMD_SET, (byte) (group & 0xff), (byte) (node & 0xff) };
result.setMessagePayload(newPayload);
return result;
}
/**
* Gets a SerialMessage with the ASSOCIATIONCMD_REMOVE command
*
* @param group
* the association group
* @param node
* the node to add to the specified group
* @return the serial message
*/
public SerialMessage removeAssociationMessage(int group, int node) {
logger.debug("NODE {}: Creating new message for application command ASSOCIATIONCMD_REMOVE",
this.getNode().getNodeId());
SerialMessage result = new SerialMessage(this.getNode().getNodeId(), SerialMessageClass.SendData,
SerialMessageType.Request, SerialMessageClass.SendData, SerialMessagePriority.Config);
byte[] newPayload = { (byte) this.getNode().getNodeId(), 4, (byte) getCommandClass().getKey(),
(byte) ASSOCIATIONCMD_REMOVE, (byte) (group & 0xff), (byte) (node & 0xff) };
result.setMessagePayload(newPayload);
return result;
}
/**
* Gets a SerialMessage with the ASSOCIATIONCMD_GET command
*
* @param group
* the association group to read
* @return the serial message
*/
public SerialMessage getAssociationMessage(int group) {
logger.debug("NODE {}: Creating new message for application command ASSOCIATIONCMD_GET group {}",
this.getNode().getNodeId(), group);
SerialMessage result = new SerialMessage(this.getNode().getNodeId(), SerialMessageClass.SendData,
SerialMessageType.Request, SerialMessageClass.ApplicationCommandHandler, SerialMessagePriority.Config);
byte[] newPayload = { (byte) this.getNode().getNodeId(), 3, (byte) getCommandClass().getKey(),
(byte) ASSOCIATIONCMD_GET, (byte) (group & 0xff) };
result.setMessagePayload(newPayload);
return result;
}
/**
* Gets a SerialMessage with the ASSOCIATIONCMD_GROUPINGSGET command
*
* @return the serial message
*/
public SerialMessage getGroupingsMessage() {
logger.debug("NODE {}: Creating new message for application command ASSOCIATIONCMD_GROUPINGSGET",
this.getNode().getNodeId());
SerialMessage result = new SerialMessage(this.getNode().getNodeId(), SerialMessageClass.SendData,
SerialMessageType.Request, SerialMessageClass.ApplicationCommandHandler, SerialMessagePriority.Config);
byte[] newPayload = { (byte) this.getNode().getNodeId(), 2, (byte) getCommandClass().getKey(),
(byte) ASSOCIATIONCMD_GROUPINGSGET };
result.setMessagePayload(newPayload);
return result;
}
/**
* Request all association groups.
* This method requests the number of groups from a node, when that
* replay is processed we request association group 1 and set flags so that
* when the response is received the command class automatically
* requests the next group. This continues until we reach the maximum
* number of group the device reports to us or until the device returns
* a group with no members.
*/
public void getAllAssociations() {
updateAssociationsNode = 1;
SerialMessage serialMessage = getAssociationMessage(updateAssociationsNode);
if (serialMessage != null) {
this.getController().sendData(serialMessage);
}
}
/**
* Returns a list of nodes that are currently members of the association
* group. This method only returns the list that is currently in the
* class - it does not interact with the device.
*
* To update the list stored in the class, call getAssociationMessage
*
* @param group
* number of the association group
* @return List of nodes in the group
*/
public List<Integer> getGroupMembers(int group) {
if (configAssociations.get(group) == null) {
return new ArrayList<Integer>();
}
return configAssociations.get(group).getNodes();
}
/**
* Returns the number of association groups
*
* @return Number of association groups
*/
public int getGroupCount() {
return configAssociations.size();
}
/**
* Returns the maximum number of association groups
*
* @return Number of association groups
*/
public int getMaxGroups() {
return maxGroups;
}
/**
* ZWave association group received event.
* Send from the association members to the binding
* Note that multiple events can be required to build up the full list.
*
* @author Chris Jackson
* @since 1.4.0
*/
public class ZWaveAssociationEvent extends ZWaveEvent {
private int group;
private List<Integer> members = new ArrayList<Integer>();
/**
* Constructor. Creates a new instance of the ZWaveAssociationEvent
* class.
*
* @param nodeId the nodeId of the event. Must be set to the controller node.
*/
public ZWaveAssociationEvent(int nodeId, int group) {
super(nodeId);
this.group = group;
}
public int getGroup() {
return group;
}
public List<Integer> getMembers() {
return members;
}
public int getMemberCnt() {
return members.size();
}
public void addMember(int member) {
members.add(member);
}
}
@Override
public Collection<SerialMessage> initialize(boolean refresh) {
ArrayList<SerialMessage> result = new ArrayList<SerialMessage>();
// If we're already initialized, then don't do it again unless we're refreshing
if (refresh == true || initialiseDone == false) {
result.add(this.getGroupingsMessage());
}
return result;
}
}