From 69a108cf478f5c9e6cd6164c8694f57a5e6fac5d Mon Sep 17 00:00:00 2001 From: Boris Labbe Date: Wed, 15 Nov 2023 14:58:47 +0100 Subject: [PATCH] GWAPPS-47: Update doc of how to implement Z-Wave CC --- .../img/attribute_store_sound_switch.png | Bin 0 -> 52080 bytes .../img/attribute_store_sound_switch.uml | 75 + .../img/zwave_command_class_integration.png | Bin 0 -> 26741 bytes .../img/zwave_command_class_integration.uml | 30 + ...ve_command_class_sound_switch_commands.png | Bin 0 -> 11095 bytes ...command_class_sound_switch_conf_report.png | Bin 0 -> 21097 bytes ..._to_implement_new_zwave_command_classes.md | 348 ---- .../how_to_implement_zwave_command_classes.md | 1415 +++++++++++++++++ 8 files changed, 1520 insertions(+), 348 deletions(-) create mode 100644 applications/zpc/doc/assets/img/attribute_store_sound_switch.png create mode 100644 applications/zpc/doc/assets/img/attribute_store_sound_switch.uml create mode 100644 applications/zpc/doc/assets/img/zwave_command_class_integration.png create mode 100644 applications/zpc/doc/assets/img/zwave_command_class_integration.uml create mode 100644 applications/zpc/doc/assets/img/zwave_command_class_sound_switch_commands.png create mode 100644 applications/zpc/doc/assets/img/zwave_command_class_sound_switch_conf_report.png delete mode 100644 applications/zpc/how_to_implement_new_zwave_command_classes.md create mode 100644 applications/zpc/how_to_implement_zwave_command_classes.md diff --git a/applications/zpc/doc/assets/img/attribute_store_sound_switch.png b/applications/zpc/doc/assets/img/attribute_store_sound_switch.png new file mode 100644 index 0000000000000000000000000000000000000000..004b95c7d892ef72660960177e923fdce8b967c0 GIT binary patch literal 52080 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≻AT@- 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 KG#~A=3j7ba AttributeStore : .uam files + +' IoT Services relation to the rest +IOTService <-d-> Broker + +UCL -d-> AttributeStore : Commands +ZPC -d-> AttributeStore : Update +AttributeStore -u-> ZPC : Send events +AttributeStore -u-> UCL : Send events +UCL <-u-> Broker + +@enduml \ No newline at end of file diff --git a/applications/zpc/doc/assets/img/zwave_command_class_sound_switch_commands.png b/applications/zpc/doc/assets/img/zwave_command_class_sound_switch_commands.png new file mode 100644 index 0000000000000000000000000000000000000000..f787f23628a71b4b06930e0ea665713c83654a41 GIT binary patch literal 11095 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 rGK=zQ#lLsAYrs_^U>o!#.c). Before the -implementation, in CMakeLists.txt in the zwave_command_classes package add the -path for the command class file that includes the implementation code -(e.g. src/zwave_command_class_< name >.c). - -## Implementation - -The easiest way to start our implementation is to copy and use as a template an -up-to-date implementation e.g. zwave_command_class_alarm_sensor.c, -zwave_command_class_binary_switch.c. In the implementation code of the command -class, can be found the following structure. - -### Command Class Functions - -#### Public interface functions - -**The initialisation function**: The function starts the procedure by -registering the rules for the attributes. Each attribute can have only one -rule, but it is not necessary to register a rule for all the attributes. -A rule is necessary only for attributes that will be used from a set or/and get -function. Register the rule through the zwave_command_class_register_rule -(Attribute type, set_function, get function) if the attribute will be used only -for a set or get command, provide only this function and NULL for the other one. - -**Attribute store callbacks registration**: Attribute store callbacks functions -are responsible for do the appropriate changes to the attribute store when a -change is taking place to an attribute (e.g. delete, update). Through the -functions that register the callbacks, we can register the function that will -make the appropriate changes to the attribute store when any change happens to -a specific attribute. The function that we will register will be implemented in -the same file. - -Register Z-wave Command Class handler: This piece of code is responsible to -register the handler of the command class. In the command_handler assign the -function that will handle the commands that are addressed to our command class. - -#### Implementation of the assigned functions - -**Resolution Functions**: Resolutions functions are the get/set functions that -we assigned the rules. In each of these get/set functions fill the frame with -the appropriate information that it needs to be based on the command class -specification to return the response that needs. If no existing command class -structure serves the scope, create a new command structure in the header file. -Before the synthesis of the frame it could be necessary to do some things to -find the information that should be included in the frame. - -**Attribute Store Callback Functions**: These functions are the functions that -handle the changes of an attribute. When an attribute is updated or deleted etc. -some actions should take place in the attribute store. An example can be when -the version of an endpoint is updating, it could need to create some of the -nodes that at a later point will be used to handle other commands. - -**Incoming commands handler**: This function is the one that will handle the -report commands. In this function, we check the size of command frame, the -header which is the Command Class identifier, if the size of the frame and the -Command Class identifier are as expected we proceed and based on the Command -identifier call the frame parsing function that is responsible to handle the -command. - -```c - zwave_command_handler_t handler = {}; - handler.support_handler = NULL; - handler.control_handler = &zwave_command_class_example_control_handler; - handler.minimal_scheme = ZWAVE_CONTROLLER_ENCAPSULATION_NONE; - handler.manual_security_validation = false; - handler.command_class = COMMAND_CLASS_EXAMPLE; - handler.version = EXAMPLE_VERSION; - handler.command_class_name = "Example"; - handler.comments = ""; -``` - - **Frame-parsing Functions**: These are the functions that handle the incoming - frames by using the structure of the frame and extracting information from the - bytes to serve the scope of the specified commands that are documented in the - Z-Wave Application Command Class Specification. - -### Post-processing - -After the implementation of the command class, you could be able to verify the -correct operation by including a device that supports the new command class and -verfiy the new command class attributes are created and updated via checking -the attribute store (i.e., using 'attribute_store_log ' CLI commands). - -To ensure the implemented functionalities are mapped to the doddot ZCl data -model, one should create the UAM file under dotodot_mappers/rules for Dotdot -Cluster to Z-Wave Command Class Mapping. - -**Implement UAM file**: A new command class can require the implementation -of a new UAM file to map the data to an existing or a new XML cluster. The -first step is to find how the data are stored through the command class, -in `attribute_store_defined_attribute_types.h`, we can find the -definitions of the attributes. For example, all attributes of the Alarm -Sensor Command class start with 0x9C this is the definition for -`COMMAND_CLASS_SENSOR_ALARM`, under the definition of the command class -version you can find the definitions for the rest of the attributes, -for the Alarm Sensor Type attribute is defining as -`COMMAND_CLASS_SENSOR_ALARM << 8 | 0x03`, this means that the type of -the alarm is stored at 0x9C03. The next step is to find corresponding -ZCL attributes to map the data from the Z-Wave Attributes. -For example, we will map the alarm sensor data to the IASZone XML. The -id of IASZone XML is 0500 we would like to have IASZone state, type, and -status, based on that we create the following UAM where the -`zbIAS_ZONE_ZONE_STATE` takes the value 1 if the Alarm sensor state exists, -in IASZone XML the type of state attribute is enum8 this means that the -attributes have tags that depend on the value that the attribute have, -in this occasion the value will give to ZoneState the Enrolled tag. -Moreover, if alarm sensor state exist, the ZoneType will take the -value 0, this attribute is type IasZoneType which is defined as enum in -`zap-type.h` and the 0 value represents the -`EMBER_ZCL_IAS_ZONE_TYPE_STANDARD_CIE`. At the end, if the state of the -alarm is bigger than 0 means that we have an alarm so the -`zbIAS_ZONE_ZONE_STATUS` will take the value 1. It is important to -know how the attributes are stored in terms of type, as some times need -to make some changes in the values, an example is if we store a value as -float but we need to map it as integer we have to multiply it to be in -the right type, so for some values should be necessary to do some proper -operation. In `zap-types.h` can be found defined enum types and structs. - -```uam -// Z-Wave Attributes -def zwALARM_SENSOR_TYPE 0x9C03 -def zwALARM_SENSOR_STATE 0x9C04 - -// DotDot Attributes -def zbIAS_ZONE_ZONE_STATE 0x05000000 -def zbIAS_ZONE_ZONE_TYPE 0x05000001 -def zbIAS_ZONE_ZONE_STATUS 0x05000002 - -scope 0 { - // Just consider the Zone enrolled if Alarm Sensor is supported. - r'zbIAS_ZONE_ZONE_STATE = if (e'zwALARM_SENSOR_TYPE.zwALARM_SENSOR_STATE) 1 undefined - // Zone type will be Standard CIE - r'zbIAS_ZONE_ZONE_TYPE = if (e'zwALARM_SENSOR_TYPE.zwALARM_SENSOR_STATE) 0 undefined - // Set Alarm 1 active if the Alarm Sensor is non-zero - r'zbIAS_ZONE_ZONE_STATUS = if (r'zwALARM_SENSOR_TYPE.zwALARM_SENSOR_STATE > 0) 1 0 -} -``` - -An example of custom XML Cluster can be found below, we gave -id FF02 to the Alarm Sensor XML Cluster, in this case, we need to map two -attributes type and state of the alarm. Each attribute in the XML has an -id the id of attribute type is 0000 and the state's id is 0001. In the -same logic as previously, we can say that the data regarding type have to -be mapped to 0xFF020000 and the state to 0xFF020001. The structure of the -XML should cope with the specification of the command class. - -```xml - - - - - - - - - - - - - - - - - - -``` - -As we found how the data are stored we can implement the UAM file that will -map the data from the Z-Wave command class to the XML Cluster. The first step -is to define the DotDot attributes and Z-Wave attributes. Below is an example -of the UAM file that maps the z-wave attributes to the dotdot. We use the -information that we found in the previous step, the alarm type for the DotDot -will be defined as 0xFF020000, the state 0xFF020001. In this example, let's -say that we want to map the state of the alarm sensor type smoke. Based on the -documentation in [UAM map for ZPC](how_to_write_uam_files_for_the_zpc.md) that -explains the structure, grammar, and keywords for UAM, we are mapping the type -of the Z-wave alarm sensor to the DotDot with this command -`r'DOTDOT_ATTRIBUTE_ID_ALARM_TYPE = r'zwALARM_SENSOR_TYPE`. For the state, -we use this command `r'zwALARM_SENSOR_TYPE[SMOKE].zwALARM_SENSOR_STATE`, in -brackets, we refer to a specific type in this occasion we want the smoke -alarm so after the type we put into brackets the smoke id which is 0x01 -based on the specification of alarm sensor command class. Moreover, this is -a simple example of how to map data from Z-Wave to DotDot a mapper can be -more complex consisting of if statements or/and more of attributes. Moreover, -after any modification on an XML file, we need to make a -clean build, after that you can verify that the changes applied by check in -`unify_dotdot_defined_attribute_types.h` under the build folder, -if the dotdot attributes are included. - -```uam -// DotDot Attributes -def DOTDOT_ATTRIBUTE_ID_ALARM_TYPE 0xFF020000 -def DOTDOT_ATTRIBUTE_ID_ALARM_STATE 0xFF020001 - -// Z-Wave Attributes -def zwALARM_SENSOR_TYPE 0x9C03 -def zwALARM_SENSOR_STATE 0x9C04 - -def SMOKE 0x01 - -scope 0 { - r'DOTDOT_ATTRIBUTE_ID_ALARM_TYPE = r'zwALARM_SENSOR_TYPE - r'DOTDOT_ATTRIBUTE_ID_ALARM_STATE = r'zwALARM_SENSOR_TYPE[SMOKE].zwALARM_SENSOR_STATE -} -``` - -If the command class attributes state required to be updated via issue 'get' -type of commands, one could add the attribute and the polling interval in the -zwave_poll_config.yaml which can be found under the zpc_rust package. More -information regarding polling can be found in the -Network Polling section in [ZPC User's Guide](readme_user.md). - -**Test class**: Under the zwave_command_class/test we need to have a test class - to test the functionality of the command class. An approach that can be used - to implement the test class is to try and think based on the command class - specification and the code of the command class the good and bad scenarios - that could happen. A suggestion is to try and create a method for each bad - scenario, based on the implementation of the command class could be necessary - in some occasions to create some nodes to test if any change happens to - others. After the implementation of the test class should add the unit test - to the CMakeLists.txt. - -> NOTE: An implementation of the command class can be more complicated and -> can consist of more than that mentioned above. This is only a guide that -> helps to start with. diff --git a/applications/zpc/how_to_implement_zwave_command_classes.md b/applications/zpc/how_to_implement_zwave_command_classes.md new file mode 100644 index 0000000000..b4dd3107f5 --- /dev/null +++ b/applications/zpc/how_to_implement_zwave_command_classes.md @@ -0,0 +1,1415 @@ +# Guideline for implementing Command Classes + +```{toctree} +--- +maxdepth: 2 +titlesonly: +hidden: +--- +``` + +This guide is meant to guide you to implement a new Z-Wave command class in Unify. We'll go trough step by step following a real example : implementing the Sound Switch command class. + +The following schema shows you a top level view of all the components we are using : + +![Overview](doc/assets/img/zwave_command_class_integration.png) + +- The Z-Wave controller handles the Z-Wave commands and update the attribute store if needed. +- The UCL updates the Attribute Store though commands. +- The Attribute Store send events to : + - The attributes monitored by the Z-Wave controller + - The attributes monitored by the UCL that triggers MQTT Broker messages. +- The .uam files allow to automatically update UCL attributes when Z-Wave attributes changes and vice versa. + +> NOTE : If you want the simplest example of a command class implementation you can look at the Binary Switch command class. + +## Defining the Z-Wave Command Class attribute state data model + +The first step is to define the data model of the Z-Wave Command Class attributes state. A good starting point is the Z-Wave command class specification (also available in the Z-Wave PC Controller tool). You can look at different commands (get/set/report) and see which attributes are worth saving. + +![Sound switch commands](doc/assets/img/zwave_command_class_sound_switch_commands.png) + +Most of the commands should be already defined in `ZW_classcmd.h`. Not that this file might not contains all the definitions you need, but you can look in the GSDK folder that contains the same file but more up to date. Simply copy the attributes and commands you need in the local `ZW_classcmd.h`. + +> NOTE : Don't replace the new file by the old one as you might have some incompatibilities with the current command classes. + +Based on the commands we need the following attributes : + +| Attribute | Description | Associated command | +|-----------|-------------|--------------------| +| Tone number | Supported tone number count | Tones Number +| Configured volume | Current default volume on the device | Configuration +| Configured tone | Current default tone on the device | Configuration +| Tone info name | Tone Description (current default tone or any tone supported) | Tone Info +| Tone info duration | Tone duration (current default tone or any tone supported) | Tone Info +| Play | The tone ID that the device plays. 0 to stop, 255 to play the default tone (Configuration command) and 1-254 to play a specific tone | Play + +With those attributes we cover all the commands needs. The following schema illustrates the attributes we use in our Sound Switch command class implementation : + +![Sound Switch](doc/assets/img/attribute_store_sound_switch.png) + +Each attribute in our Attribute Store triggers Z-Wave commands. + +We use a `TONE_INFO_IDENTIFIER` as a parent of `TONE_INFO_DURATION` and `TONE_INFO_NAME` to allow the user to change this value if needed and gather information about a specific tune. + +The `TONE_INFO_IDENTIFIER` triggers a Tone Info command while `CONFIGURED_DEFAULT_TONE_IDENTIFIER` triggers a Configuration command. + +In the attribute store attributes has two states : defined and reported. +- If the reported value is missing, the controller sends a GET command to gather the reported value. +- If the defined value is set, the controller sends a SET command to set the desired value. + +This mapping is explained in [Get/Set command section](#getset-command) + + +## Create the data model in the Attribute Store + +After defining the data model, the next step is to define the type of each node in the attribute store. + +In `applications\zpc\components\zpc_attribute_store\include\command_class_types` create an header that defines the types used in command class implementation. The naming convention is `zwave_command_class_{COMMAND_CLASS_NAME}.h` + +```C +///> Tone identifier. uint8_t +typedef uint8_t sound_switch_tone_id_t; +///> Tone duration. uint16_t +typedef uint16_t sound_switch_tone_duration_t; + +///> Volume representation. uint8_t +typedef uint8_t sound_switch_volume_t; +``` + +You might want to update this file as you are writing the logic. Here we defined 3 attributes we use often in the Sound Switch class : the tone identifier, the tone duration and the volume. + +### Attribute definition + +Then we need to create the attributes of command class to register them later in the attribute store. This is done in the +`attribute_store_defined_attribute_types.h` header. + +To add an attribute you can use the `DEFINE_ATTRIBUTE` macro. Each attribute should have an unique ID on 16 bits : + +```txt +[COMMAND_CLASS_ID][UNIQUE_ATTRIBUTE_ID] +[0x79][0x01] +``` + +Where `COMMAND_CLASS_ID` is the command class ID (found in `ZW_classcmd.h`) and +`UNIQUE_ATTRIBUTE_ID` is an arbitrary and unique ID to the command class. + +The first attribute you have to declare is the supported version : + +```C +/// zwave_cc_version_t +DEFINE_ATTRIBUTE(ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_VERSION, + ZWAVE_CC_VERSION_ATTRIBUTE(COMMAND_CLASS_SOUND_SWITCH)) +``` + +The `ZWAVE_CC_VERSION_ATTRIBUTE` macro set the unique attribute ID to 0x01. So you have to start your custom attribute listing to 0x02. We can define `CONFIGURED_DEFAULT_VOLUME` like : + +```C +// Configured volume for the Sound Switch +DEFINE_ATTRIBUTE(ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_CONFIGURED_DEFAULT_VOLUME, + ((COMMAND_CLASS_SOUND_SWITCH << 8) | 0x02)) +``` + +`ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_CONFIGURED_DEFAULT_VOLUME` has an ID of `0x7902`. This is used latter for mapping the attribute store Z-Wave attributes to the Dot Dot model. + +A complete definition of our classes attribute can be found bellow : + +```c +///////////////////////////////////////////////// +// Sound Switch Command Class +///< This represents the version of the Sound Switch Command class. +/// zwave_cc_version_t +DEFINE_ATTRIBUTE(ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_VERSION, + ZWAVE_CC_VERSION_ATTRIBUTE(COMMAND_CLASS_SOUND_SWITCH)) +// Configured volume for the Sound Switch +DEFINE_ATTRIBUTE(ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_CONFIGURED_DEFAULT_VOLUME, + ((COMMAND_CLASS_SOUND_SWITCH << 8) | 0x02)) +// Configured tone for the Sound Switch +DEFINE_ATTRIBUTE( + ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_CONFIGURED_DEFAULT_TONE_IDENTIFIER, + ((COMMAND_CLASS_SOUND_SWITCH << 8) | 0x03)) +// Number of tones supported by the receiving node +DEFINE_ATTRIBUTE(ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_TONES_NUMBER, + ((COMMAND_CLASS_SOUND_SWITCH << 8) | 0x04)) +DEFINE_ATTRIBUTE(ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_TONE_INFO_IDENTIFIER, + ((COMMAND_CLASS_SOUND_SWITCH << 8) | 0x05)) +DEFINE_ATTRIBUTE(ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_TONE_INFO_DURATION, + ((COMMAND_CLASS_SOUND_SWITCH << 8) | 0x06)) +DEFINE_ATTRIBUTE(ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_TONE_INFO_NAME, + ((COMMAND_CLASS_SOUND_SWITCH << 8) | 0x07)) +// Command is used to instruct a supporting node to play (or stop playing) a tone. +DEFINE_ATTRIBUTE(ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_TONE_PLAY, + ((COMMAND_CLASS_SOUND_SWITCH << 8) | 0x08)) +``` + +### Attribute registration + +Now that we have all our attributes defined we must register them in the attribute store. We can do that in the `zpc_attribute_store_type_registration.cpp` file. + +You just need to add an new entry with : + +1. Our attribute store name defined in `attribute_store_defined_attribute_types.h` +2. A const String description of the attribute +3. The parent node. In most cases it is the `ATTRIBUTE_ENDPOINT_ID`, but you can set it to another attribute if needed. The more attributes we put under the endpoint the poorer performance. +4. The type of the attribute. The complete enum can be found in `components/uic_attribute_store/include/attribute_store_type_registration.h`. If set the engine validates the value you try to write in it and raise an error if types are incompatible. + +```C + {ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_VERSION, "Sound Switch Version", ATTRIBUTE_ENDPOINT_ID, U8_STORAGE_TYPE}, +``` + +The full sample can be found bellow : + +```c + ///////////////////////////////////////////////////////////////////// + // Sound Switch Command Class attributes + ///////////////////////////////////////////////////////////////////// + {ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_VERSION, "Sound Switch Version", ATTRIBUTE_ENDPOINT_ID, U8_STORAGE_TYPE}, + {ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_CONFIGURED_DEFAULT_VOLUME, "Configured Default Volume", ATTRIBUTE_ENDPOINT_ID, U8_STORAGE_TYPE}, + {ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_CONFIGURED_DEFAULT_TONE_IDENTIFIER, "Configured Default Tone Identifier", ATTRIBUTE_ENDPOINT_ID, U8_STORAGE_TYPE}, + {ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_TONES_NUMBER, "Tones Number", ATTRIBUTE_ENDPOINT_ID, U8_STORAGE_TYPE}, + + {ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_TONE_INFO_IDENTIFIER, "Tone Info Identifier", ATTRIBUTE_ENDPOINT_ID, U8_STORAGE_TYPE}, + + {ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_TONE_INFO_DURATION, "Tone Info Duration", ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_TONE_INFO_IDENTIFIER, U16_STORAGE_TYPE}, + {ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_TONE_INFO_NAME, "Tone Info Name", ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_TONE_INFO_IDENTIFIER, C_STRING_STORAGE_TYPE}, + + {ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_TONE_PLAY, "Tone Play", ATTRIBUTE_ENDPOINT_ID, U8_STORAGE_TYPE}, +``` + +You can see that `ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_TONE_INFO_DURATION` and `ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_TONE_INFO_NAME` parents are `ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_TONE_INFO_IDENTIFIER` and not `ATTRIBUTE_ENDPOINT_ID`. This is because those parameters depends directly on the value `ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_TONE_INFO_IDENTIFIER` and we do not want to surcharge our endpoint tree. + +## Write the logic of the command class + +### Base skeleton creation + +Now that our attributes are known to the Attribute Store we can start implementing our command class logic. + +You can create a new header in `applications\zpc\components\zwave_command_classes\src` with the following name pattern : +`zwave_command_class_{COMMAND_CLASS_NAME}.h` where `{COMMAND_CLASS_NAME}` is your Z-Wave command class name. In our example our new header is : `zwave_command_class_sound_switch.h` + +Repeat this process for the source file. Note that you can use either `.c` or `.cpp` file as long as your header is `C` compatible. For our example we create the file `zwave_command_class_sound_switch.c`. + +We need to add the source file to the `CMakeLists.txt` located in `applications\zpc\components\zwave_command_classes\` : + +```CMake +add_library( + zwave_command_classes + platform/${COMPATIBLE_PLATFORM}/platform_date_time.c + platform/${COMPATIBLE_PLATFORM}/platform_exec.c + + //... + + src/zwave_command_class_sound_switch.c + + //... + + src/zwave_command_class_transport_service.c) +``` + +This ensures that our new file is correctly added to the build system. No need to include the header. + +We recommend to fill your new header file with the contents of existing command classes header. Doxygen documentation is based on the structure of this header to create the section about the new command class. + +We need at least one function to act as an entry point. The naming convention is `zwave_command_class_{class_name}_init`, where `{class_name}` is the current Z-Wave class name. In our example we declare the function `zwave_command_class_sound_switch_init`. Our `zwave_command_class_sound_switch.h` should look like that : + +```c +/** + * @defgroup zwave_command_class_sound_switch + * @brief Sound Switch Command Class handlers and control function + * + * This module implement some of the functions to control the + * Sound Switch Command Class + * + * @{ + */ + +#ifndef ZWAVE_COMMAND_CLASS_SOUND_SWITCH_H +#define ZWAVE_COMMAND_CLASS_SOUND_SWITCH_H + +#include "sl_status.h" + +#ifdef __cplusplus +extern "C" { +#endif + +sl_status_t zwave_command_class_sound_switch_init(); + +#ifdef __cplusplus +} +#endif + +#endif //ZWAVE_COMMAND_CLASS_SOUND_SWITCH_H +/** @} end zwave_command_class_sound_switch */ +``` + +For now we fill our source file with an empty definition : + +```C +// System +#include + +#include "zwave_command_class_sound_switch.h" +#include "zwave_command_classes_utils.h" +#include "ZW_classcmd.h" + +// Includes from other ZPC Components +#include "zwave_command_class_indices.h" +#include "zwave_command_handler.h" +#include "zwave_command_class_version_types.h" +#include "zpc_attribute_store_network_helper.h" +#include "attribute_store_defined_attribute_types.h" + +// Unify +#include "attribute_resolver.h" +#include "attribute_store.h" +#include "attribute_store_helper.h" +#include "sl_log.h" + +#define LOG_TAG "zwave_command_class_sound_switch" + + +sl_status_t zwave_command_class_sound_switch_init() +{ + return SL_STATUS_OK; +} +``` + +Note that some header were also added to the skeleton. They are here for later, but feel free to remove the unused ones when you are finished. + +The `LOG_TAG` should be used when calling `sl_log_xxx()` function (`sl_log_warning(LOG_TAG, "My warning");`). This ensure that all messages that comes from this class is under the same tag. This is useful to filter logs based on the component we are interested in. + +Now that we have our entry point we have to call it in the `zwave_command_class_fixt.c` file. First we need to include our header and then call it from the function `zwave_command_classes_init()` : + +```C +// ... +#include "zwave_command_class_sound_switch.h" +// ... + +sl_status_t zwave_command_classes_init() +{ + sl_status_t status = SL_STATUS_OK; + + // Special handlers: + status |= zwave_command_class_granted_keys_resolver_init(); + status |= zwave_command_class_node_info_resolver_init(); + + // Do not abort initialization of other CCs if one fails. + // Command Class handlers + // Note: AGI should stay first, it allows others to register commands. + status |= zwave_command_class_agi_init(); + + // ... + status |= zwave_command_class_sound_switch_init(); + // ... + + zwave_command_handler_print_info(-1); + return status; +} +``` + +Now that our skeleton is up and running we can start implement our new command class. + +### Implementation + +#### Global handler + +Let's populate our initialization function (`zwave_command_class_sound_switch_init`). The first thing to do is to add an command handler : + +```C +sl_status_t zwave_command_class_sound_switch_init() +{ + zwave_command_handler_t handler = {}; + handler.support_handler = NULL; + handler.control_handler = NULL; + handler.minimal_scheme = ZWAVE_CONTROLLER_ENCAPSULATION_NONE; + handler.manual_security_validation = false; + handler.command_class = COMMAND_CLASS_SOUND_SWITCH; + handler.version = SOUND_SWITCH_VERSION; + handler.command_class_name = "Sound Switch"; + handler.comments = ""; + + return zwave_command_handler_register_handler(handler); +} +``` + +This snippet registers the handler for your Z-Wave command class. The handler options are : + +- **support_handler** : Support handler for the Z-Wave command class. This allows you to react when receiving Get/Set commands. Some example of this handler can be found in already implemented command classes (`indicator`, `powerlevel`, `inclusion_controller`, ...). +- **control_handler** : Control handler for the Z-Wave command class. This allows you to react to Report commands. +- **minimal_scheme** : The minimal security level which this command is supported on. This is ignored for the control_handler. +- **manual_security_validation** : Use manual-security filtering for incoming frames. If set to true, the command class dispatch handler send frames to the handler without validating their security level.If set to false, the command class handler can assume that the frame has been received at an approved security level. +- **command_class** : command class ID that this handler implements +- **version** : Maximal version supported of the command class +- **command_class_name** : Description of the current command class (no need to include "Command class") +- **comments** : Comments about the implementation. It is printed to the log when starting the zpc component. + +Now that we have an handler, let's implement the get/set/report command classes for `Configuration`. + +#### Initialize attributes + +One of the first thing the controller is doing is to update the version attribute. That's why the version attribute always has an unique attribute ID of 0x01. You can subscribe to changes to an attribute with the `attribute_store_register_callback_by_type`. This is also the best place to initialize your attributes. + +The `attribute_store_register_callback_by_type` function takes 2 arguments : + +- A callback function that takes two arguments : + - **attribute_store_node_t** *updated_node* : The node ID of the attribute + - **attribute_store_change_t** *change* : Type of change +- The attribute to monitor. In our case `ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_VERSION`. + +We can call this function in our entrypoint `zwave_command_class_sound_switch_init` before the handler definition : + +```C +sl_status_t zwave_command_class_sound_switch_init() +{ + attribute_store_register_callback_by_type( + &zwave_command_class_sound_switch_on_version_attribute_update, + ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_VERSION); + + // ... + // handler definition + // ... + + return zwave_command_handler_register_handler(handler); +} +``` + +This calls `zwave_command_class_sound_switch_on_version_attribute_update` on each update of `ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_VERSION`. + +Our implementation of `zwave_command_class_sound_switch_on_version_attribute_update` look like this : + +```C +static void zwave_command_class_sound_switch_on_version_attribute_update( + attribute_store_node_t updated_node, attribute_store_change_t change) +{ + if (change == ATTRIBUTE_DELETED) { + return; + } + + zwave_cc_version_t version = 0; + attribute_store_get_reported(updated_node, &version, sizeof(version)); + + if (version == 0) { + return; + } + + attribute_store_node_t endpoint_node + = attribute_store_get_first_parent_with_type(updated_node, + ATTRIBUTE_ENDPOINT_ID); + + // The order of the attribute matter since it defines the order of the + // Z-Wave get command order. + const attribute_store_type_t attributes[] + = {ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_TONES_NUMBER, + ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_CONFIGURED_DEFAULT_VOLUME, + ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_CONFIGURED_DEFAULT_TONE_IDENTIFIER, + ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_TONE_INFO_IDENTIFIER, + ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_TONE_PLAY}; + + attribute_store_add_if_missing(endpoint_node, + attributes, + COUNT_OF(attributes)); +} +``` + +The `change` attribute tells you which operation is currently performed on this attribute (deleted, added or modified). We then try to get the version from the attribute store. It is possible that we don't have the value reported yet, so we simply return and do nothing. If we have the version we can create the base structure of our attributes with the `attribute_store_add_if_missing` function. + +> NOTE : We don't define our attributes under the `ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_TONE_INFO_IDENTIFIER`, they are created once they are reported. The `attribute_store_set_child_reported()` function adds the attribute in the tree if they are missing. You can check the details in `zwave_command_class_sound_switch_handle_tone_info_report` function. + + +#### Get/Set command + +We'll take the example of following commands : `CONFIGURATION_GET` and `CONFIGURATION_SET` + +We can easily map the set/get function to an attribute of the Attribute Store. +Here we want to map the previously defined attributes : + +- `ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_CONFIGURED_DEFAULT_VOLUME` +- `ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_CONFIGURED_DEFAULT_TONE_IDENTIFIER` + +Since they are both used in argument of the `Z-Wave set`. + +We can do this with the `attribute_resolver_register_rule` function that takes 3 arguments : + +- The attribute store identifier +- The associated callback for set function (can be `NULL`) +- The associated callback for get function (can be `NULL`) + +This function listen to changes on the given attributes and call the get/set functions accordingly : + +- If ONLY a get function is given, the get function is called if we don't have a reported value. You can test this behavior with the ZPC command `ZPC> attribute_store_undefine_reported NodeID` +- If a set function or both functions are given the defined functions are called on desired value change. You can test this behavior with the ZPC command `ZPC> attribute_store_set_desired NodeID`. + +> NOTE : ZPC CLI is treated in the [Testing our implementation](#testing-our-implementation) section. + +```C + attribute_resolver_register_rule( + ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_CONFIGURED_DEFAULT_VOLUME, + &zwave_command_class_sound_switch_configuration_set, + &zwave_command_class_sound_switch_configuration_get); + + attribute_resolver_register_rule( + ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_CONFIGURED_DEFAULT_TONE_IDENTIFIER, + &zwave_command_class_sound_switch_configuration_set, + &zwave_command_class_sound_switch_configuration_get); +``` + +Each attribute can have only one rule, but it is not necessary to register a rule for all the attributes. A rule is necessary only for attributes that are used from a set or/and get function. + +The callbacks must take 3 arguments : + +1. **attribute_store_node_t** *node* : Current node ID of the attribute that trigger this callback. +2. **int8_t** *\*frame* : A pointer to the frame that will be sent. It has to be filled by the callback. +3. **uint16_t** *\*frame_length* : A pointer to the frame length. It has to be filled by the callback. + +The node ID can be visualized with the ZPC commands `attribute_store_log_xxx`. Here is a truncated result of `ZPC> attribute_store_log_search Tone` : + +```txt +(1) Root node ................................................................. <> (<>) + │───(2) HomeID ............................................................ [58,27,e5,fc] (<>) + │ │───(8) NodeID ........................................................ 3 (<>) + │ │ │───(9) Endpoint ID ............................................... 0 (<>) + │ │ │ │───(80) Configured Default Tone Identifier ................... 4 (<>) + │ │ │ │───(81) Tones Number ......................................... 30 (<>) + │ │ │ │───(84) Tone Info Identifier ................................. 4 (<>) + │ │ │ │ │───(211) Tone Info Duration .............................. 1 (<>) + │ │ │ │ │───(212) Tone Info Name .................................. "04 Electric Apartment Buzzer" (<>) + │ │ │ │───(85) Tone Play ............................................ 0 (<>) +``` + +The node ID is represented by the number between parenthesis. The *node* argument has the ID of the monitored attribute. + +- The `Tone Info Identifier` attribute is bonded to a get only function. That means if the reported value is undefined (e.g at startup with an empty attribute store), the get callback for the endpoint 0 has its *node* argument set to 81. +- The `Configured Default Tone Identifier` attribute is bonded to both get and set function. If something changes, the callback (set or get) for the endpoint 0 has its *node* argument set to 80. + +The get function is the most straight forward : it doesn't require any argument we only need to send the frame. + +```C +static sl_status_t zwave_command_class_sound_switch_configuration_get( + attribute_store_node_t node, uint8_t *frame, uint16_t *frame_length) +{ + (void)node; // unused. + ZW_SOUND_SWITCH_CONFIGURATION_GET_FRAME *get_frame + = (ZW_SOUND_SWITCH_CONFIGURATION_GET_FRAME *)frame; + get_frame->cmdClass = COMMAND_CLASS_SOUND_SWITCH; + get_frame->cmd = SOUND_SWITCH_CONFIGURATION_GET; + *frame_length = sizeof(ZW_SOUND_SWITCH_CONFIGURATION_GET_FRAME); + return SL_STATUS_OK; +} +``` + +The set function in the other hand is less straight forward : we have to get the elements in the attribute store and give to the `Z-Wave set` command. + +```C +static sl_status_t zwave_command_class_sound_switch_configuration_set( + attribute_store_node_t node, uint8_t *frame, uint16_t *frame_length) +{ + sound_switch_configuration_t configuration = {}; + + const attribute_store_node_t endpoint_id_node + = attribute_store_get_first_parent_with_type(node, ATTRIBUTE_ENDPOINT_ID); + + get_configuration(endpoint_id_node, &configuration); + + ZW_SOUND_SWITCH_CONFIGURATION_SET_FRAME *set_frame + = (ZW_SOUND_SWITCH_CONFIGURATION_SET_FRAME *)frame; + set_frame->cmdClass = COMMAND_CLASS_SOUND_SWITCH; + set_frame->cmd = SOUND_SWITCH_CONFIGURATION_SET; + set_frame->volume = configuration.volume; + set_frame->defaultToneIdentifier = configuration.tone; + *frame_length = sizeof(ZW_SOUND_SWITCH_CONFIGURATION_SET_FRAME); + return SL_STATUS_OK; +} +``` + +You can have a peek in the current attribute store by executing `ZPC> attribute_store_log_search Tone` (more info on CLI in the section [Testing our implementation](#testing-our-implementation)). The following sample also has the result of `ZPC> attribute_store_log_search Volume` : + +```txt + │ │ │───(9) Endpoint ID ............................................... 0 (<>) + │ │ │ │───(75) Configured Default Volume ............................ 1 (<>) + │ │ │ │───(80) Configured Default Tone Identifier ................... 4 (<>) + │ │ │ │───(81) Tones Number ......................................... 30 (<>) + │ │ │ │───(84) Tone Info Identifier ................................. 4 (<>) + │ │ │ │ │───(211) Tone Info Duration .............................. 1 (<>) + │ │ │ │ │───(212) Tone Info Name .................................. "04 Electric Apartment Buzzer" (<>) + │ │ │ │───(85) Tone Play ............................................ 1 (<>) +``` + +The `node` argument here is set to either the node ID of `Configured Default Volume` (75) or `Configured Default Tone Identifier` (80). + +However we need to access both of those values. To do so we get `Endpoint ID` node ID : this allows us to have access to all its children. + +The function `attribute_store_get_first_parent_with_type(node, ATTRIBUTE_ENDPOINT_ID)` searches for the first parent with `ATTRIBUTE_ENDPOINT_ID` type : here it is the node (9). + +Once we have the parent node ID, we can pass it to the `get_configuration()` function that fetches the information we need : + +```C +typedef struct sound_switch_configuration { + sound_switch_volume_t volume; + sound_switch_tone_id_t tone; +} sound_switch_configuration_t; + +static void get_configuration(attribute_store_node_t state_node, + sound_switch_configuration_t *configuration) +{ + attribute_store_node_t volume_node + = attribute_store_get_first_child_by_type(state_node, + ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_CONFIGURED_DEFAULT_VOLUME); + + sl_status_t status = attribute_store_get_desired_else_reported(volume_node, + &configuration->volume, + sizeof(configuration->volume)); + if (status != SL_STATUS_OK) { + configuration->volume = 0; + sl_log_warning(LOG_TAG, "Can't get CONFIGURED_DEFAULT_VOLUME from attribute store. Value set to 0."); + } + + attribute_store_node_t tone_node + = attribute_store_get_first_child_by_type(state_node, + ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_CONFIGURED_DEFAULT_TONE_IDENTIFIER); + + status = attribute_store_get_desired_else_reported(tone_node, + &configuration->tone, + sizeof(configuration->tone)); + + if (status != SL_STATUS_OK) { + configuration->tone = 0; + sl_log_warning(LOG_TAG, "Can't get CONFIGURED_DEFAULT_TONE_IDENTIFIER from attribute store. Value set to 0."); + } +} +} +``` + +`attribute_store_get_first_child_by_type()` function fetches the node ID of given type. Once we have the correct node ID, we can fetch its value with various `attribute_store_get_xxx()` functions. Here we use `attribute_store_get_desired_else_reported()` because we want either the desired or reported value of the attribute. If something goes wrong the function returns an error code and we set default values. + +#### Report callback + +In the handler we defined previously we'll need to add a control handler : + +```C + // ... + handler.control_handler = &zwave_command_class_sound_switch_control_handler; + // ... +``` + +The control handler is called when the controller receives a report command. You can then check which report was sent and update the values in attribute store. + +```C +sl_status_t zwave_command_class_sound_switch_control_handler( + const zwave_controller_connection_info_t *connection_info, + const uint8_t *frame_data, + uint16_t frame_length) +{ + if (frame_length <= COMMAND_INDEX) { + return SL_STATUS_NOT_SUPPORTED; + } + + switch (frame_data[COMMAND_INDEX]) { + case SOUND_SWITCH_CONFIGURATION_REPORT: + return zwave_command_class_sound_switch_handle_configuration_report( + connection_info, + frame_data, + frame_length); + default: + return SL_STATUS_NOT_SUPPORTED; + } +} +``` + +The control handler function takes 3 arguments : + +- **zwave_controller_connection_info_t** *\*connection_info* : Structure holding information about the source and destination when transmitting and receiving Z-Wave frames. You can retrieve the endpoint node ID in the attribute store with `zwave_command_class_get_endpoint_node(connection_info)` +- **uint8_t** *\*frame_data* : frame received +- **uint16_t** *frame_length* : length of frame received + +We can take a look at the function that process the report frame : + +```C +sl_status_t zwave_command_class_sound_switch_handle_configuration_report( + const zwave_controller_connection_info_t *connection_info, + const uint8_t *frame_data, + uint16_t frame_length) +{ + + if (frame_length < 4) { + return SL_STATUS_FAIL; + } + + attribute_store_node_t endpoint_node + = zwave_command_class_get_endpoint_node(connection_info); + + sound_switch_volume_t volume = frame_data[2]; + if (volume > 100) { + sl_log_warning(LOG_TAG, "Node reported volume higher than 100"); + volume = 100; + } + + attribute_store_set_child_reported( + endpoint_node, + ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_CONFIGURED_DEFAULT_VOLUME, + &volume, + sizeof(volume)); + + sound_switch_tone_id_t tone = frame_data[3]; + + attribute_store_set_child_reported( + endpoint_node, + ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_CONFIGURED_DEFAULT_TONE_IDENTIFIER, + &tone, + sizeof(tone)); + + return SL_STATUS_OK; +} +``` + +It's good practice to check the frame length before processing. We return `SL_STATUS_FAIL` if something goes wrong parsing the frame. The command handler component uses these return codes to respond to Supervision Get Commands. Returning `SL_STATUS_FAIL` sends a fail status therefore be standard compatible. (More information on Supervision Status code descriptions CC:006C.01.02.11.006 in the Z-Wave standard). + +`zwave_command_class_get_endpoint_node` allows us to convert a `zwave_controller_connection_info_t` into a node ID and returns the current endpoint node ID. We can use it to update all the attribute we need. + +![Report Configuration schema](doc/assets/img/zwave_command_class_sound_switch_conf_report.png) + +As we can see the report frame has a total size of 4 bits. We can access the volume part with `frame_data[2]` and the default tone identifier with `frame_data[3]`. We then mark those attributes as reported. Our volume attribute is `ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_CONFIGURED_DEFAULT_VOLUME` so we set this value as reported with the value of `frame_data[2]` with the function `attribute_store_set_child_reported`. + +`attribute_store_set_child_reported` allows you to update an attribute located under a node ID. Here we have the endpoint node ID so we can set the value of any attribute right underneath. + +Now we can repeat this process for all the available commands (get/set/report) and set/get attribute based on your needs. + +#### Testing our implementation + +You can test if the request are correctly sent and received by manually making changes in the attribute store. To do so, you'll need access to the ZPC CLI. Start the zpc executable directly (and stop the uic-service if running). Once its running press enter to see the `ZPC>` command line. It supports autocompletion and the help command. + +You can search for the node ID with the various log functions. The most useful one is `attribute_store_log_search` that allows you to search (case sensitive) for an attribute description. + +Once you've got the node ID you can use functions such as `attribute_store_set_desired nodeID,value` or `attribute_store_undefine_reported nodeID` to trigger some changes. + +You can also send raw Z-Wave frames with the `zwave_tx` commands. + +## Mapping Z-Wave to Dotdot UCL with .uam file + +You should have some basic knowledge about clusters and UAM files. + +- [Cluster documentation](../../doc/unify_specifications/Chapter02-ZCL-in-uic.rst) +- [.uam file documentation](../../doc/how_to_write_uam_files.rst) +- [Z-Wave specific .uam files documentation](./how_to_write_uam_files_for_the_zpc.md) + +Now that our attribute store is correctly defined and sending the right Z-Wave commands it's time to map it to the UCL model. This allows our attributes to be controlled by the MQTT broker (and by some extend the dev UI). + + +### Map Z-Wave attribute to clusters attributes + +The clusters are represented by XML files located in `components\uic_dotdot\dotdot-xml`. Our Sound Switch command class need at least the following features : + +- Play a tone +- Configure volume +- Configure tone ID + +The play tone is basically a switch (0: stop playing, 255: playing). It can be represented by the `OnOff` cluster. + +The volume and tone ID are numerical values. It can be represented by the `Level` cluster that can control numerical values. + +We'll start by the simplest one, the `OnOff` cluster. If we take a look at the OnOff cluster we can see the attribute it defines : + +#### On/Off cluster + +```XML + + + + + + + + + + +``` + +Here, we are interested in the `OnOff` attribute with id `0000`. It's that ZCL attribute that is mapped to our `ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_TONE_PLAY` Z-Wave attribute. An other important information is the cluster ID in the `zcl:cluster` attributes (`id="0006"`). + +In `applications/zpc/components/dotdot_mapper/rules` we can create our uam file. +It must be named by the following naming convention : `{CLUSTER_NAME}_to_{COMMAND_CLASS_NAME}CC` where `{CLUSTER_NAME}` is the ZCL cluster name and `{COMMAND_CLASS_NAME}` is your Z-Wave command class name (without spaces). We name our file `OnOff_to_SoundSwitchCC.uam`. + +The first thing we'll do is to define where to find our Z-Wave `ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_TONE_PLAY` attribute. Remember in the beginning [when we defined some ID for our Z-Wave attributes](#create-the-data-model-in-the-attribute-store) ? This is where we'll use it. Our command class sound switch have the ID `0x79` and `ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_TONE_PLAY` `0x08`. We can access to this attribute by doing : + +```UAM +def zwSOUND_SWITCH_TONE_PLAY 0x7908 +``` + +It works the same way for the OnOff cluster attributes. We take the cluster ID (`0006`) followed by the attribute ID (`0000`) : + +```UAM +def zbON_OFF 0x00060000 +``` + +> NOTE : The naming convention is to prefix Z-Wave attribute with `zw` and ZCL attributes with `zb`. + +We usually use the priority 25 for this kind of rules to have higher priority over generic switches. + +We need to prevent `chain_reaction` because we map the Z-Wave to zcl and vice versa. + +- *Z-Wave to zcl* : Used when attribute store is updated (e.g Z-Wave report command) +- *zcl to Z-Wave* : Used when zcl model is updated (e.g with commands) + +```UAM +def zwave_no_sound_switch (e'zwSOUND_SWITCH_TONE_PLAY == 0) + +scope 25 chain_reaction(0) { + // Linking attributes zwave -> zigbee (note : 0 is stop playing) + r'zbON_OFF = + if (zwave_no_sound_switch) undefined + if (r'zwSOUND_SWITCH_TONE_PLAY != 0) 1 0 + d'zbON_OFF = + if (zwave_no_sound_switch) undefined + if (d'zwSOUND_SWITCH_TONE_PLAY != 0) 1 0 + + // Linking attributes zigbee -> zwave + d'zwSOUND_SWITCH_TONE_PLAY = + if (zwave_no_sound_switch) undefined + if (d'zbON_OFF != 0) 255 0 + + r'zwSOUND_SWITCH_TONE_PLAY = + if (zwave_no_sound_switch) undefined + if (r'zbON_OFF != 0) 255 0 +} +``` + +This way we ensure that both attribute are linked no matter if it's changed in the ZCL world or Z-Wave world. `chain_reaction(0)` prevent the mapper to go in a infinite loop. + +##### UAM Guard + +In our previous UAM example we have defined an function that checks if the Sound Switch Command Class is active for the current endpoint : + +```uam +def zwave_no_sound_switch (e'zwSOUND_SWITCH_TONE_PLAY == 0) +``` + +Here we check the existence of `zwSOUND_SWITCH_TONE_PLAY` to see if the endpoint is supporting Sound Switch. + +We need to make sure that if we don't have any sound switch active that : + +- `zwSOUND_SWITCH_TONE_PLAY` should not exist and not be mapped to the `zbON_OFF` value +- `zbON_OFF` doesn't take the value of `zwSOUND_SWITCH_TONE_PLAY`. This prevents conflict with other Z-Wave CC (like Binary Switch that also maps to `zbON_OFF`). + +Make sure that your UAM file contains this guard to prevent interferences with other command classes implementation. + +#### Level cluster + +The `OnOff` cluster was very straight forward, but the `Level` requires some arbitrary choices. We need to map 2 values : + +- The current volume +- The current tone ID + +Let's look at the `Level` cluster definition : + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +We see that we have access to 2 int attributes : `CurrentLevel` and `CurrentFrequency`. We can decide to map the current volume to `CurrentLevel` and current tone ID to `CurrentFrequency` (even if its make little sense). We'll see how to implement our own cluster if we need more accuracy [here](#define-custom-clusters). + +Both ZCL attributes have a min and a max values, so we need to map those as well. + +First we'll create our file `Level_to_SoundSwitchCC.uam` in `applications/zpc/components/dotdot_mapper/rules`. + +Let's define our Z-Wave attribute definitions : + +```UAM +def zwSOUND_SWITCH_CONFIGURED_VOLUME 0x7902 +def zwSOUND_SWITCH_TONE_INFO_IDENTIFIER 0x7903 +def zwSOUND_SWITCH_TONE_NUMBER 0x7904 +``` + +We bind `zwSOUND_SWITCH_TONE_NUMBER` the to the `MaxFrequency` attribute so that our `CurrentFrequency` doesn't overflow. Since `CurrentFrequency` here represent our tone ID it must be between 1 and `zwSOUND_SWITCH_TONE_NUMBER`. + +And our zcl level attributes : + +```UAM +def zbLEVEL_CLUSTER_LEVEL 0x00080000 +def zbLEVEL_CLUSTER_MIN_LEVEL 0x00080002 +def zbLEVEL_CLUSTER_MAX_LEVEL 0x00080003 +def zbLEVEL_CLUSTER_FREQ 0x00080004 +def zbLEVEL_CLUSTER_FREQ_MIN 0x00080005 +def zbLEVEL_CLUSTER_FREQ_MAX 0x00080006 +def zbLEVEL_CLUSTER_TRANSITION_TIME 0x00080010 +``` + +The `zbLEVEL_CLUSTER_TRANSITION_TIME` is here to enable the Move command. We define it to make sure we can move our value the way we want to. + +> NOTE: to understand how we get the various ID see the [`OnOff` cluster section](#onoff-cluster) + +We need some constants for the min/max volume also since it is not defined on our command class : + +```UAM +def min_level 0 +def max_level 100 +``` + +First we'll map une CurrentLevel and CurrentFrequency attributes with their counterpart : + +```uam +def zwave_no_sound_switch (e'zwSOUND_SWITCH_TONE_PLAY == 0) + +scope 25 chain_reaction(0) { + // Map current level to configured volume + // zwave -> ucl + r'zbLEVEL_CLUSTER_LEVEL = + if (zwave_no_sound_switch) undefined + r'zwSOUND_SWITCH_CONFIGURED_VOLUME + d'zbLEVEL_CLUSTER_LEVEL = + if (zwave_no_sound_switch) undefined + d'zwSOUND_SWITCH_CONFIGURED_VOLUME + // ucl -> zwave + d'zwSOUND_SWITCH_CONFIGURED_VOLUME = + if (zwave_no_sound_switch) undefined + d'zbLEVEL_CLUSTER_LEVEL + r'zwSOUND_SWITCH_CONFIGURED_VOLUME = + if (zwave_no_sound_switch) undefined + r'zbLEVEL_CLUSTER_LEVEL + + // Map frequency to current tone identifier + // zwave -> ucl + r'zbLEVEL_CLUSTER_FREQ = + if (zwave_no_sound_switch) undefined + r'zwSOUND_SWITCH_TONE_INFO_IDENTIFIER + d'zbLEVEL_CLUSTER_FREQ = + if (zwave_no_sound_switch) undefined + d'zwSOUND_SWITCH_TONE_INFO_IDENTIFIER + // ucl -> zwave + d'zwSOUND_SWITCH_TONE_INFO_IDENTIFIER = + if (zwave_no_sound_switch) undefined + d'zbLEVEL_CLUSTER_FREQ + r'zwSOUND_SWITCH_TONE_INFO_IDENTIFIER = + if (zwave_no_sound_switch) undefined + r'zbLEVEL_CLUSTER_FREQ +} +``` +> NOTE : this process is explained in [`OnOff` cluster section](#onoff-cluster) and the `zwave_no_sound_switch` in [`UAM Guard section`](#uam-guard) + +To bind the min and max values we add those rules : + +```uam + // Min and max volume + r'zbLEVEL_CLUSTER_MIN_LEVEL = + if (zwave_no_sound_switch) undefined + if (e'zwSOUND_SWITCH_CONFIGURED_VOLUME) min_level undefined + r'zbLEVEL_CLUSTER_MAX_LEVEL = + if (zwave_no_sound_switch) undefined + if (e'zwSOUND_SWITCH_CONFIGURED_VOLUME) max_level undefined +``` + +We can't bind value to raw constants so we need to add an condition to it. We choose to define it only if `zwSOUND_SWITCH_CONFIGURED_VOLUME` exists since if it doesn't the min/max value doesn't make sense. + +Same applies for `MinFrequency` and `MaxFrequency` but instead of constants we use our `zwSOUND_SWITCH_TONE_NUMBER` attribute : + +```uam + r'zbLEVEL_CLUSTER_FREQ_MIN = + if (zwave_no_sound_switch) undefined + if (e'zwSOUND_SWITCH_TONE_NUMBER) 1 undefined + r'zbLEVEL_CLUSTER_FREQ_MAX = + if (zwave_no_sound_switch) undefined + if (e'zwSOUND_SWITCH_TONE_NUMBER) r'zwSOUND_SWITCH_TONE_NUMBER undefined +``` + +The last attribute we need to define is `zbLEVEL_CLUSTER_TRANSITION_TIME` to make `Level` commands work. All we need to do is set a value. In our case we choose 0 because we don't want a transition time when updating our levels. + +```uam + // Required to enable move command + d'zbLEVEL_CLUSTER_TRANSITION_TIME = + if (zwave_no_sound_switch) undefined + if (e'zwSOUND_SWITCH_TONE_NUMBER) 0 undefined + r'zbLEVEL_CLUSTER_TRANSITION_TIME = + if (zwave_no_sound_switch) undefined + if (e'zwSOUND_SWITCH_TONE_NUMBER) 0 undefined +``` + +Now with those files we should be able to control our device from the MQTT broker and the dev UI since it monitors the ZCL attributes. + +### Quirks + +Sometimes, devices are not acting like they should. The Aeotec Doorbell 6 for example define 9 different endpoints. Each endpoint can have its own configuration (tone ID, volume) and can play a tune. However, the first endpoint is behaving differently from the others. + +It acts as a default endpoint that copy the configuration of the second one (ep1). That means if you send a play command to the first endpoint, the second one is also marked as "is playing" and both use the same configuration (tone ID, volume). + + +When we tell the first endpoint (ep0) to play, the second one (ep1) is also marked as "is playing", but when the tone finishes only the ep1 receive a report telling the sound is over leading the ep0 in a incorrect state (marked as playing but in reality it's not). + +To address this issue we can use something we call Quirks in UAM. They allow us to execute rules on a specific device. The naming convention is `Quirks_{device_name}.uam` where `{device_name}` is your device name. In our case we can create `Quirks_aeotec_doorbell.uam`. + +A device can be identified with 3 parameters : manufacturer ID, product type and product ID. We can have access to it in UAM with the following ID : + +```UAM +// Special maps to help controlling the Aeotec doorbell +def zwMANUFACTURER_ID 0x00007202 +def zwPRODUCT_TYPE 0x00007203 +def zwPRODUCT_ID 0x00007204 +``` + +We the need a reference to the `SOUND_SWITCH_TONE_PLAY` attribute : + +```UAM +def zwSOUND_SWITCH_TONE_PLAY 0x7908 +``` + +We also need to have access to the endpoint list : + +```UAM +def ep 4 +``` + +This allow us to reference each endpoint by doing `ep[0]` where `0` is the endpoint ID. This notation will be explained soon. + +The last definition we need is a condition that returns true if we are controlling our specific device. We use the manufacturer ID, product type and product ID to identify our device. You can find those either in the attribute tree (`ZPC> attribute_store_log_search Manufacturer` and `ZPC> attribute_store_log_search Product`) or directly in the vendor manual. + +```UAM +def aeotec_doorbell ((r'ep[0].zwMANUFACTURER_ID == 881) & (r'ep[0].zwPRODUCT_TYPE == 3) & (r'ep[0].zwPRODUCT_ID == 162)) +``` + +> NOTE : We have to write the endpoint (`ep[0]`) before acceding the device identifiers or it will not work. This is explained bellow. + +The Quirk need to run as a high priority rule since they are device specific. Most of the Quirks runs as a priority of 500 or higher. Also in our case we need to have access to the endpoint list. Defining `ep` is not all we need to do : we also need `common_parent_type(3)` to our rules : + +```UAM +scope 500 common_parent_type(3) { +} +``` + +The `common_parent_type(3)` changes the current scope configuration for this mapping. This allows us to have access to each endpoint (`def ep 4`). The numbers (`3` and `4`) are references to the attributes' ID. In `applications\zpc\components\zpc_attribute_store\include\attribute_store_defined_attribute_types.h` we can find : + +```C +///< This represents a Node ID. zwave_node_id_t type. +DEFINE_ATTRIBUTE(ATTRIBUTE_NODE_ID, 0x0003) +///< This represents an endpoint. zwave_endpoint_id_t type. +DEFINE_ATTRIBUTE(ATTRIBUTE_ENDPOINT_ID, 0x0004) +``` + +If we look at our current attribute store we may see something like that : + +```txt +(1) Root node ................................................................. <> (<>) + │───(2) HomeID ............................................................ [58,27,e5,fc] (<>) + │ │───(3) NodeID ........................................................ 1 (<>) + │ │ │───(4) Endpoint ID ............................................... 0 (<>) + │ │───(8) NodeID ........................................................ 3 (<>) + │ │ │───(9) Endpoint ID ............................................... 0 (<>) + │ │ │───(153) Endpoint ID ............................................. 1 (<>) + │ │ │───(157) Endpoint ID ............................................. 2 (<>) + │ │ │───(161) Endpoint ID ............................................. 3 (<>) + │ │ │───(165) Endpoint ID ............................................. 4 (<>) + │ │ │───(169) Endpoint ID ............................................. 5 (<>) + │ │ │───(173) Endpoint ID ............................................. 6 (<>) + │ │ │───(177) Endpoint ID ............................................. 7 (<>) + │ │ │───(181) Endpoint ID ............................................. 8 (<>) +``` + +If we position ourselves relative to our Node ID we can access each endpoint individually. + +`ATTRIBUTE_NODE_ID` is defined at `0x0003` and `ATTRIBUTE_ENDPOINT_ID` at `0x0004`. That's why we defined `ep` to `4` earlier. Now we tell that for this mapping our parent is the `ATTRIBUTE_NODE_ID` with `common_parent_type(3)`. This way `ep[0]` reference the tree under the first endpoint, `ep[1]` the tree under the second endpoint, etc. + +This is why we needed to add `ep[0]` before `zwMANUFACTURER_ID` and the other attributes in `def aeotec_doorbell`. Since its evaluated in the `ATTRIBUTE_NODE_ID` context the only attribute directly available is the `ATTRIBUTE_ENDPOINT_ID`. You can find more information about this in [How to write UAM files](../../doc/how_to_write_uam_files.rst) + +So we need to map the second endpoint (`ep[1]`) tone play value to match the first one (`ep[0]`) but ONLY when the device is the Aoetec doorbell : + +```UAM +scope 500 common_parent_type(3) { + r'ep[0].zwSOUND_SWITCH_TONE_PLAY = if aeotec_doorbell r'ep[1].zwSOUND_SWITCH_TONE_PLAY undefined +} +``` + +If `aeotec_doorbell` report `false` we set the reported value to `undefined` to let other rules take care of it. Otherwise we map the value to the value reported by the second endpoint. + +## Unit Testing + +An approach that can be used to implement the test class is to try and think based on the command class specification and the code of the command class the good and bad scenarios that could happen. A suggestion is to try and create a method for each bad scenario, based on the implementation of the command class could be necessary in some occasions to create some nodes to test if any change happens to others. + +The test files are located under `applications/zpc/components/zwave_command_classes/test`. The naming convention is `zwave_command_class_{COMMAND_CLASS}_test.c` so in our case the file is named `zwave_command_class_sound_switch_test.c`. + +Test are enabled by default in CMake. The CMake variable `BUILD_TESTING` is controlling the test suite. Make sure it is set to `ON` (either via the pseudo-gui `ccmake` or by passing the `-DBUILD_TESTING=ON` to cmake directly). + +Once the compilation is done, you can run all the tests with `ctest` in your build directory. You can also run a specific test with the command : `ctest -R name_of_your_test`. The name of your test is defined with the `NAME` argument of the `CMakeLists.txt` in the test folder (see next section for details). + +If you need for details about your test result, pass the `--verbose` option. + +#### Add test file to CMake + +After creating the file we need to add it to the CMake build system. To do so open the `CMakeLists.txt` located in the test folder and add : + +```CMake +# Sound switch unit test +target_add_unittest(zwave_command_classes + +NAME zwave_command_class_sound_switch_test +SOURCES zwave_command_class_sound_switch_test.c + +DEPENDS + zpc_attribute_store_test_helper + zwave_controller + zwave_command_handler_mock + uic_attribute_resolver_mock + zpc_attribute_resolver_mock + uic_dotdot_mqtt_mock +) +``` + +- `NAME` : Same name as the SOURCE argument but without the extension (used if you want to run this specific test with `ctest -R name_of_your_test`) +- `SOURCES` : Our test file name with the extension +- `DEPENDS` : Dependencies of our test. You may add some based on your needs. More is available you can look at other test definitions to see them. + +#### Base test skeleton + +Once it is added to our build system, we define the test skeleton like this : + +```cpp +#include "zwave_command_class_sound_switch.h" +#include "zwave_command_classes_utils.h" +#include "unity.h" + +// Generic includes +#include + +// Includes from other components +#include "datastore.h" +#include "attribute_store.h" +#include "attribute_store_helper.h" +#include "attribute_store_fixt.h" +#include "zpc_attribute_store_type_registration.h" + +// Interface includes +#include "attribute_store_defined_attribute_types.h" +#include "ZW_classcmd.h" +#include "zwave_utils.h" +#include "zwave_controller_types.h" + +// Test helpers +#include "zpc_attribute_store_test_helper.h" + +// Mock includes +#include "attribute_resolver_mock.h" +#include "zpc_attribute_resolver_mock.h" +#include "zwave_command_handler_mock.h" +#include "dotdot_mqtt_mock.h" +#include "dotdot_mqtt_generated_commands_mock.h" + +/// Setup the test suite (called once before all test_xxx functions are called) +void suiteSetUp() +{ + datastore_init(":memory:"); + attribute_store_init(); + zpc_attribute_store_register_known_attribute_types(); +} + +/// Teardown the test suite (called once after all test_xxx functions are called) +int suiteTearDown(int num_failures) +{ + attribute_store_teardown(); + datastore_teardown(); + return num_failures; +} + +/// Called before each and every test +void setUp() +{ + zpc_attribute_store_test_helper_create_network(); +} + +/// Called after each and every test +void tearDown() {} +``` + +You can find the different function that is called before/after each test/test suite. + +We'll add to the `setUp()` function the entrypoint of our class : + +```cpp +/// Called before each and every test +void setUp() +{ + zpc_attribute_store_test_helper_create_network(); + + // Call init + TEST_ASSERT_EQUAL(SL_STATUS_OK, zwave_command_class_sound_switch_init()); +} +``` + +Unless your specified your attribute creation in this function, your attribute tree is **NOT** available for the test cases. If your attribute creation is bound to the version update, you can set this attribute in the setUp() phase if really needed : + +```c +attribute_store_node_t version_node + = attribute_store_add_node(ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_VERSION, + endpoint_id_node); + + zwave_cc_version_t version = 1; + attribute_store_set_reported(version_node, &version, sizeof(version)); +``` + +#### Test report Z-Wave function + + We'll add an handler stub that contains our sound switch handler definition (version, security, control handler,...). The handler stub function looks like this : + +```cpp +// Private variables +static zwave_command_handler_t handler = {}; + +// Stub for registering command classes +static sl_status_t zwave_command_handler_register_handler_stub( + zwave_command_handler_t new_command_class_handler, int cmock_num_calls) +{ + handler = new_command_class_handler; + + TEST_ASSERT_EQUAL(ZWAVE_CONTROLLER_ENCAPSULATION_NONE, + handler.minimal_scheme); + TEST_ASSERT_EQUAL(COMMAND_CLASS_SOUND_SWITCH, handler.command_class); + TEST_ASSERT_EQUAL(1, handler.version); + TEST_ASSERT_NOT_NULL(handler.control_handler); + TEST_ASSERT_NULL(handler.support_handler); + TEST_ASSERT_FALSE(handler.manual_security_validation); + + return SL_STATUS_OK; +``` + +We save our handler into a global static variable so we can use it later in our test functions. All our test functions must start with the `test_` prefix. If we want to test our configuration report in the best case scenario, we can call this function `test_sound_switch_configuration_report_happy_case()`. Or if we want to test the volume doesn't go above 100, we can name it `test_sound_switch_configuration_report_volume_over_100()`. + +Then, we add the stub definition in our `setUp()` function : + +```cpp +/// Called before each and every test +void setUp() +{ + //... + + // Unset previous definition of handler + memset(&handler, 0, sizeof(zwave_command_handler_t)); + + // Handler registration + zwave_command_handler_register_handler_Stub( + &zwave_command_handler_register_handler_stub); + + //... +} +``` + +It's good practice to init the handler to its default value before each test. Now we can write a simple test for the configuration report : + +```cpp +void test_sound_switch_configuration_report_volume_over_100() +{ + zwave_controller_connection_info_t info = {}; + info.remote.node_id = node_id; + info.remote.endpoint_id = endpoint_id; + info.local.is_multicast = false; + + TEST_ASSERT_NOT_NULL(handler.control_handler); + TEST_ASSERT_EQUAL(SL_STATUS_FAIL, + handler.control_handler(&info, NULL, 0)); + + const uint8_t frame[] = {COMMAND_CLASS_SOUND_SWITCH, + SOUND_SWITCH_CONFIGURATION_REPORT, + 101, + 0x55}; + + TEST_ASSERT_EQUAL(SL_STATUS_OK, + handler.control_handler(&info, frame, sizeof(frame))); + + attribute_store_node_t volume_node = attribute_store_get_node_child_by_type( + endpoint_id_node, + ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_CONFIGURED_DEFAULT_VOLUME, + 0); + TEST_ASSERT_EQUAL(100, attribute_store_get_reported_number(volume_node)); + + attribute_store_node_t tone_node = attribute_store_get_node_child_by_type( + endpoint_id_node, + ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_CONFIGURED_DEFAULT_TONE_IDENTIFIER, + 0); + TEST_ASSERT_EQUAL(0x55, attribute_store_get_reported_number(tone_node)); +} +``` + +We can send to our control handler a Z-Wave report frame of configuration with `handler.control_handler()` and checks if the attributes are correctly updated in the attribute store. This test ensure that the sound level never goes over 100 even if reported so. + +#### Test get/set Z-Wave function + + We'll had an handler stub that contains our sound switch get and set callbacks. We defined our get and set function for `ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_CONFIGURED_DEFAULT_VOLUME` and `ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_TONES_NUMBER` like this : + + ```C + attribute_resolver_register_rule( + ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_CONFIGURED_DEFAULT_VOLUME, + &zwave_command_class_sound_switch_configuration_set, + &zwave_command_class_sound_switch_configuration_get); + + attribute_resolver_register_rule( + ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_TONES_NUMBER, + NULL, + &zwave_command_class_sound_switch_tones_number_get); + ``` + +So we can access them like this in our test file : + +```c +static attribute_resolver_function_t configuration_get = NULL; +static attribute_resolver_function_t configuration_set = NULL; +static attribute_resolver_function_t tone_number_get = NULL; + +// Buffer for frame +static uint8_t received_frame[255] = {}; +static uint16_t received_frame_size = 0; + +// Stub functions +static sl_status_t + attribute_resolver_register_rule_stub(attribute_store_type_t node_type, + attribute_resolver_function_t set_func, + attribute_resolver_function_t get_func, + int cmock_num_calls) +{ + if (node_type == ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_CONFIGURED_DEFAULT_VOLUME) { + TEST_ASSERT_NOT_NULL(set_func); + TEST_ASSERT_NOT_NULL(get_func); + configuration_get = get_func; + configuration_set = set_func; + } else if (node_type == ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_TONES_NUMBER) { + TEST_ASSERT_NULL(set_func); + TEST_ASSERT_NOT_NULL(get_func); + tone_number_get = get_func; + } + + return SL_STATUS_OK; +} +``` + +Note that we might don't have a set and get function for all attributes. We can check the null value of `set_func` or `get_func` with `TEST_ASSERT_NULL`. This way we ensure that our set/get callbacks are correctly defined. + +We also define a `received_frame` buffer that allows us to test get/set functions. + +Then we add the stub definition in our `setUp()` function : + +```c +/// Called before each and every test +void setUp() +{ + //... + + // Unset previous definition get/set functions + configuration_get = NULL; + configuration_set = NULL; + tone_number_get = NULL; + memset(received_frame, 0, sizeof(received_frame)); + received_frame_size = 0; + // Resolution functions + attribute_resolver_register_rule_Stub(&attribute_resolver_register_rule_stub); + + //... +} +``` + +It's good practice to init the functions and frame buffer to its default value before each test. + +##### Z-Wave Get test + +Now we can write a simple test for the configuration get : + +```c +void test_sound_switch_configuration_get_happy_case() +{ + // Ask for a Get Command, should always be the same + TEST_ASSERT_NOT_NULL(configuration_get); + configuration_get(0, received_frame, &received_frame_size); + const uint8_t expected_frame[] + = {COMMAND_CLASS_SOUND_SWITCH, SOUND_SWITCH_CONFIGURATION_GET}; + TEST_ASSERT_EQUAL(sizeof(expected_frame), received_frame_size); + TEST_ASSERT_EQUAL_UINT8_ARRAY(expected_frame, + received_frame, + received_frame_size); +} +``` + +Nothing special here we should have the Sound Switch command class ID and the configuration get ID. The next section shows you how to interact with the attribute store. It could be useful if your get function have an argument. + +##### Z-Wave Set test + +Let's now test the set function : + +```c +void test_sound_switch_configuration_set_happy_case() +{ + uint8_t volume = 15; + uint8_t tone = 30; + + // Attribute tree is empty as this point so we add it here + attribute_store_node_t volume_node + = attribute_store_add_node(ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_CONFIGURED_DEFAULT_VOLUME, endpoint_id_node); + attribute_store_node_t tone_node + = attribute_store_add_node(ATTRIBUTE_COMMAND_CLASS_SOUND_SWITCH_CONFIGURED_DEFAULT_TONE_IDENTIFIER, endpoint_id_node); + + attribute_store_set_desired(volume_node, &volume, sizeof(volume)); + attribute_store_set_desired(tone_node, &tone, sizeof(tone)); + + TEST_ASSERT_NOT_NULL(configuration_set); + // We can either set the volume_node or tone_here here. + configuration_set(volume_node, received_frame, &received_frame_size); + + const uint8_t expected_frame[] + = {COMMAND_CLASS_SOUND_SWITCH, SOUND_SWITCH_CONFIGURATION_SET, volume, tone}; + TEST_ASSERT_EQUAL(sizeof(expected_frame), received_frame_size); + TEST_ASSERT_EQUAL_UINT8_ARRAY(expected_frame, + received_frame, + received_frame_size); +} +``` + +We need to manually set the node value since the attribute tree should be empty. + +We set the desired values in our attribute store (`endpoint_id_node` is automatically set by test helper function) and call the set function. This way, we can see if the set function is correctly getting the attributes from the attribute store. + +This should cover the basic of unit testing your Z-Wave command class. Don't forget to also test edge cases and not just the happy cases.