Merge pull request #7979 from s-hadinger/zigbee_restore

Add command ``ZbRestore`` to restore device configuration dumped with ``ZbStatus 2``
This commit is contained in:
Theo Arends 2020-03-22 17:24:27 +01:00 committed by GitHub
commit dc531110bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 186 additions and 5 deletions

View File

@ -3,6 +3,7 @@
### 8.2.0.1 20200321
- Change HM-10 sensor type detection and add features (#7962)
- Add command ``ZbRestore`` to restore device configuration dumped with ``ZbStatus 2``
## Released

View File

@ -514,6 +514,7 @@
#define D_JSON_ZIGBEE_STATUS_MSG "StatusMessage"
#define D_CMND_ZIGBEE_LIGHT "Light"
#define D_JSON_ZIGBEE_LIGHT "Light"
#define D_CMND_ZIGBEE_RESTORE "Restore"
// Commands xdrv_25_A4988_Stepper.ino
#define D_CMND_MOTOR "MOTOR"

View File

@ -43,6 +43,42 @@ JsonVariant &getCaseInsensitive(const JsonObject &json, const char *needle) {
return *(JsonVariant*)nullptr;
}
// get the result as a string (const char*) and nullptr if there is no field or the string is empty
const char * getCaseInsensitiveConstCharNull(const JsonObject &json, const char *needle) {
const JsonVariant &val = getCaseInsensitive(json, needle);
if (&val) {
const char *val_cs = val.as<const char*>();
if (strlen(val_cs)) {
return val_cs;
}
}
return nullptr;
}
// Get an JSON attribute, with case insensitive key search starting with *needle
JsonVariant &startsWithCaseInsensitive(const JsonObject &json, const char *needle) {
// key can be in PROGMEM
if ((nullptr == &json) || (nullptr == needle) || (0 == pgm_read_byte(needle))) {
return *(JsonVariant*)nullptr;
}
String needle_s(needle);
needle_s.toLowerCase();
for (auto kv : json) {
String key_s(kv.key);
key_s.toLowerCase();
JsonVariant &value = kv.value;
if (key_s.startsWith(needle_s)) {
return value;
}
}
// if not found
return *(JsonVariant*)nullptr;
}
uint32_t parseHex(const char **data, size_t max_len = 8) {
uint32_t ret = 0;
for (uint32_t i = 0; i < max_len; i++) {

View File

@ -105,9 +105,7 @@ public:
// Add an endpoint to a device
void addEndpoint(uint16_t shortaddr, uint8_t endpoint);
// Add cluster
void addCluster(uint16_t shortaddr, uint8_t endpoint, uint16_t cluster);
void clearEndpoints(uint16_t shortaddr);
void setManufId(uint16_t shortaddr, const char * str);
void setModelId(uint16_t shortaddr, const char * str);
@ -121,6 +119,7 @@ public:
// Dump json
String dumpLightState(uint16_t shortaddr) const;
String dump(uint32_t dump_mode, uint16_t status_shortaddr = 0) const;
int32_t deviceRestore(const JsonObject &json);
// Hue support
void setHueBulbtype(uint16_t shortaddr, int8_t bulbtype);
@ -450,6 +449,20 @@ void Z_Devices::updateDevice(uint16_t shortaddr, uint64_t longaddr) {
}
}
//
// Clear all endpoints
//
void Z_Devices::clearEndpoints(uint16_t shortaddr) {
if (!shortaddr) { return; }
Z_Device &device = getShortAddr(shortaddr);
if (&device == nullptr) { return; } // don't crash if not found
for (uint32_t i = 0; i < endpoints_max; i++) {
device.endpoints[i] = 0;
// no dirty here because it doesn't make sense to store it, does it?
}
}
//
// Add an endpoint to a shortaddr
//
@ -994,6 +1007,9 @@ String Z_Devices::dump(uint32_t dump_mode, uint16_t status_shortaddr) const {
if (device.modelId) {
dev[F(D_JSON_MODEL D_JSON_ID)] = device.modelId;
}
if (-1 != device.bulbtype) {
dev[F(D_JSON_ZIGBEE_LIGHT)] = device.bulbtype; // sign extend, 0xFF changed as -1
}
if (device.manufacturerId) {
dev[F("Manufacturer")] = device.manufacturerId;
}
@ -1013,4 +1029,77 @@ String Z_Devices::dump(uint32_t dump_mode, uint16_t status_shortaddr) const {
return payload;
}
// Restore a single device configuration based on json export
// Input: json element as expported by `ZbStatus2``
// Mandatory attribue: `Device`
//
// Returns:
// 0 : Ok
// <0 : Error
//
// Ex: {"Device":"0x5ADF","Name":"IKEA_Light","IEEEAddr":"0x90FD9FFFFE03B051","ModelId":"TRADFRI bulb E27 WS opal 980lm","Manufacturer":"IKEA of Sweden","Endpoints":["0x01","0xF2"]}
int32_t Z_Devices::deviceRestore(const JsonObject &json) {
// params
uint16_t device = 0x0000; // 0x0000 is coordinator so considered invalid
uint64_t ieeeaddr = 0x0000000000000000LL; // 0 means unknown
const char * modelid = nullptr;
const char * manufid = nullptr;
const char * friendlyname = nullptr;
int8_t bulbtype = 0xFF;
size_t endpoints_len = 0;
// read mandatory "Device"
const JsonVariant &val_device = getCaseInsensitive(json, PSTR("Device"));
if (nullptr != &val_device) {
device = strToUInt(val_device);
} else {
return -1; // missing "Device" attribute
}
// read "IEEEAddr" 64 bits in format "0x0000000000000000"
const JsonVariant &val_ieeeaddr = getCaseInsensitive(json, PSTR("IEEEAddr"));
if (nullptr != &val_ieeeaddr) {
ieeeaddr = strtoull(val_ieeeaddr.as<const char*>(), nullptr, 0);
}
// read "Name"
friendlyname = getCaseInsensitiveConstCharNull(json, PSTR("Name"));
// read "ModelId"
modelid = getCaseInsensitiveConstCharNull(json, PSTR("ModelId"));
// read "Manufacturer"
manufid = getCaseInsensitiveConstCharNull(json, PSTR("Manufacturer"));
// read "Light"
const JsonVariant &val_bulbtype = getCaseInsensitive(json, PSTR(D_JSON_ZIGBEE_LIGHT));
if (nullptr != &val_bulbtype) { bulbtype = strToUInt(val_bulbtype);; }
// update internal device information
updateDevice(device, ieeeaddr);
if (modelid) { setModelId(device, modelid); }
if (manufid) { setManufId(device, manufid); }
if (friendlyname) { setFriendlyName(device, friendlyname); }
if (&val_bulbtype) { setHueBulbtype(device, bulbtype); }
// read "Endpoints"
const JsonVariant &val_endpoints = getCaseInsensitive(json, PSTR("Endpoints"));
if ((nullptr != &val_endpoints) && (val_endpoints.is<JsonArray>())) {
const JsonArray &arr_ep = val_endpoints.as<const JsonArray&>();
endpoints_len = arr_ep.size();
clearEndpoints(device); // clear even if array is empty
if (endpoints_len) {
for (auto ep_elt : arr_ep) {
uint8_t ep = strToUInt(ep_elt);
if (ep) {
addEndpoint(device, ep);
}
}
}
}
return 0;
}
#endif // USE_ZIGBEE

View File

@ -35,7 +35,7 @@ const char kZbCommands[] PROGMEM = D_PRFX_ZB "|" // prefix
D_CMND_ZIGBEE_PROBE "|" D_CMND_ZIGBEE_READ "|" D_CMND_ZIGBEEZNPRECEIVE "|"
D_CMND_ZIGBEE_FORGET "|" D_CMND_ZIGBEE_SAVE "|" D_CMND_ZIGBEE_NAME "|"
D_CMND_ZIGBEE_BIND "|" D_CMND_ZIGBEE_PING "|" D_CMND_ZIGBEE_MODELID "|"
D_CMND_ZIGBEE_LIGHT
D_CMND_ZIGBEE_LIGHT "|" D_CMND_ZIGBEE_RESTORE
;
void (* const ZigbeeCommand[])(void) PROGMEM = {
@ -44,7 +44,7 @@ void (* const ZigbeeCommand[])(void) PROGMEM = {
&CmndZbProbe, &CmndZbRead, &CmndZbZNPReceive,
&CmndZbForget, &CmndZbSave, &CmndZbName,
&CmndZbBind, &CmndZbPing, &CmndZbModelId,
&CmndZbLight,
&CmndZbLight, CmndZbRestore,
};
int32_t ZigbeeProcessInput(class SBuffer &buf) {
@ -790,6 +790,60 @@ void CmndZbSave(void) {
ResponseCmndDone();
}
// Restore a device configuration previously exported via `ZbStatus2``
// Format:
// Either the entire `ZbStatus3` export, or an array or just the device configuration.
// If array, if can contain multiple devices
// ZbRestore {"ZbStatus3":[{"Device":"0x5ADF","Name":"Petite_Lampe","IEEEAddr":"0x90FD9FFFFE03B051","ModelId":"TRADFRI bulb E27 WS opal 980lm","Manufacturer":"IKEA of Sweden","Endpoints":["0x01","0xF2"]}]}
// ZbRestore [{"Device":"0x5ADF","Name":"Petite_Lampe","IEEEAddr":"0x90FD9FFFFE03B051","ModelId":"TRADFRI bulb E27 WS opal 980lm","Manufacturer":"IKEA of Sweden","Endpoints":["0x01","0xF2"]}]
// ZbRestore {"Device":"0x5ADF","Name":"Petite_Lampe","IEEEAddr":"0x90FD9FFFFE03B051","ModelId":"TRADFRI bulb E27 WS opal 980lm","Manufacturer":"IKEA of Sweden","Endpoints":["0x01","0xF2"]}
void CmndZbRestore(void) {
if (zigbee.init_phase) { ResponseCmndChar_P(PSTR(D_ZIGBEE_NOT_STARTED)); return; }
DynamicJsonBuffer jsonBuf;
const JsonVariant json_parsed = jsonBuf.parse((const char*)XdrvMailbox.data); // const to force a copy of parameter
const JsonVariant * json = &json_parsed; // root of restore, to be changed if needed
bool success = false;
// check if parsing succeeded
if (json_parsed.is<JsonObject>()) {
success = json_parsed.as<const JsonObject&>().success();
} else if (json_parsed.is<JsonArray>()) {
success = json_parsed.as<const JsonArray&>().success();
}
if (!success) { ResponseCmndChar_P(PSTR(D_JSON_INVALID_JSON)); return; }
// Check is root contains `ZbStatus<x>` key, if so change the root
const JsonVariant * zbstatus = &startsWithCaseInsensitive(*json, PSTR("ZbStatus"));
if (nullptr != zbstatus) {
json = zbstatus;
}
// check if the root is an array
if (json->is<JsonArray>()) {
const JsonArray& arr = json->as<const JsonArray&>();
for (auto elt : arr) {
// call restore on each item
int32_t res = zigbee_devices.deviceRestore(elt);
if (res < 0) {
ResponseCmndChar_P(PSTR("Restore failed"));
return;
}
}
} else if (json->is<JsonObject>()) {
int32_t res = zigbee_devices.deviceRestore(*json);
if (res < 0) {
ResponseCmndChar_P(PSTR("Restore failed"));
return;
}
// call restore on a single object
} else {
ResponseCmndChar_P(PSTR("Missing parameters"));
return;
}
ResponseCmndDone();
}
// Send an attribute read command to a device, specifying cluster and list of attributes
void CmndZbRead(void) {
// ZigbeeRead {"Device":"0xF289","Cluster":0,"Endpoint":3,"Attr":5}