Tasmota/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_6_1_greenpow...

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