diff --git a/tasmota/CHANGELOG.md b/tasmota/CHANGELOG.md index b5f6d6ffe..02ae08cb9 100644 --- a/tasmota/CHANGELOG.md +++ b/tasmota/CHANGELOG.md @@ -6,6 +6,7 @@ - Change Energy JSON ExportActive field from ``"ExportActive":[33.736,11.717,16.978]`` to ``"ExportActive":33.736,"ExportTariff":[11.717,16.978]`` - Add Three Phase Export Active Energy to SDM630 driver - Add commands ``LedPwmOn 0..255``, ``LedPwmOff 0..255`` and ``LedPwmMode1 0/1`` to control led brightness by George (#8491) +- Add wildcard patter for JSON marching in rules using ``?`` pattern ### 8.3.1.1 20200518 diff --git a/tasmota/support.ino b/tasmota/support.ino index cecb5ffb2..c5006cf6c 100644 --- a/tasmota/support.ino +++ b/tasmota/support.ino @@ -1874,65 +1874,6 @@ void AddLogBufferSize(uint32_t loglevel, uint8_t *buffer, uint32_t count, uint32 AddLog(loglevel); } -/*********************************************************************************************\ - * JSON parsing -\*********************************************************************************************/ - -// does the character needs to be escaped, and if so with which character -char escapeJSONChar(char c) { - if ((c == '\"') || (c == '\\')) { - return c; - } - if (c == '\n') { return 'n'; } - if (c == '\t') { return 't'; } - if (c == '\r') { return 'r'; } - if (c == '\f') { return 'f'; } - if (c == '\b') { return 'b'; } - return 0; -} - -String escapeJSONString(const char *str) { - String r(""); - if (nullptr == str) { return r; } - - bool needs_escape = false; - size_t len_out = 1; - const char * c = str; - - while (*c) { - if (escapeJSONChar(*c)) { - len_out++; - needs_escape = true; - } - c++; - len_out++; - } - - if (needs_escape) { - // we need to escape some chars - // allocate target buffer - r.reserve(len_out); - c = str; - char *d = r.begin(); - while (*c) { - char c2 = escapeJSONChar(*c); - if (c2) { - c++; - *d++ = '\\'; - *d++ = c2; - } else { - *d++ = *c++; - } - } - *d = 0; // add NULL terminator - r = (char*) r.begin(); // assign the buffer to the string - } else { - r = str; - } - - return r; -} - /*********************************************************************************************\ * Uncompress static PROGMEM strings \*********************************************************************************************/ diff --git a/tasmota/support_json.ino b/tasmota/support_json.ino new file mode 100644 index 000000000..51fa0e6a3 --- /dev/null +++ b/tasmota/support_json.ino @@ -0,0 +1,106 @@ +/* + support_json.ino - JSON support functions + + 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 . +*/ + +/*********************************************************************************************\ + * JSON parsing +\*********************************************************************************************/ + +// does the character needs to be escaped, and if so with which character +char EscapeJSONChar(char c) { + if ((c == '\"') || (c == '\\')) { + return c; + } + if (c == '\n') { return 'n'; } + if (c == '\t') { return 't'; } + if (c == '\r') { return 'r'; } + if (c == '\f') { return 'f'; } + if (c == '\b') { return 'b'; } + return 0; +} + +String EscapeJSONString(const char *str) { + String r(""); + if (nullptr == str) { return r; } + + bool needs_escape = false; + size_t len_out = 1; + const char * c = str; + + while (*c) { + if (EscapeJSONChar(*c)) { + len_out++; + needs_escape = true; + } + c++; + len_out++; + } + + if (needs_escape) { + // we need to escape some chars + // allocate target buffer + r.reserve(len_out); + c = str; + char *d = r.begin(); + while (*c) { + char c2 = EscapeJSONChar(*c); + if (c2) { + c++; + *d++ = '\\'; + *d++ = c2; + } else { + *d++ = *c++; + } + } + *d = 0; // add NULL terminator + r = (char*) r.begin(); // assign the buffer to the string + } else { + r = str; + } + + return r; +} + +/*********************************************************************************************\ + * Find key - case insensitive +\*********************************************************************************************/ + +// Given a JsonObject, finds the value as JsonVariant for the key needle. +// The search is case-insensitive, and will find the first match in the order of keys in JSON +// +// If the key is not found, returns a nullptr +// Input: needle cannot be NULL but may be PROGMEM +const JsonVariant &GetCaseInsensitive(const JsonObject &json, const char *needle) { + // key can be in PROGMEM + // if needle == "?" then we return the first valid key + bool wildcard = strcmp_P("?", needle) == 0; + if ((nullptr == &json) || (nullptr == needle) || (0 == pgm_read_byte(needle))) { + return *(JsonVariant*)nullptr; + } + + for (JsonObject::const_iterator it=json.begin(); it!=json.end(); ++it) { + const char *key = it->key; + const JsonVariant &value = it->value; + + if (wildcard || (0 == strcasecmp_P(key, needle))) { + return value; + } + } + // if not found + return *(JsonVariant*)nullptr; +} diff --git a/tasmota/xdrv_10_rules.ino b/tasmota/xdrv_10_rules.ino index 912a32ae5..1ad618485 100644 --- a/tasmota/xdrv_10_rules.ino +++ b/tasmota/xdrv_10_rules.ino @@ -441,25 +441,20 @@ bool RulesRuleMatch(uint8_t rule_set, String &event, String &rule) break; } } - snprintf_P(stemp, sizeof(stemp), PSTR("%%TIME%%")); - if (rule_param.startsWith(stemp)) { + if (rule_param.startsWith(F("%TIME%"))) { rule_param = String(MinutesPastMidnight()); } - snprintf_P(stemp, sizeof(stemp), PSTR("%%UPTIME%%")); - if (rule_param.startsWith(stemp)) { + if (rule_param.startsWith(F("%UPTIME%"))) { rule_param = String(MinutesUptime()); } - snprintf_P(stemp, sizeof(stemp), PSTR("%%TIMESTAMP%%")); - if (rule_param.startsWith(stemp)) { + if (rule_param.startsWith(F("%TIMESTAMP%"))) { rule_param = GetDateAndTime(DT_LOCAL).c_str(); } #if defined(USE_TIMERS) && defined(USE_SUNRISE) - snprintf_P(stemp, sizeof(stemp), PSTR("%%SUNRISE%%")); - if (rule_param.startsWith(stemp)) { + if (rule_param.startsWith(F("%SUNRISE%"))) { rule_param = String(SunMinutes(0)); } - snprintf_P(stemp, sizeof(stemp), PSTR("%%SUNSET%%")); - if (rule_param.startsWith(stemp)) { + if (rule_param.startsWith(F("%SUNSET%"))) { rule_param = String(SunMinutes(1)); } #endif // USE_TIMERS and USE_SUNRISE @@ -493,13 +488,17 @@ bool RulesRuleMatch(uint8_t rule_set, String &event, String &rule) uint32_t i = 0; while ((pos = rule_name.indexOf("#")) > 0) { // "SUBTYPE1#SUBTYPE2#CURRENT" subtype = rule_name.substring(0, pos); - if (!(*obj)[subtype].success()) { return false; } // No subtype in JSON data - JsonObject &obj2 = (*obj)[subtype]; - obj = &obj2; + const JsonVariant & val = GetCaseInsensitive(*obj, subtype.c_str()); + if (nullptr == &val) { return false; } // not found + obj = &(val.as()); + if (!obj->success()) { return false; } // not a JsonObject + rule_name = rule_name.substring(pos +1); if (i++ > 10) { return false; } // Abandon possible loop } - if (!(*obj)[rule_name].success()) { return false; } // No name in JSON data + + const JsonVariant & val = GetCaseInsensitive(*obj, rule_name.c_str()); + if (nullptr == &val) { return false; } // last level not found const char* str_value; if (rule_name_idx) { str_value = (*obj)[rule_name][rule_name_idx -1]; // "CURRENT[1]" @@ -2036,7 +2035,7 @@ void CmndRule(void) XdrvMailbox.command, index, GetStateText(bitRead(Settings.rule_enabled, index -1)), GetStateText(bitRead(Settings.rule_once, index -1)), GetStateText(bitRead(Settings.rule_stop, index -1)), rule_len, MAX_RULE_SIZE - GetRuleLenStorage(index - 1), - escapeJSONString(rule.c_str()).c_str()); + EscapeJSONString(rule.c_str()).c_str()); } } diff --git a/tasmota/xdrv_20_hue.ino b/tasmota/xdrv_20_hue.ino index 639f198bb..17a0886db 100644 --- a/tasmota/xdrv_20_hue.ino +++ b/tasmota/xdrv_20_hue.ino @@ -437,8 +437,8 @@ void HueLightStatus2(uint8_t device, String *response) fname[fname_len] = 0x00; } snprintf_P(buf, buf_size, HUE_LIGHTS_STATUS_JSON2, - escapeJSONString(fname).c_str(), - escapeJSONString(Settings.user_template_name).c_str(), + EscapeJSONString(fname).c_str(), + EscapeJSONString(Settings.user_template_name).c_str(), PSTR("Tasmota"), GetHueDeviceId(device).c_str()); *response += buf; diff --git a/tasmota/xdrv_23_zigbee_1_headers.ino b/tasmota/xdrv_23_zigbee_1_headers.ino index 5a250c781..046450927 100644 --- a/tasmota/xdrv_23_zigbee_1_headers.ino +++ b/tasmota/xdrv_23_zigbee_1_headers.ino @@ -23,29 +23,9 @@ void ZigbeeZCLSend_Raw(uint16_t dtsAddr, uint16_t groupaddr, uint16_t clusterId, uint8_t endpoint, uint8_t cmdId, bool clusterSpecific, const uint8_t *msg, size_t len, bool needResponse, uint8_t transacId); - -// Get an JSON attribute, with case insensitive key search -const JsonVariant &getCaseInsensitive(const JsonObject &json, const char *needle) { - // key can be in PROGMEM - if ((nullptr == &json) || (nullptr == needle) || (0 == pgm_read_byte(needle))) { - return *(JsonVariant*)nullptr; - } - - for (JsonObject::const_iterator it=json.begin(); it!=json.end(); ++it) { - const char *key = it->key; - const JsonVariant &value = it->value; - - if (0 == strcasecmp_P(key, needle)) { - return value; - } - } - // if not found - 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); + const JsonVariant &val = GetCaseInsensitive(json, needle); if (&val) { const char *val_cs = val.as(); if (strlen(val_cs)) { diff --git a/tasmota/xdrv_23_zigbee_2_devices.ino b/tasmota/xdrv_23_zigbee_2_devices.ino index 74d52c73e..c115052f7 100644 --- a/tasmota/xdrv_23_zigbee_2_devices.ino +++ b/tasmota/xdrv_23_zigbee_2_devices.ino @@ -1075,7 +1075,7 @@ int32_t Z_Devices::deviceRestore(const JsonObject &json) { size_t endpoints_len = 0; // read mandatory "Device" - const JsonVariant &val_device = getCaseInsensitive(json, PSTR("Device")); + const JsonVariant &val_device = GetCaseInsensitive(json, PSTR("Device")); if (nullptr != &val_device) { device = strToUInt(val_device); } else { @@ -1083,7 +1083,7 @@ int32_t Z_Devices::deviceRestore(const JsonObject &json) { } // read "IEEEAddr" 64 bits in format "0x0000000000000000" - const JsonVariant &val_ieeeaddr = getCaseInsensitive(json, PSTR("IEEEAddr")); + const JsonVariant &val_ieeeaddr = GetCaseInsensitive(json, PSTR("IEEEAddr")); if (nullptr != &val_ieeeaddr) { ieeeaddr = strtoull(val_ieeeaddr.as(), nullptr, 0); } @@ -1098,7 +1098,7 @@ int32_t Z_Devices::deviceRestore(const JsonObject &json) { manufid = getCaseInsensitiveConstCharNull(json, PSTR("Manufacturer")); // read "Light" - const JsonVariant &val_bulbtype = getCaseInsensitive(json, PSTR(D_JSON_ZIGBEE_LIGHT)); + const JsonVariant &val_bulbtype = GetCaseInsensitive(json, PSTR(D_JSON_ZIGBEE_LIGHT)); if (nullptr != &val_bulbtype) { bulbtype = strToUInt(val_bulbtype);; } // update internal device information @@ -1109,7 +1109,7 @@ int32_t Z_Devices::deviceRestore(const JsonObject &json) { if (&val_bulbtype) { setHueBulbtype(device, bulbtype); } // read "Endpoints" - const JsonVariant &val_endpoints = getCaseInsensitive(json, PSTR("Endpoints")); + const JsonVariant &val_endpoints = GetCaseInsensitive(json, PSTR("Endpoints")); if ((nullptr != &val_endpoints) && (val_endpoints.is())) { const JsonArray &arr_ep = val_endpoints.as(); endpoints_len = arr_ep.size(); diff --git a/tasmota/xdrv_23_zigbee_3_hue.ino b/tasmota/xdrv_23_zigbee_3_hue.ino index d5c4b1f08..e4e8a8b63 100644 --- a/tasmota/xdrv_23_zigbee_3_hue.ino +++ b/tasmota/xdrv_23_zigbee_3_hue.ino @@ -85,9 +85,9 @@ void HueLightStatus2Zigbee(uint16_t shortaddr, String *response) snprintf_P(shortaddrname, sizeof(shortaddrname), PSTR("0x%04X"), shortaddr); snprintf_P(buf, buf_size, HUE_LIGHTS_STATUS_JSON2, - (friendlyName) ? escapeJSONString(friendlyName).c_str() : shortaddrname, - (modelId) ? escapeJSONString(modelId).c_str() : PSTR("Unknown"), - (manufacturerId) ? escapeJSONString(manufacturerId).c_str() : PSTR("Tasmota"), + (friendlyName) ? EscapeJSONString(friendlyName).c_str() : shortaddrname, + (modelId) ? EscapeJSONString(modelId).c_str() : PSTR("Unknown"), + (manufacturerId) ? EscapeJSONString(manufacturerId).c_str() : PSTR("Tasmota"), GetHueDeviceId(shortaddr).c_str()); *response += buf; diff --git a/tasmota/xdrv_23_zigbee_8_parsers.ino b/tasmota/xdrv_23_zigbee_8_parsers.ino index 20aecd0f9..ffe108c5a 100644 --- a/tasmota/xdrv_23_zigbee_8_parsers.ino +++ b/tasmota/xdrv_23_zigbee_8_parsers.ino @@ -600,7 +600,7 @@ void Z_SendAFInfoRequest(uint16_t shortaddr) { void Z_AqaraOccupancy(uint16_t shortaddr, uint16_t cluster, uint8_t endpoint, const JsonObject &json) { static const uint32_t OCCUPANCY_TIMEOUT = 90 * 1000; // 90 s // Read OCCUPANCY value if any - const JsonVariant &val_endpoint = getCaseInsensitive(json, PSTR(OCCUPANCY)); + const JsonVariant &val_endpoint = GetCaseInsensitive(json, PSTR(OCCUPANCY)); if (nullptr != &val_endpoint) { uint32_t occupancy = strToUInt(val_endpoint); diff --git a/tasmota/xdrv_23_zigbee_9_impl.ino b/tasmota/xdrv_23_zigbee_9_impl.ino index 40053c5a7..5c5181b6d 100644 --- a/tasmota/xdrv_23_zigbee_9_impl.ino +++ b/tasmota/xdrv_23_zigbee_9_impl.ino @@ -427,13 +427,13 @@ void CmndZbSend(void) { bool clusterSpecific = true; // parse JSON - const JsonVariant &val_device = getCaseInsensitive(json, PSTR("Device")); + const JsonVariant &val_device = GetCaseInsensitive(json, PSTR("Device")); if (nullptr != &val_device) { device = zigbee_devices.parseDeviceParam(val_device.as()); if (BAD_SHORTADDR == device) { ResponseCmndChar_P(PSTR("Invalid parameter")); return; } } if (BAD_SHORTADDR == device) { // if not found, check if we have a group - const JsonVariant &val_group = getCaseInsensitive(json, PSTR("Group")); + const JsonVariant &val_group = GetCaseInsensitive(json, PSTR("Group")); if (nullptr != &val_group) { groupaddr = strToUInt(val_group); } else { // no device nor group @@ -442,11 +442,11 @@ void CmndZbSend(void) { } } - const JsonVariant &val_endpoint = getCaseInsensitive(json, PSTR("Endpoint")); + const JsonVariant &val_endpoint = GetCaseInsensitive(json, PSTR("Endpoint")); if (nullptr != &val_endpoint) { endpoint = strToUInt(val_endpoint); } - const JsonVariant &val_manuf = getCaseInsensitive(json, PSTR("Manuf")); + const JsonVariant &val_manuf = GetCaseInsensitive(json, PSTR("Manuf")); if (nullptr != &val_manuf) { manuf = strToUInt(val_manuf); } - const JsonVariant &val_cmd = getCaseInsensitive(json, PSTR("Send")); + const JsonVariant &val_cmd = GetCaseInsensitive(json, PSTR("Send")); if (nullptr != &val_cmd) { // probe the type of the argument // If JSON object, it's high level commands @@ -582,7 +582,7 @@ void ZbBindUnbind(bool unbind) { // false = bind, true = unbind // Information about source device: "Device", "Endpoint", "Cluster" // - the source endpoint must have a known IEEE address - const JsonVariant &val_device = getCaseInsensitive(json, PSTR("Device")); + const JsonVariant &val_device = GetCaseInsensitive(json, PSTR("Device")); if (nullptr != &val_device) { srcDevice = zigbee_devices.parseDeviceParam(val_device.as()); } @@ -591,17 +591,17 @@ void ZbBindUnbind(bool unbind) { // false = bind, true = unbind uint64_t srcLongAddr = zigbee_devices.getDeviceLongAddr(srcDevice); if (0 == srcLongAddr) { ResponseCmndChar_P(PSTR("Unknown source IEEE address")); return; } // look for source endpoint - const JsonVariant &val_endpoint = getCaseInsensitive(json, PSTR("Endpoint")); + const JsonVariant &val_endpoint = GetCaseInsensitive(json, PSTR("Endpoint")); if (nullptr != &val_endpoint) { endpoint = strToUInt(val_endpoint); } // look for source cluster - const JsonVariant &val_cluster = getCaseInsensitive(json, PSTR("Cluster")); + const JsonVariant &val_cluster = GetCaseInsensitive(json, PSTR("Cluster")); if (nullptr != &val_cluster) { cluster = strToUInt(val_cluster); } // Either Device address // In this case the following parameters are mandatory // - "ToDevice" and the device must have a known IEEE address // - "ToEndpoint" - const JsonVariant &dst_device = getCaseInsensitive(json, PSTR("ToDevice")); + const JsonVariant &dst_device = GetCaseInsensitive(json, PSTR("ToDevice")); if (nullptr != &dst_device) { dstDevice = zigbee_devices.parseDeviceParam(dst_device.as()); if (BAD_SHORTADDR == dstDevice) { ResponseCmndChar_P(PSTR("Invalid parameter")); return; } @@ -612,12 +612,12 @@ void ZbBindUnbind(bool unbind) { // false = bind, true = unbind } if (0 == dstLongAddr) { ResponseCmndChar_P(PSTR("Unknown dest IEEE address")); return; } - const JsonVariant &val_toendpoint = getCaseInsensitive(json, PSTR("ToEndpoint")); + const JsonVariant &val_toendpoint = GetCaseInsensitive(json, PSTR("ToEndpoint")); if (nullptr != &val_toendpoint) { toendpoint = strToUInt(val_endpoint); } else { toendpoint = endpoint; } } // Or Group Address - we don't need a dstEndpoint in this case - const JsonVariant &to_group = getCaseInsensitive(json, PSTR("ToGroup")); + const JsonVariant &to_group = GetCaseInsensitive(json, PSTR("ToGroup")); if (nullptr != &to_group) { toGroup = strToUInt(to_group); } // make sure we don't have conflicting parameters @@ -907,13 +907,13 @@ void CmndZbRead(void) { size_t attrs_len = 0; uint8_t* attrs = nullptr; // empty string is valid - const JsonVariant &val_device = getCaseInsensitive(json, PSTR("Device")); + const JsonVariant &val_device = GetCaseInsensitive(json, PSTR("Device")); if (nullptr != &val_device) { device = zigbee_devices.parseDeviceParam(val_device.as()); if (BAD_SHORTADDR == device) { ResponseCmndChar_P(PSTR("Invalid parameter")); return; } } if (BAD_SHORTADDR == device) { // if not found, check if we have a group - const JsonVariant &val_group = getCaseInsensitive(json, PSTR("Group")); + const JsonVariant &val_group = GetCaseInsensitive(json, PSTR("Group")); if (nullptr != &val_group) { groupaddr = strToUInt(val_group); } else { // no device nor group @@ -922,14 +922,14 @@ void CmndZbRead(void) { } } - const JsonVariant &val_cluster = getCaseInsensitive(json, PSTR("Cluster")); + const JsonVariant &val_cluster = GetCaseInsensitive(json, PSTR("Cluster")); if (nullptr != &val_cluster) { cluster = strToUInt(val_cluster); } - const JsonVariant &val_endpoint = getCaseInsensitive(json, PSTR("Endpoint")); + const JsonVariant &val_endpoint = GetCaseInsensitive(json, PSTR("Endpoint")); if (nullptr != &val_endpoint) { endpoint = strToUInt(val_endpoint); } - const JsonVariant &val_manuf = getCaseInsensitive(json, PSTR("Manuf")); + const JsonVariant &val_manuf = GetCaseInsensitive(json, PSTR("Manuf")); if (nullptr != &val_manuf) { manuf = strToUInt(val_manuf); } - const JsonVariant &val_attr = getCaseInsensitive(json, PSTR("Read")); + const JsonVariant &val_attr = GetCaseInsensitive(json, PSTR("Read")); if (nullptr != &val_attr) { uint16_t val = strToUInt(val_attr); if (val_attr.is()) { @@ -1034,21 +1034,21 @@ void CmndZbConfig(void) { if (!json.success()) { ResponseCmndChar_P(PSTR(D_JSON_INVALID_JSON)); return; } // Channel - const JsonVariant &val_channel = getCaseInsensitive(json, PSTR("Channel")); + const JsonVariant &val_channel = GetCaseInsensitive(json, PSTR("Channel")); if (nullptr != &val_channel) { zb_channel = strToUInt(val_channel); } if (zb_channel < 11) { zb_channel = 11; } if (zb_channel > 26) { zb_channel = 26; } // PanID - const JsonVariant &val_pan_id = getCaseInsensitive(json, PSTR("PanID")); + const JsonVariant &val_pan_id = GetCaseInsensitive(json, PSTR("PanID")); if (nullptr != &val_pan_id) { zb_pan_id = strToUInt(val_pan_id); } // ExtPanID - const JsonVariant &val_ext_pan_id = getCaseInsensitive(json, PSTR("ExtPanID")); + const JsonVariant &val_ext_pan_id = GetCaseInsensitive(json, PSTR("ExtPanID")); if (nullptr != &val_ext_pan_id) { zb_ext_panid = strtoull(val_ext_pan_id.as(), nullptr, 0); } // KeyL - const JsonVariant &val_key_l = getCaseInsensitive(json, PSTR("KeyL")); + const JsonVariant &val_key_l = GetCaseInsensitive(json, PSTR("KeyL")); if (nullptr != &val_key_l) { zb_precfgkey_l = strtoull(val_key_l.as(), nullptr, 0); } // KeyH - const JsonVariant &val_key_h = getCaseInsensitive(json, PSTR("KeyH")); + const JsonVariant &val_key_h = GetCaseInsensitive(json, PSTR("KeyH")); if (nullptr != &val_key_h) { zb_precfgkey_h = strtoull(val_key_h.as(), nullptr, 0); } // Check if a parameter was changed after all