Skip to content

Commit

Permalink
Add support for listening to group addresses on Ember coordinator (#695)
Browse files Browse the repository at this point in the history
Signed-off-by: Chris Jackson <chris@cd-jackson.com>
  • Loading branch information
cdjackson authored Nov 12, 2021
1 parent f87b1c6 commit b38e2df
Show file tree
Hide file tree
Showing 8 changed files with 235 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import com.zsmartsystems.zigbee.console.ember.EmberConsoleNcpChildrenCommand;
import com.zsmartsystems.zigbee.console.ember.EmberConsoleNcpConfigurationCommand;
import com.zsmartsystems.zigbee.console.ember.EmberConsoleNcpCountersCommand;
import com.zsmartsystems.zigbee.console.ember.EmberConsoleNcpMulticastCommand;
import com.zsmartsystems.zigbee.console.ember.EmberConsoleNcpPolicyCommand;
import com.zsmartsystems.zigbee.console.ember.EmberConsoleNcpRoutingCommand;
import com.zsmartsystems.zigbee.console.ember.EmberConsoleNcpScanCommand;
Expand All @@ -51,13 +52,14 @@
public class EmberZigBeeConsoleCommandProvider implements ZigBeeConsoleCommandProvider {

@SuppressWarnings("null")
public static final List<ZigBeeConsoleCommand> EMBER_COMMANDS = unmodifiableList(asList(
new EmberConsoleMmoHashCommand(), new EmberConsoleNcpChildrenCommand(),
new EmberConsoleNcpConfigurationCommand(), new EmberConsoleNcpCountersCommand(),
new EmberConsoleNcpPolicyCommand(), new EmberConsoleNcpRoutingCommand(), new EmberConsoleNcpScanCommand(),
new EmberConsoleSecurityStateCommand(), new EmberConsoleNcpStateCommand(),
new EmberConsoleNcpTokenCommand(), new EmberConsoleTransientKeyCommand(), new EmberConsoleNcpValueCommand(),
new EmberConsoleNcpVersionCommand()));
public static final List<ZigBeeConsoleCommand> EMBER_COMMANDS = unmodifiableList(
asList(new EmberConsoleMmoHashCommand(), new EmberConsoleNcpChildrenCommand(),
new EmberConsoleNcpConfigurationCommand(), new EmberConsoleNcpCountersCommand(),
new EmberConsoleNcpMulticastCommand(), new EmberConsoleNcpPolicyCommand(),
new EmberConsoleNcpRoutingCommand(), new EmberConsoleNcpScanCommand(),
new EmberConsoleSecurityStateCommand(), new EmberConsoleNcpStateCommand(),
new EmberConsoleNcpTokenCommand(), new EmberConsoleTransientKeyCommand(),
new EmberConsoleNcpValueCommand(), new EmberConsoleNcpVersionCommand()));

private Map<String, ZigBeeConsoleCommand> emberCommands = EMBER_COMMANDS.stream()
.collect(toMap(ZigBeeConsoleCommand::getCommand, identity()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@
*/
package org.openhab.binding.zigbee.ember.handler;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

Expand All @@ -22,6 +27,7 @@
import org.openhab.binding.zigbee.ember.EmberBindingConstants;
import org.openhab.binding.zigbee.ember.internal.EmberConfiguration;
import org.openhab.binding.zigbee.handler.ZigBeeCoordinatorHandler;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.io.transport.serial.SerialPortManager;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.thing.Bridge;
Expand All @@ -34,7 +40,10 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.zsmartsystems.zigbee.dongle.ember.EmberNcp;
import com.zsmartsystems.zigbee.dongle.ember.ZigBeeDongleEzsp;
import com.zsmartsystems.zigbee.dongle.ember.ezsp.structure.EmberMulticastTableEntry;
import com.zsmartsystems.zigbee.dongle.ember.ezsp.structure.EmberStatus;
import com.zsmartsystems.zigbee.dongle.ember.ezsp.structure.EzspConfigId;
import com.zsmartsystems.zigbee.serialization.DefaultDeserializer;
import com.zsmartsystems.zigbee.serialization.DefaultSerializer;
Expand Down Expand Up @@ -64,6 +73,24 @@ public class EmberHandler extends ZigBeeCoordinatorHandler implements FirmwareUp
private static final String ASH_RX_NAK = "ASH_RX_NAK";
private static final String ASH_TX_NAK = "ASH_TX_NAK";

/**
* Sets the minimum size of the multicast address table
*/
private static final int GROUPS_MINIMUM = 3;

/**
* Sets the minimum headroom in the multicast table. This allows for room for the table to grow during a session.
* Note that the table size is set only on startup since this impacts the Ember memory use.
*/
private static final int GROUPS_HEADROOM = 3;

/**
* Sets the maximum size of the groups table. This is designed to be large enough to allow pretty much any
* configuration, but small enough that to protect against the user adding loads of random numbers or invalid
* formatting of the config string.
*/
private static final int GROUPS_MAXIMUM = 20;

private final Logger logger = LoggerFactory.getLogger(EmberHandler.class);

private final SerialPortManager serialPortManager;
Expand All @@ -89,6 +116,9 @@ protected void initializeDongle() {

@Override
protected void initializeDongleSpecific() {
EmberConfiguration config = getConfigAs(EmberConfiguration.class);
setGroupRegistration(config.zigbee_groupregistration);

if (pollingJob == null) {
Runnable pollingRunnable = new Runnable() {
@Override
Expand Down Expand Up @@ -132,6 +162,36 @@ public void run() {
}
}

@Override
public void handleConfigurationUpdate(Map<String, Object> configurationParameters) {
logger.debug("{}: Ember configuration received.", nodeIeeeAddress);

Map<String, Object> unhandledConfiguration = new HashMap<>();
Configuration configuration = editConfiguration();
for (Entry<String, Object> configurationParameter : configurationParameters.entrySet()) {
// Ignore any configuration parameters that have not changed
if (Objects.equals(configurationParameter.getValue(), configuration.get(configurationParameter.getKey()))) {
logger.debug("{}: Ember configuration update: Ignored {} as no change", nodeIeeeAddress,
configurationParameter.getKey());
continue;
}

logger.debug("{}: Ember configuration update: Processing {} -> {}", nodeIeeeAddress,
configurationParameter.getKey(), configurationParameter.getValue());

switch (configurationParameter.getKey()) {
case ZigBeeBindingConstants.CONFIGURATION_GROUPREGISTRATION:
setGroupRegistration((String) configurationParameter.getValue());
break;
default:
unhandledConfiguration.put(configurationParameter.getKey(), configurationParameter.getValue());
break;
}

configuration.put(configurationParameter.getKey(), configurationParameter.getValue());
}
}

@Override
public void dispose() {
super.dispose();
Expand Down Expand Up @@ -296,4 +356,44 @@ private TransportConfig createTransportConfig(EmberConfiguration config) {
transportConfig.addOption(TransportConfigOption.CONCENTRATOR_CONFIG, concentratorConfig);
return transportConfig;
}

private void setGroupRegistration(String groupsString) {
if (groupsString.isBlank()) {
return;
}

logger.debug("ZigBee Ember Coordinator group registration is {}", groupsString);
ZigBeeDongleEzsp dongle = (ZigBeeDongleEzsp) zigbeeTransport;
EmberNcp ncp = dongle.getEmberNcp();

String[] groupsArray = groupsString.split(",");
Set<Integer> groups = new HashSet<>();

for (String groupString : groupsArray) {
groups.add(Integer.parseInt(groupString.trim(), 16));
}

int multicastTableSize = groups.size() + GROUPS_HEADROOM;
if (multicastTableSize < GROUPS_MINIMUM) {
multicastTableSize = GROUPS_MINIMUM;
} else if (multicastTableSize > GROUPS_MAXIMUM) {
multicastTableSize = GROUPS_MAXIMUM;
}
logger.debug("ZigBee Ember Coordinator multicast table size set to {}", multicastTableSize);
dongle.updateDefaultConfiguration(EzspConfigId.EZSP_CONFIG_MULTICAST_TABLE_SIZE, multicastTableSize);

int index = 0;
for (Integer group : groups) {
EmberMulticastTableEntry entry = new EmberMulticastTableEntry();
entry.setEndpoint(1);
entry.setNetworkIndex(0);
entry.setMulticastId(group);
EmberStatus result = ncp.setMulticastTableEntry(index, entry);

logger.debug("ZigBee Ember Coordinator multicast table index {} updated with {}, result {}", index, entry,
result);

index++;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ public class EmberConfiguration {
public Integer zigbee_childtimeout;
public Integer zigbee_concentrator;
public Integer zigbee_networksize;
public String zigbee_groupregistration;
}
Original file line number Diff line number Diff line change
Expand Up @@ -208,11 +208,16 @@
<advanced>true</advanced>
</parameter>

<parameter name="zigbee_installcode" type="text" groupName="network">
<label>Install Code</label>
<description>Add a temporary install key for device installation</description>
<advanced>true</advanced>
</parameter>
<parameter name="zigbee_installcode" type="text" groupName="network">
<label>Install Code</label>
<description>Add a temporary install key for device installation</description>
<advanced>true</advanced>
</parameter>

<parameter name="zigbee_groupregistration" type="text" groupName="network">
<label>Group Registrations</label>
<description>Registers the binding to receive group notifications. Groups are separated by a comma and must be defined in hexadecimal.</description>
</parameter>

<parameter name="zigbee_meshupdateperiod" type="integer" groupName="network">
<label>Mesh Update Period</label>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package org.openhab.binding.zigbee.ember;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.HashMap;
import java.util.Map;

import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.openhab.binding.zigbee.ZigBeeBindingConstants;
import org.openhab.binding.zigbee.converter.ZigBeeChannelConverterFactory;
import org.openhab.binding.zigbee.ember.handler.EmberHandler;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.io.transport.serial.SerialPortManager;
import org.openhab.core.thing.Bridge;

import com.zsmartsystems.zigbee.dongle.ember.EmberNcp;
import com.zsmartsystems.zigbee.dongle.ember.ZigBeeDongleEzsp;
import com.zsmartsystems.zigbee.dongle.ember.ezsp.structure.EmberMulticastTableEntry;
import com.zsmartsystems.zigbee.dongle.ember.ezsp.structure.EzspConfigId;

/**
*
* @author Chris Jackson
*
*/
public class EmberHandlerTest {
class EmberHandlerForTest extends EmberHandler {
public EmberHandlerForTest(Bridge coordinator, SerialPortManager serialPortManager,
ZigBeeChannelConverterFactory channelFactory, ZigBeeDongleEzsp dongle) {
super(coordinator, serialPortManager, channelFactory);
this.zigbeeTransport = dongle;
}

@Override
protected Configuration editConfiguration() {
return new Configuration();
}
}

@Test
public void setGroupRegistration() {
Bridge bridge = Mockito.mock(Bridge.class);
SerialPortManager serialPortManager = Mockito.mock(SerialPortManager.class);
ZigBeeChannelConverterFactory zigBeeChannelConverterFactory = Mockito.mock(ZigBeeChannelConverterFactory.class);

ZigBeeDongleEzsp dongle = Mockito.mock(ZigBeeDongleEzsp.class);
EmberNcp ncp = Mockito.mock(EmberNcp.class);
Mockito.when(dongle.getEmberNcp()).thenReturn(ncp);
EmberHandler handler = new EmberHandlerForTest(bridge, serialPortManager, zigBeeChannelConverterFactory,
dongle);

ArgumentCaptor<Integer> indexCaptor = ArgumentCaptor.forClass(Integer.class);
ArgumentCaptor<EmberMulticastTableEntry> valueCaptor = ArgumentCaptor.forClass(EmberMulticastTableEntry.class);

Map<String, Object> cfg = new HashMap<>();
cfg.put(ZigBeeBindingConstants.CONFIGURATION_GROUPREGISTRATION, "1234,5678, 90AB ,CDEF");
handler.handleConfigurationUpdate(cfg);

Mockito.verify(dongle).updateDefaultConfiguration(EzspConfigId.EZSP_CONFIG_MULTICAST_TABLE_SIZE, 7);
Mockito.verify(ncp, Mockito.times(4)).setMulticastTableEntry(indexCaptor.capture(), valueCaptor.capture());

int[] mcastId = { 0x1234, 0x5678, 0x90AB, 0xCDEF };
for (int cnt = 0; cnt < 4; cnt++) {
assertEquals(cnt, indexCaptor.getAllValues().get(cnt));
assertEquals(1, valueCaptor.getAllValues().get(cnt).getEndpoint());
assertEquals(0, valueCaptor.getAllValues().get(cnt).getNetworkIndex());
assertEquals(mcastId[cnt], valueCaptor.getAllValues().get(cnt).getMulticastId());
}
}
}
Loading

0 comments on commit b38e2df

Please sign in to comment.