2019-10-20 15:43:50 +01:00
/*
2019-10-27 10:13:24 +00:00
xdrv_23_zigbee_converters . ino - zigbee support for Tasmota
2019-10-20 15:43:50 +01:00
2021-01-01 12:44:04 +00:00
Copyright ( C ) 2021 Theo Arends and Stephan Hadinger
2019-10-20 15:43:50 +01:00
This program is free software : you can redistribute it and / or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation , either version 3 of the License , or
( at your option ) any later version .
This program is distributed in the hope that it will be useful ,
but WITHOUT ANY WARRANTY ; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
GNU General Public License for more details .
You should have received a copy of the GNU General Public License
along with this program . If not , see < http : //www.gnu.org/licenses/>.
*/
# ifdef USE_ZIGBEE
2020-03-23 21:46:26 +00:00
/*********************************************************************************************\
* ZCL Command Structures
\ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
2019-10-20 15:43:50 +01:00
typedef struct Z_CommandConverter {
2020-08-20 20:42:36 +01:00
uint16_t tasmota_cmd_offset ;
2020-02-23 15:46:00 +00:00
uint16_t cluster ;
uint8_t cmd ; // normally 8 bits, 0xFF means it's a parameter
2020-08-13 09:59:38 +01:00
uint8_t direction ; // direction of the command. 0x01 client->server, 0x02 server->client, 0x03 both, 0x80 requires custom decoding
2020-08-20 20:42:36 +01:00
uint16_t param_offset ;
2019-10-20 15:43:50 +01:00
} Z_CommandConverter ;
2020-02-23 15:46:00 +00:00
typedef struct Z_XYZ_Var { // Holds values for vairables X, Y and Z
uint32_t x = 0 ;
uint32_t y = 0 ;
uint32_t z = 0 ;
uint8_t x_type = 0 ; // 0 = no value, 1 = 1 bytes, 2 = 2 bytes
uint8_t y_type = 0 ;
uint8_t z_type = 0 ;
} Z_XYZ_Var ;
2020-03-01 10:25:59 +00:00
// Cluster specific commands
// Note: the table is both for sending commands, but also displaying received commands
// - tasmota_cmd: the human-readable name of the command as entered or displayed, use '|' to split into multiple commands when displayed
// - cluster: cluster number of the command
// - cmd: the command number, of 0xFF if it's actually a variable to be assigned from 'xx'
// - direction: the direction of the command (bit field). 0x01=from client to server (coord to device), 0x02= from server to client (response), 0x80=needs specific decoding
2021-02-02 19:46:18 +00:00
// - param: the paylod template, x/y/z are substituted with arguments, little endian. For command display, payload must match until x/y/z character or until the end of the paylod. '_' means custom converter.
2020-03-01 10:25:59 +00:00
const Z_CommandConverter Z_Commands [ ] PROGMEM = {
2020-08-13 09:59:38 +01:00
// Identify cluster
2020-08-20 20:42:36 +01:00
{ Z_ ( Identify ) , 0x0003 , 0x00 , 0x01 , Z_ ( xxxx ) } , // Identify device, time in seconds
{ Z_ ( IdentifyQuery ) , 0x0003 , 0x01 , 0x01 , Z_ ( ) } , // Identify Query (no param)
2020-02-23 15:46:00 +00:00
// Group adress commands
2020-08-20 20:42:36 +01:00
{ Z_ ( AddGroup ) , 0x0004 , 0x00 , 0x01 , Z_ ( xxxx00 ) } , // Add group id, group name is not supported
{ Z_ ( ViewGroup ) , 0x0004 , 0x01 , 0x01 , Z_ ( xxxx ) } , // Ask for the group name
{ Z_ ( GetGroup ) , 0x0004 , 0x02 , 0x01 , Z_ ( 01 xxxx ) } , // Get one group membership
{ Z_ ( GetAllGroups ) , 0x0004 , 0x02 , 0x01 , Z_ ( 00 ) } , // Get all groups membership
{ Z_ ( RemoveGroup ) , 0x0004 , 0x03 , 0x01 , Z_ ( xxxx ) } , // Remove one group
{ Z_ ( RemoveAllGroups ) , 0x0004 , 0x04 , 0x01 , Z_ ( ) } , // Remove all groups
2020-03-14 13:17:30 +00:00
// Scenes
//{ "AddScene", 0x0005, 0x00, 0x01, "xxxxyy0100" },
2020-08-20 20:42:36 +01:00
{ Z_ ( ViewScene ) , 0x0005 , 0x01 , 0x01 , Z_ ( xxxxyy ) } ,
{ Z_ ( RemoveScene ) , 0x0005 , 0x02 , 0x01 , Z_ ( xxxxyy ) } ,
{ Z_ ( RemoveAllScenes ) , 0x0005 , 0x03 , 0x01 , Z_ ( xxxx ) } ,
{ Z_ ( RecallScene ) , 0x0005 , 0x05 , 0x01 , Z_ ( xxxxyy ) } ,
{ Z_ ( GetSceneMembership ) , 0x0005 , 0x06 , 0x01 , Z_ ( xxxx ) } ,
2020-02-23 15:46:00 +00:00
// Light & Shutter commands
2020-08-20 20:42:36 +01:00
{ Z_ ( PowerOffEffect ) , 0x0006 , 0x40 , 0x81 , Z_ ( xxyy ) } , // Power Off With Effect
{ Z_ ( PowerOnRecall ) , 0x0006 , 0x41 , 0x81 , Z_ ( ) } , // Power On With Recall Global Scene
{ Z_ ( PowerOnTimer ) , 0x0006 , 0x42 , 0x81 , Z_ ( xxyyyyzzzz ) } , // Power On with Timed Off
2021-01-17 15:12:25 +00:00
{ Z_ ( LidlPower ) , 0x0006 , 0xFD , 0x01 , Z_ ( xx ) } , // Lidl specific encoding
2020-08-20 20:42:36 +01:00
{ Z_ ( Power ) , 0x0006 , 0xFF , 0x01 , Z_ ( ) } , // 0=Off, 1=On, 2=Toggle
{ Z_ ( Dimmer ) , 0x0008 , 0x04 , 0x01 , Z_ ( xx0A00 ) } , // Move to Level with On/Off, xx=0..254 (255 is invalid)
{ Z_ ( DimmerUp ) , 0x0008 , 0x06 , 0x01 , Z_ ( 001 90200 ) } , // Step up by 10%, 0.2 secs
{ Z_ ( DimmerDown ) , 0x0008 , 0x06 , 0x01 , Z_ ( 011 90200 ) } , // Step down by 10%, 0.2 secs
{ Z_ ( DimmerStop ) , 0x0008 , 0x03 , 0x01 , Z_ ( ) } , // Stop any Dimmer animation
{ Z_ ( ResetAlarm ) , 0x0009 , 0x00 , 0x01 , Z_ ( xxyyyy ) } , // Reset alarm (alarm code + cluster identifier)
{ Z_ ( ResetAllAlarms ) , 0x0009 , 0x01 , 0x01 , Z_ ( ) } , // Reset all alarms
{ Z_ ( Hue ) , 0x0300 , 0x00 , 0x01 , Z_ ( xx000A00 ) } , // Move to Hue, shortest time, 1s
{ Z_ ( Sat ) , 0x0300 , 0x03 , 0x01 , Z_ ( xx0A00 ) } , // Move to Sat
{ Z_ ( HueSat ) , 0x0300 , 0x06 , 0x01 , Z_ ( xxyy0A00 ) } , // Hue, Sat
{ Z_ ( Color ) , 0x0300 , 0x07 , 0x01 , Z_ ( xxxxyyyy0A00 ) } , // x, y (uint16)
{ Z_ ( CT ) , 0x0300 , 0x0A , 0x01 , Z_ ( xxxx0A00 ) } , // Color Temperature Mireds (uint16)
2021-02-02 19:46:18 +00:00
{ Z_ ( RGB ) , 0x0300 , 0xF0 , 0x81 , Z_ ( _ ) } , // synthetic commands converting RGB to XY
2020-08-20 20:42:36 +01:00
{ Z_ ( ShutterOpen ) , 0x0102 , 0x00 , 0x01 , Z_ ( ) } ,
{ Z_ ( ShutterClose ) , 0x0102 , 0x01 , 0x01 , Z_ ( ) } ,
{ Z_ ( ShutterStop ) , 0x0102 , 0x02 , 0x01 , Z_ ( ) } ,
{ Z_ ( ShutterLift ) , 0x0102 , 0x05 , 0x01 , Z_ ( xx ) } , // Lift percentage, 0%=open, 100%=closed
{ Z_ ( ShutterTilt ) , 0x0102 , 0x08 , 0x01 , Z_ ( xx ) } , // Tilt percentage
{ Z_ ( Shutter ) , 0x0102 , 0xFF , 0x01 , Z_ ( ) } ,
2021-04-05 09:28:09 +01:00
// Legrand - Manuf 1021
{ Z_ ( LegrandMode ) , 0xFC40 , 0x00 , 0x01 , Z_ ( xx ) } ,
2020-02-23 15:46:00 +00:00
// Blitzwolf PIR
2020-08-20 20:42:36 +01:00
{ Z_ ( Occupancy ) , 0xEF00 , 0x01 , 0x82 , Z_ ( ) } , // Specific decoder for Blitzwolf PIR, empty name means special treatment
2020-02-23 15:46:00 +00:00
// Decoders only - normally not used to send, and names may be masked by previous definitions
2020-08-20 20:42:36 +01:00
{ Z_ ( Dimmer ) , 0x0008 , 0x00 , 0x01 , Z_ ( xx ) } ,
{ Z_ ( DimmerMove ) , 0x0008 , 0x01 , 0x01 , Z_ ( xx0A ) } ,
{ Z_ ( DimmerStepUp ) , 0x0008 , 0x02 , 0x01 , Z_ ( 00 xx0A00 ) } ,
{ Z_ ( DimmerStepDown ) , 0x0008 , 0x02 , 0x01 , Z_ ( 01 xx0A00 ) } ,
{ Z_ ( DimmerStep ) , 0x0008 , 0x02 , 0x01 , Z_ ( xx190A00 ) } ,
{ Z_ ( DimmerMove ) , 0x0008 , 0x05 , 0x01 , Z_ ( xx0A ) } ,
{ Z_ ( DimmerUp ) , 0x0008 , 0x06 , 0x01 , Z_ ( 00 ) } ,
{ Z_ ( DimmerDown ) , 0x0008 , 0x06 , 0x01 , Z_ ( 01 ) } ,
{ Z_ ( DimmerStop ) , 0x0008 , 0x07 , 0x01 , Z_ ( ) } ,
{ Z_ ( HueMove ) , 0x0300 , 0x01 , 0x01 , Z_ ( xx19 ) } ,
{ Z_ ( HueStepUp ) , 0x0300 , 0x02 , 0x01 , Z_ ( 01 xx0A00 ) } ,
{ Z_ ( HueStepDown ) , 0x0300 , 0x02 , 0x01 , Z_ ( 03 xx0A00 ) } ,
{ Z_ ( HueStep ) , 0x0300 , 0x02 , 0x01 , Z_ ( xx190A00 ) } ,
{ Z_ ( SatMove ) , 0x0300 , 0x04 , 0x01 , Z_ ( xx19 ) } ,
{ Z_ ( SatStep ) , 0x0300 , 0x05 , 0x01 , Z_ ( xx190A ) } ,
{ Z_ ( ColorMove ) , 0x0300 , 0x08 , 0x01 , Z_ ( xxxxyyyy ) } ,
{ Z_ ( ColorStep ) , 0x0300 , 0x09 , 0x01 , Z_ ( xxxxyyyy0A00 ) } ,
{ Z_ ( ColorTempMoveUp ) , 0x0300 , 0x4B , 0x01 , Z_ ( 01 xxxx000000000000 ) } ,
{ Z_ ( ColorTempMoveDown ) , 0x0300 , 0x4B , 0x01 , Z_ ( 03 xxxx000000000000 ) } ,
{ Z_ ( ColorTempMoveStop ) , 0x0300 , 0x4B , 0x01 , Z_ ( 00 xxxx000000000000 ) } ,
{ Z_ ( ColorTempMove ) , 0x0300 , 0x4B , 0x01 , Z_ ( xxyyyy000000000000 ) } ,
{ Z_ ( ColorTempStepUp ) , 0x0300 , 0x4C , 0x01 , Z_ ( 01 xxxx0A0000000000 ) } ,
{ Z_ ( ColorTempStepDown ) , 0x0300 , 0x4C , 0x01 , Z_ ( 03 xxxx0A0000000000 ) } ,
{ Z_ ( ColorTempStep ) , 0x0300 , 0x4C , 0x01 , Z_ ( xxyyyy0A0000000000 ) } , //xx = 0x01 up, 0x03 down, yyyy = step
2020-02-23 15:46:00 +00:00
// Tradfri
2020-08-20 20:42:36 +01:00
{ Z_ ( ArrowClick ) , 0x0005 , 0x07 , 0x01 , Z_ ( xx ) } , // xx == 0x01 = left, 0x00 = right
{ Z_ ( ArrowHold ) , 0x0005 , 0x08 , 0x01 , Z_ ( xx ) } , // xx == 0x01 = left, 0x00 = right
{ Z_ ( ArrowRelease ) , 0x0005 , 0x09 , 0x01 , Z_ ( ) } ,
2020-08-13 09:59:38 +01:00
// Response for Indetify cluster
2020-08-20 20:42:36 +01:00
{ Z_ ( IdentifyQuery ) , 0x0003 , 0x00 , 0x02 , Z_ ( xxxx ) } , // timeout in seconds
2020-02-23 16:11:51 +00:00
// IAS - Intruder Alarm System + leak/fire detection
2020-08-20 20:42:36 +01:00
{ Z_ ( ZoneStatusChange ) , 0x0500 , 0x00 , 0x82 , Z_ ( xxxxyyzz ) } , // xxxx = zone status, yy = extended status, zz = zone id, Delay is ignored
2020-02-23 15:46:00 +00:00
// responses for Group cluster commands
2020-08-20 20:42:36 +01:00
{ Z_ ( AddGroup ) , 0x0004 , 0x00 , 0x82 , Z_ ( xxyyyy ) } , // xx = status, yy = group id
{ Z_ ( ViewGroup ) , 0x0004 , 0x01 , 0x82 , Z_ ( xxyyyy ) } , // xx = status, yy = group id, name ignored
{ Z_ ( GetGroup ) , 0x0004 , 0x02 , 0x82 , Z_ ( xxyyzzzz ) } , // xx = capacity, yy = count, zzzz = first group id, following groups ignored
{ Z_ ( RemoveGroup ) , 0x0004 , 0x03 , 0x82 , Z_ ( xxyyyy ) } , // xx = status, yy = group id
2020-03-14 13:17:30 +00:00
// responses for Scene cluster commands
2020-08-20 20:42:36 +01:00
{ Z_ ( AddScene ) , 0x0005 , 0x00 , 0x82 , Z_ ( xxyyyyzz ) } , // xx = status, yyyy = group id, zz = scene id
{ Z_ ( ViewScene ) , 0x0005 , 0x01 , 0x82 , Z_ ( xxyyyyzz ) } , // xx = status, yyyy = group id, zz = scene id
{ Z_ ( RemoveScene ) , 0x0005 , 0x02 , 0x82 , Z_ ( xxyyyyzz ) } , // xx = status, yyyy = group id, zz = scene id
{ Z_ ( RemoveAllScenes ) , 0x0005 , 0x03 , 0x82 , Z_ ( xxyyyy ) } , // xx = status, yyyy = group id
{ Z_ ( StoreScene ) , 0x0005 , 0x04 , 0x82 , Z_ ( xxyyyyzz ) } , // xx = status, yyyy = group id, zz = scene id
{ Z_ ( GetSceneMembership ) , 0x0005 , 0x06 , 0x82 , Z_ ( xxyyzzzz ) } , // specific
2020-10-11 18:41:23 +01:00
// Tuya - Moes specific
{ Z_ ( ) , 0xEF00 , 0xFF , 0x83 , Z_ ( ) } , // capture any command in 0xEF00 cluster
2020-10-23 21:49:51 +01:00
// Terncy specific
{ Z_ ( ) , 0xFCCC , 0x00 , 0x82 , Z_ ( xxyy ) } , // Terncy button (multi-)press
2019-10-20 15:43:50 +01:00
} ;
2020-03-23 21:46:26 +00:00
/*********************************************************************************************\
* ZCL Read Light status based on cluster number
\ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
2019-12-15 15:02:41 +00:00
# define ZLE(x) ((x) & 0xFF), ((x) >> 8) // Little Endian
// Below are the attributes we wand to read from each cluster
const uint8_t CLUSTER_0006 [ ] = { ZLE ( 0x0000 ) } ; // Power
const uint8_t CLUSTER_0008 [ ] = { ZLE ( 0x0000 ) } ; // CurrentLevel
const uint8_t CLUSTER_0009 [ ] = { ZLE ( 0x0000 ) } ; // AlarmCount
2020-03-14 13:17:30 +00:00
const uint8_t CLUSTER_0300 [ ] = { ZLE ( 0x0000 ) , ZLE ( 0x0001 ) , ZLE ( 0x0003 ) , ZLE ( 0x0004 ) , ZLE ( 0x0007 ) , ZLE ( 0x0008 ) } ; // Hue, Sat, X, Y, CT, ColorMode
2019-12-15 15:02:41 +00:00
2020-02-23 15:46:00 +00:00
// This callback is registered after a cluster specific command and sends a read command for the same cluster
2020-09-12 09:57:54 +01:00
void Z_ReadAttrCallback ( uint16_t shortaddr , uint16_t groupaddr , uint16_t cluster , uint8_t endpoint , uint32_t value ) {
2019-12-15 15:02:41 +00:00
size_t attrs_len = 0 ;
const uint8_t * attrs = nullptr ;
switch ( cluster ) {
case 0x0006 : // for On/Off
attrs = CLUSTER_0006 ;
attrs_len = sizeof ( CLUSTER_0006 ) ;
break ;
case 0x0008 : // for Dimmer
attrs = CLUSTER_0008 ;
attrs_len = sizeof ( CLUSTER_0008 ) ;
break ;
case 0x0009 : // for Alarms
attrs = CLUSTER_0009 ;
attrs_len = sizeof ( CLUSTER_0009 ) ;
break ;
case 0x0300 : // for Lights
attrs = CLUSTER_0300 ;
attrs_len = sizeof ( CLUSTER_0300 ) ;
break ;
}
if ( attrs ) {
2020-05-17 17:33:42 +01:00
if ( groupaddr ) {
shortaddr = BAD_SHORTADDR ; // if group address, don't send to device
}
2021-02-02 19:46:18 +00:00
ZCLMessage zcl ( attrs_len ) ; // message is `attrs_len` bytes
zcl . shortaddr = shortaddr ;
zcl . groupaddr = groupaddr ;
zcl . cluster = cluster ;
zcl . endpoint = endpoint ;
zcl . cmd = ZCL_READ_ATTRIBUTES ;
zcl . clusterSpecific = false ;
zcl . needResponse = true ;
zcl . direct = false ; // discover route
zcl . buf . addBuffer ( attrs , attrs_len ) ;
zigbeeZCLSendCmd ( zcl ) ;
2019-12-15 15:02:41 +00:00
}
}
2020-03-26 18:34:59 +00:00
// This callback is registered after a an attribute read command was made to a light, and fires if we don't get any response after 1000 ms
2020-09-12 09:57:54 +01:00
void Z_Unreachable ( uint16_t shortaddr , uint16_t groupaddr , uint16_t cluster , uint8_t endpoint , uint32_t value ) {
2020-05-17 17:33:42 +01:00
if ( BAD_SHORTADDR ! = shortaddr ) {
2020-11-01 18:00:07 +00:00
zigbee_devices . getShortAddr ( shortaddr ) . setReachable ( false ) ; // mark device as reachable
2020-11-22 15:07:09 +00:00
Z_attribute_list attr_list ;
2020-11-30 18:25:05 +00:00
attr_list . addAttributePMEM ( PSTR ( " Reachable " ) ) . setBool ( false ) ; // "Reachable":false
2020-11-22 15:07:09 +00:00
// Z_postProcessAttributes(shortaddr, endpoint, attr_list); // make sure all is updated accordingly
zigbee_devices . jsonPublishNow ( shortaddr , attr_list ) ;
2020-03-26 18:34:59 +00:00
}
}
2020-02-23 15:46:00 +00:00
// returns true if char is 'x', 'y' or 'z'
inline bool isXYZ ( char c ) {
return ( c > = ' x ' ) & & ( c < = ' z ' ) ;
}
// returns the Hex value of a digit [0-9A-Fa-f]
// return: 0x00-0x0F
// or -1 if cannot be parsed
inline int8_t hexValue ( char c ) {
if ( ( c > = ' 0 ' ) & & ( c < = ' 9 ' ) ) {
return c - ' 0 ' ;
}
if ( ( c > = ' A ' ) & & ( c < = ' F ' ) ) {
return 10 + c - ' A ' ;
}
if ( ( c > = ' a ' ) & & ( c < = ' f ' ) ) {
return 10 + c - ' a ' ;
}
return - 1 ;
}
// Parse a Big Endian suite of max_len digits, or stops when a non-hex digit is found
uint32_t parseHex_P ( const char * * data , size_t max_len = 8 ) {
uint32_t ret = 0 ;
for ( uint32_t i = 0 ; i < max_len ; i + + ) {
int8_t v = hexValue ( pgm_read_byte ( * data ) ) ;
if ( v < 0 ) { break ; } // non hex digit, we stop parsing
ret = ( ret < < 4 ) | v ;
* data + = 1 ;
}
return ret ;
}
// Parse a model like "xxyy00"
// and fill x, y and z values
// Little Endian encoding
// On exit, xyz is updated, and x_type, y_type, z_type contain the number of bytes read for each
void parseXYZ ( const char * model , const SBuffer & payload , struct Z_XYZ_Var * xyz ) {
const char * p = model ; // pointer to the model character
uint32_t v = 0 ; // index in the payload bytes buffer
char c = pgm_read_byte ( p ) ; // cur char
while ( c ) {
char c1 = pgm_read_byte ( p + 1 ) ; // next char
if ( ! c1 ) { break ; } // unexpected end of model
if ( isXYZ ( c ) & & ( c = = c1 ) & & ( v < payload . len ( ) ) ) { // if char is [x-z] and followed by same char
uint8_t val = payload . get8 ( v ) ;
switch ( c ) {
case ' x ' :
xyz - > x = xyz - > x | ( val < < ( xyz - > x_type * 8 ) ) ;
xyz - > x_type + + ;
break ;
case ' y ' :
xyz - > y = xyz - > y | ( val < < ( xyz - > y_type * 8 ) ) ;
xyz - > y_type + + ;
break ;
case ' z ' :
xyz - > z = xyz - > z | ( val < < ( xyz - > z_type * 8 ) ) ;
xyz - > z_type + + ;
break ;
}
}
p + = 2 ;
v + + ;
c = pgm_read_byte ( p ) ;
}
}
// Parse a cluster specific command, and try to convert into human readable
2020-09-05 13:44:31 +01:00
void convertClusterSpecific ( class Z_attribute_list & attr_list , uint16_t cluster , uint8_t cmd , bool direction , uint16_t shortaddr , uint8_t srcendpoint , const SBuffer & payload ) {
const char * command_name = nullptr ;
2020-03-01 10:25:59 +00:00
uint8_t conv_direction ;
2020-02-23 15:46:00 +00:00
Z_XYZ_Var xyz ;
2021-01-25 15:02:56 +00:00
//AddLog_P(LOG_LEVEL_INFO, PSTR(">>> len = %d - %02X%02X%02X"), payload.len(), payload.get8(0), payload.get8(1), payload.get8(2));
2019-11-03 11:41:44 +00:00
for ( uint32_t i = 0 ; i < sizeof ( Z_Commands ) / sizeof ( Z_Commands [ 0 ] ) ; i + + ) {
const Z_CommandConverter * conv = & Z_Commands [ i ] ;
2020-03-01 10:25:59 +00:00
uint16_t conv_cluster = pgm_read_word ( & conv - > cluster ) ;
if ( conv_cluster = = cluster ) {
2020-02-23 15:46:00 +00:00
// cluster match
2020-03-01 10:25:59 +00:00
uint8_t conv_cmd = pgm_read_byte ( & conv - > cmd ) ;
conv_direction = pgm_read_byte ( & conv - > direction ) ;
if ( ( 0xFF = = conv_cmd ) | | ( cmd = = conv_cmd ) ) {
2020-02-23 15:46:00 +00:00
// cmd match
2020-03-01 10:25:59 +00:00
if ( ( direction & & ( conv_direction & 0x02 ) ) | | ( ! direction & & ( conv_direction & 0x01 ) ) ) {
2020-02-23 15:46:00 +00:00
// check if we have a match for params too
// Match if:
// - payload exactly matches conv->param (conv->param may be longer)
// - payload matches conv->param until 'x', 'y' or 'z'
2020-08-20 20:42:36 +01:00
const char * p = Z_strings + pgm_read_word ( & conv - > param_offset ) ;
2021-01-25 15:02:56 +00:00
//AddLog_P(LOG_LEVEL_INFO, PSTR(">>>++1 param = %s"), p);
2020-02-23 15:46:00 +00:00
bool match = true ;
for ( uint8_t i = 0 ; i < payload . len ( ) ; i + + ) {
const char c1 = pgm_read_byte ( p ) ;
2020-11-12 18:38:21 +00:00
// const char c2 = pgm_read_byte(p+1);
2021-01-25 15:02:56 +00:00
//AddLog_P(LOG_LEVEL_INFO, PSTR(">>>++2 c1 = %c, c2 = %c"), c1, c2);
2020-02-23 15:46:00 +00:00
if ( ( 0x00 = = c1 ) | | isXYZ ( c1 ) ) {
break ;
}
const char * p2 = p ;
uint32_t nextbyte = parseHex_P ( & p2 , 2 ) ;
2021-01-25 15:02:56 +00:00
//AddLog_P(LOG_LEVEL_INFO, PSTR(">>>++3 parseHex_P = %02X"), nextbyte);
2020-02-23 15:46:00 +00:00
if ( nextbyte ! = payload . get8 ( i ) ) {
match = false ;
break ;
}
p + = 2 ;
}
if ( match ) {
2020-09-05 13:44:31 +01:00
command_name = Z_strings + pgm_read_word ( & conv - > tasmota_cmd_offset ) ;
2020-08-20 20:42:36 +01:00
parseXYZ ( Z_strings + pgm_read_word ( & conv - > param_offset ) , payload , & xyz ) ;
2020-03-01 10:25:59 +00:00
if ( 0xFF = = conv_cmd ) {
2020-02-23 15:46:00 +00:00
// shift all values
xyz . z = xyz . y ;
xyz . z_type = xyz . y_type ;
xyz . y = xyz . x ;
xyz . y_type = xyz . x_type ;
xyz . x = cmd ;
xyz . x_type = 1 ; // 1 byte
}
break ;
}
}
}
2019-11-03 11:41:44 +00:00
}
}
2020-02-23 15:46:00 +00:00
// always report attribute in raw format
// Format: "0001!06": "00" = "<cluster>!<cmd>": "<payload>" for commands to devices
// Format: "0004<00": "00" = "<cluster><<cmd>": "<payload>" for commands to devices
char attrid_str [ 12 ] ;
snprintf_P ( attrid_str , sizeof ( attrid_str ) , PSTR ( " %04X%c%02X " ) , cluster , direction ? ' < ' : ' ! ' , cmd ) ;
2020-10-11 18:41:23 +01:00
Z_attribute & attr_raw = attr_list . addAttribute ( attrid_str ) ;
attr_raw . setBuf ( payload , 0 , payload . len ( ) ) ;
2020-02-23 15:46:00 +00:00
if ( command_name ) {
2020-03-01 10:25:59 +00:00
// Now try to transform into a human readable format
// if (direction & 0x80) then specific transform
if ( conv_direction & 0x80 ) {
2020-09-05 13:44:31 +01:00
uint32_t cccc00mm = ( cluster < < 16 ) | cmd ; // format = cccc00mm, cccc = cluster, mm = command
2020-03-01 10:25:59 +00:00
// IAS
2020-09-05 13:44:31 +01:00
switch ( cccc00mm ) {
case 0x05000000 : // "ZoneStatusChange"
2020-11-06 21:24:45 +00:00
{
attr_list . addAttribute ( command_name , true ) . setUInt ( xyz . x ) ;
if ( 0 ! = xyz . y ) {
attr_list . addAttribute ( command_name , PSTR ( " Ext " ) ) . setUInt ( xyz . y ) ;
}
if ( ( 0 ! = xyz . z ) & & ( 0xFF ! = xyz . z ) ) {
attr_list . addAttribute ( command_name , PSTR ( " Zone " ) ) . setUInt ( xyz . z ) ;
}
// Convert to "Occupancy" or to "Contact" if the device is PIR or Contact sensor
const Z_Data_Alarm & alarm = ( const Z_Data_Alarm & ) zigbee_devices . getShortAddr ( shortaddr ) . data . find ( Z_Data_Type : : Z_Alarm , srcendpoint ) ;
2020-12-02 18:25:17 +00:00
if ( & alarm ! = nullptr ) {
alarm . convertZoneStatus ( attr_list , xyz . x ) ;
2020-11-06 21:24:45 +00:00
}
2020-09-05 13:44:31 +01:00
}
break ;
case 0x00040000 :
case 0x00040001 :
case 0x00040003 : // AddGroupResp or ViewGroupResp (group name ignored) or RemoveGroup
attr_list . addAttribute ( command_name , true ) . setUInt ( xyz . y ) ;
attr_list . addAttribute ( command_name , PSTR ( " Status " ) ) . setUInt ( xyz . x ) ;
attr_list . addAttribute ( command_name , PSTR ( " StatusMsg " ) ) . setStr ( getZigbeeStatusMessage ( xyz . x ) . c_str ( ) ) ;
break ;
case 0x00040002 : // GetGroupResp
attr_list . addAttribute ( command_name , PSTR ( " Capacity " ) ) . setUInt ( xyz . x ) ;
attr_list . addAttribute ( command_name , PSTR ( " Count " ) ) . setUInt ( xyz . y ) ;
{
2020-10-28 09:08:15 +00:00
JsonGeneratorArray group_list ;
2020-09-05 13:44:31 +01:00
for ( uint32_t i = 0 ; i < xyz . y ; i + + ) {
group_list . add ( payload . get16 ( 2 + 2 * i ) ) ;
}
attr_list . addAttribute ( command_name , true ) . setStrRaw ( group_list . toString ( ) . c_str ( ) ) ;
2020-08-08 11:17:37 +01:00
}
2020-09-05 13:44:31 +01:00
break ;
case 0x00050000 :
case 0x00050001 : // ViewScene
case 0x00050002 :
case 0x00050004 : // AddScene or RemoveScene or StoreScene
attr_list . addAttribute ( command_name , PSTR ( " Status " ) ) . setUInt ( xyz . x ) ;
attr_list . addAttribute ( command_name , PSTR ( " StatusMsg " ) ) . setStr ( getZigbeeStatusMessage ( xyz . x ) . c_str ( ) ) ;
2020-11-30 18:25:05 +00:00
attr_list . addAttributePMEM ( PSTR ( " GroupId " ) ) . setUInt ( xyz . y ) ;
attr_list . addAttributePMEM ( PSTR ( " SceneId " ) ) . setUInt ( xyz . z ) ;
2020-09-05 13:44:31 +01:00
if ( 0x00050001 = = cccc00mm ) { // ViewScene specific
2020-11-30 18:25:05 +00:00
attr_list . addAttributePMEM ( PSTR ( " ScenePayload " ) ) . setBuf ( payload , 4 , payload . len ( ) - 4 ) ; // remove first 4 bytes
2020-03-01 10:25:59 +00:00
}
2020-09-05 13:44:31 +01:00
break ;
case 0x00050003 : // RemoveAllScenes
attr_list . addAttribute ( command_name , PSTR ( " Status " ) ) . setUInt ( xyz . x ) ;
attr_list . addAttribute ( command_name , PSTR ( " StatusMsg " ) ) . setStr ( getZigbeeStatusMessage ( xyz . x ) . c_str ( ) ) ;
2020-11-30 18:25:05 +00:00
attr_list . addAttributePMEM ( PSTR ( " GroupId " ) ) . setUInt ( xyz . y ) ;
2020-09-05 13:44:31 +01:00
break ;
case 0x00050006 : // GetSceneMembership
attr_list . addAttribute ( command_name , PSTR ( " Status " ) ) . setUInt ( xyz . x ) ;
attr_list . addAttribute ( command_name , PSTR ( " StatusMsg " ) ) . setStr ( getZigbeeStatusMessage ( xyz . x ) . c_str ( ) ) ;
2020-11-30 18:25:05 +00:00
attr_list . addAttributePMEM ( PSTR ( " Capacity " ) ) . setUInt ( xyz . y ) ;
attr_list . addAttributePMEM ( PSTR ( " GroupId " ) ) . setUInt ( xyz . z ) ;
attr_list . addAttributePMEM ( PSTR ( " ScenePayload " ) ) . setBuf ( payload , 4 , payload . len ( ) - 4 ) ; // remove first 4 bytes
2020-09-05 13:44:31 +01:00
break ;
case 0x00060040 : // Power Off With Effect
2020-11-30 18:25:05 +00:00
attr_list . addAttributePMEM ( PSTR ( " Power " ) ) . setUInt ( 0 ) ;
attr_list . addAttributePMEM ( PSTR ( " PowerEffect " ) ) . setUInt ( xyz . x ) ;
attr_list . addAttributePMEM ( PSTR ( " PowerEffectVariant " ) ) . setUInt ( xyz . y ) ;
2020-09-05 13:44:31 +01:00
break ;
case 0x00060041 : // Power On With Recall Global Scene
2020-11-30 18:25:05 +00:00
attr_list . addAttributePMEM ( PSTR ( " Power " ) ) . setUInt ( 1 ) ;
attr_list . addAttributePMEM ( PSTR ( " PowerRecallGlobalScene " ) ) . setBool ( true ) ;
2020-09-05 13:44:31 +01:00
break ;
case 0x00060042 : // Power On With Timed Off Command
2020-11-30 18:25:05 +00:00
attr_list . addAttributePMEM ( PSTR ( " Power " ) ) . setUInt ( 1 ) ;
attr_list . addAttributePMEM ( PSTR ( " PowerOnlyWhenOn " ) ) . setUInt ( xyz . x ) ;
attr_list . addAttributePMEM ( PSTR ( " PowerOnTime " ) ) . setFloat ( xyz . y / 10.0f ) ;
attr_list . addAttributePMEM ( PSTR ( " PowerOffWait " ) ) . setFloat ( xyz . z / 10.0f ) ;
2020-09-05 13:44:31 +01:00
break ;
2020-10-11 18:41:23 +01:00
case 0xEF000000 . . . 0xEF0000FF : // any Tuya - Moes command
if ( convertTuyaSpecificCluster ( attr_list , cluster , cmd , direction , shortaddr , srcendpoint , payload ) ) {
attr_list . removeAttribute ( & attr_raw ) ; // remove raw command
}
break ;
2020-10-23 21:49:51 +01:00
case 0xFCCC0000 : // Terncy button (multi-)press
2020-11-30 18:25:05 +00:00
attr_list . addAttributePMEM ( PSTR ( " TerncyPress " ) ) . setUInt ( xyz . y ) ;
attr_list . addAttributePMEM ( PSTR ( " TerncyCount " ) ) . setUInt ( xyz . x ) ;
2020-10-23 21:49:51 +01:00
break ;
2020-03-01 10:25:59 +00:00
}
2020-07-20 18:30:32 +01:00
} else { // general case
2020-09-05 13:44:31 +01:00
// do we send command with endpoint suffix
char command_suffix [ 4 ] = { 0x00 } ; // empty string by default
2020-07-20 18:30:32 +01:00
// if SO101 and multiple endpoints, append endpoint number
if ( Settings . flag4 . zb_index_ep ) {
2020-10-31 18:51:17 +00:00
if ( zigbee_devices . getShortAddr ( shortaddr ) . countEndpoints ( ) > 0 ) {
2020-09-05 13:44:31 +01:00
snprintf_P ( command_suffix , sizeof ( command_suffix ) , PSTR ( " %d " ) , srcendpoint ) ;
2020-07-20 18:30:32 +01:00
}
}
2020-03-01 10:25:59 +00:00
if ( 0 = = xyz . x_type ) {
2020-09-05 13:44:31 +01:00
attr_list . addAttribute ( command_name , command_suffix ) . setBool ( true ) ;
2020-03-01 10:25:59 +00:00
} else if ( 0 = = xyz . y_type ) {
2020-09-05 13:44:31 +01:00
attr_list . addAttribute ( command_name , command_suffix ) . setUInt ( xyz . x ) ;
2020-03-01 10:25:59 +00:00
} else {
// multiple answers, create an array
2020-10-28 09:08:15 +00:00
JsonGeneratorArray arr ;
2020-03-01 10:25:59 +00:00
arr . add ( xyz . x ) ;
arr . add ( xyz . y ) ;
if ( xyz . z_type ) {
arr . add ( xyz . z ) ;
}
2020-09-05 13:44:31 +01:00
attr_list . addAttribute ( command_name , command_suffix ) . setStrRaw ( arr . toString ( ) . c_str ( ) ) ;
2020-02-23 15:46:00 +00:00
}
}
}
2019-11-03 11:41:44 +00:00
}
2021-01-25 15:02:56 +00:00
/*********************************************************************************************\
*
* Tuya Zigbee specific protocol
*
\ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
// Parse a sinlge attribute value and convert to human readable JSON attribute value
2020-12-06 18:20:42 +00:00
void parseSingleTuyaAttribute ( Z_attribute & attr , const SBuffer & buf ,
uint32_t i , uint32_t len , int32_t attrtype ) {
// fallback - enter a null value
attr . setNone ( ) ; // set to null by default
// now parse accordingly to attr type
switch ( attrtype ) {
case 0x00 : // RAW octstr
attr . setBuf ( buf , i , len ) ;
break ;
case 0x01 : // Bool, values 0/1, len = 1
case 0x04 : // enum 8 bits
attr . setUInt ( buf . get8 ( i ) ) ;
break ;
case 0x02 : // 4 bytes value (signed?)
attr . setUInt ( buf . get32BigEndian ( i ) ) ;
break ;
case 0x03 : // String (we expect it is not ended with \00)
2020-12-07 19:04:47 +00:00
{
2020-12-06 18:20:42 +00:00
char str [ len + 1 ] ;
strncpy ( str , buf . charptr ( i ) , len ) ;
str [ len ] = 0x00 ;
attr . setStr ( str ) ;
2020-12-07 19:04:47 +00:00
}
2020-12-06 18:20:42 +00:00
break ;
case 0x05 : // enum in 1/2/4 bytes, Big Endian
if ( 1 = = len ) {
attr . setUInt ( buf . get8 ( i ) ) ;
} else if ( 2 = = len ) {
attr . setUInt ( buf . get16BigEndian ( i ) ) ;
} else if ( 4 = = len ) {
attr . setUInt ( buf . get32BigEndian ( i ) ) ;
}
}
}
2020-10-11 18:41:23 +01:00
//
// Tuya - MOES specifc cluster 0xEF00
// See https://medium.com/@dzegarra/zigbee2mqtt-how-to-add-support-for-a-new-tuya-based-device-part-2-5492707e882d
// and https://github.com/Koenkk/zigbee-herdsman-converters/blob/9f503d47d3df6a99d133b78d2b52aa5c701ddddf/converters/fromZigbee.js#L339
//
bool convertTuyaSpecificCluster ( class Z_attribute_list & attr_list , uint16_t cluster , uint8_t cmd , bool direction , uint16_t shortaddr , uint8_t srcendpoint , const SBuffer & buf ) {
// uint8_t status = buf.get8(0);
// uint8_t transid = buf.get8(1);
2020-12-06 18:20:42 +00:00
uint8_t dp = buf . get8 ( 2 ) ; // dpid from Tuya documentation
uint8_t attr_type = buf . get8 ( 3 ) ; // data type from Tuya documentation
uint16_t len = buf . get16BigEndian ( 4 ) ;
2020-10-11 18:41:23 +01:00
if ( ( 1 = = cmd ) | | ( 2 = = cmd ) ) { // attribute report or attribute response
// create a synthetic attribute with id 'dp'
2020-12-06 18:20:42 +00:00
Z_attribute & attr = attr_list . addAttribute ( cluster , ( attr_type < < 8 ) | dp ) ;
parseSingleTuyaAttribute ( attr , buf , 6 , len , attr_type ) ;
2020-10-11 18:41:23 +01:00
return true ; // true = remove the original Tuya attribute
}
return false ;
}
2021-01-25 15:02:56 +00:00
/*********************************************************************************************\
*
* Find commands
*
\ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
2020-02-23 15:46:00 +00:00
// Find the command details by command name
2020-03-01 10:25:59 +00:00
// Only take commands outgoing, i.e. direction == 0
2020-02-23 15:46:00 +00:00
// If not found:
// - returns nullptr
2021-02-02 19:46:18 +00:00
// - return PROGMEM string
const char * zigbeeFindCommand ( const char * command , uint16_t * cluster , uint16_t * cmd ) {
2020-08-08 11:17:37 +01:00
if ( nullptr = = command ) { return nullptr ; }
2020-02-23 15:46:00 +00:00
for ( uint32_t i = 0 ; i < sizeof ( Z_Commands ) / sizeof ( Z_Commands [ 0 ] ) ; i + + ) {
const Z_CommandConverter * conv = & Z_Commands [ i ] ;
2020-03-01 10:25:59 +00:00
uint8_t conv_direction = pgm_read_byte ( & conv - > direction ) ;
uint8_t conv_cmd = pgm_read_byte ( & conv - > cmd ) ;
uint16_t conv_cluster = pgm_read_word ( & conv - > cluster ) ;
2021-02-02 19:46:18 +00:00
// conv_direction must be client (coord) -> server (device), we can only send commands to end devices
2020-08-20 20:42:36 +01:00
if ( ( conv_direction & 0x01 ) & & ( 0 = = strcasecmp_P ( command , Z_strings + pgm_read_word ( & conv - > tasmota_cmd_offset ) ) ) ) {
2020-03-01 10:25:59 +00:00
* cluster = conv_cluster ;
* cmd = conv_cmd ;
2021-02-02 19:46:18 +00:00
return Z_strings + pgm_read_word ( & conv - > param_offset ) ;
2020-02-23 15:46:00 +00:00
}
}
return nullptr ;
2019-10-20 15:43:50 +01:00
}
// take the lower 4 bits and turn it to an hex char
inline char hexDigit ( uint32_t h ) {
uint32_t nybble = h & 0x0F ;
return ( nybble > 9 ) ? ' A ' - 10 + nybble : ' 0 ' + nybble ;
}
// replace all xx/yy/zz substrings with unsigned ints, and the corresponding len (8, 16 or 32 bits)
2021-02-02 19:46:18 +00:00
// Returns a SBuffer allocated object, it is the caller's responsibility to delete it
void zigbeeCmdAddParams ( SBuffer & buf , const char * zcl_cmd_P , uint32_t x , uint32_t y , uint32_t z ) {
size_t hex_len = strlen_P ( zcl_cmd_P ) ;
buf . reserve ( ( hex_len + 1 ) / 2 ) ;
const char * p = zcl_cmd_P ;
char c0 , c1 ;
while ( ( c0 = pgm_read_byte ( p ) ) & & ( c1 = pgm_read_byte ( p + 1 ) ) ) {
if ( isXYZ ( c0 ) & & ( c0 = = c1 ) ) { // if char is [x-z] and followed by same char
2020-11-12 18:38:21 +00:00
uint8_t val = 0 ;
2021-02-02 19:46:18 +00:00
switch ( pgm_read_byte ( p ) ) {
2019-10-20 15:43:50 +01:00
case ' x ' :
val = x & 0xFF ;
x = x > > 8 ;
break ;
case ' y ' :
val = y & 0xFF ;
y = y > > 8 ;
break ;
case ' z ' :
val = z & 0xFF ;
z = z > > 8 ;
break ;
}
2021-02-02 19:46:18 +00:00
buf . add8 ( val ) ;
// *p = hexDigit(val >> 4);
// *(p+1) = hexDigit(val);
} else {
char hex [ 4 ] ;
hex [ 0 ] = c0 ;
hex [ 1 ] = c1 ;
hex [ 2 ] = 0 ;
buf . add8 ( strtoul ( hex , nullptr , 16 ) & 0xFF ) ;
2019-10-20 15:43:50 +01:00
}
2021-02-02 19:46:18 +00:00
p + = 2 ;
2019-10-20 15:43:50 +01:00
}
}
2019-11-03 11:41:44 +00:00
const char kZ_Alias [ ] PROGMEM = " OFF| " D_OFF " | " D_FALSE " | " D_STOP " | " " OPEN " " | " // 0
" ON| " D_ON " | " D_TRUE " | " D_START " | " " CLOSE " " | " // 1
" TOGGLE| " D_TOGGLE " | " // 2
" ALL " ; // 255
const uint8_t kZ_Numbers [ ] PROGMEM = { 0 , 0 , 0 , 0 , 0 ,
1 , 1 , 1 , 1 , 1 ,
2 , 2 ,
255 } ;
2019-12-15 15:02:41 +00:00
// Convert an alias like "On" to the corresponding number
2019-11-03 11:41:44 +00:00
uint32_t ZigbeeAliasOrNumber ( const char * state_text ) {
char command [ 16 ] ;
int state_number = GetCommandCode ( command , sizeof ( command ) , state_text , kZ_Alias ) ;
if ( state_number > = 0 ) {
// found an alias, get its value
return pgm_read_byte ( kZ_Numbers + state_number ) ;
} else {
// no alias found, convert it as number
return strtoul ( state_text , nullptr , 0 ) ;
}
}
2021-01-25 15:02:56 +00:00
/*********************************************************************************************\
*
* Send Zigbee commands
*
\ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
2019-10-20 15:43:50 +01:00
# endif // USE_ZIGBEE