ZigBee specifications define various clusters and public application profiles to be applied in different domains meeting the industries specific requirements. In addition, manufacturers can define proprietary clusters and profiles extensions. The big variety of existing clusters and profiles and the possibility for defining various manufacturer specific extensions requires that the logic for mapping ZigBee clusters to device classes is decoupled from the core logic for home device discovery, initialization and status handling - implemented in the ZigBee DA HDM Adapter.
All Device Items are represented as Device Classes. All Device Class Providers can also provide Device Items.
To add support to a custom cluster or clusters you need to:
Providing Cluster Mapping
The mapping between the ZigBee clusters and the device classes is provided by DeviceClassProvider services that are registered in the OSGi runtime and tracked by the ZigBee DA HDM Adapter. The device class providers understand the ZigBee ZCL semantics - their main responsibilities are serving queries regarding the device class mappings based on a given matching criteria (cluster identifier, profile identifier, device identifier, manufacturer code, etc.) and creating device class object instances on demand.
When a device class provider is requested to search for a device class match, it consults the ZCL specific information (provided in the matching criteria) and provides one or more DeviceClassMatch objects - they hold the matching criteria, the matched device class name and type (if any) and a matching number that correlates to the extent at which the matching criteria is satisfied.
If the provided device class match is found feasible by the ZigBee DA HDM adapter then the device class provider may be requested to create a DeviceClassObjectSpiAccess instance corresponding to the device class match.
In order to map the cluster functionality to a Device Class Interface, a Java class that controls the cluster's functionality must be presented. The class must implement one Device Class Interface and the DeviceClassObjectSpiAccess interface.
Device Class Matching Process
The ZigBee DA HDM Adapter queries all the tracked DeviceClassProvider-s during the home device initialization. This is done in order to find the best matching device classes to map over the currently supported clusters on the ZigBee device that is being represented. Each available device class provider is requested for a match given the cluster identifier, profile identifier, device identifier, manufacturer code and other ZCL properties that are read from the ZigBee device.
Device class providers return a match number of DeviceClassMatch.DEFAULT_MATCH and are included in the ZigBee module by default. If the device class provider cannot provide a mapping for the given matching criteria DeviceClassMatch.NO_MATCH is returned. ZigBee DA HDM Adapter compares the match numbers that are included in the DeviceClassMatch objects returned by the providers and chooses the provider that has offered the highest match number.
When multiple providers has matching numbers equals to the best match, all of them will contribute with their device classes.
DeviceClassMatch.ADDITIVE_MATCH could be return as matching number apart from the best match. This means that a provider can add device classes to a home device without being the best match.
The adapter then initiates instantiation of the device class object from the device class provider that has offered the best match and providers that has suggested ADDITIVE_MATCH.
The device class objects are attached to the home device instance, after they have been created and device class configuration is triggered. The device class objects of one home device can be retrieved from different device class providers.
The sequence diagram below represents the device class matching process performed by the ZigBee DA HDM Adapter upon initialization of a home device:

Implementing the DeviceClassProvider Interface
ZigBee DA HDM Adapter obtains your cluster implementations by using the following methods of the DeviceClassProvider interface:
Registering Custom DeviceClassProvider
When the OSGi Runtime starts, the ZigBee Protocol Adapter retrieves all Device Class Providers that are registered. It can be configured to wait for a certain provider by setting its name in the mbs.zigbee.hdmadapter.providers system property.
Here is an example of a device item that implements a Switch device item. For the implementation of the device item use some of the abstract implementations which cover the basic functionality and you only need to define the exact mapping between the ZigBee cluster commands and attributes and the device operations and properties:
/********************************************************************************
* Copyright (C) 1997, 2018 Bosch.IO GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* The license text accompanies this distribution,
* and you may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*******************************************************************************/
package demo.zigbee.hdm.deviceclass.provider;
import java.io.IOException;
import com.prosyst.mbs.services.da.items.Switch;
import com.prosyst.mbs.services.zigbee.hdm.deviceclasses.common.AbstractDeviceClassObjectV3;
import com.prosyst.mbs.services.zigbee.hdm.deviceclasses.common.AttributeIdentifier;
import com.prosyst.mbs.services.zigbee.hdm.deviceclasses.common.CommandIdentifier;
import com.prosyst.mbs.services.zigbee.hdm.deviceclasses.provider.HomeDeviceAdminSpiAccess;
import com.prosyst.mbs.services.zigbee.hdm.deviceclasses.provider.HomeDeviceSpiAccess;
import com.prosyst.mbs.services.zigbee.metadata.provider.ClusterDescription;
import com.prosyst.mbs.services.zigbee.metadata.provider.CommandDescription;
import com.prosyst.mbs.services.zigbee.zcl.general.metadata.OnOff;
import com.prosyst.util.ref.Log;
/**
* SwitchImpl represents demo implementation of {@link Switch} device class on top of the OnOff cluster.
* Type of device is "test" for demo purposes.
*/
public class SwitchImpl extends AbstractDeviceClassObjectV3 implements Switch {
/**
* Represents the device class object type.
*/
public static final String TYPE = "test";
private Boolean bValue = null;
/**
* Constructs new BooleanControl device class object.
*
* @param zbHomeDevice
* {@link HomeDeviceSpiAccess}
* @param spiAdminAccess
* {@link HomeDeviceAdminSpiAccess}
* @param log
*/
public SwitchImpl(HomeDeviceSpiAccess zbHomeDevice, HomeDeviceAdminSpiAccess spi, Log log) {
super(zbHomeDevice, spi, Switch.class.getName(), TYPE, OnOff.ID, log);
Activator.printOnConsole("SwitchDCO created");
}
/*
* (non-Javadoc)
* @see com.prosyst.mbs.services.zigbee.hdm.deviceclasses.common.AbstractDeviceClassObjectV3#getPropertyNames()
*/
public String[] getPropertyNames() {
Activator.printOnConsole("Property name: '" + PROPERTY_ON + "' returned");
return new String[] {
PROPERTY_ON
};
}
/*
* (non-Javadoc)
* @see com.prosyst.mbs.services.zigbee.hdm.deviceclasses.common.AbstractDeviceClassObjectV3
* #getAttributeIdsForProperty(java.lang.String)
*/
protected AttributeIdentifier[] getAttributeIdsForProperty(String property) {
if (PROPERTY_ON.equals(property)) {
Activator.printOnConsole("Returned attribute id: " + OnOff.ON_OFF + " (ON_OFF) for property '" + property + "'");
return getAttributeIdentifiers(OnOff.ON_OFF);
}
return null;
}
/*
* (non-Javadoc)
* @see com.prosyst.mbs.services.zigbee.hdm.deviceclasses.common.AbstractDeviceClassObjectV3
* #getCommandIdForOperation(java.lang.String)
*/
protected CommandIdentifier getCommandIdForOperation(String operation) {
if (OPERATION_ON.equals(operation)) {
return getCommandIdentifier(OnOff.ON);
} else if (OPERATION_OFF.equals(operation)) {
return getCommandIdentifier(OnOff.OFF);
}
return null;
}
/*
* (non-Javadoc)
* @see com.prosyst.mbs.services.zigbee.hdm.deviceclasses.common.AbstractDeviceClassObjectV3
* #attributeValueToPropertyValue(com.prosyst.mbs.services.zigbee.hdm.deviceclasses.common.AttributeIdentifier,
* java.lang.Object)
*/
protected Object attributeValueToPropertyValue(AttributeIdentifier attributeId, Object value) {
if (attributeId.getId() == OnOff.ON_OFF) {
Activator.printOnConsole("Attribute ON_OFF value: " + value);
}
return value;
}
// Methods below are overridden only for demo messages purpose
// In real DCO implementation these must be overridden only for special needs
// as when device can handle different notifications, has resources to be
// initialized at device activation and released at deactivation.
/*
* (non-Javadoc)
* @see com.prosyst.mbs.services.zigbee.hdm.deviceclasses.common.AbstractDeviceClassObjectV3#handleNotification(
* com.prosyst.mbs.services.zigbee.metadata.provider.ClusterDescription,
* com.prosyst.mbs.services.zigbee.metadata.provider.CommandDescription, int, int, java.lang.Object[])
*/
public void handleNotification(ClusterDescription cluster, CommandDescription command, int frameControl,
int transactionId, Object[] parameters) {
Activator.printOnConsole("handleNotification() called. " + "Command: '" + command.getDescription()
+ "' parameters: ");
for (int i = 0; i < parameters.length; i++) {
Activator.printOnConsole(" " + parameters[i]);
}
super.handleNotification(cluster, command, frameControl, transactionId, parameters);
}
/*
* (non-Javadoc)
* @see com.prosyst.mbs.services.zigbee.hdm.deviceclasses.common.AbstractDeviceClassObjectV3#configure()
*/
public boolean configure() {
Activator.printOnConsole("configure() called");
return super.configure();
}
/*
* (non-Javadoc)
* @see com.prosyst.mbs.services.zigbee.hdm.deviceclasses.common.AbstractDeviceClassObjectV3#activate()
*/
public void activate() {
Activator.printOnConsole("activate(boolean configure) called");
super.activate();
}
/*
* (non-Javadoc)
* @see com.prosyst.mbs.services.zigbee.hdm.deviceclasses.common.AbstractDeviceClassObjectV3#deactivate()
*/
public void deactivate() {
Activator.printOnConsole("deactivate() called");
super.deactivate();
}
/**
* @see com.prosyst.mbs.services.zigbee.hdm.deviceclasses.common.AbstractDeviceClassObjectV3#prepare()
*/
protected boolean prepare() {
Activator.printOnConsole("prepare() called");
try { // OnOff state.
bValue = (Boolean) readPreparedAttributes(null, getAttributeIdentifiers(OnOff.ON_OFF))[0];
} catch (IOException e) {
log.error("[SwitchDI] Could not OnOff state of device: " + zbHomeDevice.getUID(), e);
}
return true;
}
/**
* Override this method when type is specifically determined.
*
* @see com.prosyst.mbs.services.zigbee.hdm.deviceclasses.common.AbstractDeviceClassObjectV3#getType()
*/
public String getType() {
String type = super.getType();
Activator.printOnConsole("getType() returned: " + type);
return type;
}
/**
* @see com.prosyst.mbs.services.da.items.Switch#isOn()
*/
public boolean isOn() throws IOException {
if (bValue != null) {
try {
return bValue.booleanValue();
} finally {
bValue = null;
}
}
return ((Boolean) getProperty(PROPERTY_ON)).booleanValue();
}
/**
* @see com.prosyst.mbs.services.da.items.Switch#on()
*/
public void on() throws IOException {
executeOperation(OPERATION_ON, PROPERTY_ON, null, true, true);
}
/**
* @see com.prosyst.mbs.services.da.items.Switch#off()
*/
public void off() throws IOException {
executeOperation(OPERATION_OFF, PROPERTY_ON, null, true, true);
}
}
Here is an example of a device class provider that provides a Switch device item with a better match than the default one:
/********************************************************************************
* Copyright (C) 1997, 2018 Bosch.IO GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* The license text accompanies this distribution,
* and you may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*******************************************************************************/
package demo.zigbee.hdm.deviceclass.provider;
import java.util.Map;
import com.prosyst.mbs.services.da.items.Switch;
import com.prosyst.mbs.services.zigbee.ZigBeeDevice;
import com.prosyst.mbs.services.zigbee.hdm.deviceclasses.common.DeviceClassUtils;
import com.prosyst.mbs.services.zigbee.hdm.deviceclasses.provider.DeviceClassMatch;
import com.prosyst.mbs.services.zigbee.hdm.deviceclasses.provider.DeviceClassObjectSpiAccess;
import com.prosyst.mbs.services.zigbee.hdm.deviceclasses.provider.DeviceClassProvider;
import com.prosyst.mbs.services.zigbee.hdm.deviceclasses.provider.HomeDeviceAdminSpiAccess;
import com.prosyst.mbs.services.zigbee.hdm.deviceclasses.provider.HomeDeviceSpiAccess;
import com.prosyst.mbs.services.zigbee.zcl.general.metadata.OnOff;
import com.prosyst.util.ref.Log;
/**
* {@link DeviceClassProvider} service implementation used for demo purpose. It is fully functional implementation and
* provides a {@link Switch} device class, from {@link OnOff} cluster. It is general device class so every public
* profile is valid. For demo purposes one type - "test" is provided for this device class.
*/
public class DemoDeviceClassProvider implements DeviceClassProvider {
private HomeDeviceAdminSpiAccess spi;
private final Log log;
private static final String[] SUPPORTED_DEVICE_CLASSES = new String[] {
Switch.class.getName()
};
private static final String[] SUPPORTED_DEVICE_CLASS_TYPES = new String[] {
SwitchImpl.TYPE
};
/**
* Constructs new DemoDeviceClassProvider object.
*
* @param log
*/
public DemoDeviceClassProvider(Log log) {
this.log = log;
}
/*
* (non-Javadoc)
* @see com.prosyst.mbs.services.zigbee.hdm.deviceclasses.provider.DeviceClassProvider#getDeviceClasses()
*/
public String[] getDeviceClasses() {
return SUPPORTED_DEVICE_CLASSES;
}
/*
* (non-Javadoc)
* @see com.prosyst.mbs.services.zigbee.hdm.deviceclasses.provider.DeviceClassProvider#getDeviceClassTypes(
* java.lang.String)
*/
public String[] getDeviceClassTypes(String deviceClass) {
return SUPPORTED_DEVICE_CLASS_TYPES;
}
/*
* (non-Javadoc)
* @see com.prosyst.mbs.services.zigbee.hdm.deviceclasses.provider.DeviceClassProvider#getDeviceClassObject(
* com.prosyst.mbs.services.zigbee.hdm.deviceclasses.provider.HomeDeviceSpiAccess,
* java.lang.String, java.lang.String, java.util.Map)
*/
public DeviceClassObjectSpiAccess getDeviceClassObject(HomeDeviceSpiAccess zbHomeDevice, String dcName, String type,
Map properties) {
if (Switch.class.getName().equals(dcName)) {
return new SwitchImpl(zbHomeDevice, spi, log);
}
return null;
}
/**
* Choose your match number to be {@link MATCH_NONE} if clusterId and isServerCluster criteria are not matched. Use
* {@link DEFAULT_MATCH} if matched are clusterId and isServerCluster criteria. And incremented with 1 for every
* additional matched criteria. Here for demo purpose match number is increased without criteria because demo version
* needs to have the highest match so it will be preferred before {@link Switch} implementation at general DC
* provider.
*
* @see com.prosyst.mbs.services.zigbee.hdm.deviceclasses.provider.DeviceClassProvider#getDeviceClassMatch(
* java.util.Map, com.prosyst.mbs.services.zigbee.ZigBeeDevice)
*/
public DeviceClassMatch[] getDeviceClassMatch(Map properties, ZigBeeDevice device) {
int clusterId = ((Integer) properties.get(DeviceClassProvider.PROPERTY_CLUSTER_ID)).intValue();
if (!DeviceClassUtils.isCoordinator(device) && !isClientCluster(properties) && clusterId == OnOff.ID) {
// overwrite default provider
return new DeviceClassMatch[] {
new DeviceClassMatch(Switch.class.getName(), null, DeviceClassMatch.DEFAULT_MATCH + 10, properties)
};
}
return new DeviceClassMatch[] {
new DeviceClassMatch(null, null, DeviceClassMatch.NO_MATCH, properties)
};
}
/*
* (non-Javadoc)
* @see com.prosyst.mbs.services.zigbee.hdm.deviceclasses.provider.DeviceClassProvider#setHomeDeviceAdminSpiAccess(
* com.prosyst.mbs.services.zigbee.hdm.deviceclasses.provider.HomeDeviceAdminSpiAccess)
*/
public void setHomeDeviceAdminSpiAccess(HomeDeviceAdminSpiAccess homeDeviceAdminSpiAccess) {
this.spi = homeDeviceAdminSpiAccess;
}
private boolean isClientCluster(Map properties) {
return CLUSTER_TYPE_CLIENT.equals(properties.get(PROPERTY_CLUSTER_TYPE));
}
}
Here is an example of a bundle activator for the demo provider:
/********************************************************************************
* Copyright (C) 1997, 2018 Bosch.IO GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* The license text accompanies this distribution,
* and you may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*******************************************************************************/
package demo.zigbee.hdm.deviceclass.provider;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceRegistration;
import com.prosyst.mbs.services.zigbee.ZigBeeDevice;
import com.prosyst.mbs.services.zigbee.hdm.deviceclasses.provider.DeviceClassProvider;
import com.prosyst.mbs.services.zigbee.metadata.provider.ZCLMetaDataService;
import com.prosyst.util.ref.Log;
/**
* The ZigBee DCO Provider Demo shows how to register a custom device class provider.
*/
public class Activator implements BundleActivator {
private BundleContext bc;
private Log log;
private DemoDeviceClassProvider dcProvider;
private ServiceRegistration deviceClassProviderReg;
/**
* Initializes Log instance. Track {@link ZCLMetaDataService} service.
*
* @see org.osgi.framework.BundleActivator#start(org.osgi.framework.BundleContext)
*/
public void start(BundleContext bc) throws Exception {
this.bc = bc;
log = new Log(bc);
registerDeviceClassProvider();
}
/**
* Stops services tracking. Unregister this {@link DeviceClassProvider} service.
*
* @see org.osgi.framework.BundleActivator#stop(org.osgi.framework.BundleContext)
*/
public void stop(BundleContext arg0) throws Exception {
log.close();
unregisterDeviceClassProvider();
}
/**
* Removes provided {@link DeviceClassProvider} service from OSGI services registry. This will cause all provided by
* this service {@link com.prosyst.mbs.services.zigbee.hdm.deviceclasses.provider.DeviceClassObjectSpiAccess} to be
* removed from {@link ZigBeeDevice} implementations.
*/
private void unregisterDeviceClassProvider() {
if (deviceClassProviderReg != null) {
deviceClassProviderReg.unregister();
deviceClassProviderReg = null;
dcProvider = null;
}
}
/**
* Initializes {@link DemoDeviceClassProvider} with services instances and registers it as {@link DeviceClassProvider}
* OSGI service. If ZigBee DA HDM Adapter bundle is already started it must be restarted to take newly added
* {@link DeviceClassProvider} service's DCOs. At real application property "mbs.zigbee.hdmadapter.providers" need to
* be set before start of ZigBee DA HDM Adapter bundle. It must define separated by comma symbolic names of those bundles
* that are registering {@link DeviceClassProvider} OSGI service. Then adapter will wait these bundles to be installed
* before its registering at hdm as adapter. Other way is to ensure that those bundles will be installed and activated
* before ZigBee DA HDM Adapter bundle.
*/
private void registerDeviceClassProvider() {
dcProvider = new DemoDeviceClassProvider(log);
deviceClassProviderReg = bc.registerService(DeviceClassProvider.class.getName(), dcProvider, null);
}
/**
* Print a text to system console.
*
* @param text Text to be printed.
*/
public static final void printOnConsole(String text) {
System.out.println("[Demo DCO Provider] " + text);
}
}
For an example implementation of a cluster and a Device Class Provider, see the ZigBee DCO Provider Demo.