mirror of https://github.com/arendst/Tasmota.git
631 lines
29 KiB
C++
631 lines
29 KiB
C++
/*
|
|
xdrv_23_zigbee_converters.ino - zigbee support for Tasmota
|
|
|
|
Copyright (C) 2021 Theo Arends and Stephan Hadinger
|
|
|
|
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
|
|
|
|
/*********************************************************************************************\
|
|
* ZCL Command Structures
|
|
\*********************************************************************************************/
|
|
|
|
typedef struct Z_CommandConverter {
|
|
uint16_t tasmota_cmd_offset;
|
|
uint16_t cluster;
|
|
uint8_t cmd; // normally 8 bits, 0xFF means it's a parameter
|
|
uint8_t direction; // direction of the command. 0x01 client->server, 0x02 server->client, 0x03 both, 0x80 requires custom decoding
|
|
uint16_t param_offset;
|
|
} Z_CommandConverter;
|
|
|
|
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;
|
|
|
|
// 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
|
|
// - 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.
|
|
const Z_CommandConverter Z_Commands[] PROGMEM = {
|
|
// Identify cluster
|
|
{ Z_(Identify), 0x0003, 0x00, 0x01, Z_(xxxx) }, // Identify device, time in seconds
|
|
{ Z_(IdentifyQuery), 0x0003, 0x01, 0x01, Z_() }, // Identify Query (no param)
|
|
// Group adress commands
|
|
{ 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_(01xxxx) }, // 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
|
|
// Scenes
|
|
//{ "AddScene", 0x0005, 0x00, 0x01, "xxxxyy0100" },
|
|
{ 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) },
|
|
// Light & Shutter commands
|
|
{ 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
|
|
{ Z_(LidlPower), 0x0006, 0xFD, 0x01, Z_(xx) }, // Lidl specific encoding
|
|
{ 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_(00190200) }, // Step up by 10%, 0.2 secs
|
|
{ Z_(DimmerDown), 0x0008, 0x06, 0x01, Z_(01190200) }, // 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)
|
|
{ Z_(RGB), 0x0300, 0xF0, 0x81, Z_(_) }, // synthetic commands converting RGB to XY
|
|
{ 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_() },
|
|
// Blitzwolf PIR
|
|
{ Z_(Occupancy), 0xEF00, 0x01, 0x82, Z_()}, // Specific decoder for Blitzwolf PIR, empty name means special treatment
|
|
// Decoders only - normally not used to send, and names may be masked by previous definitions
|
|
{ Z_(Dimmer), 0x0008, 0x00, 0x01, Z_(xx) },
|
|
{ Z_(DimmerMove), 0x0008, 0x01, 0x01, Z_(xx0A) },
|
|
{ Z_(DimmerStepUp), 0x0008, 0x02, 0x01, Z_(00xx0A00) },
|
|
{ Z_(DimmerStepDown), 0x0008, 0x02, 0x01, Z_(01xx0A00) },
|
|
{ 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_(01xx0A00) },
|
|
{ Z_(HueStepDown), 0x0300, 0x02, 0x01, Z_(03xx0A00) },
|
|
{ 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_(01xxxx000000000000) },
|
|
{ Z_(ColorTempMoveDown),0x0300, 0x4B, 0x01, Z_(03xxxx000000000000) },
|
|
{ Z_(ColorTempMoveStop),0x0300, 0x4B, 0x01, Z_(00xxxx000000000000) },
|
|
{ Z_(ColorTempMove), 0x0300, 0x4B, 0x01, Z_(xxyyyy000000000000) },
|
|
{ Z_(ColorTempStepUp), 0x0300, 0x4C, 0x01, Z_(01xxxx0A0000000000) },
|
|
{ Z_(ColorTempStepDown),0x0300, 0x4C, 0x01, Z_(03xxxx0A0000000000) },
|
|
{ Z_(ColorTempStep), 0x0300, 0x4C, 0x01, Z_(xxyyyy0A0000000000) }, //xx = 0x01 up, 0x03 down, yyyy = step
|
|
// Tradfri
|
|
{ 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_() },
|
|
// Response for Indetify cluster
|
|
{ Z_(IdentifyQuery), 0x0003, 0x00, 0x02, Z_(xxxx) }, // timeout in seconds
|
|
// IAS - Intruder Alarm System + leak/fire detection
|
|
{ Z_(ZoneStatusChange),0x0500, 0x00, 0x82, Z_(xxxxyyzz) }, // xxxx = zone status, yy = extended status, zz = zone id, Delay is ignored
|
|
// responses for Group cluster commands
|
|
{ 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
|
|
// responses for Scene cluster commands
|
|
{ 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
|
|
// Tuya - Moes specific
|
|
{ Z_(), 0xEF00, 0xFF, 0x83, Z_() }, // capture any command in 0xEF00 cluster
|
|
// Terncy specific
|
|
{ Z_(), 0xFCCC, 0x00, 0x82, Z_(xxyy) }, // Terncy button (multi-)press
|
|
};
|
|
|
|
/*********************************************************************************************\
|
|
* ZCL Read Light status based on cluster number
|
|
\*********************************************************************************************/
|
|
#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
|
|
const uint8_t CLUSTER_0300[] = { ZLE(0x0000), ZLE(0x0001), ZLE(0x0003), ZLE(0x0004), ZLE(0x0007), ZLE(0x0008) }; // Hue, Sat, X, Y, CT, ColorMode
|
|
|
|
// This callback is registered after a cluster specific command and sends a read command for the same cluster
|
|
void Z_ReadAttrCallback(uint16_t shortaddr, uint16_t groupaddr, uint16_t cluster, uint8_t endpoint, uint32_t value) {
|
|
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) {
|
|
if (groupaddr) {
|
|
shortaddr = BAD_SHORTADDR; // if group address, don't send to device
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
|
|
// 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
|
|
void Z_Unreachable(uint16_t shortaddr, uint16_t groupaddr, uint16_t cluster, uint8_t endpoint, uint32_t value) {
|
|
if (BAD_SHORTADDR != shortaddr) {
|
|
zigbee_devices.getShortAddr(shortaddr).setReachable(false); // mark device as reachable
|
|
Z_attribute_list attr_list;
|
|
attr_list.addAttributePMEM(PSTR("Reachable")).setBool(false); // "Reachable":false
|
|
// Z_postProcessAttributes(shortaddr, endpoint, attr_list); // make sure all is updated accordingly
|
|
zigbee_devices.jsonPublishNow(shortaddr, attr_list);
|
|
}
|
|
}
|
|
|
|
// 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
|
|
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;
|
|
uint8_t conv_direction;
|
|
Z_XYZ_Var xyz;
|
|
|
|
//AddLog_P(LOG_LEVEL_INFO, PSTR(">>> len = %d - %02X%02X%02X"), payload.len(), payload.get8(0), payload.get8(1), payload.get8(2));
|
|
for (uint32_t i = 0; i < sizeof(Z_Commands) / sizeof(Z_Commands[0]); i++) {
|
|
const Z_CommandConverter *conv = &Z_Commands[i];
|
|
uint16_t conv_cluster = pgm_read_word(&conv->cluster);
|
|
if (conv_cluster == cluster) {
|
|
// cluster match
|
|
uint8_t conv_cmd = pgm_read_byte(&conv->cmd);
|
|
conv_direction = pgm_read_byte(&conv->direction);
|
|
if ((0xFF == conv_cmd) || (cmd == conv_cmd)) {
|
|
// cmd match
|
|
if ((direction && (conv_direction & 0x02)) || (!direction && (conv_direction & 0x01))) {
|
|
// 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'
|
|
const char * p = Z_strings + pgm_read_word(&conv->param_offset);
|
|
//AddLog_P(LOG_LEVEL_INFO, PSTR(">>>++1 param = %s"), p);
|
|
bool match = true;
|
|
for (uint8_t i = 0; i < payload.len(); i++) {
|
|
const char c1 = pgm_read_byte(p);
|
|
// const char c2 = pgm_read_byte(p+1);
|
|
//AddLog_P(LOG_LEVEL_INFO, PSTR(">>>++2 c1 = %c, c2 = %c"), c1, c2);
|
|
if ((0x00 == c1) || isXYZ(c1)) {
|
|
break;
|
|
}
|
|
const char * p2 = p;
|
|
uint32_t nextbyte = parseHex_P(&p2, 2);
|
|
//AddLog_P(LOG_LEVEL_INFO, PSTR(">>>++3 parseHex_P = %02X"), nextbyte);
|
|
if (nextbyte != payload.get8(i)) {
|
|
match = false;
|
|
break;
|
|
}
|
|
p += 2;
|
|
}
|
|
if (match) {
|
|
command_name = Z_strings + pgm_read_word(&conv->tasmota_cmd_offset);
|
|
parseXYZ(Z_strings + pgm_read_word(&conv->param_offset), payload, &xyz);
|
|
if (0xFF == conv_cmd) {
|
|
// 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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
Z_attribute & attr_raw = attr_list.addAttribute(attrid_str);
|
|
attr_raw.setBuf(payload, 0, payload.len());
|
|
|
|
if (command_name) {
|
|
// Now try to transform into a human readable format
|
|
// if (direction & 0x80) then specific transform
|
|
if (conv_direction & 0x80) {
|
|
uint32_t cccc00mm = (cluster << 16) | cmd; // format = cccc00mm, cccc = cluster, mm = command
|
|
// IAS
|
|
switch (cccc00mm) {
|
|
case 0x05000000: // "ZoneStatusChange"
|
|
{
|
|
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);
|
|
if (&alarm != nullptr) {
|
|
alarm.convertZoneStatus(attr_list, xyz.x);
|
|
}
|
|
}
|
|
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);
|
|
{
|
|
|
|
JsonGeneratorArray group_list;
|
|
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());
|
|
}
|
|
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());
|
|
attr_list.addAttributePMEM(PSTR("GroupId")).setUInt(xyz.y);
|
|
attr_list.addAttributePMEM(PSTR("SceneId")).setUInt(xyz.z);
|
|
if (0x00050001 == cccc00mm) { // ViewScene specific
|
|
attr_list.addAttributePMEM(PSTR("ScenePayload")).setBuf(payload, 4, payload.len()-4); // remove first 4 bytes
|
|
}
|
|
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());
|
|
attr_list.addAttributePMEM(PSTR("GroupId")).setUInt(xyz.y);
|
|
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());
|
|
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
|
|
break;
|
|
case 0x00060040: // Power Off With Effect
|
|
attr_list.addAttributePMEM(PSTR("Power")).setUInt(0);
|
|
attr_list.addAttributePMEM(PSTR("PowerEffect")).setUInt(xyz.x);
|
|
attr_list.addAttributePMEM(PSTR("PowerEffectVariant")).setUInt(xyz.y);
|
|
break;
|
|
case 0x00060041: // Power On With Recall Global Scene
|
|
attr_list.addAttributePMEM(PSTR("Power")).setUInt(1);
|
|
attr_list.addAttributePMEM(PSTR("PowerRecallGlobalScene")).setBool(true);
|
|
break;
|
|
case 0x00060042: // Power On With Timed Off Command
|
|
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);
|
|
break;
|
|
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;
|
|
case 0xFCCC0000: // Terncy button (multi-)press
|
|
attr_list.addAttributePMEM(PSTR("TerncyPress")).setUInt(xyz.y);
|
|
attr_list.addAttributePMEM(PSTR("TerncyCount")).setUInt(xyz.x);
|
|
break;
|
|
}
|
|
} else { // general case
|
|
// do we send command with endpoint suffix
|
|
char command_suffix[4] = { 0x00 }; // empty string by default
|
|
// if SO101 and multiple endpoints, append endpoint number
|
|
if (Settings.flag4.zb_index_ep) {
|
|
if (zigbee_devices.getShortAddr(shortaddr).countEndpoints() > 0) {
|
|
snprintf_P(command_suffix, sizeof(command_suffix), PSTR("%d"), srcendpoint);
|
|
}
|
|
}
|
|
if (0 == xyz.x_type) {
|
|
attr_list.addAttribute(command_name, command_suffix).setBool(true);
|
|
} else if (0 == xyz.y_type) {
|
|
attr_list.addAttribute(command_name, command_suffix).setUInt(xyz.x);
|
|
} else {
|
|
// multiple answers, create an array
|
|
JsonGeneratorArray arr;
|
|
arr.add(xyz.x);
|
|
arr.add(xyz.y);
|
|
if (xyz.z_type) {
|
|
arr.add(xyz.z);
|
|
}
|
|
attr_list.addAttribute(command_name, command_suffix).setStrRaw(arr.toString().c_str());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/*********************************************************************************************\
|
|
*
|
|
* Tuya Zigbee specific protocol
|
|
*
|
|
\*********************************************************************************************/
|
|
// Parse a sinlge attribute value and convert to human readable JSON attribute value
|
|
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)
|
|
{
|
|
char str[len+1];
|
|
strncpy(str, buf.charptr(i), len);
|
|
str[len] = 0x00;
|
|
attr.setStr(str);
|
|
}
|
|
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));
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// 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);
|
|
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);
|
|
|
|
if ((1 == cmd) || (2 == cmd)) { // attribute report or attribute response
|
|
// create a synthetic attribute with id 'dp'
|
|
Z_attribute & attr = attr_list.addAttribute(cluster, (attr_type << 8) | dp);
|
|
parseSingleTuyaAttribute(attr, buf, 6, len, attr_type);
|
|
return true; // true = remove the original Tuya attribute
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/*********************************************************************************************\
|
|
*
|
|
* Find commands
|
|
*
|
|
\*********************************************************************************************/
|
|
// Find the command details by command name
|
|
// Only take commands outgoing, i.e. direction == 0
|
|
// If not found:
|
|
// - returns nullptr
|
|
// - return PROGMEM string
|
|
const char * zigbeeFindCommand(const char *command, uint16_t *cluster, uint16_t *cmd) {
|
|
if (nullptr == command) { return nullptr; }
|
|
for (uint32_t i = 0; i < sizeof(Z_Commands) / sizeof(Z_Commands[0]); i++) {
|
|
const Z_CommandConverter *conv = &Z_Commands[i];
|
|
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);
|
|
// conv_direction must be client (coord) -> server (device), we can only send commands to end devices
|
|
if ((conv_direction & 0x01) && (0 == strcasecmp_P(command, Z_strings + pgm_read_word(&conv->tasmota_cmd_offset)))) {
|
|
*cluster = conv_cluster;
|
|
*cmd = conv_cmd;
|
|
return Z_strings + pgm_read_word(&conv->param_offset);
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
// 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)
|
|
// 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
|
|
uint8_t val = 0;
|
|
switch (pgm_read_byte(p)) {
|
|
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;
|
|
}
|
|
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);
|
|
}
|
|
p += 2;
|
|
}
|
|
}
|
|
|
|
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 };
|
|
|
|
// Convert an alias like "On" to the corresponding number
|
|
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);
|
|
}
|
|
}
|
|
|
|
|
|
/*********************************************************************************************\
|
|
*
|
|
* Send Zigbee commands
|
|
*
|
|
\*********************************************************************************************/
|
|
|
|
#endif // USE_ZIGBEE
|