Merge pull request #9810 from s-hadinger/zigbee_zbinfo

Zigbee command ``ZbInfo`` and prepare support for EEPROM
This commit is contained in:
s-hadinger 2020-11-11 12:28:36 +01:00 committed by GitHub
commit 4824132413
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 470 additions and 297 deletions

View File

@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file.
- Zigbee alarm persistence (#9785)
- Support for EZO PMP sensors by Christopher Tremblay (#9760)
- Commands ``TuyaRGB``, ``TuyaEnum`` and ``TuyaEnumList`` (#9769)
- Zigbee command ``ZbInfo`` and prepare support for EEPROM
### Changed
- Core library from v2.7.4.5 to v2.7.4.7

View File

@ -227,7 +227,9 @@ Eeprom24C512::readBytes
byte remainingBytes = length % EEPROM__RD_BUFFER_SIZE;
word offset = length - remainingBytes;
readBuffer(address + offset, remainingBytes, p_data + offset);
if (remainingBytes > 0) {
readBuffer(address + offset, remainingBytes, p_data + offset);
}
}
/******************************************************************************

View File

@ -547,6 +547,7 @@
#define D_JSON_ZIGBEE_MODELID "ModelId"
#define D_CMND_ZIGBEE_PROBE "Probe"
#define D_CMND_ZIGBEE_FORGET "Forget"
#define D_CMND_ZIGBEE_INFO "Info"
#define D_CMND_ZIGBEE_SAVE "Save"
#define D_CMND_ZIGBEE_LINKQUALITY "LinkQuality"
#define D_CMND_ZIGBEE_CLUSTER "Cluster"

View File

@ -729,7 +729,8 @@
#define USE_ZIGBEE_COALESCE_ATTR_TIMER 350 // timer to coalesce attribute values (in ms)
#define USE_ZIGBEE_MODELID "Tasmota Z2T" // reported "ModelId" (cluster 0000 / attribute 0005)
#define USE_ZIGBEE_MANUFACTURER "Tasmota" // reported "Manufacturer" (cluster 0000 / attribute 0004)
#define USE_ZBBRIDGE_TLS // TLS support for zbbridge
#define USE_ZBBRIDGE_TLS // TLS support for zbbridge
#define USE_ZIGBEE_ZBBRIDGE_EEPROM 0x50 // I2C id for the ZBBridge EEPROM
// -- Other sensors/drivers -----------------------

View File

@ -19,6 +19,10 @@
#ifdef USE_ZIGBEE
#ifdef USE_ZIGBEE_EZSP
#include "Eeprom24C512.h"
#endif // USE_ZIGBEE_EZSP
// contains some definitions for functions used before their declarations
//
@ -69,7 +73,14 @@ const uint8_t ZIGBEE_LABEL_CONFIGURE_EZSP = 53; // main loop
const uint8_t ZIGBEE_LABEL_ABORT = 99; // goto label 99 in case of fatal error
const uint8_t ZIGBEE_LABEL_UNSUPPORTED_VERSION = 98; // Unsupported ZNP version
struct ZigbeeStatus {
class ZigbeeStatus {
public:
ZigbeeStatus()
#ifdef USE_ZIGBEE_EZSP
: eeprom(USE_ZIGBEE_ZBBRIDGE_EEPROM)
#endif // USE_ZIGBEE_EZSP
{}
bool active = true; // is Zigbee active for this device, i.e. GPIOs configured
bool state_machine = false; // the state machine is running
bool state_waiting = false; // the state machine is waiting for external event or timeout
@ -77,6 +88,7 @@ struct ZigbeeStatus {
bool ready = false; // cc2530 initialization is complet, ready to operate
bool init_phase = true; // initialization phase, before accepting zigbee traffic
bool recv_until = false; // ignore all messages until the received frame fully matches
bool eeprom_present = false; // is the ZBBridge EEPROM present?
uint8_t on_error_goto = ZIGBEE_LABEL_ABORT; // on error goto label, 99 default to abort
uint8_t on_timeout_goto = ZIGBEE_LABEL_ABORT; // on timeout goto label, 99 default to abort
@ -89,6 +101,10 @@ struct ZigbeeStatus {
ZB_RecvMsgFunc recv_unexpected = nullptr; // function called when unexpected message is received
uint32_t permit_end_time = 0; // timestamp when permit join ends
#ifdef USE_ZIGBEE_EZSP
Eeprom24C512 eeprom; // takes only 1 bytes of RAM
#endif // USE_ZIGBEE_EZSP
};
struct ZigbeeStatus zigbee;
SBuffer *zigbee_buffer = nullptr;

View File

@ -53,7 +53,7 @@ const uint8_t Z_Data_Type_char[] PROGMEM = {
'\0', // 0x0C
'\0', // 0x0D
'\0', // 0x0E
'E', // 0x05 Z_Data_Type::Z_Ext
'E', // 0x0F Z_Data_Type::Z_Ext
// '_' maps to 0xFF Z_Data_Type::Z_Device
};
@ -63,12 +63,13 @@ class Z_Data_Set;
\*********************************************************************************************/
class Z_Data {
public:
Z_Data(Z_Data_Type type = Z_Data_Type::Z_Unknown, uint8_t endpoint = 0) : _type(type), _endpoint(endpoint), _config(0xF), _power(0) {}
Z_Data(Z_Data_Type type = Z_Data_Type::Z_Unknown, uint8_t endpoint = 0) : _type(type), _endpoint(endpoint), _config(0xF), _align_1(0), _reserved(0) {}
inline Z_Data_Type getType(void) const { return _type; }
inline int8_t getConfig(void) const { return _config; }
inline bool validConfig(void) const { return _config != 0xF; }
inline void setConfig(int8_t config) { _config = config; }
uint8_t getConfigByte(void) const { return ( ((uint8_t)_type) << 4) | ((_config & 0xF) & 0x0F); }
uint8_t getReserved(void) const { return _reserved; }
inline uint8_t getEndpoint(void) const { return _endpoint; }
@ -79,6 +80,7 @@ public:
inline bool update(void) { return false; }
static const Z_Data_Type type = Z_Data_Type::Z_Unknown;
static size_t DataTypeToLength(Z_Data_Type t);
static bool ConfigToZData(const char * config_str, Z_Data_Type * type, uint8_t * ep, uint8_t * config);
static Z_Data_Type CharToDataType(char c);
@ -87,9 +89,10 @@ public:
friend class Z_Data_Set;
protected:
Z_Data_Type _type; // encoded on 4 bits, type of the device
uint8_t _endpoint; // source endpoint, or 0x00 if any endpoint
uint8_t _endpoint; // source endpoint, or 0x00 if any endpoint
uint8_t _config : 4; // encoded on 4 bits, customize behavior
uint8_t _power; // power state if the type supports it
uint8_t _align_1 : 4; // force aligned to bytes, and fill with zero
uint8_t _reserved; // power state if the type supports it
};
Z_Data_Type Z_Data::CharToDataType(char c) {
@ -153,28 +156,20 @@ bool Z_Data::ConfigToZData(const char * config_str, Z_Data_Type * type, uint8_t
class Z_Data_OnOff : public Z_Data {
public:
Z_Data_OnOff(uint8_t endpoint = 0) :
Z_Data(Z_Data_Type::Z_OnOff, endpoint)
{
_config = 1; // at least 1 OnOff
}
Z_Data(Z_Data_Type::Z_OnOff, endpoint),
power(0xFF)
{}
inline bool validPower(uint32_t relay = 0) const { return (_config > relay); } // power is declared
inline bool getPower(uint32_t relay = 0) const { return bitRead(_power, relay); }
void setPower(bool val, uint32_t relay = 0);
void toAttributes(Z_attribute_list & attr_list, Z_Data_Type type) const;
inline bool validPower(void) const { return 0xFF != power; } // power is declared
inline bool getPower(void) const { return validPower() ? (power != 0) : false; } // default to false if undefined
inline void setPower(bool val) { power = val ? 1 : 0; }
static const Z_Data_Type type = Z_Data_Type::Z_OnOff;
// 1 byte
uint8_t power;
};
void Z_Data_OnOff::setPower(bool val, uint32_t relay) {
if (relay < 8) {
if (_config < relay) { _config = relay; } // we update the number of valid relays
bitWrite(_power, relay, val);
}
}
/*********************************************************************************************\
* Device specific: Plug device
\*********************************************************************************************/
@ -430,6 +425,37 @@ public:
// 0x0226 Glass break sensor
// 0x0229 Security repeater*
};
const uint8_t Z_Data_Type_len[] PROGMEM = {
0, // 0x00 Z_Data_Type::Z_Unknown
sizeof(Z_Data_Light), // 0x01 Z_Data_Type::Z_Light
sizeof(Z_Data_Plug), // 0x02 Z_Data_Type::Z_Plug
sizeof(Z_Data_PIR), // 0x03 Z_Data_Type::Z_PIR
sizeof(Z_Data_Alarm), // 0x04 Z_Data_Type::Z_Alarm
sizeof(Z_Data_Thermo), // 0x05 Z_Data_Type::Z_Thermo
sizeof(Z_Data_OnOff), // 0x05 Z_Data_Type::Z_OnOff
0, // 0x06
0, // 0x07
0, // 0x08
0, // 0x09
0, // 0x0A
0, // 0x0B
0, // 0x0C
0, // 0x0D
0, // 0x0E
0, // 0x0F Z_Data_Type::Z_Ext
// '_' maps to 0xFF Z_Data_Type::Z_Device
};
size_t Z_Data::DataTypeToLength(Z_Data_Type t) {
uint32_t tt = (uint32_t) t;
if (tt < ARRAY_SIZE(Z_Data_Type_len)) {
return pgm_read_byte(&Z_Data_Type_len[tt]);
}
return 0;
}
/*********************************************************************************************\
*
* Device specific Linked List
@ -453,7 +479,10 @@ public:
template <class M>
const M & find(uint8_t ep = 0) const;
// check if the point is null, if so create a new object with the right sub-class
// create a new data object from a 4 bytes buffer
Z_Data & createFromBuffer(const SBuffer & buf, uint32_t start, uint32_t len);
// check if the pointer is null, if so create a new object with the right sub-class
template <class M>
M & addIfNull(M & cur, uint8_t ep = 0);
};
@ -489,6 +518,35 @@ Z_Data & Z_Data_Set::getByType(Z_Data_Type type, uint8_t ep) {
}
}
// Instanciate with either:
// (04)04010100 - without data except minimal Z_Data
// (08)04010100.B06DFFFF - with complete data - in this case must not exceed the structure len
//
// Byte 0: type
// Byte 1: endpoint
// Byte 2: config
// Byte 3: Power
Z_Data & Z_Data_Set::createFromBuffer(const SBuffer & buf, uint32_t start, uint32_t len) {
if (len < sizeof(Z_Data)) {
AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "Invalid len (<4) %d"), len);
return *(Z_Data*)nullptr;
}
Z_Data_Type data_type = (Z_Data_Type) buf.get8(start);
uint8_t expected_len = Z_Data::DataTypeToLength(data_type);
uint8_t endpoint = buf.get8(start + 1);
Z_Data & elt = getByType(data_type, endpoint);
if (&elt == nullptr) { return *(Z_Data*)nullptr; }
if (len <= expected_len) {
memcpy(&elt, buf.buf(start), len);
} else {
memcpy(&elt, buf.buf(start), sizeof(Z_Data));
AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "buffer len overflow %d > %d"), len, expected_len);
}
return elt;
}
template <class M>
M & Z_Data_Set::get(uint8_t ep) {
M & m = (M&) find(M::type, ep);
@ -528,11 +586,6 @@ const Z_Data & Z_Data_Set::find(Z_Data_Type type, uint8_t ep) const {
return *(Z_Data*)nullptr;
}
void Z_Data_OnOff::toAttributes(Z_attribute_list & attr_list, Z_Data_Type type) const {
if (validPower()) { attr_list.addAttribute(PSTR("Power")).setUInt(getPower() ? 1 : 0); }
}
/*********************************************************************************************\
* Structures for Rules variables related to the last received message
\*********************************************************************************************/
@ -561,11 +614,13 @@ public:
// New version of device data handling
Z_Data_Set data; // Linkedlist of device data per endpoint
// other status
// other status - device wide data is 8 bytes
// START OF DEVICE WIDE DATA
uint32_t last_seen; // Last seen time (epoch)
uint8_t lqi; // lqi from last message, 0xFF means unknown
uint8_t batterypercent; // battery percentage (0..100), 0xFF means unknwon
// power plug data-
uint32_t last_seen; // Last seen time (epoch)
uint16_t reserved_for_alignment;
// END OF DEVICE WIDE DATA
// Constructor with all defaults
Z_Device(uint16_t _shortaddr = BAD_SHORTADDR, uint64_t _longaddr = 0x00):
@ -736,7 +791,7 @@ public:
// Dump json
static String dumpLightState(const Z_Device & device);
String dumpDevice(uint32_t dump_mode, const Z_Device & device) const;
static String dumpSingleDevice(uint32_t dump_mode, const Z_Device & device);
static String dumpSingleDevice(uint32_t dump_mode, const class Z_Device & device, bool add_device_name = true, bool add_brackets = true);
int32_t deviceRestore(JsonParserObject json);
// Hue support
@ -753,11 +808,13 @@ public:
// Append or clear attributes Json structure
void jsonAppend(uint16_t shortaddr, const Z_attribute_list &attr_list);
static void jsonPublishFlushAttrList(const Z_Device & device, const String & attr_list_string);
void jsonPublishFlush(uint16_t shortaddr); // publish the json message and clear buffer
bool jsonIsConflict(uint16_t shortaddr, const Z_attribute_list &attr_list) const;
void jsonPublishNow(uint16_t shortaddr, Z_attribute_list &attr_list);
// Iterator
inline const LList<Z_Device> & getDevices(void) const { return _devices; }
size_t devicesSize(void) const {
return _devices.length();
}

View File

@ -492,64 +492,71 @@ void Z_Devices::jsonAppend(uint16_t shortaddr, const Z_attribute_list &attr_list
device.attr_list.mergeList(attr_list);
}
//
// internal function to publish device information with respect to all `SetOption`s
//
void Z_Devices::jsonPublishFlushAttrList(const Z_Device & device, const String & attr_list_string) {
const char * fname = zigbee_devices.getFriendlyName(device.shortaddr);
bool use_fname = (Settings.flag4.zigbee_use_names) && (fname); // should we replace shortaddr with friendlyname?
TasmotaGlobal.mqtt_data[0] = 0; // clear string
// Do we prefix with `ZbReceived`?
if (!Settings.flag4.remove_zbreceived) {
Response_P(PSTR("{\"" D_JSON_ZIGBEE_RECEIVED "\":"));
}
// What key do we use, shortaddr or name?
if (use_fname) {
Response_P(PSTR("%s{\"%s\":{"), TasmotaGlobal.mqtt_data, fname);
} else {
Response_P(PSTR("%s{\"0x%04X\":{"), TasmotaGlobal.mqtt_data, device.shortaddr);
}
// Add "Device":"0x...."
ResponseAppend_P(PSTR("\"" D_JSON_ZIGBEE_DEVICE "\":\"0x%04X\","), device.shortaddr);
// Add "Name":"xxx" if name is present
if (fname) {
ResponseAppend_P(PSTR("\"" D_JSON_ZIGBEE_NAME "\":\"%s\","), EscapeJSONString(fname).c_str());
}
// Add all other attributes
ResponseAppend_P(PSTR("%s}}"), attr_list_string.c_str());
if (!Settings.flag4.remove_zbreceived) {
ResponseAppend_P(PSTR("}"));
}
if (Settings.flag4.zigbee_distinct_topics) {
if (Settings.flag4.zb_topic_fname && fname) {
//Clean special characters and check size of friendly name
char stemp[TOPSZ];
strlcpy(stemp, (!strlen(fname)) ? MQTT_TOPIC : fname, sizeof(stemp));
MakeValidMqtt(0, stemp);
//Create topic with Prefix3 and cleaned up friendly name
char frtopic[TOPSZ];
snprintf_P(frtopic, sizeof(frtopic), PSTR("%s/%s/" D_RSLT_SENSOR), SettingsText(SET_MQTTPREFIX3), stemp);
MqttPublish(frtopic, Settings.flag.mqtt_sensor_retain);
} else {
char subtopic[16];
snprintf_P(subtopic, sizeof(subtopic), PSTR("%04X/" D_RSLT_SENSOR), device.shortaddr);
MqttPublishPrefixTopic_P(TELE, subtopic, Settings.flag.mqtt_sensor_retain);
}
} else {
MqttPublishPrefixTopic_P(TELE, PSTR(D_RSLT_SENSOR), Settings.flag.mqtt_sensor_retain);
}
XdrvRulesProcess(); // apply rules
}
void Z_Devices::jsonPublishFlush(uint16_t shortaddr) {
Z_Device & device = getShortAddr(shortaddr);
if (!device.valid()) { return; } // safeguard
Z_attribute_list &attr_list = device.attr_list;
if (!attr_list.isEmpty()) {
const char * fname = zigbee_devices.getFriendlyName(shortaddr);
bool use_fname = (Settings.flag4.zigbee_use_names) && (fname); // should we replace shortaddr with friendlyname?
// save parameters is global variables to be used by Rules
gZbLastMessage.device = shortaddr; // %zbdevice%
gZbLastMessage.groupaddr = attr_list.group_id; // %zbgroup%
gZbLastMessage.endpoint = attr_list.src_ep; // %zbendpoint%
TasmotaGlobal.mqtt_data[0] = 0; // clear string
// Do we prefix with `ZbReceived`?
if (!Settings.flag4.remove_zbreceived) {
Response_P(PSTR("{\"" D_JSON_ZIGBEE_RECEIVED "\":"));
}
// What key do we use, shortaddr or name?
if (use_fname) {
Response_P(PSTR("%s{\"%s\":{"), TasmotaGlobal.mqtt_data, fname);
} else {
Response_P(PSTR("%s{\"0x%04X\":{"), TasmotaGlobal.mqtt_data, shortaddr);
}
// Add "Device":"0x...."
ResponseAppend_P(PSTR("\"" D_JSON_ZIGBEE_DEVICE "\":\"0x%04X\","), shortaddr);
// Add "Name":"xxx" if name is present
if (fname) {
ResponseAppend_P(PSTR("\"" D_JSON_ZIGBEE_NAME "\":\"%s\","), EscapeJSONString(fname).c_str());
}
// Add all other attributes
ResponseAppend_P(PSTR("%s}}"), attr_list.toString().c_str());
if (!Settings.flag4.remove_zbreceived) {
ResponseAppend_P(PSTR("}"));
}
jsonPublishFlushAttrList(device, attr_list.toString());
attr_list.reset(); // clear the attributes
if (Settings.flag4.zigbee_distinct_topics) {
if (Settings.flag4.zb_topic_fname && fname) {
//Clean special characters and check size of friendly name
char stemp[TOPSZ];
strlcpy(stemp, (!strlen(fname)) ? MQTT_TOPIC : fname, sizeof(stemp));
MakeValidMqtt(0, stemp);
//Create topic with Prefix3 and cleaned up friendly name
char frtopic[TOPSZ];
snprintf_P(frtopic, sizeof(frtopic), PSTR("%s/%s/" D_RSLT_SENSOR), SettingsText(SET_MQTTPREFIX3), stemp);
MqttPublish(frtopic, Settings.flag.mqtt_sensor_retain);
} else {
char subtopic[16];
snprintf_P(subtopic, sizeof(subtopic), PSTR("%04X/" D_RSLT_SENSOR), shortaddr);
MqttPublishPrefixTopic_P(TELE, subtopic, Settings.flag.mqtt_sensor_retain);
}
} else {
MqttPublishPrefixTopic_P(TELE, PSTR(D_RSLT_SENSOR), Settings.flag.mqtt_sensor_retain);
}
XdrvRulesProcess(); // apply rules
}
}
@ -649,20 +656,24 @@ String Z_Devices::dumpLightState(const Z_Device & device) {
// Dump the internal memory of Zigbee devices
// Mode = 1: simple dump of devices addresses
// Mode = 2: simple dump of devices addresses and names, endpoints, light
String Z_Devices::dumpSingleDevice(uint32_t dump_mode, const class Z_Device & device) {
// Mode = 3: dump last known data attributes
// add_device_name : do we add shortaddr/name ?
String Z_Devices::dumpSingleDevice(uint32_t dump_mode, const class Z_Device & device, bool add_device_name, bool add_brackets) {
uint16_t shortaddr = device.shortaddr;
char hex[22];
Z_attribute_list attr_list;
snprintf_P(hex, sizeof(hex), PSTR("0x%04X"), shortaddr);
attr_list.addAttribute(F(D_JSON_ZIGBEE_DEVICE)).setStr(hex);
if (add_device_name) {
snprintf_P(hex, sizeof(hex), PSTR("0x%04X"), shortaddr);
attr_list.addAttribute(F(D_JSON_ZIGBEE_DEVICE)).setStr(hex);
if (device.friendlyName > 0) {
attr_list.addAttribute(F(D_JSON_ZIGBEE_NAME)).setStr(device.friendlyName);
if (device.friendlyName > 0) {
attr_list.addAttribute(F(D_JSON_ZIGBEE_NAME)).setStr(device.friendlyName);
}
}
if (2 <= dump_mode) {
if (dump_mode >= 2) {
hex[0] = '0'; // prefix with '0x'
hex[1] = 'x';
Uint64toHex(device.longaddr, &hex[2], 64);
@ -695,7 +706,17 @@ String Z_Devices::dumpSingleDevice(uint32_t dump_mode, const class Z_Device & de
}
attr_list.addAttribute(F("Config")).setStrRaw(arr_data.toString().c_str());
}
return attr_list.toString(true);
if (dump_mode >= 3) {
// show internal data - mostly last known values
for (auto & data_elt : device.data) {
Z_Data_Type data_type = data_elt.getType();
data_elt.toAttributes(attr_list, data_type);
}
// add device wide attributes
device.toAttributes(attr_list);
}
return attr_list.toString(add_brackets);
}
// If &device == nullptr, then dump all
@ -796,9 +817,14 @@ Z_Data_Light & Z_Devices::getLight(uint16_t shortaddr) {
* Export device specific attributes to ZbData
\*********************************************************************************************/
void Z_Device::toAttributes(Z_attribute_list & attr_list) const {
if (validLqi()) { attr_list.addAttribute(PSTR(D_CMND_ZIGBEE_LINKQUALITY)).setUInt(lqi); }
if (validBatteryPercent()) { attr_list.addAttribute(PSTR("BatteryPercentage")).setUInt(batterypercent); }
if (validLastSeen()) { attr_list.addAttribute(PSTR("LastSeen")).setUInt(last_seen); }
if (validLastSeen()) {
if (Rtc.utc_time >= last_seen) {
attr_list.addAttribute(PSTR("LastSeen")).setUInt(Rtc.utc_time - last_seen);
}
attr_list.addAttribute(PSTR("LastSeenEpoch")).setUInt(last_seen);
}
if (validLqi()) { attr_list.addAttribute(PSTR(D_CMND_ZIGBEE_LINKQUALITY)).setUInt(lqi); }
}
/*********************************************************************************************\

View File

@ -194,14 +194,6 @@ class SBuffer hibernateDevices(void) {
AddLog_P(LOG_LEVEL_ERROR, PSTR(D_LOG_ZIGBEE "Devices list too big to fit in Flash (%d)"), buf_len);
}
// Log
char *hex_char = (char*) malloc((buf_len * 2) + 2);
if (hex_char) {
AddLog_P(LOG_LEVEL_DEBUG, PSTR(D_LOG_ZIGBEE "ZbFlashStore %s"),
ToHex_P(buf.getBuffer(), buf_len, hex_char, (buf_len * 2) + 2));
free(hex_char);
}
return buf;
}
@ -301,8 +293,8 @@ void hydrateDevices(const SBuffer &buf, uint32_t version) {
}
}
void loadZigbeeDevices(void) {
// dump = true, only dump to logs, don't actually load
void loadZigbeeDevices(bool dump_only = false) {
#ifdef ESP32
// first copy SPI buffer into ram
uint8_t *spi_buffer = (uint8_t*) malloc(z_spi_len);
@ -316,7 +308,7 @@ void loadZigbeeDevices(void) {
Z_Flashentry flashdata;
memcpy_P(&flashdata, z_dev_start, sizeof(Z_Flashentry));
// AddLog_P(LOG_LEVEL_DEBUG, PSTR(D_LOG_ZIGBEE "Memory %d"), ESP_getFreeHeap());
AddLog_P(LOG_LEVEL_DEBUG, PSTR(D_LOG_ZIGBEE "Zigbee signature in Flash: %08X - %d"), flashdata.name, flashdata.len);
// AddLog_P(LOG_LEVEL_DEBUG, PSTR(D_LOG_ZIGBEE "Zigbee signature in Flash: %08X - %d"), flashdata.name, flashdata.len);
// Check the signature
if ( ((flashdata.name == ZIGB_NAME1) || (flashdata.name == ZIGB_NAME2))
@ -327,15 +319,21 @@ void loadZigbeeDevices(void) {
SBuffer buf(buf_len);
buf.addBuffer(z_dev_start + sizeof(Z_Flashentry), buf_len);
AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "Zigbee devices data in Flash v%d (%d bytes)"), version, buf_len);
// Serial.printf(">> Buffer=");
// for (uint32_t i=0; i<buf.len(); i++) Serial.printf("%02X ", buf.get8(i));
// Serial.printf("\n");
hydrateDevices(buf, version);
zigbee_devices.clean(); // don't write back to Flash what we just loaded
if (dump_only) {
size_t buf_len = buf.len();
if (buf_len > 192) { buf_len = 192; }
AddLogBuffer(LOG_LEVEL_INFO, buf.getBuffer(), buf_len);
// Serial.printf(">> Buffer=");
// for (uint32_t i=0; i<buf.len(); i++) Serial.printf("%02X ", buf.get8(i));
// Serial.printf("\n");
} else {
hydrateDevices(buf, version);
zigbee_devices.clean(); // don't write back to Flash what we just loaded
}
} else {
AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "No zigbee devices data in Flash"));
}
// AddLog_P(LOG_LEVEL_DEBUG, PSTR(D_LOG_ZIGBEE "Memory %d"), ESP_getFreeHeap());
#ifdef ESP32
free(spi_buffer);
#endif // ESP32

View File

@ -0,0 +1,185 @@
/*
xdrv_23_zigbee_4a_eeprom.ino - zigbee support for Tasmota - saving configuration in I2C Eeprom of ZBBridge
Copyright (C) 2020 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
// =======================
// ZbData v1
// File structure:
//
// uint8 - number of devices, 0=none, 0xFF=invalid entry (probably Flash was erased)
//
// [Array of devices]
// [Offset = 2]
// uint8 - length of device record (excluding the length byte)
// uint16 - short address
//
// [Device specific data first]
// uint8 - length of structure (excluding the length byte)
// uint8[] - device wide data
//
// [Array of data structures]
// uint8 - length of structure
// uint8[] - list of data
//
void dumpZigbeeDevicesData(void) {
#ifdef USE_ZIGBEE_EZSP
if (zigbee.eeprom_present) {
SBuffer buf(192);
zigbee.eeprom.readBytes(64, 192, buf.getBuffer());
AddLogBuffer(LOG_LEVEL_INFO, buf.getBuffer(), 192);
}
#endif // USE_ZIGBEE_EZSP
}
// returns the lenght of consumed buffer, or -1 if error
int32_t hydrateDeviceWideData(class Z_Device & device, const SBuffer & buf, size_t start, size_t len) {
size_t segment_len = buf.get8(start);
if ((segment_len < 6) || (segment_len > len)) {
AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "invalid device wide data length=%d"), segment_len);
return -1;
}
device.last_seen = buf.get32(start+1);
device.lqi = buf.get8(start + 5);
device.batterypercent = buf.get8(start + 6);
return segment_len + 1;
}
// return true if success
bool hydrateDeviceData(class Z_Device & device, const SBuffer & buf, size_t start, size_t len) {
// First hydrate device wide data
int32_t ret = hydrateDeviceWideData(device, buf, start, len);
if (ret < 0) { return false; }
size_t offset = 0 + ret;
while (offset + 5 <= len) { // each entry is at least 5 bytes
uint8_t data_len = buf.get8(start + offset);
Z_Data & data_elt = device.data.createFromBuffer(buf, offset + 1, data_len);
offset += data_len + 1;
}
return true;
}
// negative means error
// positive is the segment length
int32_t hydrateSingleDevice(const class SBuffer & buf, size_t start, size_t len) {
uint8_t segment_len = buf.get8(start);
if ((segment_len < 4) || (start + segment_len > len)) {
AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "invalid segment_len=%d"), segment_len);
return -1;
}
// read shortaddr
uint16_t shortaddr = buf.get16(start + 1);
if (shortaddr >= 0xFFF0) {
AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "invalid shortaddr=0x%04X"), shortaddr);
return -1;
}
// check if the device exists, if not skip the record
Z_Device & device = zigbee_devices.findShortAddr(shortaddr);
if (&device != nullptr) {
// parse the rest
bool ret = hydrateDeviceData(device, buf, start + 3, segment_len - 3);
if (!ret) { return -1; }
}
return segment_len + 1;
}
// Parse the entire blob
// return true if ok
bool hydrateDevicesDataBlob(const class SBuffer & buf, size_t start, size_t len) {
// read number of devices
uint8_t num_devices = buf.get8(start);
if (num_devices > 0x80) {
AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "wrong number of devices=%d"), num_devices);
return false;
}
size_t offset = 0;
for (uint32_t cur_dev_num = 0; (cur_dev_num < num_devices) && (offset + 4 <= len); cur_dev_num++) {
int32_t segment_len = hydrateSingleDevice(buf, offset, len);
// advance buffer
if (segment_len <= 0) { return false; }
offset += segment_len;
}
}
class SBuffer hibernateDeviceData(const struct Z_Device & device, bool log = false) {
SBuffer buf(192);
// If we have zero information about the device, just skip ir
if (device.validLqi() ||
device.validBatteryPercent() ||
device.validLastSeen() ||
!device.data.isEmpty()) {
buf.add8(0x00); // overall length, will be updated later
buf.add16(device.shortaddr);
// device wide data
buf.add8(6); // 6 bytes
buf.add32(device.last_seen);
buf.add8(device.lqi);
buf.add8(device.batterypercent);
for (const auto & data_elt : device.data) {
size_t item_len = data_elt.DataTypeToLength(data_elt.getType());
buf.add8(item_len); // place-holder for length
buf.addBuffer((uint8_t*) &data_elt, item_len);
}
// update overall length
buf.set8(0, buf.len() - 1);
if (log) {
size_t buf_len = buf.len() - 3;
char hex[2*buf_len + 1];
// skip first 3 bytes
ToHex_P(buf.buf(3), buf_len, hex, sizeof(hex));
Response_P(PSTR("{\"" D_PRFX_ZB D_CMND_ZIGBEE_DATA "\":\"ZbData 0x%04X,%s\"}"), device.shortaddr, hex);
MqttPublishPrefixTopicRulesProcess_P(RESULT_OR_STAT, PSTR(D_PRFX_ZB D_CMND_ZIGBEE_DATA));
}
}
return buf;
}
void hibernateAllData(void) {
// first prefix is number of devices
uint8_t device_num = zigbee_devices.devicesSize();
for (const auto & device : zigbee_devices.getDevices()) {
// allocte a buffer for a single device
SBuffer buf = hibernateDeviceData(device, true); // log
if (buf.len() > 0) {
// TODO store in EEPROM
}
}
}
#endif // USE_ZIGBEE

View File

@ -231,7 +231,7 @@ const Z_AttributeConverter Z_PostProcess[] PROGMEM = {
//{ Zmap8, Cx0005, 0x0004, (NameSupport), Cm1, 0 },
// On/off cluster
{ Zbool, Cx0006, 0x0000, Z_(Power), Cm1, 0 },
{ Zbool, Cx0006, 0x0000, Z_(Power), Cm1 + Z_EXPORT_DATA, Z_MAPPING(Z_Data_OnOff, power) },
{ Zenum8, Cx0006, 0x4003, Z_(StartUpOnOff), Cm1, 0 },
{ Zbool, Cx0006, 0x8000, Z_(Power), Cm1, 0 }, // See 7280
@ -558,8 +558,8 @@ const Z_AttributeConverter Z_PostProcess[] PROGMEM = {
// IAS Cluster (Intruder Alarm System)
{ Zenum8, Cx0500, 0x0000, Z_(ZoneState), Cm1, 0 }, // Occupancy (map8)
{ Zenum16, Cx0500, 0x0001, Z_(ZoneType), Cm1, Z_MAPPING(Z_Data_Alarm, zone_type) }, // Zone type for sensor
{ Zmap16, Cx0500, 0x0002, Z_(ZoneStatus), Cm1, Z_MAPPING(Z_Data_Alarm, zone_status) }, // Zone status for sensor
{ Zenum16, Cx0500, 0x0001, Z_(ZoneType), Cm1 + Z_EXPORT_DATA, Z_MAPPING(Z_Data_Alarm, zone_type) }, // Zone type for sensor
{ Zmap16, Cx0500, 0x0002, Z_(ZoneStatus), Cm1 + Z_EXPORT_DATA, Z_MAPPING(Z_Data_Alarm, zone_status) }, // Zone status for sensor
{ Zbool, Cx0500, 0xFFF0, Z_(Contact), Cm1, Z_MAPPING(Z_Data_Alarm, zone_status) }, // We fit the first bit in the LSB
// Metering (Smart Energy) cluster
@ -1844,6 +1844,7 @@ void Z_postProcessAttributes(uint16_t shortaddr, uint16_t src_ep, class Z_attrib
switch (zigbee_type) {
case Zenum8:
case Zmap8:
case Zbool:
case Zuint8: *(uint8_t*)attr_address = uval32; break;
case Zenum16:
case Zmap16:
@ -1975,6 +1976,7 @@ void Z_Data::toAttributes(Z_attribute_list & attr_list, Z_Data_Type type) const
const Z_AttributeConverter *converter = &Z_PostProcess[i];
uint8_t conv_export = pgm_read_byte(&converter->multiplier_idx) & Z_EXPORT_DATA;
uint8_t conv_mapping = pgm_read_byte(&converter->mapping);
int8_t multiplier = CmToMultiplier(pgm_read_byte(&converter->multiplier_idx));
Z_Data_Type map_type = (Z_Data_Type) ((conv_mapping & 0xF0)>>4);
uint8_t map_offset = (conv_mapping & 0x0F);
@ -1982,7 +1984,7 @@ void Z_Data::toAttributes(Z_attribute_list & attr_list, Z_Data_Type type) const
// we need to export this attribute
const char * conv_name = Z_strings + pgm_read_word(&converter->name_offset);
uint8_t zigbee_type = pgm_read_byte(&converter->type); // zigbee type to select right size 8/16/32 bits
uint8_t *attr_address = ((uint8_t*)this) + sizeof(Z_Data) + map_offset; // address of attribute in memory
uint8_t * attr_address = ((uint8_t*)this) + sizeof(Z_Data) + map_offset; // address of attribute in memory
int32_t data_size = 0;
int32_t ival32;
@ -1990,6 +1992,7 @@ void Z_Data::toAttributes(Z_attribute_list & attr_list, Z_Data_Type type) const
switch (zigbee_type) {
case Zenum8:
case Zmap8:
case Zbool:
case Zuint8: uval32 = *(uint8_t*)attr_address; if (uval32 != 0xFF) data_size = 8; break;
case Zmap16:
case Zenum16:
@ -2002,8 +2005,12 @@ void Z_Data::toAttributes(Z_attribute_list & attr_list, Z_Data_Type type) const
if (data_size != 0) {
Z_attribute & attr = attr_list.addAttribute(conv_name);
if (data_size > 0) { attr.setUInt(uval32); }
else { attr.setInt(ival32); }
float fval = (data_size > 0) ? uval32 : ival32;
if ((1 != multiplier) && (0 != multiplier)) {
if (multiplier > 0) { fval = fval * multiplier; }
else { fval = fval / (-multiplier); }
}
attr.setFloat(fval);
}
}
}

View File

@ -30,7 +30,7 @@ const char kZbCommands[] PROGMEM = D_PRFX_ZB "|" // prefix
#endif // USE_ZIGBEE_EZSP
D_CMND_ZIGBEE_PERMITJOIN "|"
D_CMND_ZIGBEE_STATUS "|" D_CMND_ZIGBEE_RESET "|" D_CMND_ZIGBEE_SEND "|" D_CMND_ZIGBEE_PROBE "|"
D_CMND_ZIGBEE_FORGET "|" D_CMND_ZIGBEE_SAVE "|" D_CMND_ZIGBEE_NAME "|"
D_CMND_ZIGBEE_INFO "|" D_CMND_ZIGBEE_FORGET "|" D_CMND_ZIGBEE_SAVE "|" D_CMND_ZIGBEE_NAME "|"
D_CMND_ZIGBEE_BIND "|" D_CMND_ZIGBEE_UNBIND "|" D_CMND_ZIGBEE_PING "|" D_CMND_ZIGBEE_MODELID "|"
D_CMND_ZIGBEE_LIGHT "|" D_CMND_ZIGBEE_OCCUPANCY "|"
D_CMND_ZIGBEE_RESTORE "|" D_CMND_ZIGBEE_BIND_STATE "|" D_CMND_ZIGBEE_MAP "|"
@ -46,7 +46,7 @@ void (* const ZigbeeCommand[])(void) PROGMEM = {
#endif // USE_ZIGBEE_EZSP
&CmndZbPermitJoin,
&CmndZbStatus, &CmndZbReset, &CmndZbSend, &CmndZbProbe,
&CmndZbForget, &CmndZbSave, &CmndZbName,
&CmndZbInfo, &CmndZbForget, &CmndZbSave, &CmndZbName,
&CmndZbBind, &CmndZbUnbind, &CmndZbPing, &CmndZbModelId,
&CmndZbLight, &CmndZbOccupancy,
&CmndZbRestore, &CmndZbBindState, &CmndZbMap,
@ -94,6 +94,16 @@ void ZigbeeInit(void)
#endif
SettingsSave(2);
}
#ifdef USE_ZIGBEE_EZSP
// Check the I2C EEprom
Wire.beginTransmission(USE_ZIGBEE_ZBBRIDGE_EEPROM);
uint8_t error = Wire.endTransmission();
if (0 == error) {
AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "ZBBridge EEPROM found at address 0x%02X"), USE_ZIGBEE_ZBBRIDGE_EEPROM);
zigbee.eeprom_present = true;
}
#endif
}
// update commands with the current settings
@ -1175,13 +1185,43 @@ void CmndZbForget(void) {
}
}
//
// Command `ZbInfo`
// Display all information known about a device, this equivalent to `2bStatus3` with a simpler JSON output
//
void CmndZbInfo(void) {
if (zigbee.init_phase) { ResponseCmndChar_P(PSTR(D_ZIGBEE_NOT_STARTED)); return; }
Z_Device & device = zigbee_devices.parseDeviceFromName(XdrvMailbox.data, true); // in case of short_addr, it must be already registered
if (!device.valid()) { ResponseCmndChar_P(PSTR("Unknown device")); return; }
// everything is good, we can send the command
String device_info = Z_Devices::dumpSingleDevice(3, device, false, false);
Z_Devices::jsonPublishFlushAttrList(device, device_info);
ResponseCmndDone();
}
//
// Command `ZbSave`
// Save Zigbee information to flash
//
void CmndZbSave(void) {
if (zigbee.init_phase) { ResponseCmndChar_P(PSTR(D_ZIGBEE_NOT_STARTED)); return; }
saveZigbeeDevices();
switch (XdrvMailbox.payload) {
case 2: // save only data
hibernateAllData();
break;
case -1: // dump configuration
loadZigbeeDevices(true); // dump only
break;
case -2: // dump data
dumpZigbeeDevicesData();
break;
default:
saveZigbeeDevices();
break;
}
ResponseCmndDone();
}
@ -1378,196 +1418,32 @@ void CmndZbStatus(void) {
}
}
//
// Innder part of ZbData parsing
//
// {"L02":{"Dimmer":10,"Sat":254}}
bool parseDeviceInnerData(class Z_Device & device, JsonParserObject root) {
for (auto data_elt : root) {
// Parse key in format "L02":....
const char * data_type_str = data_elt.getStr();
Z_Data_Type data_type;
uint8_t endpoint;
uint8_t config = 0xFF; // unspecified
// parse key in the form "L01.5"
if (!Z_Data::ConfigToZData(data_type_str, &data_type, &endpoint, &config)) { data_type = Z_Data_Type::Z_Unknown; }
if (data_type == Z_Data_Type::Z_Unknown) {
Response_P(PSTR("{\"%s\":\"%s \"%s\"\"}"), XdrvMailbox.command, PSTR("Invalid Parameters"), data_type_str);
return false;
}
JsonParserObject data_values = data_elt.getValue().getObject();
if (!data_values) { return false; }
JsonParserToken val;
if (data_type == Z_Data_Type::Z_Device) {
if (val = data_values[PSTR(D_CMND_ZIGBEE_LINKQUALITY)]) { device.lqi = val.getUInt(); }
if (val = data_values[PSTR("BatteryPercentage")]) { device.batterypercent = val.getUInt(); }
if (val = data_values[PSTR("LastSeen")]) { device.last_seen = val.getUInt(); }
} else {
// Import generic attributes first
device.addEndpoint(endpoint);
Z_Data & data = device.data.getByType(data_type, endpoint);
// scan through attributes
if (&data != nullptr) {
if (config != 0xFF) {
data.setConfig(config);
}
for (auto attr : data_values) {
JsonParserToken attr_value = attr.getValue();
uint8_t conv_zigbee_type;
Z_Data_Type conv_data_type;
uint8_t conv_map_offset;
if (zigbeeFindAttributeByName(attr.getStr(), nullptr, nullptr, nullptr, &conv_zigbee_type, &conv_data_type, &conv_map_offset) != nullptr) {
// found an attribute matching the name, does is fit the type?
if (conv_data_type == data_type) {
// we got a match. Bear in mind that a zero value is not a valid 'data_type'
uint8_t *attr_address = ((uint8_t*)&data) + sizeof(Z_Data) + conv_map_offset;
uint32_t uval32 = attr_value.getUInt(); // call converter to uint only once
int32_t ival32 = attr_value.getInt(); // call converter to int only once
switch (conv_zigbee_type) {
case Zenum8:
case Zuint8: *(uint8_t*)attr_address = uval32; break;
case Zenum16:
case Zuint16: *(uint16_t*)attr_address = uval32; break;
case Zuint32: *(uint32_t*)attr_address = uval32; break;
case Zint8: *(int8_t*)attr_address = ival32; break;
case Zint16: *(int16_t*)attr_address = ival32; break;
case Zint32: *(int32_t*)attr_address = ival32; break;
}
} else if (conv_data_type != Z_Data_Type::Z_Unknown) {
AddLog_P(LOG_LEVEL_DEBUG, PSTR(D_LOG_ZIGBEE "attribute %s is wrong type %d (expected %d)"), attr.getStr(), (uint8_t)data_type, (uint8_t)conv_data_type);
}
}
}
}
// Import specific attributes that are not handled with the generic method
switch (data_type) {
// case Z_Data_Type::Z_Plug:
// {
// Z_Data_Plug & plug = (Z_Data_Plug&) data;
// }
// break;
// case Z_Data_Type::Z_Light:
// {
// Z_Data_Light & light = (Z_Data_Light&) data;
// }
// break;
case Z_Data_Type::Z_OnOff:
{
Z_Data_OnOff & onoff = (Z_Data_OnOff&) data;
if (val = data_values[PSTR("Power")]) { onoff.setPower(val.getUInt() ? true : false); }
}
break;
// case Z_Data_Type::Z_Thermo:
// {
// Z_Data_Thermo & thermo = (Z_Data_Thermo&) data;
// }
// break;
// case Z_Data_Type::Z_Alarm:
// {
// Z_Data_Alarm & alarm = (Z_Data_Alarm&) data;
// }
// break;
}
}
}
return true;
}
//
// Command `ZbData`
//
void CmndZbData(void) {
if (zigbee.init_phase) { ResponseCmndChar_P(PSTR(D_ZIGBEE_NOT_STARTED)); return; }
RemoveSpace(XdrvMailbox.data);
if (XdrvMailbox.data[0] == '{') {
// JSON input, enter saved data into memory -- essentially for debugging
JsonParser parser(XdrvMailbox.data);
JsonParserObject root = parser.getRootObject();
if (!root) { ResponseCmndChar_P(PSTR(D_JSON_INVALID_JSON)); return; }
// Skip `ZbData` if present
JsonParserToken zbdata = root.getObject().findStartsWith(PSTR("ZbData"));
if (zbdata) {
root = zbdata;
}
// check if parameters contain a comma ','
char *p;
char *str = strtok_r(XdrvMailbox.data, ",", &p);
for (auto device_name : root) {
Z_Device & device = zigbee_devices.parseDeviceFromName(device_name.getStr(), true);
if (!device.valid()) { ResponseCmndChar_P(PSTR("Unknown device")); return; }
JsonParserObject inner_data = device_name.getValue().getObject();
if (inner_data) {
if (!parseDeviceInnerData(device, inner_data)) {
return;
}
}
}
zigbee_devices.dirty(); // save to flash
ResponseCmndDone();
// parse first part, <device_id>
Z_Device & device = zigbee_devices.parseDeviceFromName(XdrvMailbox.data, true); // in case of short_addr, it must be already registered
if (!device.valid()) { ResponseCmndChar_P(PSTR("Unknown device")); return; }
if (p) {
// set ZbData
const SBuffer buf = SBuffer::SBufferFromHex(p, strlen(p));
hydrateDeviceData(device, buf, 0, buf.len());
} else {
// non-JSON, export current data
// ZbData 0x1234
// ZbData Device_Name
Z_Device & device = zigbee_devices.parseDeviceFromName(XdrvMailbox.data, true);
if (!device.valid()) { ResponseCmndChar_P(PSTR("Unknown device")); return; }
Z_attribute_list attr_data;
{ // scope to force object deallocation
Z_attribute_list device_attr;
device.toAttributes(device_attr);
attr_data.addAttribute(F("_")).setStrRaw(device_attr.toString(true).c_str());
}
// Iterate on data objects
for (auto & data_elt : device.data) {
Z_attribute_list inner_attr;
char key[8];
if (data_elt.validConfig()) {
snprintf_P(key, sizeof(key), "?%02X.%1X", data_elt.getEndpoint(), data_elt.getConfig());
} else {
snprintf_P(key, sizeof(key), "?%02X", data_elt.getEndpoint());
}
Z_Data_Type data_type = data_elt.getType();
key[0] = Z_Data::DataTypeToChar(data_type);
switch (data_type) {
case Z_Data_Type::Z_Plug:
((Z_Data_Plug&)data_elt).toAttributes(inner_attr, data_type);
break;
case Z_Data_Type::Z_Light:
((Z_Data_Light&)data_elt).toAttributes(inner_attr, data_type);
break;
case Z_Data_Type::Z_OnOff:
((Z_Data_OnOff&)data_elt).toAttributes(inner_attr, data_type);
break;
case Z_Data_Type::Z_Thermo:
((Z_Data_Thermo&)data_elt).toAttributes(inner_attr, data_type);
break;
case Z_Data_Type::Z_Alarm:
((Z_Data_Alarm&)data_elt).toAttributes(inner_attr, data_type);
break;
case Z_Data_Type::Z_PIR:
((Z_Data_PIR&)data_elt).toAttributes(inner_attr, data_type);
break;
}
if ((key[0] != '\0') && (key[0] != '?')) {
attr_data.addAttribute(key).setStrRaw(inner_attr.toString(true).c_str());
}
}
char hex[8];
snprintf_P(hex, sizeof(hex), PSTR("0x%04X"), device.shortaddr);
Response_P(PSTR("{\"%s\":{\"%s\":%s}}"), XdrvMailbox.command, hex, attr_data.toString(true).c_str());
hibernateDeviceData(device, true); // log
}
ResponseCmndDone();
}
//
@ -1652,6 +1528,8 @@ void CmndZbConfig(void) {
\*********************************************************************************************/
extern "C" {
// comparator function used to sort Zigbee devices by alphabetical order (if friendlyname)
// then by shortaddr if they don't have friendlyname
int device_cmp(const void * a, const void * b) {
const Z_Device &dev_a = zigbee_devices.devicesAt(*(uint8_t*)a);
const Z_Device &dev_b = zigbee_devices.devicesAt(*(uint8_t*)b);
@ -1664,7 +1542,7 @@ extern "C" {
return (int32_t)dev_a.shortaddr - (int32_t)dev_b.shortaddr;
} else {
if (fn_a) return -1;
return 1;
else return 1;
}
}
@ -1829,15 +1707,16 @@ void ZigbeeShow(bool json)
// Light, switches and plugs
const Z_Data_OnOff & onoff = device.data.find<Z_Data_OnOff>();
bool onoff_display = (&onoff != nullptr) ? onoff.validPower() : false;
const Z_Data_Light & light = device.data.find<Z_Data_Light>();
bool light_display = (&light != nullptr) ? light.validDimmer() : false;
const Z_Data_Plug & plug = device.data.find<Z_Data_Plug>();
if ((&onoff != nullptr) || light_display || (&plug != nullptr)) {
if (onoff_display || light_display || (&plug != nullptr)) {
int8_t channels = device.getLightChannels();
if (channels < 0) { channels = 5; } // if number of channel is unknown, display all known attributes
WSContentSend_P(PSTR("<tr class='htr'><td colspan=\"4\">&#9478;"));
if (&onoff != nullptr) {
WSContentSend_P(PSTR(" %s"), device.getPower() ? PSTR(D_ON) : PSTR(D_OFF));
if (onoff_display) {
WSContentSend_P(PSTR(" %s"), onoff.getPower() ? PSTR(D_ON) : PSTR(D_OFF));
}
if (&light != nullptr) {
if (light.validDimmer() && (channels >= 1)) {