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 0000000000..004b95c7d8 Binary files /dev/null and b/applications/zpc/doc/assets/img/attribute_store_sound_switch.png differ diff --git a/applications/zpc/doc/assets/img/attribute_store_sound_switch.uml b/applications/zpc/doc/assets/img/attribute_store_sound_switch.uml new file mode 100644 index 0000000000..39d9b0d630 --- /dev/null +++ b/applications/zpc/doc/assets/img/attribute_store_sound_switch.uml @@ -0,0 +1,75 @@ +@startuml + +' Style for the diagram +skinparam classFontColor black +skinparam classFontSize 10 +skinparam classFontName Helvetica +skinparam shadowing false +skinparam ArrowColor #000000 + +skinparam ObjectBackgroundColor #DEDEDE +skinparam ObjectBorderColor #480509 +skinparam ObjectBorderThickness 2 +skinparam ObjectFontColor #000000 + +skinparam NoteBackgroundColor #FFFFFF +skinparam NoteBorderColor #000000 + +title Attribute Store tree representation + +object HomeID #fffae6 +HomeID : Attribute Type = ATTRIBUTE_HOME_ID +HomeID : value = Desired: [], Reported: [FB E6 8C CE] + +object "NodeID" as NodeID_1 #f2ffe6 +NodeID_1 : Attribute Type = ATTRIBUTE_NODE_ID +NodeID_1 : value = Desired: [], Reported: [03] + +object "Endpoint" as endpoint_1 #e6fff7 +endpoint_1 : Attribute Type = ATTRIBUTE_ENDPOINT_ID +endpoint_1 : value = Desired: [], Reported: [00] + +object "Attribute" as attribute_1 #FFFFFF +attribute_1 : Attribute Type = CONFIGURED_DEFAULT_VOLUME +attribute_1 : value = Desired: [00], Reported: [64] + +object "Attribute" as attribute_2 #FFFFFF +attribute_2 : Attribute Type = CONFIGURED_DEFAULT_TONE_IDENTIFIER +attribute_2 : value = Desired: [00], Reported: [04] + +object "Attribute" as attribute_3 #FFFFFF +attribute_3 : Attribute Type = TONES_NUMBER +attribute_3 : value = Desired: [00], Reported: [1E] + +object "Attribute" as attribute_4 #FFFFFF +attribute_4 : Attribute Type = TONE_INFO_IDENTIFIER +attribute_4 : value = Desired: [00], Reported: [04] + +object "Attribute" as sub_attribute_1 #FFFFFF +sub_attribute_1 : Attribute Type = TONE_INFO_DURATION +sub_attribute_1 : value = Desired: [00], Reported: [0F] + +object "Attribute" as sub_attribute_2 #FFFFFF +sub_attribute_2 : Attribute Type = TONE_INFO_NAME +sub_attribute_2 : value = Desired: [00], Reported: ["04 Electric Apartment Buzzer"] + +object "Attribute" as attribute_5 #FFFFFF +attribute_5 : Attribute Type = PLAY +attribute_5 : value = Desired: [01], Reported: [00] + + + +HomeID *-- NodeID_1 +NodeID_1 *-- endpoint_1 +endpoint_1 *-- attribute_1 +endpoint_1 *-- attribute_2 +endpoint_1 *-- attribute_3 +endpoint_1 *-- attribute_4 +endpoint_1 *-- attribute_5 +attribute_4 *-- sub_attribute_1 +attribute_4 *-- sub_attribute_2 + + + + +@enduml diff --git a/applications/zpc/doc/assets/img/zwave_command_class_integration.png b/applications/zpc/doc/assets/img/zwave_command_class_integration.png new file mode 100644 index 0000000000..0b3f2441c5 Binary files /dev/null and b/applications/zpc/doc/assets/img/zwave_command_class_integration.png differ diff --git a/applications/zpc/doc/assets/img/zwave_command_class_integration.uml b/applications/zpc/doc/assets/img/zwave_command_class_integration.uml new file mode 100644 index 0000000000..0b5c515292 --- /dev/null +++ b/applications/zpc/doc/assets/img/zwave_command_class_integration.uml @@ -0,0 +1,30 @@ +@startuml +skinparam roundCorner 15 + +top to bottom direction + +cloud "IoT Services" as IOTService { + rectangle "Developer GUI" as DEVGUI +} + +rectangle "IoT Gateway" { + rectangle "Attribute Store" as AttributeStore + rectangle "Unify Controller Language (UCL)" as UCL + rectangle "MQTT Broker" as Broker + rectangle "Z-Wave Controller - ZPC" as ZPC +} + + +' Relation between Protocol controllers and MQTT abstration layer +AttributeStore <-u-> 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 0000000000..f787f23628 Binary files /dev/null and b/applications/zpc/doc/assets/img/zwave_command_class_sound_switch_commands.png differ diff --git a/applications/zpc/doc/assets/img/zwave_command_class_sound_switch_conf_report.png b/applications/zpc/doc/assets/img/zwave_command_class_sound_switch_conf_report.png new file mode 100644 index 0000000000..cd207f8312 Binary files /dev/null and b/applications/zpc/doc/assets/img/zwave_command_class_sound_switch_conf_report.png differ diff --git a/applications/zpc/how_to_implement_new_zwave_command_classes.md b/applications/zpc/how_to_implement_new_zwave_command_classes.md deleted file mode 100644 index d8acacdd53..0000000000 --- a/applications/zpc/how_to_implement_new_zwave_command_classes.md +++ /dev/null @@ -1,348 +0,0 @@ -# Guideline for implementing Command Classes - -## 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, which will be presented under the endpoint attribute tree. -This could be achieved via reading the command class structure which is -described in the Z-Wave specification and define the type of the attributes. -The following example illustrates the Alarm Sensor command class attributes -that could hold all states related to alarm sensor command class. - -![Data Model](doc/assets/img/alarm_sensor_data_model.png) - -After defining the data model, the next step is to define the type of each -node in the attribute store. Under the include package of the attribute store, -create the header of the command class types. In the header define the -types of the node with an additional type name that will be used in the -command class implementation. - -```c - typedef uint8_t alarm_sensor_type_t; - typedef uint8_t alarm_sensor_state_t; - typedef zwave_node_id_t alarm_sensor_id_t; - typedef uint16_t alarm_sensor_seconds_t; - -``` - -Define the attributes of command class, under the -attribute_store_defined_attribute_types.h. Define them by assigning -to the name of the attribute the value that it has. The types and definitions -of many attributes can be found under the ZW_classcmd.h. By defining the -attributes of the command class the attribute mapper can interpret -the data and make it easier to visualize the attribute store in the console. - -```c - /////////////////////////////////// - // Alarm Sensor Command Class - DEFINE_ATTRIBUTE(ATTRIBUTE_COMMAND_CLASS_ALARM_SENSOR_VERSION, - ZWAVE_CC_VERSION_ATTRIBUTE(COMMAND_CLASS_SENSOR_ALARM)) - - // Bitmask Attribute contains the bytes that describe the alarm - // types that the device support. uint8_t - DEFINE_ATTRIBUTE(ATTRIBUTE_COMMAND_CLASS_ALARM_SENSOR_BITMASK, - ((COMMAND_CLASS_SENSOR_ALARM << 8) | 0x02)) - - // Alarm type contains the type of the alarm. alarm_sensor_type_t type - DEFINE_ATTRIBUTE(ATTRIBUTE_COMMAND_CLASS_ALARM_SENSOR_TYPE, - ((COMMAND_CLASS_SENSOR_ALARM << 8) | 0x03)) - - // Sensor state contains the state of the alarm if alarm is active or not - // alarm_sensor_state_t type - DEFINE_ATTRIBUTE(ATTRIBUTE_COMMAND_CLASS_ALARM_SENSOR_STATE, - ((COMMAND_CLASS_SENSOR_ALARM << 8) | 0x04)) - - // Node ID, which detected the alarm condition. alarm_sensor_id_t type - DEFINE_ATTRIBUTE(ATTRIBUTE_COMMAND_CLASS_ALARM_SENSOR_NODE_ID, - ((COMMAND_CLASS_SENSOR_ALARM << 8) | 0x05)) - - //Seconds indicates time the remote alarm must be active since last received report. - // alarm_sensor_seconds_t type - DEFINE_ATTRIBUTE(ATTRIBUTE_COMMAND_CLASS_ALARM_SENSOR_SECONDS, - ((COMMAND_CLASS_SENSOR_ALARM << 8) | 0x06)) -``` - -Under the attribute store package and under the src in -zpc_attribute_store_registration.c. In this file, define -the attribute types for the attribute store API for the new command class. - -```c - ///////////////////////////////////////////////////////////////////// - // Alarm Sensor Command Class attributes - ///////////////////////////////////////////////////////////////////// - status |= attribute_store_register_type( - ATTRIBUTE_COMMAND_CLASS_ALARM_SENSOR_VERSION, - "Alarm Sensor Command Class version", - ATTRIBUTE_ENDPOINT_ID, - U8_STORAGE_TYPE); - - status |= attribute_store_register_type( - ATTRIBUTE_COMMAND_CLASS_ALARM_SENSOR_BITMASK, - "Alarm Sensor Bitmask", - ATTRIBUTE_ENDPOINT_ID, - BYTE_ARRAY_STORAGE_TYPE); - - status |= attribute_store_register_type( - ATTRIBUTE_COMMAND_CLASS_ALARM_SENSOR_TYPE, - "Alarm Sensor Type", - ATTRIBUTE_ENDPOINT_ID, - U8_STORAGE_TYPE); - - status |= attribute_store_register_type( - ATTRIBUTE_COMMAND_CLASS_ALARM_SENSOR_STATE, - "State", - ATTRIBUTE_COMMAND_CLASS_ALARM_SENSOR_TYPE, - U8_STORAGE_TYPE); - - status |= attribute_store_register_type( - ATTRIBUTE_COMMAND_CLASS_ALARM_SENSOR_NODE_ID, - "NodeID", - ATTRIBUTE_COMMAND_CLASS_ALARM_SENSOR_TYPE, - U16_STORAGE_TYPE); - - status |= attribute_store_register_type( - ATTRIBUTE_COMMAND_CLASS_ALARM_SENSOR_SECONDS, - "Seconds", - ATTRIBUTE_COMMAND_CLASS_ALARM_SENSOR_TYPE, - U16_STORAGE_TYPE); - -``` - -Under the package zwave_command_class/src, create the header of the command -class implementation, it's easy to use a copy of another command class header -that is up to date and made the appropriate modifications to suit the new -implementation, doxygen documentation is based on the structure of this header -to create the section about the new command class, based on that the comments -of the header must be well defined. Moreover, in the zwave_command_class_fixt.c -should include the header and call the initialization function that is in the -implementation file (e.g. zwave_command_class_< name >.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.