Skip to content

Commit

Permalink
add support for attribute based generic buttons
Browse files Browse the repository at this point in the history
Signed-off-by: Thomas Weißschuh <[email protected]>
  • Loading branch information
t-8ch committed Dec 24, 2018
1 parent 2f6bdc6 commit defd455
Show file tree
Hide file tree
Showing 2 changed files with 316 additions and 65 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.*;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;

import com.zsmartsystems.zigbee.IeeeAddress;
import com.zsmartsystems.zigbee.zcl.*;
import org.eclipse.smarthome.core.thing.Channel;
import org.eclipse.smarthome.core.thing.CommonTriggerEvents;
import org.eclipse.smarthome.core.thing.ThingUID;
Expand All @@ -30,9 +31,6 @@

import com.zsmartsystems.zigbee.CommandResult;
import com.zsmartsystems.zigbee.ZigBeeEndpoint;
import com.zsmartsystems.zigbee.zcl.ZclCluster;
import com.zsmartsystems.zigbee.zcl.ZclCommand;
import com.zsmartsystems.zigbee.zcl.ZclCommandListener;

/**
* Generic converter for buttons (e.g., from remote controls).
Expand All @@ -44,22 +42,25 @@
*
* @author Henning Sudbrock - initial contribution
*/
public class ZigBeeConverterGenericButton extends ZigBeeBaseChannelConverter implements ZclCommandListener {
public class ZigBeeConverterGenericButton extends ZigBeeBaseChannelConverter implements ZclCommandListener, ZclAttributeListener {

private Logger logger = LoggerFactory.getLogger(this.getClass());
private static Logger logger = LoggerFactory.getLogger(ZigBeeConverterGenericButton.class);

private static final String CLUSTER = "cluster_id";
private static final String COMMAND = "command_id";
private static final String PARAM_NAME = "parameter_name";
private static final String PARAM_VALUE = "parameter_value";
private static final String ATTRIBUTE_ID = "attribute_id";
private static final String ATTRIBUTE_VALUE = "attribute_value";

private Map<ButtonPressType, CommandSpec> handledCommands = new HashMap<>();
private Set<ZclCluster> clientClusters = new HashSet<>();
private Set<ZclCluster> serverClusters = new HashSet<>();

@Override
public synchronized boolean initializeConverter() {
for (ButtonPressType buttonPressType : ButtonPressType.values()) {
CommandSpec commandSpec = parseCommandSpec(buttonPressType);
CommandSpec commandSpec = parseCommandSpec(channel.getProperties(), endpoint.getIeeeAddress(), buttonPressType);
if (commandSpec != null) {
handledCommands.put(buttonPressType, commandSpec);
}
Expand All @@ -73,42 +74,67 @@ public synchronized boolean initializeConverter() {

for (CommandSpec commandSpec : handledCommands.values()) {
int clusterId = commandSpec.getClusterId();

if (clientClusters.stream().anyMatch(cluster -> cluster.getClusterId().intValue() == clusterId)) {
// bind to each output cluster only once
continue;
boolean result;

if (commandSpec.isClientCluster()) {
result = bindCluster(
"client", clientClusters, clusterId,
endpoint::getOutputCluster, cluster -> cluster.addCommandListener(this));
} else {
result = bindCluster(
"server", serverClusters, clusterId,
endpoint::getInputCluster, cluster -> cluster.addAttributeListener(this));
}

ZclCluster clientCluster = endpoint.getOutputCluster(clusterId);
if (clientCluster == null) {
logger.error("{}: Error opening client cluster {} on endpoint {}", endpoint.getIeeeAddress(), clusterId,
endpoint.getEndpointId());
return false;
if (!result) {
return result;
}
}

try {
CommandResult bindResponse = bind(clientCluster).get();
if (!bindResponse.isSuccess()) {
logger.error("{}: Error 0x{} setting client binding for cluster {}", endpoint.getIeeeAddress(),
toHexString(bindResponse.getStatusCode()), clusterId);
}
} catch (InterruptedException | ExecutionException e) {
logger.error(endpoint.getIeeeAddress() + ": Exception setting binding to cluster " + clusterId, e);
}
return true;
}

private boolean bindCluster(String clusterType, Collection<ZclCluster> existingClusters, int clusterId,
Function<Integer, ZclCluster> clusterFunction,
Consumer<ZclCluster> registrationFunction
) {
if (existingClusters.stream().anyMatch(c -> c.getClusterId().intValue() == clusterId)) {
// bind to each output cluster only once
return true;
}

clientCluster.addCommandListener(this);
clientClusters.add(clientCluster);
ZclCluster cluster = clusterFunction.apply(clusterId);
if (cluster == null) {
logger.error("{}: Error opening {} cluster {} on endpoint {}", endpoint.getIeeeAddress(), clusterType, clusterId,
endpoint.getEndpointId());
return false;
}

try {
CommandResult bindResponse = bind(cluster).get();
if (!bindResponse.isSuccess()) {
logger.error("{}: Error 0x{} setting {} binding for cluster {}", endpoint.getIeeeAddress(),
clusterType, toHexString(bindResponse.getStatusCode()), clusterId);
}
} catch (InterruptedException | ExecutionException e) {
logger.error("{}: Exception setting {} binding to cluster {}", endpoint.getIeeeAddress(), clusterType, clusterId, e);
}

registrationFunction.accept(cluster);
existingClusters.add(cluster);
return true;
}

@Override
public void disposeConverter() {
for (ZclCluster clientCluster : clientClusters) {
logger.debug("{}: Closing cluster {}", endpoint.getIeeeAddress(), clientCluster.getClusterId());
logger.debug("{}: Closing client cluster {}", endpoint.getIeeeAddress(), clientCluster.getClusterId());
clientCluster.removeCommandListener(this);
}

for (ZclCluster serverCluster : serverClusters) {
logger.debug("{}: Closing server cluster {}", endpoint.getIeeeAddress(), serverCluster.getClusterId());
serverCluster.removeAttributeListener(this);
}
}

@Override
Expand All @@ -133,7 +159,17 @@ public void commandReceived(ZclCommand command) {
}
}

private String getEvent(ButtonPressType pressType) {
@Override
public void attributeUpdated(ZclAttribute attribute) {
ButtonPressType buttonPressType = getButtonPressType(attribute);
if (buttonPressType != null) {
logger.debug("{}: Matching ZigBee attribute for press type {} received: {}", endpoint.getIeeeAddress(),
buttonPressType, attribute);
thing.triggerChannel(channel.getUID(), getEvent(buttonPressType));
}
}

private static String getEvent(ButtonPressType pressType) {
switch (pressType) {
case DOUBLE_PRESS:
return CommonTriggerEvents.DOUBLE_PRESSED;
Expand All @@ -147,65 +183,104 @@ private String getEvent(ButtonPressType pressType) {
}
}

private ButtonPressType getButtonPressType(ZclAttribute attribute) {
return getButtonPressType(cs -> cs.matchesAttribute(attribute));
}

private ButtonPressType getButtonPressType(ZclCommand command) {
return getButtonPressType(cs -> cs.matchesCommand(command));
}

private ButtonPressType getButtonPressType(Predicate<CommandSpec> predicate) {
for (Entry<ButtonPressType, CommandSpec> entry : handledCommands.entrySet()) {
if (entry.getValue().matchesCommand(command)) {
if (predicate.test(entry.getValue())) {
return entry.getKey();
}
}
return null;
}

private CommandSpec parseCommandSpec(ButtonPressType pressType) {
String clusterProperty = channel.getProperties().get(getParameterName(CLUSTER, pressType));
String commandProperty = channel.getProperties().get(getParameterName(COMMAND, pressType));
String commandParameterName = channel.getProperties().get(getParameterName(PARAM_NAME, pressType));
String commandParameterValue = channel.getProperties().get(getParameterName(PARAM_VALUE, pressType));
static CommandSpec parseCommandSpec(Map<String, String> properties, IeeeAddress address, ButtonPressType pressType) {
String clusterProperty = properties.get(getParameterName(CLUSTER, pressType));
String commandProperty = properties.get(getParameterName(COMMAND, pressType));
String commandParameterName = properties.get(getParameterName(PARAM_NAME, pressType));
String commandParameterValue = properties.get(getParameterName(PARAM_VALUE, pressType));
String attributeIdProperty = properties.get(getParameterName(ATTRIBUTE_ID, pressType));
String attributeValue = properties.get(getParameterName(ATTRIBUTE_VALUE, pressType));

if (clusterProperty == null || commandProperty == null) {
if (clusterProperty == null || (commandProperty == null && attributeIdProperty == null)) {
return null;
}

int clusterId;
int commandId;
Integer commandId;
Integer attributeId;

try {
clusterId = parseId(clusterProperty);
} catch (NumberFormatException e) {
logger.warn("{}: Could not parse cluster property {}", endpoint.getIeeeAddress(), clusterProperty);
logger.warn("{}: Could not parse cluster property {}", address, clusterProperty);
return null;
}

try {
commandId = parseId(commandProperty);
} catch (NumberFormatException e) {
logger.warn("{}: Could not parse command property {}", endpoint.getIeeeAddress(), commandProperty);
if (commandProperty != null) {
try {
commandId = parseId(commandProperty);
} catch (NumberFormatException e) {
logger.warn("{}: Could not parse command property {}", address, commandProperty);
return null;
}
} else {
commandId = null;
}

if (attributeIdProperty != null) {
try {
attributeId = parseId(attributeIdProperty);
} catch (NumberFormatException e) {
logger.warn("{}: Could not parse attribute property {}", address, commandProperty);
return null;
}
} else {
attributeId = null;
}

if (attributeId != null && commandId != null) {
logger.warn("{}: Only one of command or attribute can be used", address);
return null;
}

if ((commandParameterName != null && commandParameterValue == null)
|| (commandParameterName == null && commandParameterValue != null)) {
logger.warn("{}: When specifiying a command parameter, both name and value must be specified",
endpoint.getIeeeAddress());
if (attributeId == null) {
if ((commandParameterName != null && commandParameterValue == null)
|| (commandParameterName == null && commandParameterValue != null)) {
logger.warn("{}: When specifiying a command parameter, both name and value must be specified",
address);
return null;
}
}

if (commandId == null && attributeValue == null) {
logger.warn("{}: When specifiying an attribute parameter, both id and value must be specified",
address);
return null;
}

return new CommandSpec(clusterId, commandId, commandParameterName, commandParameterValue);
return new CommandSpec(address, clusterId, commandId, commandParameterName, commandParameterValue, attributeId, attributeValue);
}

private String getParameterName(String parameterType, ButtonPressType buttonPressType) {
private static String getParameterName(String parameterType, ButtonPressType buttonPressType) {
return String.format("zigbee_%s_%s", buttonPressType, parameterType);
}

private int parseId(String id) throws NumberFormatException {
private static int parseId(String id) throws NumberFormatException {
if (id.startsWith("0x")) {
return parseInt(id.substring(2, id.length()), 16);
} else {
return parseInt(id);
}
}

private enum ButtonPressType {
protected enum ButtonPressType {
SHORT_PRESS("shortpress"),
DOUBLE_PRESS("doublepress"),
LONG_PRESS("longpress");
Expand All @@ -222,20 +297,29 @@ public String toString() {
}
}

private class CommandSpec {
protected static class CommandSpec {
private final int clusterId;
private final int commandId;
private final Integer commandId;
private final String commandParameterName;
private final String commandParameterValue;
private final Integer attributeId;
private final String attributeValue;
private final IeeeAddress address;

public CommandSpec(int clusterId, int commandId, String commandParameterName, String commandParameterValue) {
public CommandSpec(IeeeAddress address, int clusterId, Integer commandId, String commandParameterName, String commandParameterValue, Integer attributeId, String attributeValue) {
this.clusterId = clusterId;
this.commandId = commandId;
this.commandParameterName = commandParameterName;
this.commandParameterValue = commandParameterValue;
this.attributeId = attributeId;
this.attributeValue = attributeValue;
this.address = address;
}

public boolean matchesCommand(ZclCommand command) {
if (commandId == null) {
return false;
}
boolean commandIdMatches = command.getCommandId().intValue() == commandId;
boolean commandParameterMatches = commandParameterName == null || commandParameterValue == null
|| matchesParameter(command);
Expand All @@ -252,14 +336,30 @@ private boolean matchesParameter(ZclCommand command) {
return Objects.equals(result.toString(), commandParameterValue);
} catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException
| InvocationTargetException e) {
logger.warn(format("%s: Could not read parameter %s for command %s", endpoint.getIeeeAddress(),
logger.warn(format("%s: Could not read parameter %s for command %s", address,
commandParameterName, command), e);
return false;
}
}

private boolean matchesAttribute(ZclAttribute attribute) {
if (attributeId == null) {
return false;
}
boolean attributeIdMatches = attribute.getId() == attributeId;
boolean attributeValueMatches = Objects.equals(
Objects.toString(attribute.getLastValue()),
attributeValue
);
return attributeIdMatches && attributeValueMatches;
}

public int getClusterId() {
return clusterId;
}

public boolean isClientCluster() {
return commandId != null;
}
}
}
Loading

0 comments on commit defd455

Please sign in to comment.