mirror of https://github.com/arendst/Tasmota.git
337 lines
16 KiB
C++
337 lines
16 KiB
C++
/*
|
|
xdrv_23_zigbee_8_1_greenpower.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
|
|
|
|
bool convertGPDF_data(class Z_attribute_list &attr_list, uint16_t shortaddr, uint8_t gpcmd, bool wasbroadcast, const SBuffer &payload, size_t payload_start, size_t payload_len) {
|
|
bool parsed = false;
|
|
// register the raw command in synthetic cluster 0xF021 corresponding to the GPDF command
|
|
SBuffer gppayload = payload.subBuffer(payload_start, payload_len);
|
|
convertClusterSpecific(attr_list, 0xF021, gpcmd, false, shortaddr, 0xF2, gppayload);
|
|
|
|
uint16_t cluster = 0xFFFF;
|
|
uint8_t cmd = 0xFF;
|
|
SBuffer arg(8);
|
|
// arguments pre-munched
|
|
uint8_t arg8 = (payload_len > 0) ? payload.get8(payload_start) : 0xFF;
|
|
uint16_t arg16 = (payload_len > 1) ? payload.get16(payload_start) : 0xFFFF;
|
|
uint16_t arg16_1 = (payload_len > 2) ? payload.get16(payload_start + 1) : 0xFFFF; // moved by 1 bytes
|
|
uint32_t arg32 = (payload_len > 3) ? payload.get32(payload_start) : 0xFFFFFFFF;
|
|
switch (gpcmd) {
|
|
case 0x00: cluster = 0x0003; cmd = 0x00; arg.add16(0x003C); break; // Identify - Identify - 0x003C
|
|
case 0x10 ... 0x1F: cluster = 0x0005; cmd = 0x01; arg.add8(gpcmd - 0x10); break; // Scenes - View Scene
|
|
case 0x20: cluster = 0x0006; cmd = 0x00; break; // OnOff - Off
|
|
case 0x21: cluster = 0x0006; cmd = 0x01; break; // OnOff - On
|
|
case 0x22: cluster = 0x0006; cmd = 0x02; break; // OnOff - Toggle
|
|
case 0x23: break; // Release
|
|
case 0x30: cluster = 0x0008; cmd = 0x01; arg.add8(0x00); arg.add8(arg8); break; // Level Control - Move - Up, rate=arg8
|
|
case 0x31: cluster = 0x0008; cmd = 0x01; arg.add8(0x01); arg.add8(arg8); break; // Level Control - Move - Down, rate=arg8
|
|
case 0x32: cluster = 0x0008; cmd = 0x02; arg.add8(0x00); arg.add8(arg8); arg.add16(arg16); break; // Level Control - Step - Up, step size=arg8, rate=arg16 (opt)
|
|
case 0x33: cluster = 0x0008; cmd = 0x02; arg.add8(0x01); arg.add8(arg8); arg.add16(arg16); break; // Level Control - Step - Down, step size=arg8, rate=arg16 (opt)
|
|
case 0x34: cluster = 0x0008; cmd = 0x03; break; // Level Control - Stop
|
|
case 0x35: cluster = 0x0008; cmd = 0x05; arg.add8(0x00); arg.add8(arg8); break; // Level Control - Move - Up, rate=arg8
|
|
case 0x36: cluster = 0x0008; cmd = 0x05; arg.add8(0x01); arg.add8(arg8); break; // Level Control - Move - Down, rate=arg8
|
|
case 0x37: cluster = 0x0008; cmd = 0x06; arg.add8(0x00); arg.add8(arg8); arg.add16(arg16_1); break; // Level Control - Step - Up, step size=arg8, rate=arg16 (opt)
|
|
case 0x38: cluster = 0x0008; cmd = 0x06; arg.add8(0x01); arg.add8(arg8); arg.add16(arg16_1); break; // Level Control - Step - Down, step size=arg8, rate=arg16 (opt)
|
|
case 0x40: cluster = 0x0300; cmd = 0x01; arg.add16(0x1900); break; // Color Control - Move Hue - Stop 25 steps
|
|
case 0x41: cluster = 0x0300; cmd = 0x01; arg.add8(0x01); arg.add8(arg8); break; // Color Control - Move Hue - Up, rate=arg8
|
|
case 0x42: cluster = 0x0300; cmd = 0x01; arg.add8(0x03); arg.add8(arg8); break; // Color Control - Move Hue - Down, rate=arg8
|
|
case 0x43: cluster = 0x0300; cmd = 0x02; arg.add8(0x01); arg.add8(arg8); arg.add8(arg16_1); break; // Color Control - Step Hue - Up - step size/rate
|
|
case 0x44: cluster = 0x0300; cmd = 0x02; arg.add8(0x03); arg.add8(arg8); arg.add8(arg16_1); break; // Color Control - Step Hue - Down - step size/rate
|
|
case 0x45: cluster = 0x0300; cmd = 0x04; arg.add16(0x1900); break; // Color Control - Move Sat - Stop 25 steps
|
|
case 0x46: cluster = 0x0300; cmd = 0x04; arg.add8(0x01); arg.add8(arg8); break; // Color Control - Move Sat - Up, rate=arg8
|
|
case 0x47: cluster = 0x0300; cmd = 0x04; arg.add8(0x03); arg.add8(arg8); break; // Color Control - Move Sat - Down, rate=arg8
|
|
case 0x48: cluster = 0x0300; cmd = 0x05; arg.add8(0x01); arg.add8(arg8); arg.add8(arg16_1); break; // Color Control - Step Sat - Up - step size/rate
|
|
case 0x49: cluster = 0x0300; cmd = 0x05; arg.add8(0x03); arg.add8(arg8); arg.add8(arg16_1); break; // Color Control - Step Sat - Down - step size/rate
|
|
case 0x4A: cluster = 0x0300; cmd = 0x08; arg.add32(arg32); break; // Color Control - Move Color - RateX/RateY
|
|
case 0x4B: cluster = 0x0300; cmd = 0x09; arg.addBuffer(payload); break; // Color Control - Step Color - copy all
|
|
case 0x50: cluster = 0x0101; cmd = 0x00; break; // Door Lock - Lock Door
|
|
case 0x51: cluster = 0x0101; cmd = 0x01; break; // Door Lock - Unlock Door
|
|
default: break;
|
|
}
|
|
|
|
if (cluster != 0xFFFF) {
|
|
convertClusterSpecific(attr_list, cluster, cmd, false, shortaddr, 0xF2, arg);
|
|
parsed = true;
|
|
}
|
|
return parsed;
|
|
}
|
|
|
|
|
|
// Handle GPDF Commissioning 0xE0 command
|
|
bool convertGPDF_Commissioning(class Z_attribute_list &attr_list, uint16_t shortaddr, bool wasbroadcast, const SBuffer &payload, size_t payload_start, size_t payload_len) {
|
|
uint32_t idx_offset = payload_start; // offset compared to minimal packet
|
|
uint8_t gpd_device_id = payload.get8(idx_offset++); // type of device
|
|
|
|
uint8_t gpd_options = payload.get8(idx_offset++);
|
|
bool gpd_mac_seq_number = (gpd_options & 0x01);
|
|
bool gpd_rx_on = (gpd_options & 0x02); // is the device capable of receiving data (generally not)
|
|
bool gpd_app_info_present = (gpd_options & 0x04); // app info extensions at the end of the packet
|
|
bool gpd_panid_request = (gpd_options & 0x10);
|
|
bool gpd_sec_key_request = (gpd_options & 0x20);
|
|
bool gpd_fixed_location = (gpd_options & 0x40);
|
|
bool gpd_options_has_ext = (gpd_options & 0x80);
|
|
|
|
uint8_t gpd_options_ext = 0;
|
|
if (gpd_options_has_ext) {
|
|
gpd_options_ext = payload.get8(idx_offset++);
|
|
}
|
|
uint8_t gpd_sec_level_capa = (gpd_options_ext & 0x03);
|
|
uint8_t gpd_key_type = (gpd_options_ext >> 2) & 0x07;
|
|
uint8_t gpd_key_present = (gpd_options_ext & 0x20);
|
|
uint8_t gpd_key_encryption = (gpd_options_ext & 0x40);
|
|
uint8_t gpd_out_counter_present = (gpd_options_ext & 0x80);
|
|
|
|
uint64_t gpd_key_low = 0;
|
|
uint64_t gpd_key_high = 0;
|
|
if (gpd_key_present) {
|
|
gpd_key_low = payload.get64(idx_offset);
|
|
gpd_key_high = payload.get64(idx_offset + 8);
|
|
idx_offset += 16;
|
|
}
|
|
uint32_t gpd_key_mic = 0;
|
|
if (gpd_key_present && gpd_key_encryption) {
|
|
gpd_key_mic = payload.get32(idx_offset);
|
|
idx_offset += 4;
|
|
}
|
|
uint32_t gpd_out_counter = 0;
|
|
if (gpd_out_counter_present) {
|
|
gpd_out_counter = payload.get32(idx_offset);
|
|
idx_offset += 4;
|
|
}
|
|
|
|
Response_P(PSTR("{\"ZbGPDFCommissioningReceived\":{"
|
|
"\"srcaddr\":\"0x%04X\"," "\"wasbroadcast\":%d,"
|
|
"\"deviceid\":\"0x%02X\"," "\"options\":\"0x%02X\""),
|
|
shortaddr, wasbroadcast,
|
|
gpd_device_id, gpd_options
|
|
);
|
|
|
|
if (gpd_options_has_ext) {
|
|
ResponseAppend_P(PSTR(",\"optionsext\":\"0x%02X\""), gpd_options_ext);
|
|
}
|
|
if (gpd_key_present) {
|
|
ResponseAppend_P(PSTR(",\"securitykey\":\"0x%_X%_X\""), &gpd_key_high, &gpd_key_low);
|
|
}
|
|
if (gpd_key_present && gpd_key_encryption) {
|
|
ResponseAppend_P(PSTR(",\"keymic\":\"0x%08X\""), gpd_key_mic);
|
|
}
|
|
if (gpd_out_counter_present) {
|
|
ResponseAppend_P(PSTR(",\"outcounter\":\"0x%08X\""), gpd_out_counter);
|
|
}
|
|
|
|
// App info extension for Commissioning
|
|
if (gpd_app_info_present && idx_offset < payload_len + payload_start) {
|
|
uint8_t gp_app_info = payload.get8(idx_offset++);
|
|
bool gp_app_manuf_present = (gp_app_info & 0x01);
|
|
bool gp_app_modelid_present = (gp_app_info & 0x02);
|
|
bool gp_app_cmd_list_present = (gp_app_info & 0x04);
|
|
bool gp_app_cluster_reports_present = (gp_app_info & 0x08);
|
|
|
|
if (gp_app_manuf_present) {
|
|
uint16_t gp_app_manuf_id = payload.get8(idx_offset);
|
|
idx_offset += 2;
|
|
ResponseAppend_P(PSTR(",\"manufid\":\"0x%04X\""), gp_app_manuf_id);
|
|
}
|
|
|
|
if (gp_app_modelid_present) {
|
|
uint16_t gp_app_model_id = payload.get8(idx_offset);
|
|
idx_offset += 2;
|
|
ResponseAppend_P(PSTR(",\"modelid\":\"0x%04X\""), gp_app_model_id);
|
|
}
|
|
|
|
if (gp_app_cmd_list_present) {
|
|
uint8_t gpid_len = payload.get8(idx_offset++); // number of entries
|
|
JsonGeneratorArray gpdi_list;
|
|
for (uint32_t i = 0; i < gpid_len; i++) {
|
|
if (idx_offset >= payload_len + payload_start) { break; } // end of payload
|
|
gpdi_list.add((uint32_t)payload.get8(idx_offset++));
|
|
}
|
|
ResponseAppend_P(PSTR(",\"commandid\":%s"), gpdi_list.toString().c_str());
|
|
}
|
|
|
|
if (gp_app_cluster_reports_present) {
|
|
uint8_t clust_report_len = payload.get8(idx_offset++); // number of entries
|
|
JsonGeneratorArray gpdi_list;
|
|
for (uint32_t i = 0; i < clust_report_len; i++) {
|
|
if (idx_offset >= payload_len + payload_start) { break; } // end of payload
|
|
gpdi_list.add((uint32_t)payload.get16(idx_offset));
|
|
idx_offset += 2;
|
|
}
|
|
ResponseAppend_P(PSTR(",\"clusterreports\":%s"), gpdi_list.toString().c_str());
|
|
}
|
|
}
|
|
|
|
ResponseAppend_P(PSTR("}}"));
|
|
|
|
if (Settings->flag3.tuya_serial_mqtt_publish) {
|
|
MqttPublishPrefixTopicRulesProcess_P(TELE, PSTR(D_RSLT_SENSOR));
|
|
} else {
|
|
AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_ZIGBEE "%s"), ResponseData());
|
|
}
|
|
// TODO publish to Berry
|
|
|
|
return true;
|
|
}
|
|
|
|
// Handle GPDF Channel Request 0xE3 command
|
|
bool convertGPDF_ChannelRequest(class Z_attribute_list &attr_list, uint16_t shortaddr, bool wasbroadcast, const SBuffer &payload, size_t payload_start, size_t payload_len) {
|
|
uint32_t idx_offset = payload_start; // offset compared to minimal packet
|
|
uint8_t gpd_channel_toggling = payload.get8(idx_offset++);
|
|
|
|
uint8_t gpd_next_channel = gpd_channel_toggling & 0x0F;
|
|
uint8_t gpd_second_next_channel = (gpd_channel_toggling >> 4) & 0x0F;
|
|
|
|
Response_P(PSTR("{\"ZbGPDFChannelRequest\":{"
|
|
"\"nextchannel\":%i," "\"secondnextchannel\":%i,"
|
|
"}}"),
|
|
11 + gpd_next_channel, 11 + gpd_second_next_channel
|
|
);
|
|
|
|
if (Settings->flag3.tuya_serial_mqtt_publish) {
|
|
MqttPublishPrefixTopicRulesProcess_P(TELE, PSTR(D_RSLT_SENSOR));
|
|
} else {
|
|
AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_ZIGBEE "%s"), ResponseData());
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Parse a GPDF (Green Power Data Frame) tunnelled by a Green Power Proxy
|
|
//
|
|
// Returns `true` if parsed successfully
|
|
bool convertGPDF(class Z_attribute_list &attr_list, uint16_t shortaddr, uint8_t gpcmd, bool wasbroadcast, const SBuffer &payload, size_t payload_start, size_t payload_len) {
|
|
// dispatch depending on the GP command
|
|
switch (gpcmd) {
|
|
case 0x00:
|
|
case 0x10 ... 0x6F:
|
|
return convertGPDF_data(attr_list, shortaddr, gpcmd, wasbroadcast, payload, payload_start, payload_len);
|
|
case ZGP_COMMISSIONING:
|
|
return convertGPDF_Commissioning(attr_list, shortaddr, wasbroadcast, payload, payload_start, payload_len);
|
|
case ZGP_CHANNEL_REQUEST:
|
|
return convertGPDF_ChannelRequest(attr_list, shortaddr, wasbroadcast, payload, payload_start, payload_len);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool convertGPCommissioningNotification(class Z_attribute_list &attr_list, uint16_t shortaddr, bool wasbroadcast, const SBuffer &payload) {
|
|
uint32_t idx_offset = 0; // offset due to address format: 0 if 4 bytes, 4 if 8 bytes
|
|
|
|
uint16_t gp_options = payload.get16(0);
|
|
uint8_t gp_app_id = gp_options & 0x07;
|
|
bool gp_rx_after_tx = (gp_options & 0x08);
|
|
uint8_t gp_security_level = (gp_options >> 4) & 0x03;
|
|
uint8_t gp_security_type = (gp_options >> 6) & 0x07;
|
|
bool gp_security_failed = (gp_options >> 9) & 0x01;
|
|
bool gp_bidir_cap = (gp_options >> 10) & 0x01;
|
|
bool gp_proxy_info_present = (gp_options >> 11) & 0x01;
|
|
|
|
|
|
// #define ZBEE_ZCL_GP_COMMISSIONING_NOTIFICATION_OPTION_APP_ID (7<<0)
|
|
// #define ZBEE_ZCL_GP_COMMISSIONING_NOTIFICATION_OPTION_RX_AFTER_TX (1<<3)
|
|
// #define ZBEE_ZCL_GP_COMMISSIONING_NOTIFICATION_OPTION_SECUR_LEVEL (3<<4)
|
|
// #define ZBEE_ZCL_GP_COMMISSIONING_NOTIFICATION_OPTION_SECUR_KEY_TYPE (7<<6)
|
|
// #define ZBEE_ZCL_GP_COMMISSIONING_NOTIFICATION_OPTION_SECUR_FAILED (1<<9)
|
|
// #define ZBEE_ZCL_GP_COMMISSIONING_NOTIFICATION_OPTION_BIDIR_CAP (1<<10)
|
|
// #define ZBEE_ZCL_GP_COMMISSIONING_NOTIFICATION_OPTION_PROXY_INFO_PRESENT (1<<11)
|
|
|
|
uint64_t gp_pgd_id;
|
|
if (gp_app_id == 0x02) {
|
|
idx_offset = 4;
|
|
gp_pgd_id = payload.get64(2);
|
|
} else if (gp_app_id == 0x00) {
|
|
gp_pgd_id = payload.get32(2);
|
|
} else {
|
|
AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_ZIGBEE "Invalild GP app_id=%i"), gp_app_id);
|
|
return false;
|
|
}
|
|
uint32_t gp_frame_counter = payload.get32(idx_offset + 6);
|
|
uint8_t gp_cmd = payload.get8(idx_offset + 10);
|
|
|
|
uint16_t gpp_shortaddr = 0xFFFF; // shortaddress of the proxy
|
|
uint8_t gpp_distance = 0xFF; // actual LQI from the GP device to the proxy
|
|
|
|
uint32_t gp_payload_start = idx_offset + 12;
|
|
uint32_t gp_payload_len = payload.get8(gp_payload_start - 1);
|
|
|
|
idx_offset = gp_payload_start + gp_payload_len; // reset to after payload
|
|
if (gp_proxy_info_present) {
|
|
gpp_shortaddr = payload.get16(idx_offset);
|
|
gpp_distance = payload.get8(idx_offset + 2);
|
|
idx_offset += 3;
|
|
}
|
|
uint32_t gp_mic = 0x00000000;
|
|
if (gp_security_failed) {
|
|
gp_mic = payload.get32(idx_offset);
|
|
idx_offset += 4;
|
|
}
|
|
|
|
Response_P(PSTR("{\"" D_JSON_ZIGBEEGP_RECEIVED "\":{"
|
|
"\"srcaddr\":\"0x%04X\"," "\"wasbroadcast\":%d," "\"zclcmd\":\"GP_COMMISSIONING_NOTIFICATION\","
|
|
"\"gpoptions\":\"0x%04X\","
|
|
"\"appid\":%i," "\"rxaftertx\":%i," "\"seclevel\":%i," "\"sectype\":%i," "\"secfailed\":%i,"
|
|
"\"bidircap\":%i," "\"proxyinfo\":%i,"
|
|
"\"gpsrcid\":\"0x%_X\"," "\"gpfrmcounter\":\"0x%04X\","
|
|
"\"gpproxy\":\"0x%04X\"," "\"gpplqi\":%i," "\"gpmic\":\"0x%08X\","
|
|
"\"gpcmd\":\"0x%02X\","
|
|
"\"gppayload\":\"%*_H\"}}"),
|
|
shortaddr, wasbroadcast,
|
|
gp_options,
|
|
gp_app_id, gp_rx_after_tx, gp_security_level, gp_security_type, gp_security_failed,
|
|
gp_bidir_cap, gp_proxy_info_present,
|
|
&gp_pgd_id, gp_frame_counter,
|
|
gpp_shortaddr, gpp_distance, gp_mic,
|
|
gp_cmd,
|
|
gp_payload_len, payload.buf(gp_payload_start)
|
|
);
|
|
|
|
if (Settings->flag3.tuya_serial_mqtt_publish) {
|
|
MqttPublishPrefixTopicRulesProcess_P(TELE, PSTR(D_RSLT_SENSOR));
|
|
} else {
|
|
AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_ZIGBEE "%s"), ResponseData());
|
|
}
|
|
|
|
return convertGPDF(attr_list, shortaddr, gp_cmd, wasbroadcast, payload, gp_payload_start, gp_payload_len);
|
|
}
|
|
|
|
|
|
|
|
// Parse Green Power specific commands
|
|
// Returns: true if parsed successfully, false if revenrt to generix parsing
|
|
bool convertGPSpecific(class Z_attribute_list &attr_list, uint8_t cmd, bool direction, uint16_t shortaddr, bool wasbroadcast, const SBuffer &payload) {
|
|
if (!direction) {
|
|
// server commands
|
|
switch (cmd) {
|
|
case ZGP_COMMISSIONING_NOTIFICATION:
|
|
return convertGPCommissioningNotification(attr_list, shortaddr, wasbroadcast, payload);
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
} else {
|
|
// client commands
|
|
switch (cmd) {
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
#endif // USE_ZIGBEE
|