diff --git a/lib/jsmn-shadinger-1.0/README.md b/lib/jsmn-shadinger-1.0/README.md new file mode 100644 index 000000000..f98503eb1 --- /dev/null +++ b/lib/jsmn-shadinger-1.0/README.md @@ -0,0 +1,181 @@ +# JSMN lightweight JSON parser for Tasmota + +Intro: + +## Benefits + +## How to use + +`{"Device":"0x1234","Power":true,"Temperature":25.5}` + +The JSON above is split into 8 tokens, and requires 32 bytes of dynamic memory. + +- Token 0: `OBJECT`, size=3: `{"Device":"0x1234","Power":true,"Temperature":25.5}` +- Token 1: `KEY`: `Device` +- Token 2: `STRING`: `0x1234` +- Token 3: `KEY`: `Power` +- Token 4: `BOOL_TRUE`: `true` +- Token 5: `KEY`: `Temperature` +- Token 6: `FLOAT`: `25.5` +- Token 7: `INVALID` + +If what you need is to parse a JSON Object for values with default values: +``` +#include "JsonParser.h" + +char json_buffer[] = "{\"Device\":\"0x1234\",\"Power\":true,\"Temperature\":25.6}"; +JsonParserObject root = JsonParser(json_buffer).getRootObject(); +if (!root) { ResponseCmndChar_P(PSTR("Invalid JSON")); return; } + +uint16_t d = root.getUInt(PSTR("DEVICE"), 0xFFFF); // case insensitive +bool b = root.getBool(PSTR("Power"), false); +float f = root.getFloat(PSTR("Temperature), -100); +``` + +Alternative pattern, if you want to test the existence of the attribute first: +``` +#include "JsonParser.h" + +char json_buffer[] = "{\"Device\":\"0x1234\",\"Power\":true,\"Temperature\":25.6}"; +JsonParserObject root = JsonParser(json_buffer).getRootObject(); +if (!root) { ResponseCmndChar_P(PSTR("Invalid JSON")); return; } + +JsonParserToken val = root[PSTR("DEVICE")]; +if (val) { + d = val.getUInt(); +} +val = root[PSTR("Power")]; +if (val) { + b = val.getBool(); +} +val = root[PSTR("Temperature)]; +if (val) { + f = val.getFloat(); +} +``` + +## Types and conversion + +JSMN relies on the concept of JSON Tokens `JsonParserToken`. Tokens do not hold any data, but point to token boundaries in JSON string instead. Every jsmn token has a type, which indicates the type of corresponding JSON token. JSMN for Tasmota extends the type from JSMN to ease further parsing. + +Types are: +- `INVALID` invalid token or end of stream, see Error Handling below +- `OBJECT` a JSON sub-object, `size()` contains the number of key/values of the object +- `ARRAY` a JSON array, `size()` contains the number of sub values +- `STRING` a JSON string, return the sub-string, unescaped, without surrounding quotes. UTF-8 is supported. +- `PRIMITIVE` an unrecognized JSON token, consider as an error +- `KEY` a JSON key in an object as a string +- `NULL` a JSON `null` value, automatically converted to `0` or `""` +- `BOOL_FALSE` a JSON `false` value, automatically converted to `0` or `""` +- `BOOL_TRUE` a JSON `true` value, automatically converted to `1` or `""` +- `UINT` a JSON unsigned int +- `INT` a JSON negative int +- `FLOAT` a JSON floating point value, i.e. a numerical value containing a decimal ot `.` + +Note: values are converted in this priority: 1/ `UINT`, 2/ `INT`, 3/ `FLOAT`. + +`JsonParserToken` support the following getters: +- `getBool()`: returns true if 'true' or non-zero int (default false) +- `getUInt()`: converts to unsigned int (default 0), boolean true is 1 +- `getInt()`: converts to signed int (default 0), boolean true is 1 +- `getULong()`: converts to unsigned 64 bits (default 0), boolean is 1 +- `getStr()`: converts to string (default "") + +There are variants of the same function for which you can choose the default values. Remember that using a getter if the token type is INVALID returns the default value. + +Conversion to `OBJECT` or `ARRAY`: +- `getObject()`: converts token to `OBJECT` or `INVALID` +- `getArray()`: converts token to `ARRAY` or `INVALID` + +For `JsonParserKey`: +- `getValue()`: returns the value token associated to the key + +## Checking Token types + +Type checking for `JsonParserToken`: +- `isSingleToken()` returns `true` for a single level token, `false` for `OBJECT`, `ARRAY` and `INVALID` +- `isKey()` returns `true` for a `KEY` within an `OBJECT` +- `isStr()` returns `true` for `STRING` (note: other types can still be read as strings with `getStr()`) +- `isNull()` returns `true` for `NULL` +- `isBool()` returns `true` for `BOOL_FALSE` or `BOOL_TRUE` +- `isUInt()` returns `true` for `UINT` (see below for number conversions) +- `isInt()` returns `true` for `INT` (see below for number conversions) +- `isFloat()` returns `true` for `FLOAT` (see below for number conversions) +- `isNum()` returns `true` for any numerical value, i.e. `UINT`, `INT` or `FLOAT` +- `isObject()` returns `true` for `OBJECT` +- `isArray()` returns `true` for `ARRAY` +- `isValid()` returns `true`for any type except `INVALID` + +JSMN for Tasmota provides sub-classes: +- `JsonParserKey` of type `KEY` or `INVALID`, used as a return value for Object iterators +- `JsonParserObject` of type `OBJECT` or `INVALID`, providing iterators +- `JsonParserArray` of type `ARRAY` or `INVALID`, providing iterators + +Converting from Token to Object or Array is done with `token.getObject()` or `token.getArray()`. If the conversion is invalid, the resulting object has type `INVALID` (see Error Handling). + +## Iterators and accessors for Objects and Arrays + +The `JsonParserObject` provides an easy to use iterator: +``` +JsonParserToken obj = <...> +for (auto key : obj) { + // key is of type JsonParserKey + JsonParserToken valie = key.getValue(); // retrieve the value associated to key +} +``` + +If the object contains only one element, you can retrieve it with `obj.getFirstElement()`. + +You can access any key with `obj["Key"]`. Note: the search is on purpose **case insensitive** as it is the norm in Tasmota. The search string can be in PROGMEM. If the token is not found, it is of type `INVALID`. + +The `JsonParserArray` provides an easy to use iterator: +``` +JsonParserArray arr = <...> +for (auto elt : arr) { + // elt is of type JsonParserToken +} +``` + +You can access any element in the array with the `[]` operator. Ex: `arr[0]` fof first element. If the index is invalid, the token has type `INVALID`. + +## Memory + +The `JsonParserToken` fits in 32 bits, so it can be freely returned or copied without any penalty of object copying. Hence it doesn't need the `const` modifier either, since it is always passed by value. + +## Error handling + +This library uses a `zero error` pattern. This means that calls never report any error nor throw exceptions. If something wrong happens (bad JSON, token not found...), function return an **Invalid Token**. You can call any function on an Invalid Token, they will always return the same Invalid Token (aka fixed point). + +You can easily test if the current token is invalid with the following: + +Short version: +``` +if (token) { /* token is valid */ } +``` + +Long version: +``` +if (token->isValiid()) { /* token is valid */ } +``` + +This pattern allows to cascade calls and check only the final result: +``` +char json_buffer[] = ""; +JsonParserObject json = JsonParser(json_buffer).getRootObject(); +JsonParserToken zbstatus_tok = json["ZbStatus"]; +JsonParserObject zbstatus = zbstatus_tok.getObject(); +if (zbstatus) { /* do some stuff */ + // reaching this point means: JSON is valid, there is a root object, there is a `ZbStatus` key and it contains a sub-object +} +``` + +Warning: there is an explicit convert to `bool` to allow the short version. Be careful, `(bool)token` is equivalent to `token->isValid()`, it is **NOT** equivalent to `token->getBool()`. + +## Limits + +Please keep in mind the current limits for this library: +- Maximum JSON buffer size 2047 bytes +- Maximum 63 JSON tokens +- No support for exponent in floats (i.e. `1.0e3` is invalid) + +These limits shouldn't be a problem since buffers in Tasmota are limited to 1.2KB. The support for exponent in floats is commented out and can be added if needed (slight increase in Flash size) \ No newline at end of file diff --git a/lib/jsmn-shadinger-1.0/library.properties b/lib/jsmn-shadinger-1.0/library.properties new file mode 100644 index 000000000..674aa76e7 --- /dev/null +++ b/lib/jsmn-shadinger-1.0/library.properties @@ -0,0 +1,8 @@ +name=JSMN JSON parser customized and optimized for ESP8266 and Tasmota +version=1.0 +author=Serge Zaitsev, Stephan Hadinger +maintainer=Stephan +sentence=Lightweight in-place JSON parser +paragraph= +url=https://github.com/zserge/jsmn +architectures=esp8266 diff --git a/lib/jsmn-shadinger-1.0/src/JsonParser.cpp b/lib/jsmn-shadinger-1.0/src/JsonParser.cpp new file mode 100644 index 000000000..f2ae50a97 --- /dev/null +++ b/lib/jsmn-shadinger-1.0/src/JsonParser.cpp @@ -0,0 +1,524 @@ +/* + JsonParser.h - lightweight JSON parser + + Copyright (C) 2020 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 . +*/ + +#include "JsonParser.h" +#include "WSTring.h" + +/*********************************************************************************************\ + * Utilities +\*********************************************************************************************/ + +const char * k_current_json_buffer = ""; + +/*********************************************************************************************\ + * Lightweight String to Float, because atof() or strtof() takes 10KB + * + * To remove code, exponents are not parsed + * (commented out below, just in case we need them after all) +\*********************************************************************************************/ +// Inspired from https://searchcode.com/codesearch/view/22115068/ +float json_strtof(const char* s) { + const char* p = s; + float value = 0.; + int32_t sign = +1; + float factor; + // unsigned int expo; + + while (isspace(*p)){ // skip any leading white-spaces + p++; + } + + switch (*p) { + case '-': sign = -1; + case '+': p++; + default : break; + } + + while ((unsigned int)(*p - '0') < 10u) { + value = value*10 + (*p++ - '0'); + } + + if (*p == '.' ) { + factor = 1.0f; + + p++; + while ((unsigned int)(*p - '0') < 10u) { + factor *= 0.1f; + value += (*p++ - '0') * factor; + } + } + +// if ( (*p | 32) == 'e' ) { +// expo = 0; +// factor = 10.L; + +// switch (*++p) { // ja hier weiß ich nicht, was mindestens nach einem 'E' folgenden MUSS. +// case '-': factor = 0.1; +// case '+': p++; +// break; +// case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': +// break; +// default : value = 0.L; +// p = s; +// goto done; +// } + +// while ( (unsigned int)(*p - '0') < 10u ) +// expo = 10 * expo + (*p++ - '0'); + +// while ( 1 ) { +// if ( expo & 1 ) +// value *= factor; +// if ( (expo >>= 1) == 0 ) +// break; +// factor *= factor; +// } +// } + +// done: + // if ( endptr != NULL ) + // *endptr = (char*)p; + + return value * sign; +} + +/*********************************************************************************************\ + * Read-only JSON token object, fits in 32 bits +\*********************************************************************************************/ + +void JsonParserToken::skipToken(void) { + // printf("skipToken type = %d %s\n", t->type, json_string + t->start); + switch (t->type) { + case JSMN_OBJECT: + skipObject(); + break; + case JSMN_ARRAY: + skipArray(); + break; + case JSMN_STRING: + case JSMN_PRIMITIVE: + case JSMN_KEY: + case JSMN_NULL: + case JSMN_BOOL_FALSE: + case JSMN_BOOL_TRUE: + case JSMN_FLOAT: + case JSMN_INT: + case JSMN_UINT: + t++; // skip 1 token + break; + case JSMN_INVALID: + default: + break; // end of stream, stop advancing + } +} + +void JsonParserToken::skipArray(void) { + if (t->type == JSMN_ARRAY) { + size_t obj_size = t->size; + t++; // array root + if (t->type == JSMN_INVALID) { return; } + for (uint32_t i=0; itype == JSMN_OBJECT) { + size_t obj_size = t->size; + t++; // object root + if (t->type == JSMN_INVALID) { return; } + for (uint32_t i=0; itype == JSMN_INVALID) { return; } + skipToken(); + } + } +} + +/*********************************************************************************************\ + * JsonParserArray +\*********************************************************************************************/ + +JsonParserArray::JsonParserArray(const jsmntok_t * token) : JsonParserToken(token) { + if (t->type != JSMN_ARRAY) { + t = &token_bad; + } +} +JsonParserArray::JsonParserArray(const JsonParserToken token) : JsonParserToken(token.t) { + if (t->type != JSMN_ARRAY) { + t = &token_bad; + } +} + +JsonParserArray::const_iterator::const_iterator(const JsonParserArray t): tok(t), remaining(0) { + if (tok.t == &token_bad) { tok.t = nullptr; } + if (nullptr != tok.t) { + // ASSERT type == JSMN_ARRAY by constructor + remaining = tok.t->size; + tok.nextOne(); // skip array root token + } +} + +JsonParserArray::const_iterator JsonParserArray::const_iterator::const_iterator::operator++() { + if (remaining == 0) { tok.t = nullptr; } + else { + remaining--; + tok.skipToken(); // munch value + if (tok.t->type == JSMN_INVALID) { tok.t = nullptr; } // unexpected end of stream + } + return *this; +} + +JsonParserToken JsonParserArray::operator[](int32_t i) const { + if ((i >= 0) && (i < t->size)) { + uint32_t index = 0; + for (const auto elt : *this) { + if (i == index) { + return elt; + } + index++; + } + } + // fallback + return JsonParserToken(&token_bad); +} + +/*********************************************************************************************\ + * JsonParserObject +\*********************************************************************************************/ + +JsonParserObject::JsonParserObject(const jsmntok_t * token) : JsonParserToken(token) { + if (t->type != JSMN_OBJECT) { + t = &token_bad; + } +} +JsonParserObject::JsonParserObject(const JsonParserToken token) : JsonParserToken(token.t) { + if (t->type != JSMN_OBJECT) { + t = &token_bad; + } +} + +JsonParserKey JsonParserObject::getFirstElement(void) const { + if (t->size > 0) { + return JsonParserKey(t+1); // return next element and cast to Key + } else { + return JsonParserKey(&token_bad); + } +} + +JsonParserObject::const_iterator::const_iterator(const JsonParserObject t): tok(t), remaining(0) { + if (tok.t == &token_bad) { tok.t = nullptr; } + if (nullptr != tok.t) { + // ASSERT type == JSMN_OBJECT by constructor + remaining = tok.t->size; + tok.nextOne(); + } +} + +JsonParserObject::const_iterator JsonParserObject::const_iterator::operator++() { + if (remaining == 0) { tok.t = nullptr; } + else { + remaining--; + tok.nextOne(); // munch key + if (tok.t->type == JSMN_INVALID) { tok.t = nullptr; } // unexpected end of stream + tok.skipToken(); // munch value + if (tok.t->type == JSMN_INVALID) { tok.t = nullptr; } // unexpected end of stream + } + return *this; +} + +/*********************************************************************************************\ + * JsonParserKey +\*********************************************************************************************/ + + +JsonParserKey::JsonParserKey(const jsmntok_t * token) : JsonParserToken(token) { + if (t->type != JSMN_KEY) { + t = &token_bad; + } +} +JsonParserKey::JsonParserKey(const JsonParserToken token) : JsonParserToken(token.t) { + if (t->type != JSMN_KEY) { + t = &token_bad; + } +} + +JsonParserToken JsonParserKey::getValue(void) const { + return JsonParserToken(t+1); +} + +/*********************************************************************************************\ + * Implementation for JSON Parser +\*********************************************************************************************/ + +// fall-back token object when parsing failed +const jsmntok_t token_bad = { JSMN_INVALID, 0, 0, 0 }; + +JsonParser::JsonParser(char * json_in) : + _size(0), + _token_len(0), + _tokens(nullptr), + _json(nullptr) +{ + parse(json_in); +} + +JsonParser::~JsonParser() { + this->free(); +} + +const JsonParserObject JsonParser::getRootObject(void) const { + return JsonParserObject(&_tokens[0]); +} + +const JsonParserToken JsonParser::operator[](int32_t i) const { +if ((_token_len > 0) && (i < _token_len)) { + return JsonParserToken(&_tokens[i]); + } else { + return JsonParserToken(&token_bad); + } +} + +// pointer arithmetic +// ptrdiff_t JsonParser::index(JsonParserToken token) const { +// return token.t - _tokens; +// } + +bool JsonParserToken::getBool(bool val) const { + if (t->type == JSMN_INVALID) { return val; } + if (t->type == JSMN_BOOL_TRUE) return true; + if (t->type == JSMN_BOOL_FALSE) return false; + if (isSingleToken()) return strtol(&k_current_json_buffer[t->start], nullptr, 0) != 0; + return false; +} +int32_t JsonParserToken::getInt(int32_t val) const { + if (t->type == JSMN_INVALID) { return val; } + if (t->type == JSMN_BOOL_TRUE) return 1; + if (isSingleToken()) return strtol(&k_current_json_buffer[t->start], nullptr, 0); + return 0; +} +uint32_t JsonParserToken::getUInt(uint32_t val) const { + if (t->type == JSMN_INVALID) { return val; } + if (t->type == JSMN_BOOL_TRUE) return 1; + if (isSingleToken()) return strtoul(&k_current_json_buffer[t->start], nullptr, 0); + return 0; +} +uint64_t JsonParserToken::getULong(uint64_t val) const { + if (t->type == JSMN_INVALID) { return val; } + if (t->type == JSMN_BOOL_TRUE) return 1; + if (isSingleToken()) return strtoull(&k_current_json_buffer[t->start], nullptr, 0); + return 0; +} +float JsonParserToken::getFloat(float val) const { + if (t->type == JSMN_INVALID) { return val; } + if (t->type == JSMN_BOOL_TRUE) return 1; + if (isSingleToken()) return json_strtof(&k_current_json_buffer[t->start]); + return 0; +} +const char * JsonParserToken::getStr(const char * val) const { + if (t->type == JSMN_INVALID) { return val; } + if (t->type == JSMN_NULL) return ""; + return (t->type >= JSMN_STRING) ? &k_current_json_buffer[t->start] : ""; +} + + +JsonParserObject JsonParserToken::getObject(void) const { return JsonParserObject(*this); } +JsonParserArray JsonParserToken::getArray(void) const { return JsonParserArray(*this); } + + +bool JsonParserToken::getBool(void) const { return getBool(false); } +int32_t JsonParserToken::getInt(void) const { return getInt(0); } +uint32_t JsonParserToken::getUInt(void) const { return getUInt(0); } +uint64_t JsonParserToken::getULong(void) const { return getULong(0); } +float JsonParserToken::getFloat(void) const { return getFloat(0); } +const char * JsonParserToken::getStr(void) const { return getStr(""); } + +int32_t JsonParserObject::getInt(const char * needle, int32_t val) const { + return (*this)[needle].getInt(val); +} +uint32_t JsonParserObject::getUInt(const char * needle, uint32_t val) const { + return (*this)[needle].getUInt(val); +} +uint64_t JsonParserObject::getULong(const char * needle, uint64_t val) const { + return (*this)[needle].getULong(val); +} +const char * JsonParserObject::getStr(const char * needle, const char * val) const { + return (*this)[needle].getStr(val); +} + +void JsonParser::parse(char * json_in) { + k_current_json_buffer = ""; + if (nullptr == json_in) { return; } + _json = json_in; + k_current_json_buffer = _json; + size_t json_len = strlen(json_in); + if (_size == 0) { + // first run is used to count tokens before allocation + jsmn_init(&this->_parser); + int32_t _token_len = jsmn_parse(&this->_parser, json_in, json_len, nullptr, 0); + if (_token_len <= 0) { return; } + _size = _token_len + 1; + } + allocate(); + jsmn_init(&this->_parser); + _token_len = jsmn_parse(&this->_parser, json_in, json_len, _tokens, _size); + // TODO error checking + if (_token_len >= 0) { + postProcess(json_len); + } +} + +// post process the parsing by pre-munching extended types +void JsonParser::postProcess(size_t json_len) { + // add an end marker + if (_size > _token_len) { + _tokens[_token_len].type = JSMN_INVALID; + _tokens[_token_len].start = json_len; + _tokens[_token_len].len = 0; + _tokens[_token_len].size = 0; + } + for (uint32_t i=0; i<_token_len; i++) { + jsmntok_t & tok = _tokens[i]; + + if (tok.type >= JSMN_STRING) { + // we modify to null-terminate the primitive + _json[tok.start + tok.len] = 0; + } + + if (tok.type == JSMN_STRING) { + if (tok.size == 1) { tok.type = JSMN_KEY; } + else { json_unescape(&_json[tok.start]); } + } else if (tok.type == JSMN_PRIMITIVE) { + if (tok.len >= 0) { + // non-null string + char c0 = _json[tok.start]; + switch (c0) { + case 'n': + case 'N': + tok.type = JSMN_NULL; + break; + case 't': + case 'T': + tok.type = JSMN_BOOL_TRUE; + break; + case 'f': + case 'F': + tok.type = JSMN_BOOL_FALSE; + break; + case '-': + case '0'...'9': + // look if there is a '.' in the string + if (nullptr != memchr(&_json[tok.start], '.', tok.len)) { + tok.type = JSMN_FLOAT; + } else if (c0 == '-') { + tok.type = JSMN_INT; + } else { + tok.type = JSMN_UINT; + } + break; + default: + tok.type = JSMN_PRIMITIVE; + break; + } + } else { + tok.type = JSMN_PRIMITIVE; + } + } + } +} + +JsonParserToken JsonParserObject::operator[](const char * needle) const { + // key can be in PROGMEM + if ((!this->isValid()) || (nullptr == needle) || (0 == pgm_read_byte(needle))) { + return JsonParserToken(&token_bad); + } + // if needle == "?" then we return the first valid key + bool wildcard = (strcmp_P("?", needle) == 0); + + for (const auto key : *this) { + if (wildcard) { return key.getValue(); } + if (0 == strcasecmp_P(key.getStr(), needle)) { return key.getValue(); } + } + // if not found + return JsonParserToken(&token_bad); +} + + +JsonParserToken JsonParserObject::findStartsWith(const char * needle) const { + // key can be in PROGMEM + if ((!this->isValid()) || (nullptr == needle) || (0 == pgm_read_byte(needle))) { + return JsonParserToken(&token_bad); + } + + String needle_s((const __FlashStringHelper *)needle); + needle_s.toLowerCase(); + + for (const auto key : *this) { + String key_s(key.getStr()); + key_s.toLowerCase(); + + if (key_s.startsWith(needle_s)) { + return key.getValue(); + } + } + // if not found + return JsonParserToken(&token_bad); +} + +const char * JsonParserObject::findConstCharNull(const char * needle) const { + const char * r = (*this)[needle].getStr(); + if (*r == 0) { r = nullptr; } // if empty string + return r; +} + +// JsonParserToken JsonParser::find(JsonParserObject obj, const char *needle, bool case_sensitive) const { +// // key can be in PROGMEM +// if ((!obj.isValid()) || (nullptr == needle) || (0 == pgm_read_byte(needle))) { +// return JsonParserToken(&token_bad); +// } +// // if needle == "?" then we return the first valid key +// bool wildcard = (strcmp_P("?", needle) == 0); + +// for (const auto key : obj) { +// if (wildcard) { return key.getValue(); } +// if (case_sensitive) { +// if (0 == strcmp_P(this->getStr(key), needle)) { return key.getValue(); } +// } else { +// if (0 == strcasecmp_P(this->getStr(key), needle)) { return key.getValue(); } +// } +// } +// // if not found +// return JsonParserToken(&token_bad); +// } + +void JsonParser::free(void) { + if (nullptr != _tokens) { + delete[] _tokens; // TODO + _tokens = nullptr; + } +} + +void JsonParser::allocate(void) { + this->free(); + if (_size != 0) { + _tokens = new jsmntok_t[_size]; + } +} diff --git a/lib/jsmn-shadinger-1.0/src/JsonParser.h b/lib/jsmn-shadinger-1.0/src/JsonParser.h new file mode 100644 index 000000000..42886d628 --- /dev/null +++ b/lib/jsmn-shadinger-1.0/src/JsonParser.h @@ -0,0 +1,260 @@ +/* + JsonParser.h - lightweight JSON parser + + Copyright (C) 2020 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 . +*/ + +#ifndef __JSON_PARSER__ +#define __JSON_PARSER__ + +#include "jsmn.h" +#include +#include + +// #define strcmp_P(x, y) strcmp(x,y) +// #define strcasecmp_P(x,y) strcasecmp(x,y) +// #define pgm_read_byte(x) (*(uint8_t*)(x)) +#ifndef ARRAY_SIZE +#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0])) +#endif + +/*********************************************************************************************\ + * Utilities +\*********************************************************************************************/ + +// The code uses a zero-error approach. Functions never return an error code nor an exception. +// In case an operation fails, it returns an "Invalid Token". +// To know if a token is valid, use the `isValid()` method or just use it in an if statement. +// +// Internally, the bad token is a pointer to a constant token of type JSMN_INVALID +// fall-back token object when parsing failed +const extern jsmntok_t token_bad; + +// To reduce code size, the current buffer is stored in a global variable. +// This prevents all calls to add this parameter on the stack and reduces code size. +// The caveat is that code is not re-entrant. +// If you ever intermix two or more JSON parsers, use `parser->setCurrent()` before further calls +// +// the current json buffer being used, for convenience +// Warning: this makes code non-reentrant. +extern const char * k_current_json_buffer; + +/*********************************************************************************************\ + * Read-only JSON token object, fits in 32 bits +\*********************************************************************************************/ +// forward class declarations +class JsonParserObject; +class JsonParserArray; + +class JsonParserToken { +public: + + // constructor + // If parameter is null, we use the Invalid Token instead. + JsonParserToken(const jsmntok_t * token) : t(token) { + if (nullptr == t) { t = &token_bad; } + } + // no explicit destructor (not needed) + + inline bool isValid(void) const { return t->type != JSMN_INVALID; } + inline size_t size(void) const { return t->size; } + + inline bool isSingleToken(void) const { return (t->type >= JSMN_STRING); } + inline bool isKey(void) const { return (t->type == JSMN_KEY); } + inline bool isStr(void) const { return (t->type == JSMN_STRING); } + inline bool isNull(void) const { return (t->type == JSMN_NULL); } + inline bool isBool(void) const { return (t->type == JSMN_BOOL_TRUE) || (t->type == JSMN_BOOL_FALSE); } + inline bool isFloat(void) const { return (t->type == JSMN_FLOAT); } + inline bool isInt(void) const { return (t->type == JSMN_INT); } + inline bool isUint(void) const { return (t->type == JSMN_UINT); } + inline bool isNum(void) const { return (t->type >= JSMN_FLOAT) && (t->type <= JSMN_UINT); } + inline bool isObject(void) const { return (t->type == JSMN_OBJECT); } + inline bool isArray(void) const { return (t->type == JSMN_ARRAY); } + + // move to token immediately after in the buffer + void nextOne(void) { if (t->type != JSMN_INVALID) { t++; } } + + // conversion operators + // Warning - bool does not test for Boolean value but for validity, i.e. equivalent to token.valid() + inline explicit operator bool() const { return t->type != JSMN_INVALID; }; + + // all the following conversion will try to give a meaninful value + // if the content is not of the right type or the token is invalid, returns the 'default' + bool getBool(void) const; // true if 'true' or non-zero int (default false) + int32_t getInt(void) const; // convert to int (default 0) + uint32_t getUInt(void) const; // convert to unsigned int (default 0) + uint64_t getULong(void) const; // convert to unsigned 64 bits (default 0) + float getFloat(void) const; // convert to float (default 0), does not support exponent + const char * getStr(void) const; // convert to string (default "") + + // same as above, but you can choose the default value + bool getBool(bool val) const; + int32_t getInt(int32_t val) const; + uint32_t getUInt(uint32_t val) const; + uint64_t getULong(uint64_t val) const; + float getFloat(float val) const; + const char * getStr(const char * val) const; + + // convert to JsonParserObject or JsonParserArray, or Invalid Token if not allowed + JsonParserObject getObject(void) const; + JsonParserArray getArray(void) const; + +public: + // the following should be 'protected' but then it can't be accessed by iterators + const jsmntok_t * t; + // skip the next Token as a whole (i.e. skip an entire array) + void skipToken(void); + +protected: + + // skip the next token knowing it's an array + void skipArray(void); + + // skip the next token knowing it's an object + void skipObject(void); +}; + +/*********************************************************************************************\ + * Subclass for Key +\*********************************************************************************************/ +class JsonParserKey : public JsonParserToken { +public: + JsonParserKey(const jsmntok_t * token); + explicit JsonParserKey(const JsonParserToken token); + + // get the value token associated to the key + JsonParserToken getValue(void) const; +}; + +/*********************************************************************************************\ + * Subclass for Object +\*********************************************************************************************/ +class JsonParserObject : public JsonParserToken { +public: + JsonParserObject(const jsmntok_t * token); + explicit JsonParserObject(const JsonParserToken token); + + // find key with name, case-insensitive, '?' matches any key. Returns Invalid Token if not found + JsonParserToken operator[](const char * needle) const; + // find a key starting with `needle`, case insensitive + JsonParserToken findStartsWith(const char * needle) const; + // find a key, case-insensitive, return nullptr if not found (instead of "") + const char * findConstCharNull(const char * needle) const; + + // all-in-one methods: search for key (case insensitive), convert value and set default + int32_t getInt(const char *, int32_t) const; + uint32_t getUInt(const char *, uint32_t) const; + uint64_t getULong(const char *, uint64_t) const; + float getFloat(const char *, float) const; + const char * getStr(const char *, const char *) const; + + // get first element (key) + JsonParserKey getFirstElement(void) const; + + // + // const iterator + // + class const_iterator { + public: + const_iterator(const JsonParserObject t); + const_iterator operator++(); + bool operator!=(const_iterator & other) const { return tok.t != other.tok.t; } + const JsonParserKey operator*() const { return JsonParserKey(tok); } + private: + JsonParserToken tok; + size_t remaining; + }; + const_iterator begin() const { return const_iterator(*this); } // start with 'head' + const_iterator end() const { return const_iterator(JsonParserObject(&token_bad)); } // end with null pointer +}; + +/*********************************************************************************************\ + * Subclass for Array +\*********************************************************************************************/ +class JsonParserArray : public JsonParserToken { +public: + JsonParserArray(const jsmntok_t * token); + JsonParserArray(const JsonParserToken token); + + // get the element if index `i` from 0 to `size() - 1` + JsonParserToken operator[](int32_t i) const; + + // + // const iterator + // + class const_iterator { + public: + const_iterator(const JsonParserArray t); + const_iterator operator++(); + bool operator!=(const_iterator & other) const { return tok.t != other.tok.t; } + const JsonParserToken operator*() const { return tok; } + private: + JsonParserToken tok; + size_t remaining; + }; + const_iterator begin() const { return const_iterator(*this); } // start with 'head' + const_iterator end() const { return const_iterator(JsonParserArray(&token_bad)); } // end with null pointer +}; + +/*********************************************************************************************\ + * JSON Parser +\*********************************************************************************************/ + +class JsonParser { +public: + // constructor, parse the json buffer + // Warning: the buffer is modified in the process (in-place parsing) + // Input: `json_in` can be nullptr, but CANNOT be in PROGMEM (remember we need to change characters in-place) + JsonParser(char * json_in); + + // destructor + ~JsonParser(); + + // set the current buffer for attribute access (i.e. set the global) + void setCurrent(void) { k_current_json_buffer = _json; } + + // test if the parsing was successful + inline explicit operator bool() const { return _token_len > 0; } + + const JsonParserToken getRoot(void) { return JsonParserToken(&_tokens[0]); } + // const JsonParserObject getRootObject(void) { return JsonParserObject(&_tokens[0]); } + const JsonParserObject getRootObject(void) const; + + // pointer arithmetic + // ptrdiff_t index(JsonParserToken token) const; + +protected: + uint16_t _size; // size of tokens buffer + int16_t _token_len; // how many tokens have been parsed + jsmntok_t * _tokens; // pointer to token buffer + jsmn_parser _parser; // jmsn_parser structure + char * _json; // json buffer + + // disallocate token buffer + void free(void); + + // allocate token buffer of size _size + void allocate(void); + + // access tokens by index + const JsonParserToken operator[](int32_t i) const; + // parse + void parse(char * json_in); + // post-process parsing: insert NULL chars to split strings, compute a more precise token type + void postProcess(size_t json_len); +}; + +#endif // __JSON_PARSER__ \ No newline at end of file diff --git a/lib/jsmn-shadinger-1.0/src/JsonParser.hpp.gch b/lib/jsmn-shadinger-1.0/src/JsonParser.hpp.gch new file mode 100644 index 000000000..e3d9ed686 Binary files /dev/null and b/lib/jsmn-shadinger-1.0/src/JsonParser.hpp.gch differ diff --git a/lib/jsmn-shadinger-1.0/src/jsmn.cpp b/lib/jsmn-shadinger-1.0/src/jsmn.cpp new file mode 100644 index 000000000..8f7a59708 --- /dev/null +++ b/lib/jsmn-shadinger-1.0/src/jsmn.cpp @@ -0,0 +1,449 @@ +/* + * MIT License + * + * Copyright (c) 2010 Serge Zaitsev + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "jsmn.h" + +#define JSMN_STRICT // force strict mode + +const uint32_t JSMN_START_MAX = (1U << JSMN_START_B) - 1; +const uint32_t JSMN_LEN_MAX = (1U << JSMN_LEN_B) - 1; + +/** + * Allocates a fresh unused token from the token pool. + */ +static jsmntok_t *jsmn_alloc_token(jsmn_parser *parser, jsmntok_t *tokens, + const size_t num_tokens) { + jsmntok_t *tok; + if (parser->toknext >= num_tokens) { + return NULL; + } + tok = &tokens[parser->toknext++]; + tok->start = JSMN_START_MAX; + tok->len = JSMN_LEN_MAX; + tok->size = 0; + return tok; +} + +/** + * Fills token type and boundaries. + */ +static void jsmn_fill_token(jsmntok_t *token, const jsmntype_t type, + const int start, const int len) { + token->type = type; + token->start = start; + token->len = len; + token->size = 0; +} + +/** + * Fills next available token with JSON primitive. + */ +static int jsmn_parse_primitive(jsmn_parser *parser, const char *js, + const size_t len, jsmntok_t *tokens, + const size_t num_tokens) { + jsmntok_t *token; + int start; + + start = parser->pos; + + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + switch (js[parser->pos]) { +#ifndef JSMN_STRICT + /* In strict mode primitive must be followed by "," or "}" or "]" */ + case ':': +#endif + case '\t': + case '\r': + case '\n': + case ' ': + case ',': + case ']': + case '}': + goto found; + default: + /* to quiet a warning from gcc*/ + break; + } + if (js[parser->pos] < 32 || js[parser->pos] >= 127) { + parser->pos = start; + return JSMN_ERROR_INVAL; + } + } +#ifdef JSMN_STRICT + /* In strict mode primitive must be followed by a comma/object/array */ + parser->pos = start; + return JSMN_ERROR_PART; +#endif + +found: + if (tokens == NULL) { + parser->pos--; + return 0; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) { + parser->pos = start; + return JSMN_ERROR_NOMEM; + } + jsmn_fill_token(token, JSMN_PRIMITIVE, start, parser->pos - start); + parser->pos--; + return 0; +} + +/** + * Fills next token with JSON string. + */ +static int jsmn_parse_string(jsmn_parser *parser, const char *js, + const size_t len, jsmntok_t *tokens, + const size_t num_tokens) { + jsmntok_t *token; + + int start = parser->pos; + + parser->pos++; + + /* Skip starting quote */ + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + char c = js[parser->pos]; + + /* Quote: end of string */ + if (c == '\"') { + if (tokens == NULL) { + return 0; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) { + parser->pos = start; + return JSMN_ERROR_NOMEM; + } + jsmn_fill_token(token, JSMN_STRING, start + 1, parser->pos - start - 1); + return 0; + } + + /* Backslash: Quoted symbol expected */ + if (c == '\\' && parser->pos + 1 < len) { + int i; + parser->pos++; + switch (js[parser->pos]) { + /* Allowed escaped symbols */ + case '\"': + case '/': + case '\\': + case 'b': + case 'f': + case 'r': + case 'n': + case 't': + break; + /* Allows escaped symbol \uXXXX */ + case 'u': + parser->pos++; + for (i = 0; i < 4 && parser->pos < len && js[parser->pos] != '\0'; + i++) { + /* If it isn't a hex character we have an error */ + if (!((js[parser->pos] >= 48 && js[parser->pos] <= 57) || /* 0-9 */ + (js[parser->pos] >= 65 && js[parser->pos] <= 70) || /* A-F */ + (js[parser->pos] >= 97 && js[parser->pos] <= 102))) { /* a-f */ + parser->pos = start; + return JSMN_ERROR_INVAL; + } + parser->pos++; + } + parser->pos--; + break; + /* Unexpected symbol */ + default: + parser->pos = start; + return JSMN_ERROR_INVAL; + } + } + } + parser->pos = start; + return JSMN_ERROR_PART; +} + +/** + * Parse JSON string and fill tokens. + */ +JSMN_API int jsmn_parse(jsmn_parser *parser, const char *js, const size_t len, + jsmntok_t *tokens, const unsigned int num_tokens) { + int r; + int i; + jsmntok_t *token; + int count = parser->toknext; + + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + char c; + jsmntype_t type; + + c = js[parser->pos]; + switch (c) { + case '{': + case '[': + count++; + if (tokens == NULL) { + break; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) { + return JSMN_ERROR_NOMEM; + } + if (parser->toksuper != -1) { + jsmntok_t *t = &tokens[parser->toksuper]; +#ifdef JSMN_STRICT + /* In strict mode an object or array can't become a key */ + if (t->type == JSMN_OBJECT) { + return JSMN_ERROR_INVAL; + } +#endif + t->size++; + } + token->type = (c == '{' ? JSMN_OBJECT : JSMN_ARRAY); + token->start = parser->pos; + parser->toksuper = parser->toknext - 1; + break; + case '}': + case ']': + if (tokens == NULL) { + break; + } + type = (c == '}' ? JSMN_OBJECT : JSMN_ARRAY); + for (i = parser->toknext - 1; i >= 0; i--) { + token = &tokens[i]; + if ((token->start != JSMN_START_MAX) && (token->len == JSMN_LEN_MAX)) { + if (token->type != type) { + return JSMN_ERROR_INVAL; + } + parser->toksuper = -1; + token->len = parser->pos + 1 - token->start; + break; + } + } + /* Error if unmatched closing bracket */ + if (i == -1) { + return JSMN_ERROR_INVAL; + } + for (; i >= 0; i--) { + token = &tokens[i]; + if ((token->start != JSMN_START_MAX) && (token->len == JSMN_LEN_MAX)) { + parser->toksuper = i; + break; + } + } + break; + case '\"': + r = jsmn_parse_string(parser, js, len, tokens, num_tokens); + if (r < 0) { + return r; + } + count++; + if (parser->toksuper != -1 && tokens != NULL) { + tokens[parser->toksuper].size++; + } + break; + case '\t': + case '\r': + case '\n': + case ' ': + break; + case ':': + parser->toksuper = parser->toknext - 1; + break; + case ',': + if (tokens != NULL && parser->toksuper != -1 && + tokens[parser->toksuper].type != JSMN_ARRAY && + tokens[parser->toksuper].type != JSMN_OBJECT) { + for (i = parser->toknext - 1; i >= 0; i--) { + if (tokens[i].type == JSMN_ARRAY || tokens[i].type == JSMN_OBJECT) { + if ((tokens[i].start != JSMN_START_MAX) && (tokens[i].len == JSMN_LEN_MAX)) { + parser->toksuper = i; + break; + } + } + } + } + break; +#ifdef JSMN_STRICT + /* In strict mode primitives are: numbers and booleans */ + case '-': + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case 't': + case 'f': + case 'n': + /* And they must not be keys of the object */ + if (tokens != NULL && parser->toksuper != -1) { + const jsmntok_t *t = &tokens[parser->toksuper]; + if (t->type == JSMN_OBJECT || + (t->type == JSMN_STRING && t->size != 0)) { + return JSMN_ERROR_INVAL; + } + } +#else + /* In non-strict mode every unquoted value is a primitive */ + default: +#endif + r = jsmn_parse_primitive(parser, js, len, tokens, num_tokens); + if (r < 0) { + return r; + } + count++; + if (parser->toksuper != -1 && tokens != NULL) { + tokens[parser->toksuper].size++; + } + break; + +#ifdef JSMN_STRICT + /* Unexpected char in strict mode */ + default: + return JSMN_ERROR_INVAL; +#endif + } + } + + if (tokens != NULL) { + for (i = parser->toknext - 1; i >= 0; i--) { + /* Unmatched opened object or array */ + if ((tokens[i].start != JSMN_START_MAX) && (tokens[i].len == JSMN_LEN_MAX)) { + return JSMN_ERROR_PART; + } + } + } + + return count; +} + +/** + * Creates a new parser based over a given buffer with an array of tokens + * available. + */ +JSMN_API void jsmn_init(jsmn_parser *parser) { + parser->pos = 0; + parser->toknext = 0; + parser->toksuper = -1; +} + +// +// Json in-place string unescape +// inpired from https://github.com/mjansson/json/blob/master/json.h +// +//! Define a bitmask with the given number of bits set to 1 +#define JSON_BITMASK(numbits) ((1U << (numbits)) - 1) + +static uint32_t json_get_num_bytes_as_utf8(uint32_t val) { + if (val >= 0x04000000) return 6; + else if (val >= 0x00200000) return 5; + else if (val >= 0x00010000) return 4; + else if (val >= 0x00000800) return 3; + else if (val >= 0x00000080) return 2; + return 1; +} + +static uint32_t json_encode_utf8(char* str, uint32_t val) { + if (val < 0x80) { + *str = (char)val; + return 1; + } + + //Get number of _extra_ bytes + uint32_t num = json_get_num_bytes_as_utf8(val) - 1; + + *str++ = (char)((0x80U | (JSON_BITMASK(num) << (7U - num))) | + ((val >> (6U * num)) & JSON_BITMASK(6U - num))); + for (uint32_t j = 1; j <= num; ++j) + *str++ = (char)(0x80U | ((val >> (6U * (num - j))) & 0x3F)); + + return num + 1; +} + +void json_unescape(char* string) { + size_t outlength = 0; + uint32_t hexval, numbytes; + + char c; + for (uint32_t i = 0; (c = string[i]) != 0; i++) { + if ('\\' == c) { + c = string[++i]; + switch (c) { + case 0: + return; // end of stream + case '\"': + case '/': + case '\\': + string[outlength++] = c; + break; + + case 'b': + string[outlength++] = '\b'; + break; + case 'f': + string[outlength++] = '\f'; + break; + case 'r': + string[outlength++] = '\r'; + break; + case 'n': + string[outlength++] = '\n'; + break; + case 't': + string[outlength++] = '\t'; + break; + + case 'u': + { + uint32_t hexval = 0; + for (uint32_t j = 0; j < 4; ++j) { + char val = string[++i]; + if (0 == val) { return; } // we reached end of string + uint32_t uival = 0; + if ((val >= 'a') && (val <= 'f')) + uival = 10 + (val - 'a'); + else if ((val >= 'A') && (val <= 'F')) + uival = 10 + (val - 'A'); + else if ((val >= '0') && (val <= '9')) + uival = val - '0'; + hexval |= uival << (3 - j); + } + numbytes = json_get_num_bytes_as_utf8(hexval); + outlength += json_encode_utf8(string + outlength, hexval); + } + break; + + default: + break; + } + } + else { + string[outlength++] = c; + } + } +} \ No newline at end of file diff --git a/lib/jsmn-shadinger-1.0/src/jsmn.h b/lib/jsmn-shadinger-1.0/src/jsmn.h new file mode 100644 index 000000000..36fd11db8 --- /dev/null +++ b/lib/jsmn-shadinger-1.0/src/jsmn.h @@ -0,0 +1,118 @@ +/* + * MIT License + * + * Copyright (c) 2010 Serge Zaitsev + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#ifndef JSMN_H +#define JSMN_H + +#include +#include + +// #ifdef JSMN_STATIC +// #define JSMN_API static +// #else +#define JSMN_API extern +// #endif + +/** + * JSON type identifier. Basic types are: + * o Object + * o Array + * o String + * o Other primitive: number, boolean (true/false) or null + */ +typedef enum { + // end market + JSMN_INVALID = 0, // type == 0 is invalid + JSMN_OBJECT = 1, + JSMN_ARRAY = 2, + JSMN_STRING = 3, + JSMN_PRIMITIVE = 4, + // new types created during post-processing + JSMN_KEY = 5, // JSMN_STRING with size 1 + JSMN_NULL = 6, // JSMN_PRIMITIVE starting with 'n' + JSMN_BOOL_FALSE = 7, // JSMN_PRIMITIVE starting with 'f' or 'F' + JSMN_BOOL_TRUE = 8, // JSMN_PRIMITIVE starting with 't' or 'T' + JSMN_FLOAT = 9, // JSMN_PRIMITIVE starting with '.', '-', '0-9' and containing a '.' + JSMN_INT = 10, // JSMN_PRIMITIVE starting with '-', '0-9' and not containing a '.' + JSMN_UINT = 11, // JSMN_PRIMITIVE starting with '0-9' and not containing a '.' +} jsmntype_t; + +enum jsmnerr { + /* Not enough tokens were provided */ + JSMN_ERROR_NOMEM = -1, + /* Invalid character inside JSON string */ + JSMN_ERROR_INVAL = -2, + /* The string is not a full JSON packet, more bytes expected */ + JSMN_ERROR_PART = -3 +}; + +/** + * JSON token description. + * type type (object, array, string etc.) + * start start position in JSON data string + * end end position in JSON data string + */ +// size of bitfield, sum is 32 +#define JSMN_TYPE_B 4 +#define JSMN_SIZE_B 6 // max 63 items per level (ex: max 63 keys per object) +#define JSMN_START_B 11 // max 2KB input buffer +#define JSMN_LEN_B 11 // max 2KB per item + +typedef struct jsmntok { + jsmntype_t type : JSMN_TYPE_B; + unsigned int size : JSMN_SIZE_B; + unsigned int start : JSMN_START_B; + unsigned int len : JSMN_LEN_B; +} jsmntok_t; + +/** + * JSON parser. Contains an array of token blocks available. Also stores + * the string being parsed now and current position in that string. + */ +typedef struct jsmn_parser { + unsigned int pos; /* offset in the JSON string */ + unsigned int toknext; /* next token to allocate */ + int toksuper; /* superior token node, e.g. parent object or array */ +} jsmn_parser; + +/** + * Create JSON parser over an array of tokens + */ +JSMN_API void jsmn_init(jsmn_parser *parser); + +/** + * Run JSON parser. It parses a JSON data string into and array of tokens, each + * describing + * a single JSON object. + */ +JSMN_API int jsmn_parse(jsmn_parser *parser, const char *js, const size_t len, + jsmntok_t *tokens, const unsigned int num_tokens); + +/** + * + * In-place json unescape + * + */ +void json_unescape(char* string); + +#endif /* JSMN_H */ diff --git a/lib/jsmn-shadinger-1.0/test/test-json.cpp b/lib/jsmn-shadinger-1.0/test/test-json.cpp new file mode 100644 index 000000000..9ede0a2bb --- /dev/null +++ b/lib/jsmn-shadinger-1.0/test/test-json.cpp @@ -0,0 +1,55 @@ +#include +#include +#include +#include +#include "../src/JsonParser.h" + + +static char test_simple[] = "{\"Device\":\"0x9C33\",\"Illuminance\":42,\"Occupancy\":1,\"Endpoint\":1,\"LinkQuality\":59}"; +static char test_moderate[] = "{\"ZbReceived\":{\"Prez\":{\"Device\":\"0x9C33\",\"Illuminance\":42,\"Occupancy\":1,\"Endpoint\":1,\"LinkQuality\":59}}}"; +static char test_complex[] = "{\"ZbStatus3\":[{\"Device\":\"0x7869\",\"INT\":-3,\"Name\":\"Tilt\",\"IEEEAddr\":\"0x00158D00031310F4\",\"ModelId\":\"lumi.vibration.aq1\",\"Manufacturer\":\"LUMI\",\"Endpoints\":{\"0x01\":{\"ProfileId\":\"0x0104\",\"ClustersIn\":[\"0x0000\",\"0x0003\",\"0x0019\",\"0x0101\"],\"ClustersOut\":[\"0x0000\",\"0x0004\",\"0x0003\",\"0x0005\",\"0x0019\",\"0x0101\"]},\"0x02\":{\"ProfileId\":\"0x0000\\ta\",\"ClustersIn\":[2],\"ClustersOut\":[-3,0.4,5.8]}}}]}"; + +int main(int argc, char* argv[]) { + printf("Starting... sizeof = %lu / %lu\n", sizeof(jsmntok_t), sizeof(JsonParserToken)); + + // char * json_str = test_complex; + char * json_str = test_simple; + + JsonParser parser(64); // size for 64 tokens + + int r = parser.parse(json_str); + + printf("r = %d\n", r); + + for (uint32_t i=0; istart; + uint32_t len = token.t->len; + printf("Tok[%2d]= type=%s, start=%d, len=%d, size=%d, str ='%s'\n", i, JSMNTypeName(token.t->type), start, len, token.t->size, (token.t->type >= JSMN_STRING || 1) ? &json_str[start] : ""); + } + printf("==================\n"); + JsonParserObject root = parser.getRootObject(); + + for (const auto key : root) { + // printf("Index = %ld\n", parser.index(key)); + JsonParserToken value = key.getValue(); + printf("Key = %s, Val type = %s\n", parser.getStr(key), JSMNTypeName(value.t->type)); + if (value.isArray()) { + for (const auto arr_val : JsonParserArray(value)) { + printf("Array = %s, type = %s\n", parser.getStr(arr_val), JSMNTypeName(arr_val.t->type)); + } + } else { + printf("Value = %s\n", parser.getStr(value)); + } + } + + // root.nextOne(); + // printf("Index = %ld\n", parser.index(root)); + // root.skipObject(); + // printf("Index = %ld\n", parser.index(root)); + + JsonParserToken oc = parser.GetCaseInsensitive(root, "occupancy"); + printf("Looking for 'Occupancy': %s, %d\n", parser.getStr(oc), parser.getInt(oc)); + JsonParserToken oc2 = parser.GetCaseInsensitive(root, "occupanc"); + printf("Looking for 'Occupanc': %s, %d\n", parser.getStr(oc2), parser.getInt(oc2)); +} diff --git a/tasmota/CHANGELOG.md b/tasmota/CHANGELOG.md index 689b71748..2af073b29 100644 --- a/tasmota/CHANGELOG.md +++ b/tasmota/CHANGELOG.md @@ -13,6 +13,7 @@ - Add new shutter modes (#9244) - Add Zigbee auto-config when pairing - Add support for MLX90640 IR array temperature sensor by Christian Baars +- Change replace ArduinoJson with JSMN for JSON parsing ### 8.5.0 20200907 diff --git a/tasmota/support.ino b/tasmota/support.ino index eddffeab6..0c6ab80d3 100644 --- a/tasmota/support.ino +++ b/tasmota/support.ino @@ -29,6 +29,7 @@ extern struct rst_info resetInfo; \*********************************************************************************************/ #include +#include "JsonParser.h" Ticker tickerOSWatch; @@ -1412,8 +1413,9 @@ bool GetUsedInModule(uint32_t val, uint16_t *arr) return false; } -bool JsonTemplate(const char* dataBuf) +bool JsonTemplate(char* dataBuf) { +#if 0 // {"NAME":"Generic","GPIO":[17,254,29,254,7,254,254,254,138,254,139,254,254],"FLAG":1,"BASE":255} if (strlen(dataBuf) < 9) { return false; } // Workaround exception if empty JSON like {} - Needs checks @@ -1454,6 +1456,46 @@ bool JsonTemplate(const char* dataBuf) Settings.user_template_base = base -1; // Default WEMOS } return true; +#else + // {"NAME":"Generic","GPIO":[17,254,29,254,7,254,254,254,138,254,139,254,254],"FLAG":1,"BASE":255} + + if (strlen(dataBuf) < 9) { return false; } // Workaround exception if empty JSON like {} - Needs checks + + JsonParserObject root = JsonParser((char*) dataBuf).getRootObject(); + if (!root) { return false; } + + // All parameters are optional allowing for partial changes + JsonParserToken val = root[PSTR(D_JSON_NAME)]; + if (val) { + SettingsUpdateText(SET_TEMPLATE_NAME, val.getStr()); + } + JsonParserArray arr = root[PSTR(D_JSON_GPIO)]; + if (arr) { + for (uint32_t i = 0; i < ARRAY_SIZE(Settings.user_template.gp.io); i++) { +#ifdef ESP8266 + Settings.user_template.gp.io[i] = arr[i].getUInt(); +#else // ESP32 + uint16_t gpio = arr[i].getUInt(); + if (gpio == (AGPIO(GPIO_NONE) +1)) { + gpio = AGPIO(GPIO_USER); + } + Settings.user_template.gp.io[i] = gpio; +#endif + } + } + val = root[PSTR(D_JSON_FLAG)]; + if (val) { + uint32_t flag = val.getUInt(); + memcpy(&Settings.user_template.flag, &flag, sizeof(gpio_flag)); + } + val = root[PSTR(D_JSON_BASE)]; + if (val) { + uint32_t base = val.getUInt(); + if ((0 == base) || !ValidTemplateModule(base -1)) { base = 18; } + Settings.user_template_base = base -1; // Default WEMOS + } + return true; +#endif } void TemplateJson(void) diff --git a/tasmota/support_json.ino b/tasmota/support_json.ino index b78c68eab..f1b2d1fd2 100644 --- a/tasmota/support_json.ino +++ b/tasmota/support_json.ino @@ -88,6 +88,7 @@ String EscapeJSONString(const char *str) { // // If the key is not found, returns a nullptr // Input: needle cannot be NULL but may be PROGMEM +#if 0 const JsonVariant &GetCaseInsensitive(const JsonObject &json, const char *needle) { // key can be in PROGMEM // if needle == "?" then we return the first valid key @@ -113,3 +114,4 @@ const JsonVariant &GetCaseInsensitive(const JsonObject &json, const char *needle bool HasKeyCaseInsensitive(const JsonObject &json, const char *needle) { return &GetCaseInsensitive(json, needle) != nullptr; } +#endif \ No newline at end of file diff --git a/tasmota/xdrv_01_webserver.ino b/tasmota/xdrv_01_webserver.ino index ef80c4abf..16daa8f6f 100644 --- a/tasmota/xdrv_01_webserver.ino +++ b/tasmota/xdrv_01_webserver.ino @@ -27,6 +27,8 @@ #define XDRV_01 1 +#include "JsonParser.h" + #ifndef WIFI_SOFT_AP_CHANNEL #define WIFI_SOFT_AP_CHANNEL 1 // Soft Access Point Channel number between 1 and 11 as used by WifiManager web GUI #endif @@ -3344,6 +3346,7 @@ bool JsonWebColor(const char* dataBuf) // Default pre v7 (Light theme) // {"WebColor":["#000","#fff","#f2f2f2","#000","#fff","#000","#fff","#f00","#008000","#fff","#1fa3ec","#0e70a4","#d43535","#931f1f","#47c266","#5aaf6f","#fff","#999","#000"]} // {"WebColor":["#000000","#ffffff","#f2f2f2","#000000","#ffffff","#000000","#ffffff","#ff0000","#008000","#ffffff","#1fa3ec","#0e70a4","#d43535","#931f1f","#47c266","#5aaf6f","#ffffff","#999999","#000000"]} +#if 0 char dataBufLc[strlen(dataBuf) +1]; LowerCase(dataBufLc, dataBuf); RemoveSpace(dataBufLc); @@ -3362,6 +3365,21 @@ bool JsonWebColor(const char* dataBuf) } } } +#else + JsonParserObject root = JsonParser((char*) dataBuf).getRootObject(); + JsonParserArray arr = root[PSTR(D_CMND_WEBCOLOR)].getArray(); + if (arr) { // if arr is valid, i.e. json is valid, the key D_CMND_WEBCOLOR was found and the token is an arra + uint32_t i = 0; + for (auto color : arr) { + if (i < COL_LAST) { + WebHexCode(i, color.getStr()); + } else { + break; + } + i++; + } + } +#endif return true; } diff --git a/tasmota/xdrv_05_irremote.ino b/tasmota/xdrv_05_irremote.ino index bb5ce0c3c..77a89a697 100644 --- a/tasmota/xdrv_05_irremote.ino +++ b/tasmota/xdrv_05_irremote.ino @@ -25,6 +25,7 @@ #define XDRV_05 5 #include +#include "JsonParser.h" enum IrErrors { IE_NO_ERROR, IE_INVALID_RAWDATA, IE_INVALID_JSON, IE_SYNTAX_IRSEND }; @@ -180,6 +181,7 @@ uint32_t IrRemoteCmndIrSendJson(void) // IRsend { "protocol": "RC5", "bits": 12, "data":"0xC86" } // IRsend { "protocol": "SAMSUNG", "bits": 32, "data": 551502015 } +#if 0 char dataBufUc[XdrvMailbox.data_len + 1]; UpperCase(dataBufUc, XdrvMailbox.data); RemoveSpace(dataBufUc); @@ -200,6 +202,18 @@ uint32_t IrRemoteCmndIrSendJson(void) uint16_t bits = root[UpperCase_P(parm_uc, PSTR(D_JSON_IR_BITS))]; uint64_t data = strtoull(root[UpperCase_P(parm_uc, PSTR(D_JSON_IR_DATA))], nullptr, 0); uint16_t repeat = root[UpperCase_P(parm_uc, PSTR(D_JSON_IR_REPEAT))]; +#else + RemoveSpace(XdrvMailbox.data); // TODO is this really needed? + JsonParserObject root = JsonParser((chat*) XdrvMailbox.data).getRootObject(); + if (!root) { return IE_INVALID_JSON; } + + // IRsend { "protocol": "SAMSUNG", "bits": 32, "data": 551502015 } + // IRsend { "protocol": "NEC", "bits": 32, "data":"0x02FDFE80", "repeat": 2 } + const char *protocol = root.getStr(PSTR(D_JSON_IR_PROTOCOL)); + uint16_t bits = root.getUInt(PSTR(D_JSON_IR_BITS)); + uint64_t data = root.getULong(PSTR(D_JSON_IR_DATA)); + uint16_t repeat = root.getUInt(PSTR(D_JSON_IR_REPEAT)); +#endif // check if the IRSend is great than repeat if (XdrvMailbox.index > repeat + 1) { repeat = XdrvMailbox.index - 1; diff --git a/tasmota/xdrv_05_irremote_full.ino b/tasmota/xdrv_05_irremote_full.ino index 9649e87b4..846bd1b5d 100644 --- a/tasmota/xdrv_05_irremote_full.ino +++ b/tasmota/xdrv_05_irremote_full.ino @@ -29,6 +29,7 @@ #include #include #include +#include "JsonParser.h" enum IrErrors { IE_RESPONSE_PROVIDED, IE_NO_ERROR, IE_INVALID_RAWDATA, IE_INVALID_JSON, IE_SYNTAX_IRSEND, IE_SYNTAX_IRHVAC, IE_UNSUPPORTED_HVAC, IE_UNSUPPORTED_PROTOCOL }; @@ -267,6 +268,16 @@ String listSupportedProtocols(bool hvac) { return l; } +bool strToBool(class JsonParserToken token, bool def) { + if (token.isBool() || token.isNum()) { + return token.getBool(); + } else if (token.isStr()) { + return IRac::strToBool(token.getStr()); + } else { + return def; + } +} + // used to convert values 0-5 to fanspeed_t const stdAc::fanspeed_t IrHvacFanSpeed[] PROGMEM = { stdAc::fanspeed_t::kAuto, stdAc::fanspeed_t::kMin, stdAc::fanspeed_t::kLow,stdAc::fanspeed_t::kMedium, @@ -275,17 +286,10 @@ const stdAc::fanspeed_t IrHvacFanSpeed[] PROGMEM = { stdAc::fanspeed_t::kAuto, uint32_t IrRemoteCmndIrHvacJson(void) { stdAc::state_t state, prev; - char parm_uc[12]; //AddLog_P2(LOG_LEVEL_DEBUG, PSTR("IRHVAC: Received %s"), XdrvMailbox.data); - char dataBufUc[XdrvMailbox.data_len + 1]; - UpperCase(dataBufUc, XdrvMailbox.data); - RemoveSpace(dataBufUc); - if (strlen(dataBufUc) < 8) { return IE_INVALID_JSON; } - - DynamicJsonBuffer jsonBuf; - JsonObject &json = jsonBuf.parseObject(dataBufUc); - if (!json.success()) { return IE_INVALID_JSON; } + JsonParserObject root = JsonParser((char*) XdrvMailbox.data).getRootObject(); + if (!root) { return IE_INVALID_JSON; } // from: https://github.com/crankyoldgit/IRremoteESP8266/blob/master/examples/CommonAcControl/CommonAcControl.ino state.protocol = decode_type_t::UNKNOWN; @@ -307,60 +311,47 @@ uint32_t IrRemoteCmndIrHvacJson(void) state.clean = false; // Turn off any Cleaning options if we can. state.clock = -1; // Don't set any current time if we can avoid it. - UpperCase_P(parm_uc, PSTR(D_JSON_IRHVAC_VENDOR)); - if (json.containsKey(parm_uc)) { state.protocol = strToDecodeType(json[parm_uc]); } - UpperCase_P(parm_uc, PSTR(D_JSON_IRHVAC_PROTOCOL)); - if (json.containsKey(parm_uc)) { state.protocol = strToDecodeType(json[parm_uc]); } // also support 'protocol' + if (root[PSTR(D_JSON_IRHVAC_VENDOR)]) { state.protocol = strToDecodeType(root.getStr(PSTR(D_JSON_IRHVAC_VENDOR))); } + if (root[PSTR(D_JSON_IRHVAC_PROTOCOL)]) { state.protocol = strToDecodeType(root.getStr(PSTR(D_JSON_IRHVAC_PROTOCOL))); } + // UpperCase_P(parm_uc, PSTR(D_JSON_IRHVAC_VENDOR)); + // if (json.containsKey(parm_uc)) { state.protocol = strToDecodeType(json[parm_uc]); } + // UpperCase_P(parm_uc, PSTR(D_JSON_IRHVAC_PROTOCOL)); + // if (json.containsKey(parm_uc)) { state.protocol = strToDecodeType(json[parm_uc]); } // also support 'protocol' if (decode_type_t::UNKNOWN == state.protocol) { return IE_UNSUPPORTED_HVAC; } if (!IRac::isProtocolSupported(state.protocol)) { return IE_UNSUPPORTED_HVAC; } // for fan speed, we also support 1-5 values - UpperCase_P(parm_uc, PSTR(D_JSON_IRHVAC_FANSPEED)); - if (json.containsKey(parm_uc)) { - uint32_t fan_speed = json[parm_uc]; + JsonParserToken tok_fan_speed = root[PSTR(D_JSON_IRHVAC_FANSPEED)]; + if (tok_fan_speed) { + uint32_t fan_speed = tok_fan_speed.getUInt(); if ((fan_speed >= 1) && (fan_speed <= 5)) { state.fanspeed = (stdAc::fanspeed_t) pgm_read_byte(&IrHvacFanSpeed[fan_speed]); } else { - state.fanspeed = IRac::strToFanspeed(json[parm_uc]); + state.fanspeed = IRac::strToFanspeed(tok_fan_speed.getStr()); } } - UpperCase_P(parm_uc, PSTR(D_JSON_IRHVAC_MODEL)); - if (json.containsKey(parm_uc)) { state.model = IRac::strToModel(json[parm_uc]); } - UpperCase_P(parm_uc, PSTR(D_JSON_IRHVAC_MODE)); - if (json.containsKey(parm_uc)) { state.mode = IRac::strToOpmode(json[parm_uc]); } - UpperCase_P(parm_uc, PSTR(D_JSON_IRHVAC_SWINGV)); - if (json.containsKey(parm_uc)) { state.swingv = IRac::strToSwingV(json[parm_uc]); } - UpperCase_P(parm_uc, PSTR(D_JSON_IRHVAC_SWINGH)); - if (json.containsKey(parm_uc)) { state.swingh = IRac::strToSwingH(json[parm_uc]); } - UpperCase_P(parm_uc, PSTR(D_JSON_IRHVAC_TEMP)); - if (json.containsKey(parm_uc)) { state.degrees = json[parm_uc]; } + if (root[PSTR(D_JSON_IRHVAC_MODEL)]) { state.model = IRac::strToModel(PSTR(D_JSON_IRHVAC_MODEL)]); } + if (root[PSTR(D_JSON_IRHVAC_MODE)]) { state.mode = IRac::strToOpmode(PSTR(D_JSON_IRHVAC_MODE)]); } + if (root[PSTR(D_JSON_IRHVAC_SWINGV)]) { state.swingv = IRac::strToSwingV(PSTR(D_JSON_IRHVAC_SWINGV)]); } + if (root[PSTR(D_JSON_IRHVAC_SWINGH)]) { state.swingh = IRac::strToSwingV(PSTR(D_JSON_IRHVAC_SWINGH)]); } + state.degrees = root.getFloat(PSTR(D_JSON_IRHVAC_TEMP), state.degrees); // AddLog_P2(LOG_LEVEL_DEBUG, PSTR("model %d, mode %d, fanspeed %d, swingv %d, swingh %d"), // state.model, state.mode, state.fanspeed, state.swingv, state.swingh); // decode booleans - UpperCase_P(parm_uc, PSTR(D_JSON_IRHVAC_POWER)); - if (json.containsKey(parm_uc)) { state.power = IRac::strToBool(json[parm_uc]); } - UpperCase_P(parm_uc, PSTR(D_JSON_IRHVAC_CELSIUS)); - if (json.containsKey(parm_uc)) { state.celsius = IRac::strToBool(json[parm_uc]); } - UpperCase_P(parm_uc, PSTR(D_JSON_IRHVAC_LIGHT)); - if (json.containsKey(parm_uc)) { state.light = IRac::strToBool(json[parm_uc]); } - UpperCase_P(parm_uc, PSTR(D_JSON_IRHVAC_BEEP)); - if (json.containsKey(parm_uc)) { state.beep = IRac::strToBool(json[parm_uc]); } - UpperCase_P(parm_uc, PSTR(D_JSON_IRHVAC_ECONO)); - if (json.containsKey(parm_uc)) { state.econo = IRac::strToBool(json[parm_uc]); } - UpperCase_P(parm_uc, PSTR(D_JSON_IRHVAC_FILTER)); - if (json.containsKey(parm_uc)) { state.filter = IRac::strToBool(json[parm_uc]); } - UpperCase_P(parm_uc, PSTR(D_JSON_IRHVAC_TURBO)); - if (json.containsKey(parm_uc)) { state.turbo = IRac::strToBool(json[parm_uc]); } - UpperCase_P(parm_uc, PSTR(D_JSON_IRHVAC_QUIET)); - if (json.containsKey(parm_uc)) { state.quiet = IRac::strToBool(json[parm_uc]); } - UpperCase_P(parm_uc, PSTR(D_JSON_IRHVAC_CLEAN)); - if (json.containsKey(parm_uc)) { state.clean = IRac::strToBool(json[parm_uc]); } + state.power = strToBool(root[PSTR(D_JSON_IRHVAC_POWER)], state.power); + state.celsius = strToBool(root[PSTR(D_JSON_IRHVAC_CELSIUS)], state.celsius); + state.light = strToBool(root[PSTR(D_JSON_IRHVAC_LIGHT)], state.light); + state.beep = strToBool(root[PSTR(D_JSON_IRHVAC_BEEP)], state.beep); + state.econo = strToBool(root[PSTR(D_JSON_IRHVAC_ECONO)], state.econo); + state.filter = strToBool(root[PSTR(D_JSON_IRHVAC_FILTER)], state.filter); + state.turbo = strToBool(root[PSTR(D_JSON_IRHVAC_TURBO)], state.turbo); + state.quiet = strToBool(root[PSTR(D_JSON_IRHVAC_QUIET)], state.quiet); + state.clean = strToBool(root[PSTR(D_JSON_IRHVAC_CLEAN)], state.clean); // optional timer and clock - UpperCase_P(parm_uc, PSTR(D_JSON_IRHVAC_SLEEP)); - if (json[parm_uc]) { state.sleep = json[parm_uc]; } + state.sleep = root.getInt(PSTR(D_JSON_IRHVAC_SLEEP), state.sleep); //if (json[D_JSON_IRHVAC_CLOCK]) { state.clock = json[D_JSON_IRHVAC_CLOCK]; } // not sure it's useful to support 'clock' IRac ac(Pin(GPIO_IRSEND)); diff --git a/tasmota/xdrv_09_timers.ino b/tasmota/xdrv_09_timers.ino index 1ecfb0b67..79efc1f1c 100644 --- a/tasmota/xdrv_09_timers.ino +++ b/tasmota/xdrv_09_timers.ino @@ -337,6 +337,7 @@ void CmndTimer(void) Settings.timer[index -1].data = Settings.timer[XdrvMailbox.payload -1].data; // Copy timer } } else { +#if 0 //#ifndef USE_RULES #if defined(USE_RULES)==0 && defined(USE_SCRIPT)==0 if (devices_present) { @@ -419,6 +420,95 @@ void CmndTimer(void) index++; } //#ifndef USE_RULES +#else +//#ifndef USE_RULES +#if defined(USE_RULES)==0 && defined(USE_SCRIPT)==0 + if (devices_present) { +#endif + JsonParserObject root = JsonParser(XdrvMailbox.data).getRootObject(); + if (!root) { + Response_P(PSTR("{\"" D_CMND_TIMER "%d\":\"" D_JSON_INVALID_JSON "\"}"), index); // JSON decode failed + error = 1; + } + else { + char parm_uc[10]; + index--; + JsonParserToken val = root[PSTR(D_JSON_TIMER_ARM)]; + if (val) { + Settings.timer[index].arm = (val.getInt() != 0); + } +#ifdef USE_SUNRISE + val = root[PSTR(D_JSON_TIMER_MODE)]; + if (val) { + Settings.timer[index].mode = val.getUInt() & 0x03; + } +#endif + val = root[PSTR(D_JSON_TIMER_TIME)]; + if (val) { + uint16_t itime = 0; + int8_t value = 0; + uint8_t sign = 0; + char time_str[10]; + + strlcpy(time_str, val.getStr(), sizeof(time_str)); + const char *substr = strtok(time_str, ":"); + if (substr != nullptr) { + if (strchr(substr, '-')) { + sign = 1; + substr++; + } + value = atoi(substr); + if (sign) { value += 12; } // Allow entering timer offset from -11:59 to -00:01 converted to 12:01 to 23:59 + if (value > 23) { value = 23; } + itime = value * 60; + substr = strtok(nullptr, ":"); + if (substr != nullptr) { + value = atoi(substr); + if (value < 0) { value = 0; } + if (value > 59) { value = 59; } + itime += value; + } + } + Settings.timer[index].time = itime; + } + val = root[PSTR(D_JSON_TIMER_WINDOW)]; + if (val) { + Settings.timer[index].window = val.getUInt() & 0x0F; + TimerSetRandomWindow(index); + } + val = root[PSTR(D_JSON_TIMER_DAYS)]; + if (val) { + // SMTWTFS = 1234567 = 0011001 = 00TW00S = --TW--S + Settings.timer[index].days = 0; + const char *tday = val.getStr(); + uint8_t i = 0; + char ch = *tday++; + while ((ch != '\0') && (i < 7)) { + if (ch == '-') { ch = '0'; } + uint8_t mask = 1 << i++; + Settings.timer[index].days |= (ch == '0') ? 0 : mask; + ch = *tday++; + } + } + val = root[PSTR(D_JSON_TIMER_REPEAT)]; + if (val) { + Settings.timer[index].repeat = (val.getUInt() != 0); + } + val = root[PSTR(D_JSON_TIMER_OUTPUT)]; + if (val) { + uint8_t device = (val.getUInt() -1) & 0x0F; + Settings.timer[index].device = (device < devices_present) ? device : 0; + } + val = root[PSTR(D_JSON_TIMER_ACTION)]; + if (val) { + uint8_t action = val.getUInt() & 0x03; + Settings.timer[index].power = (devices_present) ? action : 3; // If no devices than only allow rules + } + + index++; + } +//#ifndef USE_RULES +#endif #if defined(USE_RULES)==0 && defined(USE_SCRIPT)==0 } else { Response_P(PSTR("{\"" D_CMND_TIMER "%d\":\"" D_JSON_TIMER_NO_DEVICE "\"}"), index); // No outputs defined so nothing to control diff --git a/tasmota/xdrv_10_rules.ino b/tasmota/xdrv_10_rules.ino index 4476fc2b0..5e2441de5 100644 --- a/tasmota/xdrv_10_rules.ino +++ b/tasmota/xdrv_10_rules.ino @@ -497,6 +497,7 @@ bool RulesRuleMatch(uint8_t rule_set, String &event, String &rule) rule_name = rule_name.substring(0, pos); // "SUBTYPE1#CURRENT" } +#if 0 // StaticJsonBuffer<1280> jsonBuf; // Was 1024 until 20200811 DynamicJsonBuffer jsonBuf; // Was static until 20200812 JsonObject &root = jsonBuf.parseObject(event); @@ -528,6 +529,43 @@ bool RulesRuleMatch(uint8_t rule_set, String &event, String &rule) } else { str_value = (*obj)[rule_name]; // "CURRENT" } +#else + + char buf[event.length()+1]; + strcpy(buf, event.c_str()); + JsonParser parser = JsonParser(buf); + JsonParserObject obj = parser.getRootObject(); + if (!obj) { + AddLog_P2(LOG_LEVEL_DEBUG, PSTR("RUL: Event too long (%d)"), event.length()); + return false; // No valid JSON data + } + String subtype; + uint32_t i = 0; + while ((pos = rule_name.indexOf("#")) > 0) { // "SUBTYPE1#SUBTYPE2#CURRENT" + subtype = rule_name.substring(0, pos); + obj = obj[subtype.c_str()].getObject(); + if (!obj) { return false; } // not found + + rule_name = rule_name.substring(pos +1); + if (i++ > 10) { return false; } // Abandon possible loop + + yield(); + } + + JsonParserToken val = obj[rule_name.c_str()]; + if (!val) { return false; } // last level not found + const char* str_value; + if (rule_name_idx) { + if (val.isArray()) { + str_value = (val.getArray())[rule_name_idx -1].getStr(); + } else { + str_value = val.getStr(); + } + } else { + str_value = val.getStr(); // "CURRENT" + } +#endif + //AddLog_P2(LOG_LEVEL_DEBUG, PSTR("RUL: Name %s, Value |%s|, TrigCnt %d, TrigSt %d, Source %s, Json %s"), // rule_name.c_str(), rule_svalue, Rules.trigger_count[rule_set], bitRead(Rules.triggers[rule_set], Rules.trigger_count[rule_set]), event.c_str(), (str_value) ? str_value : "none"); @@ -1034,20 +1072,28 @@ bool RulesMqttData(void) if (event_item.Key.length() == 0) { //If did not specify Key value = sData; } else { //If specified Key, need to parse Key/Value from JSON data - StaticJsonBuffer<500> jsonBuf; - JsonObject& jsonData = jsonBuf.parseObject(sData); + JsonParserObject jsonData = JsonParser((char*)sData.c_str()).getRootObject(); + + // StaticJsonBuffer<500> jsonBuf; + // JsonObject& jsonData = jsonBuf.parseObject(sData); String key1 = event_item.Key; String key2; - if (!jsonData.success()) break; //Failed to parse JSON data, ignore this message. + if (!jsonData) break; //Failed to parse JSON data, ignore this message. int dot; if ((dot = key1.indexOf('.')) > 0) { key2 = key1.substring(dot+1); key1 = key1.substring(0, dot); - if (!jsonData[key1][key2].success()) break; //Failed to get the key/value, ignore this message. - value = (const char *)jsonData[key1][key2]; + JsonParserToken value_tok = jsonData[key1.c_str()][key2.c_str()]; + if (!value_tok) break; //Failed to get the key/value, ignore this message. + value = value_tok.getStr(); + // if (!jsonData[key1][key2].success()) break; //Failed to get the key/value, ignore this message. + // value = (const char *)jsonData[key1][key2]; } else { - if (!jsonData[key1].success()) break; - value = (const char *)jsonData[key1]; + JsonParserToken value_tok = jsonData[key1.c_str()]; + if (!value_tok) break; //Failed to get the key/value, ignore this message. + value = value_tok.getStr(); + // if (!jsonData[key1].success()) break; + // value = (const char *)jsonData[key1]; } } value.trim(); diff --git a/tasmota/xdrv_20_hue.ino b/tasmota/xdrv_20_hue.ino index 974ebbb78..6bdc420af 100644 --- a/tasmota/xdrv_20_hue.ino +++ b/tasmota/xdrv_20_hue.ino @@ -569,10 +569,12 @@ void HueLightsCommand(uint8_t device, uint32_t device_id, String &response) { if (Webserver->args()) { response = "["; - StaticJsonBuffer<300> jsonBuffer; - JsonObject &hue_json = jsonBuffer.parseObject(Webserver->arg((Webserver->args())-1)); - if (hue_json.containsKey("on")) { - on = hue_json["on"]; + JsonParser parser = JsonParser((char*) Webserver->arg((Webserver->args())-1).c_str()); + JsonParserObject root = parser.getRootObject(); + + JsonParserToken hue_on = root[PSTR("on")]; + if (hue_on) { + on = hue_on.getBool(); snprintf_P(buf, buf_size, PSTR("{\"success\":{\"/lights/%d/state/on\":%s}}"), device_id, on ? "true" : "false"); @@ -587,15 +589,6 @@ void HueLightsCommand(uint8_t device, uint32_t device_id, String &response) { } } else { #endif -/* - switch(on) - { - case false : ExecuteCommandPower(device, POWER_OFF, SRC_HUE); - break; - case true : ExecuteCommandPower(device, POWER_ON, SRC_HUE); - break; - } -*/ ExecuteCommandPower(device, (on) ? POWER_ON : POWER_OFF, SRC_HUE); response += buf; resp = true; @@ -619,8 +612,10 @@ void HueLightsCommand(uint8_t device, uint32_t device_id, String &response) { } prev_x_str[0] = prev_y_str[0] = 0; // reset xy string - if (hue_json.containsKey("bri")) { // Brightness is a scale from 1 (the minimum the light is capable of) to 254 (the maximum). Note: a brightness of 1 is not off. - bri = hue_json["bri"]; + parser.setCurrent(); + JsonParserToken hue_bri = root[PSTR("bri")]; + if (hue_bri) { // Brightness is a scale from 1 (the minimum the light is capable of) to 254 (the maximum). Note: a brightness of 1 is not off. + bri = hue_bri.getUInt(); prev_bri = bri; // store command value if (resp) { response += ","; } snprintf_P(buf, buf_size, @@ -634,15 +629,19 @@ void HueLightsCommand(uint8_t device, uint32_t device_id, String &response) { } resp = true; } + // handle xy before Hue/Sat // If the request contains both XY and HS, we wan't to give priority to HS - if (hue_json.containsKey("xy")) { - float x = hue_json["xy"][0]; - float y = hue_json["xy"][1]; - const String &x_str = hue_json["xy"][0]; - const String &y_str = hue_json["xy"][1]; - x_str.toCharArray(prev_x_str, sizeof(prev_x_str)); - y_str.toCharArray(prev_y_str, sizeof(prev_y_str)); + parser.setCurrent(); + JsonParserToken hue_xy = root[PSTR("xy")]; + if (hue_xy) { + JsonParserArray arr_xy = JsonParserArray(hue_xy); + JsonParserToken tok_x = arr_xy[0]; + JsonParserToken tok_y = arr_xy[1]; + float x = tok_x.getFloat(); + float y = tok_y.getFloat(); + strlcpy(prev_x_str, tok_x.getStr(), sizeof(prev_x_str)); + strlcpy(prev_y_str, tok_y.getStr(), sizeof(prev_y_str)); uint8_t rr,gg,bb; LightStateClass::XyToRgb(x, y, &rr, &gg, &bb); LightStateClass::RgbToHsb(rr, gg, bb, &hue, &sat, nullptr); @@ -658,8 +657,11 @@ void HueLightsCommand(uint8_t device, uint32_t device_id, String &response) { resp = true; change = true; } - if (hue_json.containsKey("hue")) { // The hue value is a wrapping value between 0 and 65535. Both 0 and 65535 are red, 25500 is green and 46920 is blue. - hue = hue_json["hue"]; + + parser.setCurrent(); + JsonParserToken hue_hue = root[PSTR("hue")]; + if (hue_hue) { // The hue value is a wrapping value between 0 and 65535. Both 0 and 65535 are red, 25500 is green and 46920 is blue. + hue = hue_hue.getUInt(); prev_hue = hue; if (resp) { response += ","; } snprintf_P(buf, buf_size, @@ -674,8 +676,11 @@ void HueLightsCommand(uint8_t device, uint32_t device_id, String &response) { } resp = true; } - if (hue_json.containsKey("sat")) { // Saturation of the light. 254 is the most saturated (colored) and 0 is the least saturated (white). - sat = hue_json["sat"]; + + parser.setCurrent(); + JsonParserToken hue_sat = root[PSTR("sat")]; + if (hue_sat) { // Saturation of the light. 254 is the most saturated (colored) and 0 is the least saturated (white). + sat = hue_sat.getUInt(); prev_sat = sat; // store command value if (resp) { response += ","; } snprintf_P(buf, buf_size, @@ -690,8 +695,11 @@ void HueLightsCommand(uint8_t device, uint32_t device_id, String &response) { } resp = true; } - if (hue_json.containsKey("ct")) { // Color temperature 153 (Cold) to 500 (Warm) - ct = hue_json["ct"]; + + parser.setCurrent(); + JsonParserToken hue_ct = root[PSTR("ct")]; + if (hue_ct) { // Color temperature 153 (Cold) to 500 (Warm) + ct = hue_ct.getUInt(); prev_ct = ct; // store commande value if (resp) { response += ","; } snprintf_P(buf, buf_size, @@ -704,6 +712,7 @@ void HueLightsCommand(uint8_t device, uint32_t device_id, String &response) { } resp = true; } + if (change) { #ifdef USE_SHUTTER if (ShutterState(device)) { diff --git a/tasmota/xdrv_23_zigbee_1_headers.ino b/tasmota/xdrv_23_zigbee_1_headers.ino index c7e3093e2..3ccb54672 100644 --- a/tasmota/xdrv_23_zigbee_1_headers.ino +++ b/tasmota/xdrv_23_zigbee_1_headers.ino @@ -39,42 +39,6 @@ public: void ZigbeeZCLSend_Raw(const ZigbeeZCLSendMessage &zcl); bool ZbAppendWriteBuf(SBuffer & buf, const Z_attribute & attr, bool prepend_status_ok = false); -// 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(); - 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((const __FlashStringHelper *)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++) { diff --git a/tasmota/xdrv_23_zigbee_2_devices.ino b/tasmota/xdrv_23_zigbee_2_devices.ino index fa7775d03..e3182faad 100644 --- a/tasmota/xdrv_23_zigbee_2_devices.ino +++ b/tasmota/xdrv_23_zigbee_2_devices.ino @@ -19,6 +19,8 @@ #ifdef USE_ZIGBEE +#include "JsonParser.h" + #ifndef ZIGBEE_SAVE_DELAY_SECONDS #define ZIGBEE_SAVE_DELAY_SECONDS 2 // wait for 2s before saving Zigbee info #endif @@ -261,7 +263,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); + int32_t deviceRestore(JsonParserObject json); // General Zigbee device profile support void setZbProfile(uint16_t shortaddr, uint8_t zb_profile); @@ -936,7 +938,7 @@ void Z_Devices::clean(void) { // - a number 0..99, the index number in ZigbeeStatus // - a friendly name, between quotes, example: "Room_Temp" uint16_t Z_Devices::parseDeviceParam(const char * param, bool short_must_be_known) const { - if (nullptr == param) { return 0; } + if (nullptr == param) { return BAD_SHORTADDR; } size_t param_len = strlen(param); char dataBuf[param_len + 1]; strcpy(dataBuf, param); @@ -1070,7 +1072,7 @@ String Z_Devices::dump(uint32_t dump_mode, uint16_t status_shortaddr) const { // <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) { +int32_t Z_Devices::deviceRestore(JsonParserObject json) { // params uint16_t device = 0x0000; // 0x0000 is coordinator so considered invalid @@ -1078,56 +1080,38 @@ int32_t Z_Devices::deviceRestore(const JsonObject &json) { const char * modelid = nullptr; const char * manufid = nullptr; const char * friendlyname = nullptr; - int8_t bulbtype = 0xFF; + int8_t bulbtype = -1; size_t endpoints_len = 0; // read mandatory "Device" - const JsonVariant &val_device = GetCaseInsensitive(json, PSTR("Device")); - if (nullptr != &val_device) { - device = strToUInt(val_device); + JsonParserToken val_device = json[PSTR("Device")]; + if (val_device) { + device = (uint32_t) val_device.getUInt(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(), 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);; } + ieeeaddr = json.getULong(PSTR("IEEEAddr"), ieeeaddr); // read "IEEEAddr" 64 bits in format "0x0000000000000000" + friendlyname = json.getStr(PSTR("Name"), nullptr); // read "Name" + modelid = json.getStr(PSTR("ModelId"), nullptr); + manufid = json.getStr(PSTR("Manufacturer"), nullptr); + JsonParserToken tok_bulbtype = json[PSTR(D_JSON_ZIGBEE_LIGHT)]; // 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); } + if (tok_bulbtype) { setHueBulbtype(device, tok_bulbtype.getInt()); } // read "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(); + JsonParserToken val_endpoints = json[PSTR("Endpoints")]; + if (val_endpoints.isArray()) { + JsonParserArray arr_ep = JsonParserArray(val_endpoints); 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); - } - } + for (auto ep_elt : arr_ep) { + uint8_t ep = ep_elt.getUInt(); + if (ep) { addEndpoint(device, ep); } } } diff --git a/tasmota/xdrv_23_zigbee_3_hue.ino b/tasmota/xdrv_23_zigbee_3_hue.ino index 2accf849c..4b95a58be 100644 --- a/tasmota/xdrv_23_zigbee_3_hue.ino +++ b/tasmota/xdrv_23_zigbee_3_hue.ino @@ -218,10 +218,12 @@ void ZigbeeHandleHue(uint16_t shortaddr, uint32_t device_id, String &response) { if (Webserver->args()) { response = "["; - StaticJsonBuffer<300> jsonBuffer; - JsonObject &hue_json = jsonBuffer.parseObject(Webserver->arg((Webserver->args())-1)); - if (hue_json.containsKey("on")) { - on = hue_json["on"]; + JsonParser parser = JsonParser((char*) Webserver->arg((Webserver->args())-1).c_str()); + JsonParserObject root = parser.getRootObject(); + + JsonParserToken hue_on = root[PSTR("on")]; + if (hue_on) { + on = hue_on.getBool(); snprintf_P(buf, buf_size, PSTR("{\"success\":{\"/lights/%d/state/on\":%s}}"), device_id, on ? "true" : "false"); @@ -235,8 +237,10 @@ void ZigbeeHandleHue(uint16_t shortaddr, uint32_t device_id, String &response) { resp = true; } - if (hue_json.containsKey("bri")) { // Brightness is a scale from 1 (the minimum the light is capable of) to 254 (the maximum). Note: a brightness of 1 is not off. - bri = hue_json["bri"]; + parser.setCurrent(); + JsonParserToken hue_bri = root[PSTR("bri")]; + if (hue_bri) { // Brightness is a scale from 1 (the minimum the light is capable of) to 254 (the maximum). Note: a brightness of 1 is not off. + bri = hue_bri.getUInt(); prev_bri = bri; // store command value if (resp) { response += ","; } snprintf_P(buf, buf_size, @@ -252,13 +256,16 @@ void ZigbeeHandleHue(uint16_t shortaddr, uint32_t device_id, String &response) { } // handle xy before Hue/Sat // If the request contains both XY and HS, we wan't to give priority to HS - if (hue_json.containsKey("xy")) { - float x = hue_json["xy"][0]; - float y = hue_json["xy"][1]; - const String &x_str = hue_json["xy"][0]; - const String &y_str = hue_json["xy"][1]; - x_str.toCharArray(prev_x_str, sizeof(prev_x_str)); - y_str.toCharArray(prev_y_str, sizeof(prev_y_str)); + parser.setCurrent(); + JsonParserToken hue_xy = root[PSTR("xy")]; + if (hue_xy) { + JsonParserArray arr_xy = JsonParserArray(hue_xy); + JsonParserToken tok_x = arr_xy[0]; + JsonParserToken tok_y = arr_xy[1]; + float x = tok_x.getFloat(); + float y = tok_y.getFloat(); + strlcpy(prev_x_str, tok_x.getStr(), sizeof(prev_x_str)); + strlcpy(prev_y_str, tok_y.getStr(), sizeof(prev_y_str)); if (resp) { response += ","; } snprintf_P(buf, buf_size, PSTR("{\"success\":{\"/lights/%d/state/xy\":[%s,%s]}}"), @@ -270,8 +277,11 @@ void ZigbeeHandleHue(uint16_t shortaddr, uint32_t device_id, String &response) { ZigbeeHueXY(shortaddr, xi, yi); } bool huesat_changed = false; - if (hue_json.containsKey("hue")) { // The hue value is a wrapping value between 0 and 65535. Both 0 and 65535 are red, 25500 is green and 46920 is blue. - hue = hue_json["hue"]; + + parser.setCurrent(); + JsonParserToken hue_hue = root[PSTR("hue")]; + if (hue_hue) { // The hue value is a wrapping value between 0 and 65535. Both 0 and 65535 are red, 25500 is green and 46920 is blue. + hue = hue_hue.getUInt(); prev_hue = hue; if (resp) { response += ","; } snprintf_P(buf, buf_size, @@ -285,8 +295,11 @@ void ZigbeeHandleHue(uint16_t shortaddr, uint32_t device_id, String &response) { } resp = true; } - if (hue_json.containsKey("sat")) { // Saturation of the light. 254 is the most saturated (colored) and 0 is the least saturated (white). - sat = hue_json["sat"]; + + parser.setCurrent(); + JsonParserToken hue_sat = root[PSTR("sat")]; + if (hue_sat) { // Saturation of the light. 254 is the most saturated (colored) and 0 is the least saturated (white). + sat = hue_sat.getUInt(); prev_sat = sat; // store command value if (resp) { response += ","; } snprintf_P(buf, buf_size, @@ -303,8 +316,11 @@ void ZigbeeHandleHue(uint16_t shortaddr, uint32_t device_id, String &response) { } resp = true; } - if (hue_json.containsKey("ct")) { // Color temperature 153 (Cold) to 500 (Warm) - ct = hue_json["ct"]; + + parser.setCurrent(); + JsonParserToken hue_ct = root[PSTR("ct")]; + if (hue_ct) { // Color temperature 153 (Cold) to 500 (Warm) + ct = hue_ct.getUInt(); prev_ct = ct; // store commande value if (resp) { response += ","; } snprintf_P(buf, buf_size, diff --git a/tasmota/xdrv_23_zigbee_A_impl.ino b/tasmota/xdrv_23_zigbee_A_impl.ino index 9871f628e..b35296370 100644 --- a/tasmota/xdrv_23_zigbee_A_impl.ino +++ b/tasmota/xdrv_23_zigbee_A_impl.ino @@ -21,6 +21,8 @@ #define XDRV_23 23 +#include "JsonParser.h" + const char kZbCommands[] PROGMEM = D_PRFX_ZB "|" // prefix #ifdef USE_ZIGBEE_ZNP D_CMND_ZIGBEEZNPSEND "|" D_CMND_ZIGBEEZNPRECEIVE "|" @@ -97,19 +99,6 @@ void ZigbeeInit(void) * Commands \*********************************************************************************************/ -uint32_t strToUInt(const JsonVariant &val) { - // if the string starts with 0x, it is considered Hex, otherwise it is an int - if (val.is()) { - return val.as(); - } else { - if (val.is()) { - String sval = val.as(); - return strtoull(sval.c_str(), nullptr, 0); - } - } - return 0; // couldn't parse anything -} - #ifdef USE_ZIGBEE_ZNP // Do a factory reset of the CC2530 const unsigned char ZIGBEE_FACTORY_RESET[] PROGMEM = @@ -242,7 +231,7 @@ bool ZbAppendWriteBuf(SBuffer & buf, const Z_attribute & attr, bool prepend_stat // Parse "Report", "Write", "Response" or "Condig" attribute // Operation is one of: ZCL_REPORT_ATTRIBUTES (0x0A), ZCL_WRITE_ATTRIBUTES (0x02) or ZCL_READ_ATTRIBUTES_RESPONSE (0x01) -void ZbSendReportWrite(const JsonObject &val_pubwrite, uint16_t device, uint16_t groupaddr, uint16_t cluster, uint8_t endpoint, uint16_t manuf, uint8_t operation) { +void ZbSendReportWrite(class JsonParserToken val_pubwrite, uint16_t device, uint16_t groupaddr, uint16_t cluster, uint8_t endpoint, uint16_t manuf, uint8_t operation) { SBuffer buf(200); // buffer to store the binary output of attibutes if (nullptr == XdrvMailbox.command) { @@ -250,12 +239,11 @@ void ZbSendReportWrite(const JsonObject &val_pubwrite, uint16_t device, uint16_t } // iterate on keys - for (JsonObject::const_iterator it=val_pubwrite.begin(); it!=val_pubwrite.end(); ++it) { - const char *key = it->key; - const JsonVariant &value = it->value; + for (auto key : val_pubwrite.getObject()) { + JsonParserToken value = key.getValue(); Z_attribute attr; - attr.setKeyName(key); + attr.setKeyName(key.getStr()); if (Z_parseAttributeKey(attr)) { // Buffer ready, do some sanity checks if (0xFFFF == cluster) { @@ -276,10 +264,10 @@ void ZbSendReportWrite(const JsonObject &val_pubwrite, uint16_t device, uint16_t } } - if (value.is()) { - attr.setStr(value.as()); - } else if (value.is()) { - attr.setFloat(value.as()); + if (value.isStr()) { + attr.setStr(value.getStr()); + } else if (value.isNum()) { + attr.setFloat(value.getFloat()); } double val_d = 0; // I try to avoid `double` but this type capture both float and (u)int32_t without prevision loss @@ -293,41 +281,30 @@ void ZbSendReportWrite(const JsonObject &val_pubwrite, uint16_t device, uint16_t } else { // //////////////////////////////////////////////////////////////////////////////// // ZCL_CONFIGURE_REPORTING - if (!value.is()) { + if (!value.isObject()) { ResponseCmndChar_P(PSTR("Config requires JSON objects")); return; } - JsonObject &attr_config = value.as(); + JsonParserObject attr_config = value.getObject(); bool attr_direction = false; - const JsonVariant &val_attr_direction = GetCaseInsensitive(attr_config, PSTR("DirectionReceived")); - if (nullptr != &val_attr_direction) { - uint32_t dir = strToUInt(val_attr_direction); - if (dir) { - attr_direction = true; - } - } + uint32_t dir = attr_config.getUInt(PSTR("DirectionReceived"), 0); + if (dir) { attr_direction = true; } // read MinInterval and MaxInterval, default to 0xFFFF if not specified - uint16_t attr_min_interval = 0xFFFF; - uint16_t attr_max_interval = 0xFFFF; - const JsonVariant &val_attr_min = GetCaseInsensitive(attr_config, PSTR("MinInterval")); - if (nullptr != &val_attr_min) { attr_min_interval = strToUInt(val_attr_min); } - const JsonVariant &val_attr_max = GetCaseInsensitive(attr_config, PSTR("MaxInterval")); - if (nullptr != &val_attr_max) { attr_max_interval = strToUInt(val_attr_max); } + uint16_t attr_min_interval = attr_config.getUInt(PSTR("MinInterval"), 0xFFFF); + uint16_t attr_max_interval = attr_config.getUInt(PSTR("MaxInterval"), 0xFFFF); // read ReportableChange - const JsonVariant &val_attr_rc = GetCaseInsensitive(attr_config, PSTR("ReportableChange")); - if (nullptr != &val_attr_rc) { - val_d = val_attr_rc.as(); - val_str = val_attr_rc.as(); + JsonParserToken val_attr_rc = attr_config[PSTR("ReportableChange")]; + if (val_attr_rc) { + val_d = val_attr_rc.getFloat(); + val_str = val_attr_rc.getStr(); ZbApplyMultiplier(val_d, attr.attr_multiplier); } // read TimeoutPeriod - uint16_t attr_timeout = 0x0000; - const JsonVariant &val_attr_timeout = GetCaseInsensitive(attr_config, PSTR("TimeoutPeriod")); - if (nullptr != &val_attr_timeout) { attr_timeout = strToUInt(val_attr_timeout); } + uint16_t attr_timeout = attr_config.getUInt(PSTR("TimeoutPeriod"), 0x0000); bool attr_discrete = Z_isDiscreteDataType(attr.attr_type); @@ -376,37 +353,36 @@ void ZbSendReportWrite(const JsonObject &val_pubwrite, uint16_t device, uint16_t } // Parse the "Send" attribute and send the command -void ZbSendSend(const JsonVariant &val_cmd, uint16_t device, uint16_t groupaddr, uint16_t cluster, uint8_t endpoint, uint16_t manuf) { +void ZbSendSend(class JsonParserToken val_cmd, uint16_t device, uint16_t groupaddr, uint16_t cluster, uint8_t endpoint, uint16_t manuf) { uint8_t cmd = 0; String cmd_str = ""; // the actual low-level command, either specified or computed - const char *cmd_s; // pointer to payload string + const char *cmd_s = ""; // pointer to payload string bool clusterSpecific = true; static char delim[] = ", "; // delimiters for parameters // probe the type of the argument // If JSON object, it's high level commands // If String, it's a low level command - if (val_cmd.is()) { + if (val_cmd.isObject()) { // we have a high-level command - const JsonObject &cmd_obj = val_cmd.as(); + JsonParserObject cmd_obj = val_cmd.getObject(); int32_t cmd_size = cmd_obj.size(); if (cmd_size > 1) { Response_P(PSTR("Only 1 command allowed (%d)"), cmd_size); return; } else if (1 == cmd_size) { // We have exactly 1 command, parse it - JsonObject::const_iterator it = cmd_obj.begin(); // just get the first key/value - String key = it->key; - const JsonVariant& value = it->value; + JsonParserKey key = cmd_obj.getFirstElement(); + JsonParserToken value = key.getValue(); uint32_t x = 0, y = 0, z = 0; uint16_t cmd_var; uint16_t local_cluster_id; - const __FlashStringHelper* tasmota_cmd = zigbeeFindCommand(key.c_str(), &local_cluster_id, &cmd_var); + const __FlashStringHelper* tasmota_cmd = zigbeeFindCommand(key.getStr(), &local_cluster_id, &cmd_var); if (tasmota_cmd) { cmd_str = tasmota_cmd; } else { - Response_P(PSTR("Unrecognized zigbee command: %s"), key.c_str()); + Response_P(PSTR("Unrecognized zigbee command: %s"), key.getStr()); return; } // check cluster @@ -418,13 +394,17 @@ void ZbSendSend(const JsonVariant &val_cmd, uint16_t device, uint16_t groupaddr, } // parse the JSON value, depending on its type fill in x,y,z - if (value.is()) { - x = value.as() ? 1 : 0; - } else if (value.is()) { - x = value.as(); + if (value.isNum()) { + x = value.getUInt(); // automatic conversion to 0/1 + // if (value.is()) { + // // x = value.as() ? 1 : 0; + // } else if + // } else if (value.is()) { + // x = value.as(); } else { // if non-bool or non-int, trying char* - const char *s_const = value.as(); + const char *s_const = value.getStr(nullptr); + // const char *s_const = value.as(); if (s_const != nullptr) { char s[strlen(s_const)+1]; strcpy(s, s_const); @@ -459,14 +439,13 @@ void ZbSendSend(const JsonVariant &val_cmd, uint16_t device, uint16_t groupaddr, } else { // we have zero command, pass through until last error for missing command } - } else if (val_cmd.is()) { + } else if (val_cmd.isStr()) { // low-level command - cmd_str = val_cmd.as(); // Now parse the string to extract cluster, command, and payload // Parse 'cmd' in the form "AAAA_BB/CCCCCCCC" or "AAAA!BB/CCCCCCCC" // where AA is the cluster number, BBBB the command number, CCCC... the payload // First delimiter is '_' for a global command, or '!' for a cluster specific command - const char * data = cmd_str.c_str(); + const char * data = val_cmd.getStr(); uint16_t local_cluster_id = parseHex(&data, 4); // check cluster @@ -505,7 +484,7 @@ void ZbSendSend(const JsonVariant &val_cmd, uint16_t device, uint16_t groupaddr, // Parse the "Send" attribute and send the command -void ZbSendRead(const JsonVariant &val_attr, uint16_t device, uint16_t groupaddr, uint16_t cluster, uint8_t endpoint, uint16_t manuf, uint8_t operation) { +void ZbSendRead(JsonParserToken val_attr, uint16_t device, uint16_t groupaddr, uint16_t cluster, uint8_t endpoint, uint16_t manuf, uint8_t operation) { // ZbSend {"Device":"0xF289","Cluster":0,"Endpoint":3,"Read":5} // ZbSend {"Device":"0xF289","Cluster":"0x0000","Endpoint":"0x0003","Read":"0x0005"} // ZbSend {"Device":"0xF289","Cluster":0,"Endpoint":3,"Read":[5,6,7,4]} @@ -525,32 +504,30 @@ void ZbSendRead(const JsonVariant &val_attr, uint16_t device, uint16_t groupaddr attr_item_offset = 1; } - uint16_t val = strToUInt(val_attr); - if (val_attr.is()) { + if (val_attr.isArray()) { // value is an array [] - const JsonArray& attr_arr = val_attr.as(); + JsonParserArray attr_arr = val_attr.getArray(); attrs_len = attr_arr.size() * attr_item_len; attrs = (uint8_t*) calloc(attrs_len, 1); uint32_t i = 0; for (auto value : attr_arr) { - uint16_t val = strToUInt(value); + uint16_t val = value.getUInt(); i += attr_item_offset; attrs[i++] = val & 0xFF; attrs[i++] = val >> 8; i += attr_item_len - 2 - attr_item_offset; // normally 0 } - } else if (val_attr.is()) { + } else if (val_attr.isObject()) { // value is an object {} - const JsonObject& attr_obj = val_attr.as(); + JsonParserObject attr_obj = val_attr.getObject(); attrs_len = attr_obj.size() * attr_item_len; attrs = (uint8_t*) calloc(attrs_len, 1); uint32_t actual_attr_len = 0; // iterate on keys - for (JsonObject::const_iterator it=attr_obj.begin(); it!=attr_obj.end(); ++it) { - const char *key = it->key; - const JsonVariant &value = it->value; // we don't need the value here, only keys are relevant + for (auto key : attr_obj) { + JsonParserToken value = key.getValue(); bool found = false; // scan attributes to find by name, and retrieve type @@ -561,11 +538,11 @@ void ZbSendRead(const JsonVariant &val_attr, uint16_t device, uint16_t groupaddr uint16_t local_cluster_id = CxToCluster(pgm_read_byte(&converter->cluster_short)); // uint8_t local_type_id = pgm_read_byte(&converter->type); - if ((pgm_read_word(&converter->name_offset)) && (0 == strcasecmp_P(key, Z_strings + pgm_read_word(&converter->name_offset)))) { + if ((pgm_read_word(&converter->name_offset)) && (0 == strcasecmp_P(key.getStr(), Z_strings + pgm_read_word(&converter->name_offset)))) { // match name // check if there is a conflict with cluster // TODO - if (!value && attr_item_offset) { + if (!(value.getBool()) && attr_item_offset) { // If value is false (non-default) then set direction to 1 (for ReadConfig) attrs[actual_attr_len] = 0x01; } @@ -594,6 +571,7 @@ void ZbSendRead(const JsonVariant &val_attr, uint16_t device, uint16_t groupaddr } else { // value is a literal if (0xFFFF != cluster) { + uint16_t val = val_attr.getUInt(); attrs_len = attr_item_len; attrs = (uint8_t*) calloc(attrs_len, 1); attrs[0 + attr_item_offset] = val & 0xFF; // little endian @@ -648,9 +626,8 @@ void CmndZbSend(void) { // ZbSend { "device":"0x1234", "endpoint":"0x03", "send":{"Color":"1,2"} } // ZbSend { "device":"0x1234", "endpoint":"0x03", "send":{"Color":"0x1122,0xFFEE"} } if (zigbee.init_phase) { ResponseCmndChar_P(PSTR(D_ZIGBEE_NOT_STARTED)); return; } - DynamicJsonBuffer jsonBuf; - const JsonObject &json = jsonBuf.parseObject((const char*) XdrvMailbox.data); - if (!json.success()) { ResponseCmndChar_P(PSTR(D_JSON_INVALID_JSON)); return; } + JsonParserObject root = JsonParser(XdrvMailbox.data).getRootObject(); + if (!root) { ResponseCmndChar_P(PSTR(D_JSON_INVALID_JSON)); return; } // params uint16_t device = BAD_SHORTADDR; // BAD_SHORTADDR is broadcast, so considered invalid @@ -661,15 +638,15 @@ void CmndZbSend(void) { // parse "Device" and "Group" - const JsonVariant &val_device = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_DEVICE)); - if (nullptr != &val_device) { - device = zigbee_devices.parseDeviceParam(val_device.as()); + JsonParserToken val_device = root[PSTR(D_CMND_ZIGBEE_DEVICE)]; + if (val_device) { + device = zigbee_devices.parseDeviceParam(val_device.getStr()); 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(D_CMND_ZIGBEE_GROUP)); - if (nullptr != &val_group) { - groupaddr = strToUInt(val_group); + JsonParserToken val_group = root[PSTR(D_CMND_ZIGBEE_GROUP)]; + if (val_group) { + groupaddr = val_group.getUInt(); } else { // no device nor group ResponseCmndChar_P(PSTR("Unknown device")); return; @@ -679,12 +656,9 @@ void CmndZbSend(void) { // Note: groupaddr == 0 is valid // read other parameters - const JsonVariant &val_cluster = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_CLUSTER)); - if (nullptr != &val_cluster) { cluster = strToUInt(val_cluster); } - const JsonVariant &val_endpoint = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_ENDPOINT)); - if (nullptr != &val_endpoint) { endpoint = strToUInt(val_endpoint); } - const JsonVariant &val_manuf = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_MANUF)); - if (nullptr != &val_manuf) { manuf = strToUInt(val_manuf); } + cluster = root.getUInt(PSTR(D_CMND_ZIGBEE_CLUSTER), cluster); + endpoint = root.getUInt(PSTR(D_CMND_ZIGBEE_ENDPOINT), endpoint); + manuf = root.getUInt(PSTR(D_CMND_ZIGBEE_MANUF), manuf); // infer endpoint if (BAD_SHORTADDR == device) { @@ -700,61 +674,61 @@ void CmndZbSend(void) { // from here endpoint is valid and non-zero // cluster may be already specified or 0xFFFF - const JsonVariant &val_cmd = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_SEND)); - const JsonVariant &val_read = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_READ)); - const JsonVariant &val_write = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_WRITE)); - const JsonVariant &val_publish = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_REPORT)); - const JsonVariant &val_response = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_RESPONSE)); - const JsonVariant &val_read_config = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_READ_CONFIG)); - const JsonVariant &val_config = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_CONFIG)); - uint32_t multi_cmd = (nullptr != &val_cmd) + (nullptr != &val_read) + (nullptr != &val_write) + (nullptr != &val_publish) - + (nullptr != &val_response) + (nullptr != &val_read_config) + (nullptr != &val_config); + JsonParserToken val_cmd = root[PSTR(D_CMND_ZIGBEE_SEND)]; + JsonParserToken val_read = root[PSTR(D_CMND_ZIGBEE_READ)]; + JsonParserToken val_write = root[PSTR(D_CMND_ZIGBEE_WRITE)]; + JsonParserToken val_publish = root[PSTR(D_CMND_ZIGBEE_REPORT)]; + JsonParserToken val_response = root[PSTR(D_CMND_ZIGBEE_RESPONSE)]; + JsonParserToken val_read_config = root[PSTR(D_CMND_ZIGBEE_READ_CONFIG)]; + JsonParserToken val_config = root[PSTR(D_CMND_ZIGBEE_CONFIG)]; + uint32_t multi_cmd = ((bool)val_cmd) + ((bool)val_read) + ((bool)val_write) + ((bool)val_publish) + + ((bool)val_response) + ((bool)val_read_config) + ((bool)val_config); if (multi_cmd > 1) { ResponseCmndChar_P(PSTR("Can only have one of: 'Send', 'Read', 'Write', 'Report', 'Reponse', 'ReadConfig' or 'Config'")); return; } // from here we have one and only one command - if (nullptr != &val_cmd) { + if (val_cmd) { // "Send":{...commands...} // we accept either a string or a JSON object ZbSendSend(val_cmd, device, groupaddr, cluster, endpoint, manuf); - } else if (nullptr != &val_read) { + } else if (val_read) { // "Read":{...attributes...}, "Read":attribute or "Read":[...attributes...] // we accept eitehr a number, a string, an array of numbers/strings, or a JSON object ZbSendRead(val_read, device, groupaddr, cluster, endpoint, manuf, ZCL_READ_ATTRIBUTES); - } else if (nullptr != &val_write) { + } else if (val_write) { // only KSON object - if (!val_write.is()) { + if (!val_write.isObject()) { ResponseCmndChar_P(PSTR("Missing parameters")); return; } // "Write":{...attributes...} ZbSendReportWrite(val_write, device, groupaddr, cluster, endpoint, manuf, ZCL_WRITE_ATTRIBUTES); - } else if (nullptr != &val_publish) { + } else if (val_publish) { // "Publish":{...attributes...} // only KSON object - if (!val_publish.is()) { + if (!val_publish.isObject()) { ResponseCmndChar_P(PSTR("Missing parameters")); return; } ZbSendReportWrite(val_publish, device, groupaddr, cluster, endpoint, manuf, ZCL_REPORT_ATTRIBUTES); - } else if (nullptr != &val_response) { + } else if (val_response) { // "Report":{...attributes...} // only KSON object - if (!val_response.is()) { + if (!val_response.isObject()) { ResponseCmndChar_P(PSTR("Missing parameters")); return; } ZbSendReportWrite(val_response, device, groupaddr, cluster, endpoint, manuf, ZCL_READ_ATTRIBUTES_RESPONSE); - } else if (nullptr != &val_read_config) { + } else if (val_read_config) { // "ReadConfg":{...attributes...}, "ReadConfg":attribute or "ReadConfg":[...attributes...] // we accept eitehr a number, a string, an array of numbers/strings, or a JSON object ZbSendRead(val_read_config, device, groupaddr, cluster, endpoint, manuf, ZCL_READ_REPORTING_CONFIGURATION); - } else if (nullptr != &val_config) { + } else if (val_config) { // "Config":{...attributes...} // only JSON object - if (!val_config.is()) { + if (!val_config.isObject()) { ResponseCmndChar_P(PSTR("Missing parameters")); return; } @@ -774,61 +748,56 @@ void ZbBindUnbind(bool unbind) { // false = bind, true = unbind // local endpoint is always 1, IEEE addresses are calculated if (zigbee.init_phase) { ResponseCmndChar_P(PSTR(D_ZIGBEE_NOT_STARTED)); return; } - DynamicJsonBuffer jsonBuf; - const JsonObject &json = jsonBuf.parseObject((const char*) XdrvMailbox.data); - if (!json.success()) { ResponseCmndChar_P(PSTR(D_JSON_INVALID_JSON)); return; } + JsonParserObject root = JsonParser(XdrvMailbox.data).getRootObject(); + if (!root) { ResponseCmndChar_P(PSTR(D_JSON_INVALID_JSON)); return; } // params - uint16_t srcDevice = BAD_SHORTADDR; // BAD_SHORTADDR is broadcast, so considered invalid + uint16_t srcDevice = BAD_SHORTADDR; // BAD_SHORTADDR is broadcast, so considered invalid uint16_t dstDevice = BAD_SHORTADDR; // BAD_SHORTADDR is broadcast, so considered invalid uint64_t dstLongAddr = 0; uint8_t endpoint = 0x00; // 0x00 is invalid for the src endpoint - uint8_t toendpoint = 0x00; // 0x00 is invalid for the dst endpoint + uint8_t toendpoint = 0x01; // default dest endpoint to 0x01 uint16_t toGroup = 0x0000; // group address uint16_t cluster = 0; // 0xFFFF is invalid uint32_t group = 0xFFFFFFFF; // 16 bits values, otherwise 0xFFFFFFFF is unspecified // Information about source device: "Device", "Endpoint", "Cluster" // - the source endpoint must have a known IEEE address - const JsonVariant &val_device = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_DEVICE)); - if (nullptr != &val_device) { - srcDevice = zigbee_devices.parseDeviceParam(val_device.as()); - } - if ((nullptr == &val_device) || (BAD_SHORTADDR == srcDevice)) { ResponseCmndChar_P(PSTR("Unknown source device")); return; } + srcDevice = zigbee_devices.parseDeviceParam(root.getStr(PSTR(D_CMND_ZIGBEE_DEVICE), nullptr)); + if (BAD_SHORTADDR == srcDevice) { ResponseCmndChar_P(PSTR("Unknown source device")); return; } // check if IEEE address is known 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(D_CMND_ZIGBEE_ENDPOINT)); - if (nullptr != &val_endpoint) { endpoint = strToUInt(val_endpoint); } - else { endpoint = zigbee_devices.findFirstEndpoint(srcDevice); } + endpoint = root.getUInt(PSTR(D_CMND_ZIGBEE_ENDPOINT), endpoint); + if (0 == endpoint) { endpoint = zigbee_devices.findFirstEndpoint(srcDevice); } // look for source cluster - const JsonVariant &val_cluster = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_CLUSTER)); - if (nullptr != &val_cluster) { - cluster = strToUInt(val_cluster); // first convert as number + JsonParserToken val_cluster = root[PSTR(D_CMND_ZIGBEE_CLUSTER)]; + if (val_cluster) { + cluster = val_cluster.getUInt(cluster); // first convert as number if (0 == cluster) { - zigbeeFindAttributeByName(val_cluster.as(), &cluster, nullptr, nullptr); + zigbeeFindAttributeByName(val_cluster.getStr(), &cluster, nullptr, nullptr); } } // Or Group Address - we don't need a dstEndpoint in this case - const JsonVariant &to_group = GetCaseInsensitive(json, PSTR("ToGroup")); - if (nullptr != &to_group) { toGroup = strToUInt(to_group); } + JsonParserToken to_group = root[PSTR("ToGroup")]; + if (to_group) { toGroup = to_group.getUInt(toGroup); } // 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")); + JsonParserToken dst_device = root[PSTR("ToDevice")]; // If no target is specified, we default to coordinator 0x0000 - if ((nullptr == &to_group) && (nullptr == &dst_device)) { + if ((!to_group) && (!dst_device)) { dstDevice = 0x0000; } - if ((nullptr != &dst_device) || (BAD_SHORTADDR != dstDevice)) { + if ((dst_device) || (BAD_SHORTADDR != dstDevice)) { if (BAD_SHORTADDR == dstDevice) { - dstDevice = zigbee_devices.parseDeviceParam(dst_device.as()); + dstDevice = zigbee_devices.parseDeviceParam(dst_device.getStr(nullptr)); if (BAD_SHORTADDR == dstDevice) { ResponseCmndChar_P(PSTR("Invalid parameter")); return; } } if (0x0000 == dstDevice) { @@ -838,14 +807,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")); - if (nullptr != &val_toendpoint) { toendpoint = strToUInt(val_toendpoint); } - else { toendpoint = 0x01; } // default to endpoint 1 + toendpoint = root.getUInt(PSTR("ToEndpoint"), toendpoint); } // make sure we don't have conflicting parameters - if (&to_group && dstLongAddr) { ResponseCmndChar_P(PSTR("Cannot have both \"ToDevice\" and \"ToGroup\"")); return; } - if (!&to_group && !dstLongAddr) { ResponseCmndChar_P(PSTR("Missing \"ToDevice\" or \"ToGroup\"")); return; } + if (to_group && dstLongAddr) { ResponseCmndChar_P(PSTR("Cannot have both \"ToDevice\" and \"ToGroup\"")); return; } + if (!to_group && !dstLongAddr) { ResponseCmndChar_P(PSTR("Missing \"ToDevice\" or \"ToGroup\"")); return; } #ifdef USE_ZIGBEE_ZNP SBuffer buf(34); @@ -1097,38 +1064,32 @@ void CmndZbSave(void) { // 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; + JsonParser p(XdrvMailbox.data); + JsonParserToken root = p.getRoot(); - // check if parsing succeeded - if (json_parsed.is()) { - success = json_parsed.as().success(); - } else if (json_parsed.is()) { - success = json_parsed.as().success(); - } - if (!success) { ResponseCmndChar_P(PSTR(D_JSON_INVALID_JSON)); return; } + if (!p || !(root.isObject() || root.isArray())) { ResponseCmndChar_P(PSTR(D_JSON_INVALID_JSON)); return; } // Check is root contains `ZbStatus` key, if so change the root - const JsonVariant * zbstatus = &startsWithCaseInsensitive(*json, PSTR("ZbStatus")); - if (nullptr != zbstatus) { - json = zbstatus; + JsonParserToken zbstatus = root.getObject().findStartsWith(PSTR("ZbStatus")); + if (zbstatus) { + root = zbstatus; } // check if the root is an array - if (json->is()) { - const JsonArray& arr = json->as(); - for (auto elt : arr) { + if (root.isArray()) { + JsonParserArray arr = JsonParserArray(root); + for (const auto elt : arr) { // call restore on each item - int32_t res = zigbee_devices.deviceRestore(elt); - if (res < 0) { - ResponseCmndChar_P(PSTR("Restore failed")); - return; + if (elt.isObject()) { + int32_t res = zigbee_devices.deviceRestore(JsonParserObject(elt)); + if (res < 0) { + ResponseCmndChar_P(PSTR("Restore failed")); + return; + } } } - } else if (json->is()) { - int32_t res = zigbee_devices.deviceRestore(*json); + } else if (root.isObject()) { + int32_t res = zigbee_devices.deviceRestore(JsonParserObject(root)); if (res < 0) { ResponseCmndChar_P(PSTR("Restore failed")); return; @@ -1258,31 +1219,19 @@ void CmndZbConfig(void) { // if (zigbee.init_phase) { ResponseCmndChar_P(PSTR(D_ZIGBEE_NOT_STARTED)); return; } RemoveAllSpaces(XdrvMailbox.data); if (strlen(XdrvMailbox.data) > 0) { - DynamicJsonBuffer jsonBuf; - const JsonObject &json = jsonBuf.parseObject((const char*) XdrvMailbox.data); - if (!json.success()) { ResponseCmndChar_P(PSTR(D_JSON_INVALID_JSON)); return; } - + JsonParserObject root = JsonParser(XdrvMailbox.data).getRootObject(); + if (!root) { ResponseCmndChar_P(PSTR(D_JSON_INVALID_JSON)); return; } // Channel - const JsonVariant &val_channel = GetCaseInsensitive(json, PSTR("Channel")); - if (nullptr != &val_channel) { zb_channel = strToUInt(val_channel); } + + zb_channel = root.getUInt(PSTR("Channel"), zb_channel); + zb_pan_id = root.getUInt(PSTR("PanID"), zb_pan_id); + zb_ext_panid = root.getULong(PSTR("ExtPanID"), zb_ext_panid); + zb_precfgkey_l = root.getULong(PSTR("KeyL"), zb_precfgkey_l); + zb_precfgkey_h = root.getULong(PSTR("KeyH"), zb_precfgkey_h); + zb_txradio_dbm = root.getUInt(PSTR("TxRadio"), zb_txradio_dbm); + if (zb_channel < 11) { zb_channel = 11; } if (zb_channel > 26) { zb_channel = 26; } - // 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")); - 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")); - 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")); - if (nullptr != &val_key_h) { zb_precfgkey_h = strtoull(val_key_h.as(), nullptr, 0); } - // TxRadio dBm - const JsonVariant &val_txradio = GetCaseInsensitive(json, PSTR("TxRadio")); - if (nullptr != &val_txradio) { zb_txradio_dbm = strToUInt(val_txradio); } - // if network key is zero, we generate a truly random key with a hardware generator from ESP if ((0 == zb_precfgkey_l) && (0 == zb_precfgkey_h)) { AddLog_P2(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "generating random Zigbee network key"));