" for commands to devices
- char attrid_str[12];
- snprintf_P(attrid_str, sizeof(attrid_str), PSTR("%04X%c%02X"), cluster, direction ? '<' : '!', cmd);
- Z_attribute & attr_raw = attr_list.addAttribute(attrid_str);
+ // char attrid_str[12];
+ // snprintf_P(attrid_str, sizeof(attrid_str), PSTR("%04X%c%02X"), cluster, direction ? '<' : '!', cmd);
+ // Z_attribute & attr_raw = attr_list.addAttribute(attrid_str);
+ Z_attribute & attr_raw = attr_list.addAttributeCmd(cluster, cmd, direction);
attr_raw.setBuf(payload, 0, payload.len());
+ // TODO Berry encode command
+
if (command_name) {
// Now try to transform into a human readable format
// if (direction & 0x80) then specific transform
diff --git a/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_7_0_statemachine.ino b/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_7_0_statemachine.ino
index 13f301b29..08211a1d0 100644
--- a/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_7_0_statemachine.ino
+++ b/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_7_0_statemachine.ino
@@ -315,16 +315,22 @@ ZBM(ZBR_ZDO_ACTIVEEPREQ, Z_SRSP | Z_ZDO, ZDO_ACTIVE_EP_REQ, Z_SUCCESS) // 65050
// Change #14819 - we now allow some EP to be alreaady declared
ZBM(ZBR_ZDO_ACTIVEEPRSP_SUCESS, Z_AREQ | Z_ZDO, ZDO_ACTIVE_EP_RSP, 0x00, 0x00 /* srcAddr */, Z_SUCCESS) // 45050000xxxx - no Ep running
ZBM(ZBR_ZDO_ACTIVEEPRSP_OK, Z_AREQ | Z_ZDO, ZDO_ACTIVE_EP_RSP, 0x00, 0x00 /* srcAddr */, Z_SUCCESS,
- 0x00, 0x00 /* nwkaddr */, 0x02 /* activeepcount */, 0x0B, 0x01 /* the actual endpoints */) // 25050000 - no Ep running
+ 0x00, 0x00 /* nwkaddr */, 0x03 /* activeepcount */, 0xF2, 0x0B, 0x01 /* the actual endpoints */) // 4585000000000003F20B01
// Z_AF:register profile:104, ep:01
ZBM(ZBS_AF_REGISTER01, Z_SREQ | Z_AF, AF_REGISTER, 0x01 /* endpoint */, Z_B0(Z_PROF_HA), Z_B1(Z_PROF_HA), // 24000401050000000000
0x05, 0x00 /* AppDeviceId */, 0x00 /* AppDevVer */, 0x00 /* LatencyReq */,
- 0x00 /* AppNumInClusters */, 0x00 /* AppNumInClusters */)
+ 0x00 /* AppNumInClusters */, 0x00 /* AppNumOutClusters */)
ZBM(ZBR_AF_REGISTER, Z_SRSP | Z_AF, AF_REGISTER, Z_SUCCESS) // 640000
ZBM(ZBS_AF_REGISTER0B, Z_SREQ | Z_AF, AF_REGISTER, 0x0B /* endpoint */, Z_B0(Z_PROF_HA), Z_B1(Z_PROF_HA), // 2400040B050000000000
0x05, 0x00 /* AppDeviceId */, 0x00 /* AppDevVer */, 0x00 /* LatencyReq */,
- 0x00 /* AppNumInClusters */, 0x00 /* AppNumInClusters */)
+ 0x00 /* AppNumInClusters */, 0x00 /* AppNumOutClusters */)
+// Green Power endpoint 242 0xF2
+ZBM(ZBS_AF_REGISTERF2, Z_SREQ | Z_AF, AF_REGISTER, 0xF2 /* endpoint */, Z_B0(Z_PROF_GP), Z_B1(Z_PROF_GP), //
+ 0x05, 0x61 /* AppDeviceId */, 0x00 /* AppDevVer */, 0x00 /* LatencyReq */,
+ 0x00 /* AppNumInClusters */,
+ 0x01 /* AppNumOutClusters */,
+ 0x21,0x00) // 0x0021
// Z_AF:register profile:104, ep:01 - main clusters for router or device
ZBM(ZBS_AF_REGISTER_ALL, Z_SREQ | Z_AF, AF_REGISTER, 0x01 /* endpoint */, Z_B0(Z_PROF_HA), Z_B1(Z_PROF_HA), // 24000401050000000000
0x05, 0x00 /* AppDeviceId */, 0x00 /* AppDevVer */, 0x00 /* LatencyReq */,
@@ -333,7 +339,7 @@ ZBM(ZBS_AF_REGISTER_ALL, Z_SREQ | Z_AF, AF_REGISTER, 0x01 /* endpoint */, Z_B0(Z
0x07,0x00, 0x08,0x00, 0x0A,0x00, 0x02,0x01, // 0x0007, 0x0008, 0x000A, 0X0102
0x00,0x03, 0x00,0x04, 0x02,0x04, 0x03,0x04, // 0x0300, 0x0400, 0x0402, 0x0403
0x05,0x04, 0x06,0x04, // 0x0405, 0x0406
- 0x00 /* AppNumInClusters */)
+ 0x00 /* AppNumOutClusters */)
// Z_ZDO:mgmtPermitJoinReq
ZBM(ZBS_PERMITJOINREQ_CLOSE, Z_SREQ | Z_ZDO, ZDO_MGMT_PERMIT_JOIN_REQ, 0x02 /* AddrMode */, // 25360200000000
@@ -467,7 +473,7 @@ static const Zigbee_Instruction zb_prog[] PROGMEM = {
ZI_MQTT_STATE(ZIGBEE_STATUS_STARTING, kConfiguredCoord)
ZI_ON_ERROR_GOTO(ZIGBEE_LABEL_ABORT) // set any failure to ABORT
ZI_SEND(ZBS_STARTUPFROMAPP) // start coordinator
- ZI_WAIT_RECV(5000, ZBR_STARTUPFROMAPP) // wait for sync ack of command
+ ZI_WAIT_RECV(10000, ZBR_STARTUPFROMAPP) // wait for sync ack of command
ZI_WAIT_UNTIL_FUNC(20000, AREQ_STARTUPFROMAPP, &ZNP_ReceiveStateChange) // wait for async message that coordinator started, max 20s
ZI_GOTO(ZIGBEE_LABEL_COORD_STARTED)
@@ -487,7 +493,7 @@ static const Zigbee_Instruction zb_prog[] PROGMEM = {
ZI_WAIT_RECV(2000, ZBR_W_BDB_CHANN_OK)
// all is good, we can start
ZI_SEND(ZBS_BDB_START_COMMIS) // start coordinator
- ZI_WAIT_RECV(5000, ZBR_BDB_START_COMMIS) // wait for sync ack of command
+ ZI_WAIT_RECV(10000, ZBR_BDB_START_COMMIS) // wait for sync ack of command
ZI_WAIT_UNTIL_FUNC(20000, AREQ_STARTUPFROMAPP, &ZNP_ReceiveStateChange) // wait for async message that coordinator started, max 20s
// ======================================================================
@@ -506,6 +512,8 @@ static const Zigbee_Instruction zb_prog[] PROGMEM = {
ZI_WAIT_RECV(1000, ZBR_AF_REGISTER)
ZI_SEND(ZBS_AF_REGISTER0B) // Z_AF register for endpoint 0B, profile 0x0104 Home Automation
ZI_WAIT_RECV(1000, ZBR_AF_REGISTER)
+ ZI_SEND(ZBS_AF_REGISTERF2) // Z_AF register for endpoint F2, profile 0xa1e0 Green Power
+ ZI_WAIT_RECV(1000, ZBR_AF_REGISTER)
// Write again channels, see https://github.com/Koenkk/zigbee-herdsman/blob/37bea20ba04ee5d4938abc21a7569b43f831de32/src/adapter/z-stack/adapter/startZnp.ts#L244-L245
ZI_SEND(ZBS_W_CHANN) // write CHANNEL
ZI_WAIT_RECV(1000, ZBR_WNV_OK)
@@ -529,6 +537,7 @@ static const Zigbee_Instruction zb_prog[] PROGMEM = {
ZI_CALL(&Z_Load_Devices, 0)
ZI_CALL(&Z_Load_Data, 0)
ZI_CALL(&Z_Set_Save_Data_Timer, 0)
+ ZI_CALL(&Z_ZbAutoload, 0)
ZI_CALL(&Z_Query_Bulbs, 0)
ZI_LABEL(ZIGBEE_LABEL_MAIN_LOOP)
@@ -966,6 +975,7 @@ static const Zigbee_Instruction zb_prog[] PROGMEM = {
ZI_CALL(&Z_Load_Devices, 0)
ZI_CALL(&Z_Load_Data, 0)
ZI_CALL(&Z_Set_Save_Data_Timer, 0)
+ ZI_CALL(&Z_ZbAutoload, 0)
ZI_CALL(&Z_Query_Bulbs, 0)
ZI_LABEL(ZIGBEE_LABEL_MAIN_LOOP)
diff --git a/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_7_6_plugin.ino b/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_7_6_plugin.ino
new file mode 100644
index 000000000..d6f9a0089
--- /dev/null
+++ b/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_7_6_plugin.ino
@@ -0,0 +1,478 @@
+/*
+ xdrv_23_zigbee.ino - zigbee support for Tasmota
+
+ Copyright (C) 2021 Theo Arends and Stephan Hadinger
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+*/
+
+#ifdef USE_ZIGBEE
+
+const char Z_MUL[] PROGMEM = "mul:";
+const char Z_DIV[] PROGMEM = "div:";
+const char Z_MANUF[] PROGMEM = "manuf:";
+const char Z_ADD[] PROGMEM = "add:";
+
+char * Z_subtoken(char * token, const char * prefix) {
+ size_t prefix_len = strlen_P(prefix);
+ if (!strncmp_P(token, prefix, prefix_len)) {
+ return token + prefix_len;
+ }
+ return nullptr;
+}
+
+// global singleton
+Z_plugin_templates g_plugin_templates;
+
+class Z_attribute_synonym Z_plugin_matchAttributeSynonym(const char *model, const char *manufacturer, uint16_t cluster, uint16_t attribute) {
+ const Z_attribute_synonym * attr_syn;
+ attr_syn = g_plugin_templates.matchAttributeSynonym(model == nullptr ? "" : model,
+ manufacturer == nullptr ? "" : manufacturer,
+ cluster, attribute);
+
+ Z_attribute_synonym syn;
+ if (attr_syn != nullptr) {
+ syn = *attr_syn;
+ }
+ return syn;
+}
+
+Z_attribute_match Z_plugin_matchAttributeById(const char *model, const char *manufacturer, uint16_t cluster, uint16_t attribute) {
+ const Z_plugin_attribute * attr_tmpl;
+ attr_tmpl = g_plugin_templates.matchAttributeById(model == nullptr ? "" : model,
+ manufacturer == nullptr ? "" : manufacturer,
+ cluster, attribute);
+
+ Z_attribute_match attr;
+ if (attr_tmpl != nullptr) {
+ attr.cluster = attr_tmpl->cluster;
+ attr.attribute = attr_tmpl->attribute;
+ attr.name = attr_tmpl->name.c_str();
+ attr.zigbee_type = attr_tmpl->type;
+ attr.multiplier = attr_tmpl->multiplier;
+ attr.divider = attr_tmpl->divider;
+ attr.base = attr_tmpl->base;
+ attr.manuf = attr_tmpl->manuf;
+ }
+ return attr;
+}
+
+Z_attribute_match Z_plugin_matchAttributeByName(const char *model, const char *manufacturer, const char * name) {
+ const Z_plugin_attribute * attr_tmpl;
+ Z_attribute_match attr;
+
+ if (name != nullptr) {
+ attr_tmpl = g_plugin_templates.matchAttributeByName(model == nullptr ? "" : model,
+ manufacturer == nullptr ? "" : manufacturer,
+ name);
+
+ if (attr_tmpl != nullptr) {
+ attr.cluster = attr_tmpl->cluster;
+ attr.attribute = attr_tmpl->attribute;
+ attr.name = attr_tmpl->name.c_str();
+ attr.zigbee_type = attr_tmpl->type;
+ attr.multiplier = attr_tmpl->multiplier;
+ attr.divider = attr_tmpl->divider;
+ attr.base = attr_tmpl->base;
+ attr.manuf = attr_tmpl->manuf;
+ }
+ }
+ return attr;
+}
+
+bool Zb_readline(class File *f, char* buf, size_t size) {
+ bool eof = 0;
+ while (1) {
+ // read line
+ bool comment = false; // did we encounter '#', if so ignore anything until '\n'
+ char * p = buf;
+ while (1) {
+ int c = f->read();
+ if (c == -1) { eof = true; break; } // EOF reached
+ if (c == '#') { comment = true; } // rest of line is ignored
+ if (c == '\n') { break; } // end of line
+ if (!comment) {
+ if (p < buf + size - 1) {
+ *p++ = c; // append character
+ } else {
+ AddLog(LOG_LEVEL_INFO, "ZIG: ZbLoad line exceeds 96 bytes, aborting");
+ return false;
+ }
+ }
+ }
+ int32_t ret = p - buf; // len in bytes
+ if (eof && ret == 0) { return false; } // nothing more to read
+ // found something, don't add the \n since we don't need it
+ buf[ret] = 0; // add string terminator
+ RemoveSpace(buf); // remove anything that looks like a space, tab, crlf...
+ // AddLog(LOG_LEVEL_INFO, "ZIG: ZbRead>'%s'", buf);
+ return true;
+ }
+}
+
+#ifdef USE_UFILESYS
+extern FS *ffsp;
+#endif
+
+// load a file from filesystem
+// returns `true` if success
+bool ZbLoad(const char *filename_raw) {
+
+#ifdef USE_UFILESYS
+ if (ffsp) {
+ // first unload previsou definitions
+ ZbUnload(filename_raw);
+
+ String filename = filename_raw;
+ if (filename_raw[0] != '/') {
+ filename = "/";
+ filename += filename_raw;
+ }
+ File fp;
+ fp = ffsp->open(filename.c_str(), "r");
+
+ if (fp <= 0) {
+ AddLog(LOG_LEVEL_INFO, "ZIG: unable to load file '%s'", filename.c_str());
+ return false;
+ }
+
+ char * filename_imported = nullptr;
+ Z_plugin_template * tmpl = nullptr; // current template with matchers and attributes
+ bool new_matchers = false; // indicates that we have finished the current matchers
+ char buf_line[96]; // max line is 96 bytes (comments don't count)
+
+ // read the first 6 chars
+ bool invalid_header = false;
+ static const char Z2T_HEADER_V1[] PROGMEM = "#Z2Tv1";
+ for (uint32_t i = 0; i < 6; i++) {
+ int c = fp.read();
+ if (c < 0) {
+ invalid_header = true;
+ break;
+ }
+ buf_line[i] = c;
+ buf_line[i+1] = 0;
+ }
+ if (!invalid_header) {
+ if (strcmp_P(buf_line, Z2T_HEADER_V1) != 0) {
+ invalid_header = true;
+ }
+ }
+
+ if (invalid_header) {
+ AddLog(LOG_LEVEL_INFO, "ZIG: ZbLoad '%s' invalid header", filename_raw);
+ return false;
+ }
+
+ // parse line by line
+ while (1) {
+ if (!Zb_readline(&fp, buf_line, sizeof(buf_line))) {
+ // EOF
+ break;
+ }
+
+ // at first valid line, we instanciate a new plug-in instance and assign a filemane
+ if (filename_imported == nullptr) {
+ // allocate only once the filename for multiple entries
+ // freed only by `ZbUnload`
+ filename_imported = (char*) malloc(strlen(filename.c_str())+1);
+ strcpy(filename_imported, filename.c_str());
+ }
+
+ // there is a non-empty line, containing no space/tab/crlf
+ // depending on the first char, parse either device name or cluster/attribute+name
+ if (buf_line[0] == ':') {
+ if (tmpl == nullptr || new_matchers) {
+ tmpl = &g_plugin_templates.addToLast();
+ tmpl->filename = filename_imported;
+ new_matchers = false;
+ }
+ // parse device name
+ char *rest = buf_line + 1; // skip first char ':'
+ char *token = strtok_r(rest, ",", &rest);
+ Z_plugin_matcher & matcher = tmpl->matchers.addToLast();
+ if (token != nullptr) {
+ matcher.model = token;
+ }
+ token = strtok_r(rest, ",", &rest);
+ if (token != nullptr) {
+ matcher.manufacturer = token;
+ }
+ } else {
+ if (tmpl == nullptr) {
+ continue;
+ }
+ new_matchers = true;
+ char *rest = buf_line;
+ char *token = strtok_r(rest, ",", &rest);
+ if (token == nullptr) {
+ continue;
+ }
+
+ // detect if token contains '=', if yes it is a synonym
+ char * delimiter_equal = strchr(token, '=');
+
+ if (delimiter_equal == nullptr) {
+ // NORMAL ATTRIBUTE
+ // token is of from '0000/0000' or '0000/0000%00'
+ char * delimiter_slash = strchr(token, '/');
+ char * delimiter_percent = strchr(token, '%');
+ if (delimiter_slash == nullptr || (delimiter_percent != nullptr && delimiter_slash > delimiter_percent)) {
+ AddLog(LOG_LEVEL_INFO, "ZIG: ZbLoad '%s' wrong delimiter '%s'", filename_raw, token);
+ }
+
+ uint16_t attr_id = 0xFFFF;
+ uint16_t cluster_id = 0xFFFF;
+ uint8_t type_id = Zunk;
+ int8_t multiplier = 1;
+ int8_t divider = 1;
+ int16_t base = 0;
+ char * name = nullptr;
+ uint16_t manuf = 0;
+
+ cluster_id = strtoul(token, &delimiter_slash, 16);
+ if (!delimiter_percent) {
+ attr_id = strtoul(delimiter_slash+1, nullptr, 16);
+ } else {
+ attr_id = strtoul(delimiter_slash+1, &delimiter_percent, 16);
+ type_id = Z_getTypeByName(delimiter_percent+1);
+ }
+ // NAME of the attribute
+ token = strtok_r(rest, ",", &rest);
+ if (token == nullptr) {
+ AddLog(LOG_LEVEL_INFO, "ZIG: ZbLoad '%s' ignore missing name '%s'", filename_raw, buf_line);
+ continue;
+ }
+ name = token;
+ // ADDITIONAL ELEMENTS?
+ // Ex: `manuf:1037`
+ while (token = strtok_r(rest, ",", &rest)) {
+ char * sub_token;
+ // look for multiplier
+ if (sub_token = Z_subtoken(token, Z_MUL)) {
+ multiplier = strtol(sub_token, nullptr, 10);
+ }
+ // look for divider
+ else if (sub_token = Z_subtoken(token, Z_DIV)) {
+ divider = strtol(sub_token, nullptr, 10); // negative to indicate divider
+ }
+ // look for offset (base)
+ else if (sub_token = Z_subtoken(token, Z_ADD)) {
+ base = strtol(sub_token, nullptr, 10); // negative to indicate divider
+ }
+ // look for `manuf:HHHH`
+ else if (sub_token = Z_subtoken(token, Z_MANUF)) {
+ manuf = strtoul(sub_token, nullptr, 16);
+ }
+ else {
+ AddLog(LOG_LEVEL_DEBUG, "ZIG: ZbLoad unrecognized modifier '%s'", token);
+ }
+ }
+
+ // token contains the name of the attribute
+ Z_plugin_attribute & plugin_attr = tmpl->attributes.addToLast();
+ plugin_attr.cluster = cluster_id;
+ plugin_attr.attribute = attr_id;
+ plugin_attr.type = type_id;
+ plugin_attr.name = name;
+ plugin_attr.multiplier = multiplier;
+ plugin_attr.divider = divider;
+ plugin_attr.base = base;
+ plugin_attr.manuf = manuf;
+ } else {
+ // ATTRIBUTE SYNONYM
+ // token is of from '0000/0000=0000/0000,1'
+ char * rest2 = token;
+ char * tok2 = strtok_r(rest2, "=", &rest2);
+ char * delimiter_slash = strchr(tok2, '/');
+ uint16_t cluster_id = strtoul(tok2, &delimiter_slash, 16);
+ uint16_t attr_id = strtoul(delimiter_slash+1, nullptr, 16);
+ tok2 = strtok_r(rest2, "=", &rest2);
+ char * delimiter_slash2 = strchr(tok2, '/');
+ uint16_t new_cluster_id = strtoul(tok2, &delimiter_slash2, 16);
+ uint16_t new_attr_id = strtoul(delimiter_slash2+1, nullptr, 16);
+ int8_t multiplier = 1;
+ int8_t divider = 1;
+ int16_t base = 0;
+
+ // ADDITIONAL ELEMENTS?
+ while (token = strtok_r(rest, ",", &rest)) {
+ char * sub_token;
+ // look for multiplier
+ if (sub_token = Z_subtoken(token, Z_MUL)) {
+ multiplier = strtol(sub_token, nullptr, 10);
+ }
+ // look for divider
+ else if (sub_token = Z_subtoken(token, Z_DIV)) {
+ divider = strtol(sub_token, nullptr, 10); // negative to indicate divider
+ }
+ // look for offset (base)
+ else if (sub_token = Z_subtoken(token, Z_ADD)) {
+ base = strtol(sub_token, nullptr, 10); // negative to indicate divider
+ }
+ else {
+ AddLog(LOG_LEVEL_DEBUG, "ZIG: ZbLoad unrecognized modifier '%s'", token);
+ }
+ }
+ // create the synonym
+ Z_attribute_synonym & syn = tmpl->synonyms.addToLast();
+ syn.cluster = cluster_id;
+ syn.attribute = attr_id;
+ syn.new_cluster = new_cluster_id;
+ syn.new_attribute = new_attr_id;
+ syn.multiplier = multiplier;
+ syn.divider = divider;
+ syn.base = base;
+ }
+ }
+ }
+ } else {
+ AddLog(LOG_LEVEL_ERROR, "ZIG: filesystem not enabled");
+ }
+#else
+ AddLog(LOG_LEVEL_INFO, "ZIG: ZbLoad requires file system");
+#endif
+ return true;
+}
+
+// Unlaod previously loaded definitions
+bool ZbUnload(const char *filename_raw) {
+ String filename = filename_raw;
+ if (filename_raw[0] != '/') {
+ filename = "/";
+ filename += filename_raw;
+ }
+
+ char * filename_registered = nullptr; // internal allocation for filename
+ for (const Z_plugin_template & tmpl : g_plugin_templates) {
+ bool to_be_freed = false;
+ if (filename_registered) {
+ // if filename_registered is not NULL, compare pointers
+ if (tmpl.filename == filename_registered) { to_be_freed = true; }
+ } else {
+ if (strcmp(tmpl.filename, filename.c_str()) == 0) {
+ filename_registered = tmpl.filename;
+ to_be_freed = true;
+ }
+ }
+ // check if we remove this node
+ if (to_be_freed) {
+ g_plugin_templates.remove(&tmpl);
+ }
+ }
+ // free memory for filename
+ if (filename_registered) {
+ free(filename_registered);
+ AddLog(LOG_LEVEL_INFO, "ZIG: ZbUnload '%s' sucessful", filename_raw);
+ return true;
+ }
+ return false;
+}
+
+// append modifiers like mul/div/manuf
+void Z_AppendModifiers(char * buf, size_t buf_len, int8_t multiplier, int8_t divider, int16_t base, uint16_t manuf) {
+ if (multiplier != 0 && multiplier != 1) {
+ ext_snprintf_P(buf, buf_len, "%s,%s%i", buf, Z_MUL, multiplier);
+ }
+ if (divider != 0 && divider != 1) {
+ ext_snprintf_P(buf, buf_len, "%s,%s%i", buf, Z_DIV, divider);
+ }
+ if (base != 0) {
+ ext_snprintf_P(buf, buf_len, "%s,%s%i", buf, Z_ADD, base);
+ }
+ if (manuf) {
+ ext_snprintf_P(buf, buf_len, "%s,%s%04X", buf, Z_MANUF, manuf);
+ }
+}
+
+// Dump the ZbLoad structure in a format compatible with ZbLoad
+void ZbLoadDump(void) {
+ char buf[96];
+ AddLog(LOG_LEVEL_INFO, "ZIG: ZbLoad dump all current information");
+ AddLog(LOG_LEVEL_INFO, "====> START");
+
+ for (const Z_plugin_template & tmpl : g_plugin_templates) {
+ if (tmpl.filename != nullptr) {
+ ext_snprintf_P(buf, sizeof(buf), "# imported from '%s'", tmpl.filename);
+ AddLog(LOG_LEVEL_INFO, PSTR("%s"), buf);
+ }
+ // marchers
+ if (tmpl.matchers.length() == 0) {
+ ext_snprintf_P(buf, sizeof(buf), ": # no matcher");
+ AddLog(LOG_LEVEL_INFO, PSTR("%s"), buf);
+ } else {
+ for (const Z_plugin_matcher & matcher : tmpl.matchers) {
+ ext_snprintf_P(buf, sizeof(buf), ":%s,%s", matcher.model.c_str(), matcher.manufacturer.c_str());
+ AddLog(LOG_LEVEL_INFO, PSTR("%s"), buf);
+ }
+ }
+ // attributes
+ if (tmpl.attributes.length() == 0 && tmpl.synonyms.length() == 0) {
+ // no content, output an empty line
+ AddLog(LOG_LEVEL_INFO, "");
+ } else {
+ for (const Z_plugin_attribute & attr : tmpl.attributes) {
+ ext_snprintf_P(buf, sizeof(buf), "%04X/%04X", attr.cluster, attr.attribute);
+ // add type if known
+ if (attr.type != Zunk) {
+ char type_str[16];
+ Z_getTypeByNumber(type_str, sizeof(type_str), attr.type);
+ ext_snprintf_P(buf, sizeof(buf), "%s%%%s", buf, type_str);
+ }
+ Z_AppendModifiers(buf, sizeof(buf), attr.multiplier, attr.divider, attr.base, attr.manuf);
+ AddLog(LOG_LEVEL_INFO, PSTR("%s"), buf);
+ }
+ for (const Z_attribute_synonym & syn : tmpl.synonyms) {
+ ext_snprintf_P(buf, sizeof(buf), "%04X/%04X=%04X/%04X", syn.cluster, syn.attribute, syn.new_cluster, syn.new_attribute);
+ Z_AppendModifiers(buf, sizeof(buf), syn.multiplier, syn.divider, syn.base, 0);
+ AddLog(LOG_LEVEL_INFO, PSTR("%s"), buf);
+ }
+ }
+ }
+
+ AddLog(LOG_LEVEL_INFO, "<==== END");
+}
+
+// Auto-load all files ending with '.zb'
+void ZbAutoload(void) {
+#ifdef USE_UFILESYS
+ if (ffsp) {
+ File dir = ffsp->open("/", "r");
+ if (dir) {
+ dir.rewindDirectory();
+ while (1) {
+ File entry = dir.openNextFile();
+ if (!entry) { break; }
+ const char * fn = entry.name();
+ if (strcmp(fn, ".") && strcmp(fn, "..")) {
+ // check suffix
+ size_t l = strlen(fn);
+ if (l > 3) {
+ if (fn[l-3] == '.' && fn[l-2] == 'z' && fn[l-1] == 'b') {
+ bool ret = ZbLoad(fn);
+ if (ret) {
+ AddLog(LOG_LEVEL_INFO, "ZIG: ZbLoad '%s' loaded successfully", fn);
+ }
+ }
+ }
+ }
+
+ }
+
+ }
+ }
+#endif // USE_UFILESYS
+}
+
+#endif // USE_ZIGBEE
diff --git a/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_8_parsers.ino b/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_8_parsers.ino
index 14b64109a..4e82814ba 100644
--- a/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_8_parsers.ino
+++ b/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_8_parsers.ino
@@ -1553,25 +1553,24 @@ void Z_AutoConfigReportingForCluster(uint16_t shortaddr, uint16_t groupaddr, uin
uint16_t max_interval = pgm_read_word(&(Z_autoAttributeReporting[i].max_interval));
float report_change_raw = Z_autoAttributeReporting[i].report_change;
double report_change = report_change_raw;
- uint8_t attr_type;
- int8_t multiplier;
+ // uint8_t attr_type;
+ // int8_t multiplier;
- const __FlashStringHelper* attr_name = zigbeeFindAttributeById(cluster, attr_id, &attr_type, &multiplier);
-
- if (attr_name) {
+ Z_attribute_match attr_matched = Z_findAttributeMatcherById(shortaddr, cluster, attr_id, false);
+ if (attr_matched.found()) {
if (comma) { ResponseAppend_P(PSTR(",")); }
comma = true;
- ResponseAppend_P(PSTR("\"%s\":{\"MinInterval\":%d,\"MaxInterval\":%d"), attr_name, min_interval, max_interval);
+ ResponseAppend_P(PSTR("\"%s\":{\"MinInterval\":%d,\"MaxInterval\":%d"), attr_matched.name, min_interval, max_interval);
buf.add8(0); // direction, always 0
buf.add16(attr_id);
- buf.add8(attr_type);
+ buf.add8(attr_matched.zigbee_type);
buf.add16(min_interval);
buf.add16(max_interval);
- if (!Z_isDiscreteDataType(attr_type)) { // report_change is only valid for non-discrete data types (numbers)
- ZbApplyMultiplier(report_change, multiplier);
+ if (!Z_isDiscreteDataType(attr_matched.zigbee_type)) { // report_change is only valid for non-discrete data types (numbers)
+ ZbApplyMultiplier(report_change, attr_matched.multiplier, attr_matched.divider, attr_matched.base);
// encode value
- int32_t res = encodeSingleAttribute(buf, report_change, "", attr_type);
+ int32_t res = encodeSingleAttribute(buf, report_change, "", attr_matched.zigbee_type);
if (res < 0) {
AddLog(LOG_LEVEL_ERROR, PSTR(D_LOG_ZIGBEE "internal error, unsupported attribute type"));
} else {
@@ -1651,7 +1650,7 @@ void Z_IncomingMessage(class ZCLFrame &zcl_received) {
#ifdef USE_BERRY
// Berry pre-process messages
- callBerryZigbeeDispatcher("incoming", "zcl_frame", &zcl_received, 0);
+ callBerryZigbeeDispatcher("frame_received", &zcl_received, nullptr, srcaddr);
#endif // USE_BERRY
// create the device entry if it does not exist and if it's not the local device
@@ -1690,12 +1689,12 @@ void Z_IncomingMessage(class ZCLFrame &zcl_received) {
zcl_received.parseReadAttributesResponse(attr_list);
if (clusterid) { defer_attributes = true; } // don't defer system Cluster=0 messages
} else if ( (!zcl_received.isClusterSpecificCommand()) && (ZCL_READ_ATTRIBUTES == zcl_received.getCmdId())) {
- zcl_received.parseReadAttributes(attr_list);
+ zcl_received.parseReadAttributes(srcaddr, attr_list);
// never defer read_attributes, so the auto-responder can send response back on a per cluster basis
} else if ( (!zcl_received.isClusterSpecificCommand()) && (ZCL_READ_REPORTING_CONFIGURATION_RESPONSE == zcl_received.getCmdId())) {
- zcl_received.parseReadConfigAttributes(attr_list);
+ zcl_received.parseReadConfigAttributes(srcaddr, attr_list);
} else if ( (!zcl_received.isClusterSpecificCommand()) && (ZCL_CONFIGURE_REPORTING_RESPONSE == zcl_received.getCmdId())) {
- zcl_received.parseConfigAttributes(attr_list);
+ zcl_received.parseConfigAttributes(srcaddr, attr_list);
} else if ( (!zcl_received.isClusterSpecificCommand()) && (ZCL_WRITE_ATTRIBUTES_RESPONSE == zcl_received.getCmdId())) {
zcl_received.parseWriteAttributesResponse(attr_list);
} else if (zcl_received.isClusterSpecificCommand()) {
@@ -1705,6 +1704,11 @@ void Z_IncomingMessage(class ZCLFrame &zcl_received) {
AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_ZIGBEE D_JSON_ZIGBEEZCL_RAW_RECEIVED ": {\"0x%04X\":{%s}}"), srcaddr, attr_list.toString(false, false).c_str()); // don't include battery
+#ifdef USE_BERRY
+ // Berry pre-process messages
+ callBerryZigbeeDispatcher("attributes_raw", &zcl_received, &attr_list, srcaddr);
+#endif // USE_BERRY
+
// discard the message if it was sent by us (broadcast or group loopback)
if (srcaddr == localShortAddr) {
AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_ZIGBEE "loopback message, ignoring"));
@@ -1717,6 +1721,11 @@ void Z_IncomingMessage(class ZCLFrame &zcl_received) {
zcl_received.generateCallBacks(attr_list); // set deferred callbacks, ex: Occupancy
Z_postProcessAttributes(srcaddr, zcl_received.getSrcEndpoint(), attr_list);
+#ifdef USE_BERRY
+ // Berry pre-process messages
+ callBerryZigbeeDispatcher("attributes_refined", &zcl_received, &attr_list, srcaddr);
+#endif // USE_BERRY
+
if (defer_attributes) {
// Prepare for publish
if (zigbee_devices.jsonIsConflict(srcaddr, attr_list)) {
@@ -2123,6 +2132,15 @@ int32_t Z_Query_Bulbs(uint8_t value) {
return 0; // continue
}
+//
+// Z_ZbAutoload - autoload all definitions from filesystem
+// files with ending '.zb' suffix
+//
+int32_t Z_ZbAutoload(uint8_t value) {
+ ZbAutoload();
+ return 0;
+}
+
//
// Zigbee initialization is complete, let the party begin
//
@@ -2193,7 +2211,7 @@ void ZCLFrame::autoResponder(const uint16_t *attr_list_ids, size_t attr_len) {
break;
}
if (!attr.isNone()) {
- Z_parseAttributeKey(attr, cluster);
+ Z_parseAttributeKey(shortaddr, attr, cluster);
attr_list.addAttribute(cluster, attr_id) = attr;
}
}
@@ -2214,7 +2232,7 @@ void ZCLFrame::autoResponder(const uint16_t *attr_list_ids, size_t attr_len) {
",\"Endpoint\":%d"
",\"Response\":%s}"
),
- shortaddr, cluster, _srcendpoint,
+ shortaddr, cluster, srcendpoint,
attr_list.toString().c_str());
// send
@@ -2222,7 +2240,7 @@ void ZCLFrame::autoResponder(const uint16_t *attr_list_ids, size_t attr_len) {
ZCLFrame zcl(buf.len()); // message is 4 bytes
zcl.shortaddr = shortaddr;
zcl.cluster = cluster;
- zcl.dstendpoint = _srcendpoint;
+ zcl.dstendpoint = srcendpoint;
zcl.cmd = ZCL_READ_ATTRIBUTES_RESPONSE;
zcl.clusterSpecific = false; /* not cluster specific */
zcl.needResponse = false; /* noresponse */
diff --git a/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_9_serial.ino b/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_9_serial.ino
index c532e2363..05a049c8c 100644
--- a/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_9_serial.ino
+++ b/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_9_serial.ino
@@ -775,14 +775,19 @@ void ZigbeeZCLSend_Raw(const ZCLFrame &zcl) {
buf.add8(zcl.dstendpoint); // dest endpoint
}
buf.add16(0x0000); // dest Pan ID, 0x0000 = intra-pan
- buf.add8(0x01); // source endpoint
+ if (zcl.srcendpoint) {
+ buf.add8(zcl.srcendpoint); // source endpoint
+ } else {
+ buf.add8(1); // set endpoint to 1 if not specified
+ }
buf.add16(zcl.cluster);
buf.add8(zcl.transactseq); // transactseq
- buf.add8(0x30); // 30 options
+ buf.add8(zcl.direct ? 0x00 : 0x30); // 30 options
buf.add8(0x1E); // 1E radius
buf.add16(3 + zcl.payload.len() + (zcl.manuf ? 2 : 0));
- buf.add8((zcl.needResponse ? 0x00 : 0x10) | (zcl.clusterSpecific ? 0x01 : 0x00) | (zcl.manuf ? 0x04 : 0x00)); // Frame Control Field
+ buf.add8((zcl.needResponse ? 0x00 : 0x10) | (zcl.clusterSpecific ? 0x01 : 0x00) |
+ (zcl.manuf ? 0x04 : 0x00) | (zcl.direction ? 0x08 : 0x00)); // Frame Control Field
if (zcl.manuf) {
buf.add16(zcl.manuf); // add Manuf Id if not null
}
diff --git a/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_A_impl.ino b/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_A_impl.ino
index c7182d609..37b000b00 100644
--- a/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_A_impl.ino
+++ b/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_A_impl.ino
@@ -39,7 +39,8 @@ const char kZbCommands[] PROGMEM = D_PRFX_ZB "|" // prefix
D_CMND_ZIGBEE_BIND "|" D_CMND_ZIGBEE_UNBIND "|" D_CMND_ZIGBEE_PING "|" D_CMND_ZIGBEE_MODELID "|"
D_CMND_ZIGBEE_LIGHT "|" D_CMND_ZIGBEE_OCCUPANCY "|"
D_CMND_ZIGBEE_RESTORE "|" D_CMND_ZIGBEE_BIND_STATE "|" D_CMND_ZIGBEE_MAP "|" D_CMND_ZIGBEE_LEAVE "|"
- D_CMND_ZIGBEE_CONFIG "|" D_CMND_ZIGBEE_DATA "|" D_CMND_ZIGBEE_SCAN "|" D_CMND_ZIGBEE_ENROLL "|" D_CMND_ZIGBEE_CIE
+ D_CMND_ZIGBEE_CONFIG "|" D_CMND_ZIGBEE_DATA "|" D_CMND_ZIGBEE_SCAN "|" D_CMND_ZIGBEE_ENROLL "|" D_CMND_ZIGBEE_CIE "|"
+ D_CMND_ZIGBEE_LOAD "|" D_CMND_ZIGBEE_UNLOAD "|" D_CMND_ZIGBEE_LOADDUMP
;
SO_SYNONYMS(kZbSynonyms,
@@ -62,6 +63,7 @@ void (* const ZigbeeCommand[])(void) PROGMEM = {
&CmndZbRestore, &CmndZbBindState, &CmndZbMap, &CmndZbLeave,
&CmndZbConfig, &CmndZbData, &CmndZbScan,
&CmndZbenroll, &CmndZbcie,
+ &CmndZbLoad, &CmndZbUnload, &CmndZbLoadDump,
};
/********************************************************************************************/
@@ -220,15 +222,15 @@ void zigbeeZCLSendCmd(class ZCLFrame &zcl) {
// Special encoding for multiplier:
// multiplier == 0: ignore
// multiplier == 1: ignore
-// multiplier > 0: divide by the multiplier
-// multiplier < 0: multiply by the -multiplier (positive)
-void ZbApplyMultiplier(double &val_d, int8_t multiplier) {
+void ZbApplyMultiplier(double &val_d, int8_t multiplier, int8_t divider, int8_t base) {
if ((0 != multiplier) && (1 != multiplier)) {
- if (multiplier > 0) { // inverse of decoding
- val_d = val_d / multiplier;
- } else {
- val_d = val_d * (-multiplier);
- }
+ val_d = val_d * multiplier;
+ }
+ if ((0 != divider) && (1 != divider)) {
+ val_d = val_d / divider;
+ }
+ if (0 != base) {
+ val_d = val_d + base;
}
}
@@ -239,15 +241,15 @@ bool ZbTuyaWrite(SBuffer & buf, const Z_attribute & attr) {
double val_d = attr.getOptimisticDouble();
const char * val_str = attr.getStr();
- if (attr.key_is_str) { return false; } // couldn't find attr if so skip
- if (attr.isNum() && (1 != attr.attr_multiplier)) {
- ZbApplyMultiplier(val_d, attr.attr_multiplier);
+ if (attr.key_is_str || attr.key_is_cmd) { return false; } // couldn't find attr if so skip
+ if (attr.isNum()) {
+ ZbApplyMultiplier(val_d, attr.attr_multiplier, attr.attr_divider, 0);
}
uint32_t u32 = val_d;
int32_t i32 = val_d;
- uint8_t tuyatype = (attr.key.id.attr_id >> 8);
- uint8_t dpid = (attr.key.id.attr_id & 0xFF);
+ uint8_t tuyatype = (attr.attr_id >> 8);
+ uint8_t dpid = (attr.attr_id & 0xFF);
buf.add8(dpid);
buf.add8(tuyatype);
@@ -298,13 +300,13 @@ bool ZbAppendWriteBuf(SBuffer & buf, const Z_attribute & attr, bool prepend_stat
double val_d = attr.getOptimisticDouble();
const char * val_str = attr.getStr();
- if (attr.key_is_str) { return false; } // couldn't find attr if so skip
- if (attr.isNum() && (1 != attr.attr_multiplier)) {
- ZbApplyMultiplier(val_d, attr.attr_multiplier);
+ if (attr.key_is_str && attr.key_is_cmd) { return false; } // couldn't find attr if so skip
+ if (attr.isNum()) {
+ ZbApplyMultiplier(val_d, attr.attr_multiplier, attr.attr_divider, 0);
}
// push the value in the buffer
- buf.add16(attr.key.id.attr_id); // prepend with attribute identifier
+ buf.add16(attr.attr_id); // prepend with attribute identifier
if (prepend_status_ok) {
buf.add8(Z_SUCCESS); // status OK = 0x00
}
@@ -313,7 +315,7 @@ bool ZbAppendWriteBuf(SBuffer & buf, const Z_attribute & attr, bool prepend_stat
if (res < 0) {
// remove the attribute type we just added
// buf.setLen(buf.len() - (operation == ZCL_READ_ATTRIBUTES_RESPONSE ? 4 : 3));
- AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE D_ZIGBEE_UNSUPPORTED_ATTRIBUTE_TYPE " %04X/%04X '0x%02X'"), attr.key.id.cluster, attr.key.id.attr_id, attr.attr_type);
+ AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE D_ZIGBEE_UNSUPPORTED_ATTRIBUTE_TYPE " %04X/%04X '0x%02X'"), attr.cluster, attr.attr_id, attr.attr_type);
return false;
}
return true;
@@ -339,24 +341,33 @@ void ZbSendReportWrite(class JsonParserToken val_pubwrite, class ZCLFrame & zcl)
Z_attribute attr;
attr.setKeyName(key.getStr());
- if (Z_parseAttributeKey(attr, tuya_protocol ? 0xEF00 : 0xFFFF)) { // favor tuya protocol if needed
+ if (Z_parseAttributeKey(zcl.shortaddr, attr, tuya_protocol ? 0xEF00 : 0xFFFF)) { // favor tuya protocol if needed
// Buffer ready, do some sanity checks
// all attributes must use the same cluster
if (0xFFFF == zcl.cluster) {
- zcl.cluster = attr.key.id.cluster; // set the cluster for this packet
- } else if (zcl.cluster != attr.key.id.cluster) {
+ zcl.cluster = attr.cluster; // set the cluster for this packet
+ } else if (zcl.cluster != attr.cluster) {
ResponseCmndChar_P(PSTR(D_ZIGBEE_TOO_MANY_CLUSTERS));
return;
}
+ // check for manuf code
+ if (attr.manuf) {
+ if (zcl.manuf != 0 && zcl.manuf != attr.manuf) {
+ AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "conflicting manuf code 0x%04X (was 0x%04X)"), attr.manuf, zcl.manuf);
+ } else {
+ zcl.manuf = attr.manuf;
+ }
+ }
+
} else {
if (attr.key_is_str) {
- Response_P(PSTR("{\"%s\":\"%s'%s'\"}"), XdrvMailbox.command, PSTR(D_ZIGBEE_UNKNOWN_ATTRIBUTE " "), key);
+ Response_P(PSTR("{\"%s\":\"%s'%s'\"}"), XdrvMailbox.command, PSTR(D_ZIGBEE_UNKNOWN_ATTRIBUTE " "), key.getStr());
return;
}
if (Zunk == attr.attr_type) {
- Response_P(PSTR("{\"%s\":\"%s'%s'\"}"), XdrvMailbox.command, PSTR(D_ZIGBEE_UNSUPPORTED_ATTRIBUTE_TYPE " "), key);
+ Response_P(PSTR("{\"%s\":\"%s'%s'\"}"), XdrvMailbox.command, PSTR(D_ZIGBEE_UNSUPPORTED_ATTRIBUTE_TYPE " "), key.getStr());
return;
}
}
@@ -410,7 +421,7 @@ void ZbSendReportWrite(class JsonParserToken val_pubwrite, class ZCLFrame & zcl)
if (val_attr_rc) {
val_d = val_attr_rc.getFloat();
val_str = val_attr_rc.getStr();
- ZbApplyMultiplier(val_d, attr.attr_multiplier);
+ ZbApplyMultiplier(val_d, attr.attr_multiplier, attr.attr_divider, 0);
}
// read TimeoutPeriod
@@ -421,7 +432,7 @@ void ZbSendReportWrite(class JsonParserToken val_pubwrite, class ZCLFrame & zcl)
// all fields are gathered, output the butes into the buffer, ZCL 2.5.7.1
// common bytes
buf.add8(attr_direction ? 0x01 : 0x00);
- buf.add16(attr.key.id.attr_id);
+ buf.add16(attr.attr_id);
if (attr_direction) {
buf.add16(attr_timeout);
} else {
@@ -623,34 +634,34 @@ void ZbSendRead(JsonParserToken val_attr, ZCLFrame & zcl) {
bool found = false;
// scan attributes to find by name, and retrieve type
- for (uint32_t i = 0; i < nitems(Z_PostProcess); i++) {
- const Z_AttributeConverter *converter = &Z_PostProcess[i];
- uint16_t local_attr_id = pgm_read_word(&converter->attribute);
- 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.getStr(), Z_strings + pgm_read_word(&converter->name_offset)))) {
- // match name
- // check if there is a conflict with cluster
- // TODO
- if (!(value.getBool()) && attr_item_offset) {
- // If value is false (non-default) then set direction to 1 (for ReadConfig)
- attrs[actual_attr_len] = 0x01;
+ Z_attribute_match matched_attr = Z_findAttributeMatcherByName(zcl.shortaddr, key.getStr());
+ if (matched_attr.found()) {
+ // match name
+ // check if there is a conflict with cluster
+ if (!(value.getBool()) && attr_item_offset) {
+ // If value is false (non-default) then set direction to 1 (for ReadConfig)
+ attrs[actual_attr_len] = 0x01;
+ }
+ actual_attr_len += attr_item_offset;
+ attrs[actual_attr_len++] = matched_attr.attribute & 0xFF;
+ attrs[actual_attr_len++] = matched_attr.attribute >> 8;
+ actual_attr_len += attr_item_len - 2 - attr_item_offset; // normally 0
+ found = true;
+ // check cluster
+ if (!zcl.validCluster()) {
+ zcl.cluster = matched_attr.cluster;
+ } else if (zcl.cluster != matched_attr.cluster) {
+ ResponseCmndChar_P(PSTR(D_ZIGBEE_TOO_MANY_CLUSTERS));
+ if (attrs) { free(attrs); }
+ return;
+ }
+ // check for manuf code
+ if (matched_attr.manuf) {
+ if (zcl.manuf != 0 && zcl.manuf != matched_attr.manuf) {
+ AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "conflicting manuf code 0x%04X (was 0x%04X)"), matched_attr.manuf, zcl.manuf);
+ } else {
+ zcl.manuf = matched_attr.manuf;
}
- actual_attr_len += attr_item_offset;
- attrs[actual_attr_len++] = local_attr_id & 0xFF;
- attrs[actual_attr_len++] = local_attr_id >> 8;
- actual_attr_len += attr_item_len - 2 - attr_item_offset; // normally 0
- found = true;
- // check cluster
- if (!zcl.validCluster()) {
- zcl.cluster = local_cluster_id;
- } else if (zcl.cluster != local_cluster_id) {
- ResponseCmndChar_P(PSTR(D_ZIGBEE_TOO_MANY_CLUSTERS));
- if (attrs) { free(attrs); }
- return;
- }
- break; // found, exit loop
}
}
if (!found) {
@@ -719,8 +730,17 @@ void CmndZbSend(void) {
// parse "Device" and "Group"
JsonParserToken val_device = root[PSTR(D_CMND_ZIGBEE_DEVICE)];
if (val_device) {
- zcl.shortaddr = zigbee_devices.parseDeviceFromName(val_device.getStr()).shortaddr;
- if (!zcl.validShortaddr()) { ResponseCmndChar_P(PSTR(D_ZIGBEE_INVALID_PARAM)); return; }
+ uint16_t parsed_shortaddr = BAD_SHORTADDR;
+ zcl.shortaddr = zigbee_devices.parseDeviceFromName(val_device.getStr(), &parsed_shortaddr).shortaddr;
+ if (!zcl.validShortaddr()) {
+ if (parsed_shortaddr != BAD_SHORTADDR) {
+ // we still got a short address
+ zcl.shortaddr = parsed_shortaddr;
+ } else {
+ ResponseCmndChar_P(PSTR(D_ZIGBEE_INVALID_PARAM));
+ return;
+ }
+ }
}
if (!zcl.validShortaddr()) { // if not found, check if we have a group
JsonParserToken val_group = root[PSTR(D_CMND_ZIGBEE_GROUP)];
@@ -749,6 +769,10 @@ void CmndZbSend(void) {
ResponseCmndChar_P(PSTR("Missing endpoint"));
return;
}
+ // Special case for Green Power, if dstendpoint is 0xF2, then source endpoint should also be 0xF2
+ if (zcl.dstendpoint == 0xF2) {
+ zcl.srcendpoint = 0xF2;
+ }
// from here endpoint is valid and non-zero
// cluster may be already specified or 0xFFFF
@@ -863,7 +887,10 @@ void ZbBindUnbind(bool unbind) { // false = bind, true = unbind
if (val_cluster) {
cluster = val_cluster.getUInt(cluster); // first convert as number
if (0 == cluster) {
- zigbeeFindAttributeByName(val_cluster.getStr(), &cluster, nullptr, nullptr);
+ Z_attribute_match attr_matched = Z_findAttributeMatcherByName(BAD_SHORTADDR, val_cluster.getStr());
+ if (attr_matched.found()) {
+ cluster = attr_matched.cluster;
+ }
}
}
@@ -1306,6 +1333,53 @@ void CmndZbSave(void) {
ResponseCmndDone();
}
+//
+// Command `ZbLoad`
+// Load a device specific zigbee template
+//
+void CmndZbLoad(void) {
+ // can be called before Zigbee is initialized
+ RemoveSpace(XdrvMailbox.data);
+
+ bool ret = true;;
+ if (strcmp(XdrvMailbox.data, "*") == 0) {
+ ZbAutoload();
+ } else {
+ ret = ZbLoad(XdrvMailbox.data);
+ }
+ if (ret) {
+ ResponseCmndDone();
+ } else {
+ ResponseCmndError();
+ }
+}
+
+//
+// Command `ZbUnload`
+// Unload a template previously loaded
+//
+void CmndZbUnload(void) {
+ // can be called before Zigbee is initialized
+ RemoveSpace(XdrvMailbox.data);
+
+ bool ret = ZbUnload(XdrvMailbox.data);
+ if (ret) {
+ ResponseCmndDone();
+ } else {
+ ResponseCmndError();
+ }
+}
+
+//
+// Command `ZbLoadDump`
+// Load a device specific zigbee template
+//
+void CmndZbLoadDump(void) {
+ // can be called before Zigbee is initialized
+ ZbLoadDump();
+ ResponseCmndDone();
+}
+
//
// Command `ZbScan`
// Run an energy scan
@@ -1456,6 +1530,7 @@ void CmndZbPermitJoin(void) {
// ZNP Version
#ifdef USE_ZIGBEE_ZNP
+ // put all routers in pairing mode
SBuffer buf(34);
buf.add8(Z_SREQ | Z_ZDO); // 25
buf.add8(ZDO_MGMT_PERMIT_JOIN_REQ); // 36
@@ -1466,6 +1541,22 @@ void CmndZbPermitJoin(void) {
ZigbeeZNPSend(buf.getBuffer(), buf.len());
+ // send Green Power pairing mode
+ ZCLFrame zcl(4); // message is 4 bytes max
+
+ zcl.cmd = 0x02; // GP Proxy Commissioning Mode
+ zcl.cluster = 0x0021; // GP cluster
+ zcl.shortaddr = 0xFFFC; // Broadcast to all routers
+ zcl.srcendpoint = 0xF2; // GP endpoint
+ zcl.dstendpoint = 0xF2; // GP endpoint
+ zcl.needResponse = false; // as per GP spec The Disable default response sub-field of the Frame Control Field of the ZCL header shall be set to 0b1."
+ zcl.clusterSpecific = true; // command
+ zcl.direct = true; // broadcast so no need to discover routes
+ zcl.direction = true; // server to client
+ zcl.payload.add8(0x0B); // Action=1, gpsCommissioningExitMode=0b101 (window expiration + GP Proxy Commissioning Mode)
+ zcl.payload.add16(duration);
+ zigbeeZCLSendCmd(zcl);
+
#endif // USE_ZIGBEE_ZNP
// EZSP VERSION
@@ -1781,6 +1872,10 @@ const char ZB_WEB_U[] PROGMEM =
""
"\0"
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++
+ //=ZB_WEB_GP
+ "GP"
+ "\0"
+ // +++++++++++++++++++++++++++++++++++++++++++++++++++++++
//=ZB_WEB_LAST_SEEN
"🕗%02d%c"
"\0"
@@ -1820,16 +1915,17 @@ enum {
ZB_WEB_MAP_REFRESH=1164,
ZB_WEB_STATUS_LINE=1230,
ZB_WEB_BATTERY=1338,
- ZB_WEB_LAST_SEEN=1410,
- ZB_WEB_COLOR_RGB=1458,
- ZB_WEB_LINE_START=1518,
- ZB_WEB_LIGHT_CT=1558,
- ZB_WEB_END_STATUS=1613,
- ZB_WEB_LINE_END=1630,
+ ZB_WEB_GP=1410,
+ ZB_WEB_LAST_SEEN=1466,
+ ZB_WEB_COLOR_RGB=1514,
+ ZB_WEB_LINE_START=1574,
+ ZB_WEB_LIGHT_CT=1614,
+ ZB_WEB_END_STATUS=1669,
+ ZB_WEB_LINE_END=1686,
};
-// Compressed from 1649 to 1124, -31.8%
-const char ZB_WEB[] PROGMEM = "\x00\x68\x3D\x0E\xCA\xB1\xC1\x33\xF0\xF6\xD1\xEE\x3D\x3D\x46\x41\x33\xF0\xE8\x6D"
+// Compressed from 1705 to 1156, -32.2%
+const char ZB_WEB[] PROGMEM = "\x00\x6B\x3D\x0E\xCA\xB1\xC1\x33\xF0\xF6\xD1\xEE\x3D\x3D\x46\x41\x33\xF0\xE8\x6D"
"\xA1\x15\x08\x79\xF6\x51\xDD\x3C\xCC\x6F\xFD\x47\x58\x62\xB4\x21\x0E\xF1\xED\x1F"
"\xD1\x28\x51\xE6\x72\x99\x0C\x36\x1E\x0C\x67\x51\xD7\xED\x36\xB3\xCC\xE7\x99\xF4"
"\x7D\x1E\xE2\x04\x3C\x40\x2B\x04\x3C\x28\x10\xB0\x93\x99\xA4\x30\xD8\x08\x36\x8E"
@@ -1877,15 +1973,16 @@ const char ZB_WEB[] PROGMEM = "\x00\x68\x3D\x0E\xCA\xB1\xC1\x33\xF0\xF6\xD1\xEE\
"\x35\x16\xA3\xEB\xC7\xD8\x21\xE7\x1E\xD3\xEC\xFC\x9C\x2F\x9E\x9A\x08\x52\xCF\x60"
"\xEA\x3D\x80\x85\x82\x9E\xC3\xE8\x43\xE8\xFA\x3E\xBC\x08\x9D\x2A\x01\x03\xAC\xEB"
"\x1C\x11\xE6\x7D\x08\x30\xD8\x08\x7C\xFA\x1F\x47\x1D\x11\xB0\xFA\x38\xE8\x8D\x87"
- "\xD1\xC7\x44\x6C\x3D\x87\xE1\xE8\x76\x69\xF9\x38\x5F\x3D\x28\x40\x43\xC2\xC1\x43"
- "\x01\x3F\x47\x91\xB0\xE4\x22\x30\x73\x77\xC7\x83\xE9\xD1\x08\x7D\x07\x38\x5F\x40"
- "\x8D\xAA\x9B\x01\x1B\x46\x0C\x23\xCC\xF2\x3E\x8E\x3A\x22\x36\x1F\x47\x1D\x11\x1B"
- "\x0F\xA3\x8E\x88\x8D\x80\x83\x9D\x82\x44\xF0\x47\xE1\xF0\x10\xF8\x78\x41\xE0\x5E"
- "\x19\x7C\x7C\x3D\x87\x30\xF6\x1F\x87\xE8\xF2\x59\xEF\x9E\x0A\x70\xBE\x08\x5D\x17"
- "\x2A\x01\x42\xE0\xC4\x83\x2A\x2B\x47\xD0\x87\xB0\xFC\x3D\x3C\x36\xC2\x08\xFC\x3F"
- "\x47\x91\xC5\xF5\xF3\xC1\xDC\x3D\x0E\xC2\x04\x19\x87\xD0\x84\x68\x08\x5D\x18\x29"
- "\xC2\xF8\x21\x74\x1D\xCE\xCA\x10\xFC\x3E\xBC\x7B\x59\xEE\x9C\x2F\x82\x3F\x4E\xE8"
- "\x10\x79\x39\x9C\x2F\x9B";
+ "\xD1\xC7\x44\x6C\x3D\x87\xE1\xE8\x76\x69\xF9\x38\x5F\x04\x1E\x86\xD8\x21\x68\xA4"
+ "\x3D\xF6\xF9\x10\xCC\x1F\x7F\x3E\xC1\x2B\xA1\xDE\x73\x08\x8C\x1C\xC3\xC1\xF6\x7E"
+ "\x11\x0F\x10\xC0\x42\xE8\x77\xCE\x17\xCF\x4A\x10\x10\xF4\x30\x50\xCE\x4F\xD1\xE4"
+ "\x6C\x39\x08\x8C\x1C\xDD\xF1\xE0\xFA\x74\x42\x1F\x41\xCE\x17\xD0\x23\x70\x1A\x6C"
+ "\x04\x6D\xF8\x30\x8F\x33\xC8\xFA\x38\xE8\x88\xD8\x7D\x1C\x74\x44\x6C\x3E\x8E\x3A"
+ "\x22\x36\x02\x0E\xE6\x09\x13\xC1\x1F\x8B\x40\x43\xE2\xC1\x07\x81\x78\x65\xF1\xF0"
+ "\xF6\x1C\xC3\xD8\x7E\x1F\xA3\xC9\x67\xBE\x78\x29\xC2\xF8\x21\x74\x6A\x01\x0B\x86"
+ "\x92\x0C\xA9\x1F\x42\x1E\xC3\xF0\xF4\xF0\xDB\x08\x23\xF0\xFD\x1E\x47\x17\xD7\xCF"
+ "\x07\x70\xF4\x3B\x08\x10\x66\x1F\x42\x11\xA0\x22\x70\x4E\x08\x3D\x0A\xF3\xB2\x84"
+ "\x3F\x0F\xAF\x1E\xD6\x7B\xA7\x0B\xE0\x8F\xD3\xF2\x04\x1E\x5C\x67\x0B\xE6";
// ++++++++++++++++++++^^^^^^^^^^^^^^^^^^^++++++++++++++++++++
// ++++++++++++++++++++ DO NOT EDIT ABOVE ++++++++++++++++++++
@@ -2045,6 +2142,8 @@ void ZigbeeShow(bool json)
changeUIntScale(device.batt_percent, 0, 100, 0, 14),
(color & 0xFF0000) >> 16, (color & 0x00FF00) >> 8, (color & 0x0000FF)
);
+ } else if (device.isGP()) { // display GP in green for Green Power
+ snprintf_P(sbatt, sizeof(sbatt), msg[ZB_WEB_GP]);
}
uint32_t num_bars = 0;
diff --git a/tasmota/tasmota_xdrv_driver/xdrv_27_shutter.ino b/tasmota/tasmota_xdrv_driver/xdrv_27_shutter.ino
index 153a54f38..d92d82025 100644
--- a/tasmota/tasmota_xdrv_driver/xdrv_27_shutter.ino
+++ b/tasmota/tasmota_xdrv_driver/xdrv_27_shutter.ino
@@ -853,6 +853,12 @@ void ShutterButtonHandler(void)
}
if (NOT_PRESSED == button) {
+ if (Shutter[shutter_index].direction && Button.hold_timer[button_index] > 0) {
+ XdrvMailbox.index = shutter_index +1;
+ XdrvMailbox.payload = XdrvMailbox.index;
+ //AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("SHT: Shtr%d, Button %d, hold %d, dir %d, index %d, payload %d"), shutter_index+1, button_index+1, Button.hold_timer[button_index],Shutter[shutter_index].direction,XdrvMailbox.index,XdrvMailbox.payload);
+ CmndShutterStop();
+ }
Button.hold_timer[button_index] = 0;
} else {
Button.hold_timer[button_index]++;
diff --git a/tasmota/tasmota_xdrv_driver/xdrv_39_thermostat.ino b/tasmota/tasmota_xdrv_driver/xdrv_39_thermostat.ino
index 26620394a..4d341fd4d 100644
--- a/tasmota/tasmota_xdrv_driver/xdrv_39_thermostat.ino
+++ b/tasmota/tasmota_xdrv_driver/xdrv_39_thermostat.ino
@@ -1236,7 +1236,7 @@ bool ThermostatTimerArm(uint8_t ctr_output, int16_t tempVal)
bool result = false;
// TempVal unit is tenths of degrees celsius
if ((tempVal >= -1000)
- && (tempVal <= 1000)
+ && (tempVal <= 2000)
&& (tempVal >= Thermostat[ctr_output].temp_frost_protect)) {
Thermostat[ctr_output].temp_target_level = tempVal;
Thermostat[ctr_output].status.thermostat_mode = THERMOSTAT_AUTOMATIC_OP;
@@ -1368,7 +1368,7 @@ void ThermostatGetLocalSensor(uint8_t ctr_output) {
value = ThermostatFahrenheitToCelsius(value, TEMP_CONV_ABSOLUTE);
}
if ( (value >= -1000)
- && (value <= 1000)
+ && (value <= 2000)
&& (Thermostat[ctr_output].status.sensor_type == SENSOR_LOCAL)) {
uint32_t timestamp = TasmotaGlobal.uptime;
// Calculate temperature gradient if temperature value has changed
@@ -1440,7 +1440,7 @@ void CmndTempFrostProtectSet(void)
value = (int16_t)(CharToFloat(XdrvMailbox.data) * 10);
}
if ( (value >= -1000)
- && (value <= 1000)) {
+ && (value <= 2000)) {
Thermostat[ctr_output].temp_frost_protect = value;
}
}
@@ -1571,7 +1571,7 @@ void CmndTempMeasuredSet(void)
value = (int16_t)(CharToFloat(XdrvMailbox.data) * 10);
}
if ( (value >= -1000)
- && (value <= 1000)
+ && (value <= 2000)
&& (Thermostat[ctr_output].status.sensor_type == SENSOR_MQTT)) {
uint32_t timestamp = TasmotaGlobal.uptime;
// Calculate temperature gradient if temperature value has changed
@@ -1609,7 +1609,7 @@ void CmndTempTargetSet(void)
value = (int16_t)(CharToFloat(XdrvMailbox.data) * 10);
}
if ( (value >= -1000)
- && (value <= 1000)
+ && (value <= 2000)
&& (value >= Thermostat[ctr_output].temp_frost_protect)) {
Thermostat[ctr_output].temp_target_level = value;
}
@@ -2061,7 +2061,7 @@ void ThermostatShow(uint8_t ctr_output, bool json)
int16_t value = Thermostat[ctr_output].temp_measured_gradient;
if (Thermostat[ctr_output].status.temp_format == TEMP_FAHRENHEIT) {
value = ThermostatCelsiusToFahrenheit((int32_t)Thermostat[ctr_output].temp_measured_gradient, TEMP_CONV_RELATIVE);
- }
+ }
f_temperature = value / 1000.0f;
WSContentSend_PD(HTTP_THERMOSTAT_TEMPERATURE, D_THERMOSTAT_GRADIENT, Settings->flag2.temperature_resolution, &f_temperature, c_unit);
WSContentSend_P(HTTP_THERMOSTAT_DUTY_CYCLE, ThermostatGetDutyCycle(ctr_output) );
diff --git a/tasmota/tasmota_xdrv_driver/xdrv_42_0_i2s_audio.ino b/tasmota/tasmota_xdrv_driver/xdrv_42_0_i2s_audio.ino
index 8c1259d0f..26ad5412c 100644
--- a/tasmota/tasmota_xdrv_driver/xdrv_42_0_i2s_audio.ino
+++ b/tasmota/tasmota_xdrv_driver/xdrv_42_0_i2s_audio.ino
@@ -67,14 +67,25 @@
#define i2s_port_t uint8_t
#endif
-#define MODE_MIC 0
-#define MODE_SPK 1
+#define MODE_MIC 1
+#define MODE_SPK 2
#ifndef MICSRATE
#define MICSRATE 32000
#endif
+typedef union {
+ uint8_t data;
+ struct {
+ uint8_t master : 1;
+ uint8_t enabled : 1;
+ uint8_t swap_mic : 1;
+ uint8_t mode : 2;
+ };
+} BRIDGE_MODE;
+
+
struct AUDIO_I2S_t {
uint8_t is2_volume; // should be in settings
i2s_port_t i2s_port;
@@ -134,6 +145,16 @@ struct AUDIO_I2S_t {
uint8_t mode;
+#ifdef I2S_BRIDGE
+ BRIDGE_MODE bridge_mode;
+ WiFiUDP i2s_bridge_udp;
+ WiFiUDP i2s_bridgec_udp;
+ IPAddress i2s_bridge_ip;
+ TaskHandle_t i2s_bridge_h;
+ int8_t ptt_pin = -1;
+#endif
+
+
} audio_i2s;
#ifndef MIC_CHANNELS
@@ -308,11 +329,6 @@ int32_t I2S_Init_0(void) {
audio_i2s.mode = MODE_SPK;
-#ifdef USE_I2S_COMMON_IO
- audio_i2s.out->SetRate(MICSRATE);
-#endif
-
-
return 0;
}
@@ -327,11 +343,12 @@ void I2S_Init(void) {
#endif
#ifdef USE_W8960
- W8960_Init();
+ W8960_Init1();
#endif
audio_i2s.is2_volume = 10;
audio_i2s.out->SetGain(((float)audio_i2s.is2_volume / 100.0) * 4.0);
+ audio_i2s.out->begin();
audio_i2s.out->stop();
audio_i2s.mp3ram = nullptr;
@@ -356,7 +373,12 @@ void I2S_Init(void) {
audio_i2s.mic_channels = MIC_CHANNELS;
audio_i2s.mic_rate = MICSRATE;
+#ifdef USE_I2S_COMMON_IO
+ i2s_set_clk(audio_i2s.mic_port, audio_i2s.mic_rate, I2S_BITS_PER_SAMPLE_16BIT, I2S_CHANNEL_STEREO);
+#endif
+
#endif // ESP32
+
}
#ifdef ESP32
@@ -592,9 +614,12 @@ const char kI2SAudio_Commands[] PROGMEM = "I2S|"
#if defined(USE_SHINE) && ( (defined(USE_I2S_AUDIO) && defined(USE_I2S_MIC)) || defined(USE_M5STACK_CORE2) || defined(ESP32S3_BOX) )
"|REC"
"|MGain"
-#ifdef MP3_MIC_STREAM
+#if defined(USE_SHINE) && defined(MP3_MIC_STREAM)
"|STREAM"
#endif // MP3_MIC_STREAM
+#ifdef I2S_BRIDGE
+ "|BRIDGE"
+#endif // I2S_BRIDGE
#endif // USE_SHINE
#endif // ESP32
;
@@ -609,12 +634,15 @@ void (* const I2SAudio_Command[])(void) PROGMEM = {
#ifdef USE_I2S_WEBRADIO
,&Cmd_WebRadio
#endif // USE_I2S_WEBRADIO
-#if defined(USE_SHINE) && ( (defined(USE_I2S_AUDIO) && defined(USE_I2S_MIC)) || defined(USE_M5STACK_CORE2) || defined(ESP32S3_BOX) )
+#if defined(USE_SHINE) && ( (defined(USE_I2S_AUDIO) && defined(USE_I2S_MIC)) || defined(USE_M5STACK_CORE2) || defined(ESP32S3_BOX) )
,&Cmd_MicRec
,&Cmd_MicGain
-#ifdef MP3_MIC_STREAM
+#if defined(USE_SHINE) && defined(MP3_MIC_STREAM)
,&Cmd_MP3Stream
#endif // MP3_MIC_STREAM
+#ifdef I2S_BRIDGE
+ ,&Cmd_I2SBridge
+#endif // I2S_BRIDGE
#endif // USE_SHINE
#endif // ESP32
};
@@ -661,19 +689,27 @@ bool Xdrv42(uint8_t function) {
case FUNC_INIT:
I2S_Init();
break;
-#if defined(MP3_MIC_STREAM)
-//#if defined(USE_SHINE) && defined(MP3_MIC_STREAM)
case FUNC_WEB_ADD_MAIN_BUTTON:
//MP3ShowStream();
break;
case FUNC_LOOP:
+#if defined(USE_SHINE) && defined(MP3_MIC_STREAM)
i2s_mp3_loop();
+#endif
+#if defined(I2S_BRIDGE) && ( (defined(USE_I2S_AUDIO) && defined(USE_I2S_MIC)) || defined(USE_M5STACK_CORE2) || defined(ESP32S3_BOX) )
+ i2s_bridge_loop();
+#endif
break;
case FUNC_WEB_ADD_HANDLER:
+#if defined(USE_SHINE) && defined(MP3_MIC_STREAM)
audio_i2s.stream_enable = 1;
i2s_mp3_init(1);
- break;
#endif
+#if defined(I2S_BRIDGE) && ( (defined(USE_I2S_AUDIO) && defined(USE_I2S_MIC)) || defined(USE_M5STACK_CORE2) || defined(ESP32S3_BOX) )
+ I2SBridgeInit();
+#endif
+ break;
+
#ifdef USE_WEBSERVER
#ifdef USE_I2S_WEBRADIO
case FUNC_WEB_SENSOR:
diff --git a/tasmota/tasmota_xdrv_driver/xdrv_42_1_i2s_mp3mic.ino b/tasmota/tasmota_xdrv_driver/xdrv_42_1_i2s_mp3mic.ino
index d9ba85259..01a41f959 100644
--- a/tasmota/tasmota_xdrv_driver/xdrv_42_1_i2s_mp3mic.ino
+++ b/tasmota/tasmota_xdrv_driver/xdrv_42_1_i2s_mp3mic.ino
@@ -19,7 +19,7 @@
#ifdef ESP32
-#if defined(USE_SHINE) && ( (defined(USE_I2S_AUDIO) && defined(USE_I2S_MIC)) || defined(USE_M5STACK_CORE2) || defined(ESP32S3_BOX) )
+#if ( (defined(USE_I2S_AUDIO) && defined(USE_I2S_MIC)) || defined(USE_M5STACK_CORE2) || defined(ESP32S3_BOX) )
uint32_t SpeakerMic(uint8_t spkr) {
@@ -27,7 +27,11 @@ esp_err_t err = ESP_OK;
#ifndef USE_I2S_COMMON_IO
+ if (audio_i2s.mode == spkr) {
+ return 0;
+ }
if (spkr == MODE_SPK) {
+
if (audio_i2s.i2s_port == audio_i2s.mic_port) {
if (audio_i2s.mode != MODE_SPK) {
i2s_driver_uninstall(audio_i2s.mic_port);
@@ -61,24 +65,26 @@ esp_err_t err = ESP_OK;
.channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT,
.communication_format = I2S_COMM_FORMAT_I2S,
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
+ //.dma_buf_count = 8,
.dma_buf_count = 2,
//.dma_buf_len = 128,
.dma_buf_len = 1024,
.use_apll = 0, // Use audio PLL
.tx_desc_auto_clear = true,
- .fixed_mclk = 0,
+ .fixed_mclk = 12000000,
.mclk_multiple = I2S_MCLK_MULTIPLE_DEFAULT, // I2S_MCLK_MULTIPLE_128
.bits_per_chan = I2S_BITS_PER_CHAN_16BIT
};
-#ifdef ESP32S3_BOX
+
+#ifdef USE_I2S_MIC
+ // mic select to GND
i2s_config.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_TX);
i2s_config.communication_format = I2S_COMM_FORMAT_STAND_I2S;
#endif
-#ifdef USE_I2S_MIC
- // mic select to GND
- i2s_config.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX);
+#ifdef ESP32S3_BOX
+ i2s_config.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_TX);
i2s_config.communication_format = I2S_COMM_FORMAT_STAND_I2S;
#endif
@@ -117,8 +123,8 @@ esp_err_t err = ESP_OK;
return err;
}
-
#ifdef USE_SHINE
+
#include
#include
@@ -306,6 +312,8 @@ void Cmd_MicRec(void) {
}
}
+#endif // USE_SHINE
+
// mic gain in factor not percent
void Cmd_MicGain(void) {
@@ -315,6 +323,5 @@ void Cmd_MicGain(void) {
ResponseCmndNumber(audio_i2s.mic_gain);
}
-#endif // USE_SHINE
#endif // USE_I2S_AUDIO
#endif // ESP32
diff --git a/tasmota/tasmota_xdrv_driver/xdrv_42_4_i2s_codecs.ino b/tasmota/tasmota_xdrv_driver/xdrv_42_4_i2s_codecs.ino
index 1b66edbf9..c7092475b 100644
--- a/tasmota/tasmota_xdrv_driver/xdrv_42_4_i2s_codecs.ino
+++ b/tasmota/tasmota_xdrv_driver/xdrv_42_4_i2s_codecs.ino
@@ -142,7 +142,7 @@ void S3boxInit(void) {
#include
-void W8960_Init(void) {
+void W8960_Init1(void) {
if (TasmotaGlobal.i2c_enabled_2) {
if (I2cSetDevice(W8960_ADDR, 1)) {
I2cSetActiveFound(W8960_ADDR, "W8960-I2C", 1);
diff --git a/tasmota/tasmota_xdrv_driver/xdrv_42_5_i2s_bridge.ino b/tasmota/tasmota_xdrv_driver/xdrv_42_5_i2s_bridge.ino
new file mode 100644
index 000000000..d0a74f99d
--- /dev/null
+++ b/tasmota/tasmota_xdrv_driver/xdrv_42_5_i2s_bridge.ino
@@ -0,0 +1,270 @@
+
+/*
+ audio is2 support pcm bridge
+
+ Copyright (C) 2022 Gerhard Mutz and Theo Arends
+
+ 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 .
+*/
+
+
+#ifdef ESP32
+#if (defined(USE_I2S_AUDIO) || defined(USE_TTGO_WATCH) || defined(USE_M5STACK_CORE2) || defined(ESP32S3_BOX) || defined(USE_I2S_MIC))
+#ifdef I2S_BRIDGE
+
+#ifndef I2S_BRIDGE_PORT
+#define I2S_BRIDGE_PORT 6970
+#endif
+
+#define I2S_BRIDGE_BUFFER_SIZE 512
+
+#define I2S_BRIDGE_MODE_OFF 0
+#define I2S_BRIDGE_MODE_READ 1
+#define I2S_BRIDGE_MODE_WRITE 2
+
+void i2s_bridge_init(uint8_t mode) {
+
+ audio_i2s.bridge_mode.mode = mode;
+
+ if (I2S_BRIDGE_MODE_OFF == mode) {
+ audio_i2s.i2s_bridge_udp.flush();
+ audio_i2s.i2s_bridge_udp.stop();
+ //SpeakerMic(MODE_SPK);
+ AUDIO_PWR_OFF
+ } else {
+ i2s_set_clk(audio_i2s.mic_port, audio_i2s.mic_rate, I2S_BITS_PER_SAMPLE_16BIT, I2S_CHANNEL_STEREO);
+
+ if ((mode & 3) == I2S_BRIDGE_MODE_WRITE) {
+ //SpeakerMic(MODE_MIC);
+ //REG_SET_BIT(I2S_TIMING_REG(audio_i2s.mic_port), BIT(9));
+ //REG_SET_BIT(I2S_CONF_REG(audio_i2s.mic_port), I2S_RX_MSB_SHIFT);
+ } else {
+ //SpeakerMic(MODE_SPK);
+ }
+
+ audio_i2s.i2s_bridge_udp.begin(I2S_BRIDGE_PORT);
+ xTaskCreatePinnedToCore(i2s_bridge_task, "BRIDGE", 8192, NULL, 3, &audio_i2s.i2s_bridge_h, 1);
+ if (!audio_i2s.bridge_mode.master) {
+ AddLog(LOG_LEVEL_INFO, PSTR("I2S_bridge: slave started"));
+ } else {
+ char buffer[32];
+ sprintf_P(buffer, PSTR("%u.%u.%u.%u"), audio_i2s.i2s_bridge_ip[0], audio_i2s.i2s_bridge_ip[1], audio_i2s.i2s_bridge_ip[2], audio_i2s.i2s_bridge_ip[3]);
+ AddLog(LOG_LEVEL_INFO, PSTR("I2S_bridge: master started sending to ip: %s"), buffer);
+ }
+ AUDIO_PWR_ON
+ }
+}
+
+// make mono
+void make_mono(int16_t *packet_buffer, uint32_t size) {
+ int16_t *wp = (int16_t*)packet_buffer;
+ for (uint32_t cnt = 0; cnt < size / 2; cnt += 2) {
+ int16_t val;
+ if (audio_i2s.bridge_mode.swap_mic) {
+ val = wp[cnt + 1] * audio_i2s.mic_gain;
+ } else {
+ val = wp[cnt] * audio_i2s.mic_gain;
+ }
+ wp[cnt] = val;
+ wp[cnt + 1] = val;
+ }
+}
+
+void i2s_bridge_task(void *arg) {
+int16_t packet_buffer[I2S_BRIDGE_BUFFER_SIZE/2];
+uint16_t bytesize;
+
+ while (I2S_BRIDGE_MODE_OFF != audio_i2s.bridge_mode.mode) {
+ if ((audio_i2s.bridge_mode.mode & 3) == 3) {
+ // loopback test mode
+ uint32_t bytes_read;
+ bytesize = I2S_BRIDGE_BUFFER_SIZE;
+ i2s_read(audio_i2s.mic_port, (char *)packet_buffer, bytesize, &bytes_read, (100 / portTICK_RATE_MS));
+ make_mono(packet_buffer, bytes_read);
+ uint32_t bytes_written;
+ i2s_write(audio_i2s.i2s_port, (const uint8_t*)packet_buffer, bytes_read, &bytes_written, 0);
+ } else {
+ if (audio_i2s.bridge_mode.mode & I2S_BRIDGE_MODE_READ) {
+ if (audio_i2s.i2s_bridge_udp.parsePacket()) {
+ uint32_t bytes_written;
+ uint32_t len = audio_i2s.i2s_bridge_udp.available();
+ if (len > I2S_BRIDGE_BUFFER_SIZE) {
+ len = I2S_BRIDGE_BUFFER_SIZE;
+ }
+ len = audio_i2s.i2s_bridge_udp.read((uint8_t *)packet_buffer, len);
+ audio_i2s.i2s_bridge_udp.flush();
+ i2s_write(audio_i2s.i2s_port, (const uint8_t*)packet_buffer, len, &bytes_written, 0);
+ } else {
+ delay(1);
+ }
+ }
+
+ if (audio_i2s.bridge_mode.mode & I2S_BRIDGE_MODE_WRITE) {
+ uint32_t bytes_read;
+ bytesize = I2S_BRIDGE_BUFFER_SIZE;
+ i2s_read(audio_i2s.mic_port, (char *)packet_buffer, bytesize, &bytes_read, (100 / portTICK_RATE_MS));
+ make_mono(packet_buffer, bytes_read);
+ audio_i2s.i2s_bridge_udp.beginPacket(audio_i2s.i2s_bridge_ip, I2S_BRIDGE_PORT);
+ audio_i2s.i2s_bridge_udp.write((const uint8_t*)packet_buffer, bytes_read);
+ audio_i2s.i2s_bridge_udp.endPacket();
+ }
+ }
+ }
+ AddLog(LOG_LEVEL_INFO, PSTR("I2S_bridge: stopped"));
+ vTaskDelete(NULL);
+}
+
+void Cmd_I2SBridge(void) {
+ if (XdrvMailbox.data_len > 0) {
+ char *cp = XdrvMailbox.data;
+ if (strchr(cp, '.')) {
+ // enter destination ip
+ audio_i2s.i2s_bridge_ip.fromString(cp);
+ Response_P(PSTR("{\"I2S_bridge\":{\"IP\":\"%s\"}}"), cp);
+ } else if (cp = strchr(cp, 'p')) {
+ // enter push to talk pin
+ cp++;
+ audio_i2s.ptt_pin = atoi(cp);
+ pinMode(audio_i2s.ptt_pin, INPUT_PULLUP);
+ Response_P(PSTR("{\"I2S_bridge\":{\"PTT-PIN\":%d}}"), audio_i2s.ptt_pin);
+ } else {
+ I2SBridgeCmd(XdrvMailbox.payload, 1);
+ }
+ }
+}
+
+void SendBridgeCmd(uint8_t mode) {
+char slavecmd[16];
+ if (audio_i2s.bridge_mode.master) {
+ sprintf(slavecmd,"cmd:%d", mode);
+ audio_i2s.i2s_bridgec_udp.beginPacket(audio_i2s.i2s_bridge_ip, I2S_BRIDGE_PORT + 1);
+ audio_i2s.i2s_bridgec_udp.write((const uint8_t*)slavecmd,strlen(slavecmd));
+ audio_i2s.i2s_bridgec_udp.endPacket();
+ }
+}
+
+void I2SBridgeCmd(uint8_t val, uint8_t flg) {
+ if ((val >= 0) && (val <= 11)) {
+ if (val > 3) {
+ switch (val) {
+ case 4:
+ audio_i2s.bridge_mode.master = 1;
+ break;
+ case 5:
+ audio_i2s.bridge_mode.master = 0;
+ break;
+ case 6:
+ audio_i2s.bridge_mode.swap_mic = 1;
+ break;
+ case 7:
+ audio_i2s.bridge_mode.swap_mic = 0;
+ break;
+ }
+ Response_P(PSTR("{\"I2S_bridge\":{\"SWAP_MIC\":%d}}"), audio_i2s.bridge_mode.swap_mic);
+ } else {
+ if (audio_i2s.bridge_mode.mode != val) {
+ if ((val == I2S_BRIDGE_MODE_OFF) && (audio_i2s.bridge_mode.mode != I2S_BRIDGE_MODE_OFF)) {
+ if (flg && (audio_i2s.bridge_mode.master)) {
+ // shutdown slave
+ SendBridgeCmd(I2S_BRIDGE_MODE_OFF);
+ }
+ i2s_bridge_init(I2S_BRIDGE_MODE_OFF);
+ } else {
+ if (audio_i2s.bridge_mode.mode == I2S_BRIDGE_MODE_OFF) {
+ // initial on
+ i2s_bridge_init(val);
+ } else {
+ // change mode
+ if (val & I2S_BRIDGE_MODE_READ) {
+ //SpeakerMic(MODE_SPK);
+ }
+ if (val & I2S_BRIDGE_MODE_WRITE) {
+ //SpeakerMic(MODE_MIC);
+ }
+ }
+ }
+
+ audio_i2s.bridge_mode.mode = val;
+
+ if (flg) {
+ if (audio_i2s.bridge_mode.master) {
+ // set slave to complementary mode
+ if (audio_i2s.bridge_mode.mode && ((audio_i2s.bridge_mode.mode & 3) != 3)) {
+ uint8_t slavemode = I2S_BRIDGE_MODE_READ;
+ if (audio_i2s.bridge_mode.mode & I2S_BRIDGE_MODE_READ) {
+ slavemode = I2S_BRIDGE_MODE_WRITE;
+ }
+ SendBridgeCmd(slavemode);
+ }
+ }
+ }
+ }
+ ResponseCmndNumber(audio_i2s.bridge_mode.mode);
+ }
+ }
+}
+
+void i2s_bridge_loop(void) {
+ uint8_t packet_buffer[64];
+
+ if (TasmotaGlobal.global_state.wifi_down) {
+ return;
+ }
+
+ if (audio_i2s.ptt_pin >= 0) {
+ if (audio_i2s.bridge_mode.mode != I2S_BRIDGE_MODE_OFF) {
+ if (digitalRead(audio_i2s.ptt_pin) == 0) {
+ // push to talk
+ if (audio_i2s.bridge_mode.mode != I2S_BRIDGE_MODE_WRITE) {
+ I2SBridgeCmd(I2S_BRIDGE_MODE_WRITE, 1);
+ }
+
+ } else {
+ if (audio_i2s.bridge_mode.mode != I2S_BRIDGE_MODE_READ) {
+ I2SBridgeCmd(I2S_BRIDGE_MODE_READ, 1);
+ }
+ }
+ }
+ }
+
+ if (audio_i2s.i2s_bridgec_udp.parsePacket()) {
+ // received control command
+ memset(packet_buffer,0,sizeof(packet_buffer));
+ audio_i2s.i2s_bridgec_udp.read(packet_buffer, 63);
+ char *cp = (char*)packet_buffer;
+ if (!strncmp(cp, "cmd:", 4)) {
+ cp += 4;
+ I2SBridgeCmd(atoi(cp), 0);
+ audio_i2s.i2s_bridge_ip = audio_i2s.i2s_bridgec_udp.remoteIP();
+ AddLog(LOG_LEVEL_INFO, PSTR("I2S_bridge: remote cmd %d"), audio_i2s.bridge_mode.mode);
+ }
+ }
+
+
+
+}
+
+
+void I2SBridgeInit(void) {
+ // start udp control channel
+ audio_i2s.i2s_bridgec_udp.begin(I2S_BRIDGE_PORT + 1);
+ //audio_i2s.i2s_bridgec_udp.flush();
+ //audio_i2s.i2s_bridgec_udp.stop();
+ //I2SBridgeCmd(audio_i2s.bridge_mode.mode, 1);
+ AddLog(LOG_LEVEL_INFO, PSTR("I2S_bridge: command server created on port: %d "), I2S_BRIDGE_PORT + 1);
+}
+
+#endif // I2S_BRIDGE
+#endif // USE_I2S_AUDIO
+#endif // ESP32
diff --git a/tasmota/tasmota_xdrv_driver/xdrv_52_3_berry_zigbee.ino b/tasmota/tasmota_xdrv_driver/xdrv_52_3_berry_zigbee.ino
index 37f506c0d..2642ac280 100644
--- a/tasmota/tasmota_xdrv_driver/xdrv_52_3_berry_zigbee.ino
+++ b/tasmota/tasmota_xdrv_driver/xdrv_52_3_berry_zigbee.ino
@@ -69,7 +69,7 @@ extern "C" {
return d->batt_percent == 255 ? -1 : d->batt_percent;
}
int32_t zd_battery_lastseen(const class Z_Device* d) {
- return 0; // TODO not yet known
+ return d->batt_last_seen; // TODO not yet known
}
}
@@ -166,8 +166,8 @@ extern "C" {
be_return(vm);
}
- int32_t callBerryZigbeeDispatcher(const char* cmd, const char* type, void* data, int32_t idx);
- int32_t callBerryZigbeeDispatcher(const char* cmd, const char* type, void* data, int32_t idx) {
+ int32_t callBerryZigbeeDispatcher(const char* event, const class ZCLFrame* zcl_frame, const class Z_attribute_list* attr_list, int32_t idx);
+ int32_t callBerryZigbeeDispatcher(const char* event, const class ZCLFrame* zcl_frame, const class Z_attribute_list* attr_list, int32_t idx) {
int32_t ret = 0;
bvm *vm = berry.vm;
@@ -178,9 +178,9 @@ extern "C" {
be_getmethod(vm, -1, PSTR("dispatch")); // method dispatch
if (!be_isnil(vm, -1)) {
be_pushvalue(vm, -2); // add instance as first arg
- be_pushstring(vm, cmd != nullptr ? cmd : "");
- be_pushstring(vm, type != nullptr ? type : "");
- be_pushcomptr(vm, data);
+ be_pushstring(vm, event != nullptr ? event : "");
+ be_pushcomptr(vm, (void*) zcl_frame);
+ be_pushcomptr(vm, (void*) attr_list);
be_pushint(vm, idx);
BrTimeoutStart();
ret = be_pcall(vm, 5); // 5 arguments
@@ -205,7 +205,7 @@ extern "C" {
}
/*********************************************************************************************\
- * Mapping for zcl_message
+ * Mapping for zcl_frame_ntv
*
\*********************************************************************************************/
extern "C" {
@@ -234,6 +234,52 @@ extern "C" {
#pragma GCC diagnostic pop
}
+/*********************************************************************************************\
+ * Mapping for zcl_attribute_ntv
+ *
+\*********************************************************************************************/
+extern "C" {
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Winvalid-offsetof" // avoid warnings since we're using offsetof() in a risky way
+
+ extern const be_ctypes_structure_t be_zigbee_zcl_attribute_list_struct = {
+ sizeof(Z_attribute_list), /* size in bytes */
+ 3, /* number of elements */
+ nullptr,
+ (const be_ctypes_structure_item_t[3]) {
+ { "_groupaddr", offsetof(Z_attribute_list, group_id), 0, 0, ctypes_u16, 0 },
+ { "_lqi", offsetof(Z_attribute_list, lqi), 0, 0, ctypes_u8, 0 },
+ { "_src_ep", offsetof(Z_attribute_list, src_ep), 0, 0, ctypes_u8, 0 },
+ }};
+
+ extern const be_ctypes_structure_t be_zigbee_zcl_attribute_struct = {
+ sizeof(Z_attribute), /* size in bytes */
+ 9, /* number of elements */
+ nullptr,
+ (const be_ctypes_structure_item_t[9]) {
+ { "_attr_id", offsetof(Z_attribute, attr_id), 0, 0, ctypes_u16, 0 },
+ { "_cluster", offsetof(Z_attribute, cluster), 0, 0, ctypes_u16, 0 },
+ { "_cmd", offsetof(Z_attribute, attr_id), 0, 0, ctypes_u8, 0 }, // low 8 bits of attr_id
+ { "_direction", offsetof(Z_attribute, attr_id) + 1, 0, 0, ctypes_u8, 0 }, // high 8 bits of attr_id
+ { "_iscmd", offsetof(Z_attribute, key_is_cmd), 0, 0, ctypes_u8, 0 },
+ { "attr_multiplier", offsetof(Z_attribute, attr_multiplier), 0, 0, ctypes_i8, 0 },
+ { "attr_divider", offsetof(Z_attribute, attr_divider), 0, 0, ctypes_i8, 0 },
+ { "attr_type", offsetof(Z_attribute, attr_type), 0, 0, ctypes_u8, 0 },
+ // { "key", offsetof(Z_attribute, key), 0, 0, ctypes_ptr32, 0 },
+ // { "key_is_pmem", offsetof(Z_attribute, key_is_pmem), 0, 0, ctypes_u8, 0 },
+ // { "key_is_str", offsetof(Z_attribute, key_is_str), 0, 0, ctypes_u8, 0 },
+ { "key_suffix", offsetof(Z_attribute, key_suffix), 0, 0, ctypes_u8, 0 },
+ // { "type", offsetof(Z_attribute, type), 0, 0, ctypes_u8, 0 },
+ // { "val_float", offsetof(Z_attribute, val), 0, 0, ctypes_float, 0 },
+ // { "val_i32", offsetof(Z_attribute, val), 0, 0, ctypes_i32, 0 },
+ // { "val_str_raw", offsetof(Z_attribute, val_str_raw), 0, 0, ctypes_u8, 0 },
+ // { "val_ptr", offsetof(Z_attribute, val), 0, 0, ctypes_ptr32, 0 },
+ // { "val_u32", offsetof(Z_attribute, val), 0, 0, ctypes_u32, 0 },
+ }};
+
+#pragma GCC diagnostic pop
+}
+
/*********************************************************************************************\
* Functions for zcl_frame
*
@@ -258,5 +304,295 @@ extern "C" {
}
}
+/*********************************************************************************************\
+ * Functions for zcl_attribute
+ *
+\*********************************************************************************************/
+extern "C" {
+ extern const bclass be_class_zcl_attribute_list;
+ extern const bclass be_class_zcl_attribute;
+ extern const bclass be_class_zcl_attribute_ntv;
+
+ void zat_zcl_attribute(struct bvm *vm, const Z_attribute *attr);
+
+ // Pushes the Z_attribute_list on the stack as a simple list
+ // Adds the output on top of stack and does not change rest of stack (stack size incremented by 1)
+ void zat_zcl_attribute_list_inner(struct bvm *vm, const Z_attribute_list* attrlist) {
+ be_newobject(vm, "list");
+
+ for (const auto & attr : *attrlist) {
+ zat_zcl_attribute(vm, &attr);
+ be_data_push(vm, -2);
+ be_pop(vm, 1);
+ }
+ be_pop(vm, 1);
+ }
+
+ // Pushes the Z_attribute on the stack as `zcl_attribute_ntv`
+ void zat_zcl_attribute(struct bvm *vm, const Z_attribute *attr) {
+ be_pushntvclass(vm, &be_class_zcl_attribute);
+ be_pushcomptr(vm, (void*) attr);
+
+ // instantiate
+ be_call(vm, 1); // 1 parameter
+ be_pop(vm, 1);
+ }
+
+ // Get typed value from zcl_attributes
+ int be_zigbee_zcl_attribute_ntv_get_val(struct bvm *vm) {
+ const Z_attribute *attr = (const Z_attribute*) be_tobytes(vm, 1, NULL);
+ // value
+ switch (attr->type) {
+ case Za_type::Za_bool:
+ be_pushbool(vm, attr->val.uval32 ? btrue : bfalse);
+ break;
+ case Za_type::Za_uint:
+ case Za_type::Za_int:
+ be_pushint(vm, attr->val.ival32);
+ break;
+ case Za_type::Za_float:
+ be_pushreal(vm, (breal)attr->val.fval);
+ break;
+ case Za_type::Za_raw:
+ be_pushbytes(vm, attr->val.bval->getBuffer(), attr->val.bval->len());
+ break;
+ case Za_type::Za_str:
+ be_pushstring(vm, attr->val.sval);
+ break;
+
+ case Za_type::Za_obj:
+ zat_zcl_attribute_list_inner(vm, attr->val.objval);
+ break;
+
+ case Za_type::Za_arr:
+ // json_format = true;
+ if (attr->val.arrval) {
+ String arrval = attr->val.arrval->toString();
+ be_module_load(vm, be_newstr(vm, "json"));
+ be_getmember(vm, -1, "load");
+ be_remove(vm, -2); // remove module 'json'
+ be_pushstring(vm, arrval.c_str());
+ be_call(vm, 1);
+ be_pop(vm, 1);
+ } else {
+ // push empty list
+ be_newobject(vm, "list");
+ be_pop(vm, 1);
+ }
+ break;
+
+ case Za_type::Za_none:
+ default:
+ be_pushnil(vm);
+ break;
+ }
+
+ be_return(vm);
+ }
+
+ // Initialize the Z_attribute_list memory zone with provided address
+ int be_zigbee_zcl_attribute_list_ntv_init(struct bvm *vm) {
+ size_t len;
+ Z_attribute_list *attr_list = (Z_attribute_list*) be_tobytes(vm, 1, &len);
+ attr_list = new(attr_list) Z_attribute_list(); // "placement new" to provide a fixed address https://isocpp.org/wiki/faq/dtors#placement-new
+ be_return_nil(vm);
+ }
+
+ // Deinitialize the Z_attribute_list memory zone with provided address
+ int be_zigbee_zcl_attribute_list_ntv_deinit(struct bvm *vm) {
+ size_t len;
+ Z_attribute_list *attr_list = (Z_attribute_list*) be_tobytes(vm, 1, &len);
+ if (attr_list) {
+ attr_list->~Z_attribute_list();
+ }
+ be_return_nil(vm);
+ }
+
+ // Size
+ int be_zigbee_zcl_attribute_list_ntv_size(struct bvm *vm) {
+ Z_attribute_list *attr_list = (Z_attribute_list*) be_tobytes(vm, 1, nullptr);
+ be_pushint(vm, attr_list->length());
+ be_return(vm);
+ }
+
+ // Item
+ int be_zigbee_zcl_attribute_list_ntv_item(struct bvm *vm) {
+ int32_t argc = be_top(vm);
+ if (argc >= 2) {
+ int32_t idx = be_toint(vm, 2);
+ Z_attribute_list *attr_list = (Z_attribute_list*) be_tobytes(vm, 1, nullptr);
+ const Z_attribute* attr = attr_list->at(idx);
+ if (attr) {
+ zat_zcl_attribute(vm, attr);
+ be_return(vm);
+ }
+ }
+ be_return_nil(vm);
+ }
+
+ // new_head
+ int be_zigbee_zcl_attribute_list_ntv_new_head(struct bvm *vm) {
+ Z_attribute_list *attr_list = (Z_attribute_list*) be_tobytes(vm, 1, nullptr);
+ Z_attribute &attr = attr_list->addHead();
+ zat_zcl_attribute(vm, &attr);
+ be_return(vm);
+ }
+
+ // new_tail
+ int be_zigbee_zcl_attribute_list_ntv_new_tail(struct bvm *vm) {
+ Z_attribute_list *attr_list = (Z_attribute_list*) be_tobytes(vm, 1, nullptr);
+ Z_attribute &attr = attr_list->addToLast();
+ zat_zcl_attribute(vm, &attr);
+ be_return(vm);
+ }
+
+ // Remove
+ int be_zigbee_zcl_attribute_list_ntv_remove(struct bvm *vm) {
+ int32_t argc = be_top(vm);
+ if (argc >= 2) {
+ int32_t idx = be_toint(vm, 2);
+ Z_attribute_list *attr_list = (Z_attribute_list*) be_tobytes(vm, 1, nullptr);
+ const Z_attribute* attr = attr_list->at(idx);
+ if (attr) {
+ attr_list->remove(attr);
+ }
+ }
+ be_return_nil(vm);
+ }
+
+ // Initialize the Z_attribute memory zone with provided address
+ int be_zigbee_zcl_attribute_ntv_init(struct bvm *vm) {
+ size_t len;
+ Z_attribute *attr = (Z_attribute*) be_tobytes(vm, 1, &len);
+ attr = new(attr) Z_attribute(); // "placement new" to provide a fixed address https://isocpp.org/wiki/faq/dtors#placement-new
+ be_return_nil(vm);
+ }
+
+ // Deinitialize the Z_attribute memory zone with provided address
+ int be_zigbee_zcl_attribute_ntv_deinit(struct bvm *vm) {
+ size_t len;
+ Z_attribute *attr = (Z_attribute*) be_tobytes(vm, 1, &len);
+ if (attr) {
+ attr->~Z_attribute();
+ }
+ be_return_nil(vm);
+ }
+
+ // Set typed value from zcl_attributes
+ int be_zigbee_zcl_attribute_ntv_set_val(struct bvm *vm) {
+ int32_t argc = be_top(vm);
+ if (argc >= 2) {
+ Z_attribute *attr = (Z_attribute*) be_tobytes(vm, 1, NULL);
+
+ if (be_isnil(vm, 2)) {
+ attr->setNone();
+ } else if (be_isbool(vm, 2)) {
+ attr->setBool(be_tobool(vm, 2));
+ } else if (be_isint(vm, 2)) {
+ attr->setInt(be_toint(vm, 2));
+ } else if (be_isreal(vm, 2)) {
+ attr->setFloat(be_toreal(vm, 2));
+ } else if (be_isstring(vm, 2)) {
+ attr->setStr(be_tostring(vm, 2));
+ } else if (be_isbytes(vm, 2)) {
+ size_t len;
+ const void* buf = be_tobytes(vm, 2, &len);
+ attr->setRaw(buf, len);
+ }
+ }
+
+ be_return(vm);
+ }
+
+ // returns the key as string or `nil` if no string key. Suffix is not appended
+ int be_zigbee_zcl_attribute_ntv_get_key(struct bvm *vm) {
+ const Z_attribute *attr = (const Z_attribute*) be_tobytes(vm, 1, NULL);
+ if (attr->key_is_str) {
+ be_pushstring(vm, attr->key);
+ } else {
+ be_pushnil(vm);
+ }
+ be_return(vm);
+ }
+
+ // set string key, or remove if `nil` or no parameter
+ int be_zigbee_zcl_attribute_ntv_set_key(struct bvm *vm) {
+ Z_attribute *attr = (Z_attribute*) be_tobytes(vm, 1, NULL);
+ int32_t argc = be_top(vm);
+ if (argc >= 2 && be_isstring(vm, 2)) {
+ const char* key = be_tostring(vm, 2);
+ attr->setKeyName(key, false);
+ } else {
+ attr->setKeyId(attr->cluster, attr->attr_id);
+ }
+ be_return_nil(vm);
+ }
+}
+
+extern "C" {
+ int zigbee_test_attr(struct bvm *vm) {
+ int32_t mode = be_toint(vm, 2);
+ if (mode < 10) {
+ //
+ } else {
+ Z_attribute *a = new Z_attribute();
+ if (mode == 10) {
+ a->setKeyId(1111, 2222);
+ a->setUInt(1337);
+ } else if (mode == 11) {
+ a->setKeyName("super_attribute");
+ a->key_suffix = 2;
+ a->setFloat(3.14);
+ } else if (mode == 12) {
+ a->setKeyName("array");
+ a->newJsonArray();
+ a->val.arrval->add(-1);
+ a->val.arrval->addStr("foo");
+ a->val.arrval->addStr("bar");
+ a->val.arrval->addStr("bar\"baz\'toto");
+ } else if (mode == 13) {
+ a->setKeyName("list");
+ a->newAttrList();
+ Z_attribute &subattr1 = a->val.objval->addAttribute(10,20);
+ subattr1.setStr("sub1");
+ Z_attribute &subattr2 = a->val.objval->addAttribute(11,21);
+ subattr2.setStr("sub2");
+ }
+ zat_zcl_attribute(vm, a);
+ }
+ be_return(vm);
+ }
+
+
+ // Creates a zcl_attributes from Z_attribute_list
+ // Adds the output on top of stack and does not change rest of stack (stack size incremented by 1)
+ void zat_zcl_attribute_list(struct bvm *vm, uint16_t shortaddr, const Z_attribute_list* attr_list) {
+ be_pushntvclass(vm, &be_class_zcl_attribute_list);
+ be_pushcomptr(vm, (void*) attr_list);
+ // // instantiate
+ be_call(vm, 1); // 1 parameter
+ be_pop(vm, 1);
+
+ if (shortaddr != BAD_SHORTADDR) {
+ be_pushint(vm, shortaddr);
+ be_setmember(vm, -2, "shortaddr");
+ be_pop(vm, 1);
+ }
+ }
+
+ int zigbee_test_msg(struct bvm *vm) {
+ Z_attribute_list attr_list;
+
+ attr_list.lqi = 250;
+ Z_attribute &subattr1 = attr_list.addAttribute(10,20);
+ subattr1.setStr("sub1");
+ Z_attribute &subattr2 = attr_list.addAttribute(11,21);
+ subattr2.setStr("sub2");
+
+ zat_zcl_attribute_list(vm, 100, &attr_list);
+ be_return(vm);
+ }
+}
+
#endif // USE_ZIGBEE
#endif // USE_BERRY
diff --git a/tasmota/tasmota_xdrv_driver/xdrv_52_9_berry.ino b/tasmota/tasmota_xdrv_driver/xdrv_52_9_berry.ino
index e4c302bdb..36ac8b4e0 100644
--- a/tasmota/tasmota_xdrv_driver/xdrv_52_9_berry.ino
+++ b/tasmota/tasmota_xdrv_driver/xdrv_52_9_berry.ino
@@ -230,6 +230,30 @@ void BerryObservability(bvm *vm, int event...) {
vm_usage, vm_usage2, vm_freed, vm_scanned, gc_elapsed,
slots_used_before_gc, slots_allocated_before_gc,
slots_used_after_gc, slots_allocated_after_gc);
+
+ // Add more in-deptch metrics
+ AddLog(LOG_LEVEL_DEBUG_MORE, D_LOG_BERRY "GC timing (us) 1:%i 2:%i 3:%i 4:%i 5:%i total:%i",
+ vm->micros_gc1 - vm->micros_gc0,
+ vm->micros_gc2 - vm->micros_gc1,
+ vm->micros_gc3 - vm->micros_gc2,
+ vm->micros_gc4 - vm->micros_gc3,
+ vm->micros_gc5 - vm->micros_gc4,
+ vm->micros_gc5 - vm->micros_gc0
+ );
+ AddLog(LOG_LEVEL_DEBUG_MORE, D_LOG_BERRY "GC by type "
+ "string:%i class:%i proto:%i instance:%i map:%i "
+ "list:%i closure:%i ntvclos:%i module:%i comobj:%i",
+ vm->gc_mark_string,
+ vm->gc_mark_class,
+ vm->gc_mark_proto,
+ vm->gc_mark_instance,
+ vm->gc_mark_map,
+ vm->gc_mark_list,
+ vm->gc_mark_closure,
+ vm->gc_mark_ntvclos,
+ vm->gc_mark_module,
+ vm->gc_mark_comobj
+ );
// make new threshold tighter when we reach high memory usage
if (!UsePSRAM() && vm->gc.threshold > 20*1024) {
vm->gc.threshold = vm->gc.usage + 10*1024; // increase by only 10 KB
@@ -287,10 +311,15 @@ void BerryInit(void) {
do {
berry.vm = be_vm_new(); /* create a virtual machine instance */
be_set_obs_hook(berry.vm, &BerryObservability); /* attach observability hook */
+ be_set_obs_micros(berry.vm, (bmicrosfnct)µs);
comp_set_named_gbl(berry.vm); /* Enable named globals in Berry compiler */
comp_set_strict(berry.vm); /* Enable strict mode in Berry compiler, equivalent of `import strict` */
be_set_ctype_func_hanlder(berry.vm, be_call_ctype_func);
+ if (UsePSRAM()) { // if PSRAM is available, raise the max size to 512kb
+ berry.vm->bytesmaxsize = 512 * 1024;
+ }
+
be_load_custom_libs(berry.vm); // load classes and modules
// Set the GC threshold to 3584 bytes to avoid the first useless GC
diff --git a/tasmota/tasmota_xdrv_driver/xdrv_56_rtc_chips.ino b/tasmota/tasmota_xdrv_driver/xdrv_56_rtc_chips.ino
index ac3c31c2d..0de267656 100644
--- a/tasmota/tasmota_xdrv_driver/xdrv_56_rtc_chips.ino
+++ b/tasmota/tasmota_xdrv_driver/xdrv_56_rtc_chips.ino
@@ -446,7 +446,7 @@ bool Xdrv56(uint8_t function) {
}
#endif // RTC_NTP_SERVER
- if (FUNC_MODULE_INIT == function) {
+ if (FUNC_I2C_INIT == function) {
RtcChipDetect();
}
else if (RtcChip.detected) {
diff --git a/tasmota/tasmota_xdrv_driver/xdrv_59_influxdb.ino b/tasmota/tasmota_xdrv_driver/xdrv_59_influxdb.ino
index 48adb935c..d98011399 100644
--- a/tasmota/tasmota_xdrv_driver/xdrv_59_influxdb.ino
+++ b/tasmota/tasmota_xdrv_driver/xdrv_59_influxdb.ino
@@ -373,7 +373,7 @@ void InfluxDbProcessJson(bool use_copy = false) {
}
void InfluxDbProcess(bool use_copy) {
- if (Settings->sbflag1.influxdb_sensor) {
+ if (Settings->sbflag1.influxdb_sensor) { // IfxSensor
InfluxDbProcessJson(use_copy);
}
}
@@ -476,9 +476,9 @@ void CmndInfluxDbState(void) {
void CmndInfluxDbSensor(void) {
if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= 1)) {
- Settings->sbflag1.influxdb_sensor = XdrvMailbox.payload;
+ Settings->sbflag1.influxdb_sensor = XdrvMailbox.payload; // IfxSensor
}
- ResponseCmndStateText(Settings->sbflag1.influxdb_sensor);
+ ResponseCmndStateText(Settings->sbflag1.influxdb_sensor); // IfxSensor
}
void CmndInfluxDbLog(void) {
diff --git a/tasmota/tasmota_xdrv_driver/xdrv_63_modbus_bridge.ino b/tasmota/tasmota_xdrv_driver/xdrv_63_modbus_bridge.ino
index b5127deb2..479ea560b 100644
--- a/tasmota/tasmota_xdrv_driver/xdrv_63_modbus_bridge.ino
+++ b/tasmota/tasmota_xdrv_driver/xdrv_63_modbus_bridge.ino
@@ -22,12 +22,27 @@
* Modbus Bridge using Modbus library (TasmotaModbus)
*
* Can be used trough web/mqtt commands and also via direct TCP connection (when defined)
- *
+ *
* When USE_MODBUS_BRIDGE_TCP is also defined, this bridge can also be used as an ModbusTCP
* bridge.
*
- * Example Command:
+ * Example Commands:
+ * -- Read Coils --
+ * ModbusSend {"deviceaddress": 1, "functioncode": 1, "startaddress": 1, "type":"bit", "count":2}
+ *
+ * -- Read Input Register --
* ModbusSend {"deviceaddress": 1, "functioncode": 3, "startaddress": 1, "type":"uint16", "count":2}
+ *
+ * -- Write multiple coils --
+ * ModbusSend {"deviceaddress": 1, "functioncode": 15, "startaddress": 1, "type":"bit", "count":4, "values":[1,0,1,1]}
+ *
+ * Info for modbusBridgeTCPServer:
+ * https://ipc2u.com/articles/knowledge-base/detailed-description-of-the-modbus-tcp-protocol-with-command-examples/
+ *
+ * Info for modbus serial communications:
+ * https://ozeki.hu/p_5879-mobdbus-function-code-4-read-input-registers.html
+ * https://www.modbustools.com/modbus.html
+ * https://ipc2u.com/articles/knowledge-base/modbus-rtu-made-simple-with-detailed-descriptions-and-examples/
\*********************************************************************************************/
#define XDRV_63 63
@@ -97,30 +112,36 @@ enum class ModbusBridgeError
wrongfunctioncode = 3,
wrongstartaddress = 4,
wrongtype = 5,
- wrongregistercount = 6,
- wrongcount = 7
+ wrongdataCount = 6,
+ wrongcount = 7,
+ tomanydata = 8
};
enum class ModbusBridgeFunctionCode
{
mb_undefined = 0,
- mb_readCoilstartregister = 1,
- mb_readContactstartregister = 2,
- mb_readHoldingstartregister = 3,
- mb_readInputstartregister = 4,
+ mb_readCoilStatus = 1,
+ mb_readInputStatus = 2,
+ mb_readHoldingRegisters = 3,
+ mb_readInputRegisters = 4,
mb_writeSingleCoil = 5,
- mb_writeSinglestartregister = 6
+ mb_writeSingleRegister = 6,
+ mb_writeMultipleCoils = 15,
+ mb_writeMultipleRegisters = 16
};
enum class ModbusBridgeType
{
mb_undefined,
+ mb_uint8,
mb_uint16,
mb_uint32,
+ mb_int8,
mb_int16,
mb_int32,
mb_float,
mb_raw,
+ mb_hex,
mb_bit
};
@@ -139,7 +160,8 @@ struct ModbusBridge
ModbusBridgeFunctionCode functionCode = ModbusBridgeFunctionCode::mb_undefined;
ModbusBridgeType type = ModbusBridgeType::mb_undefined;
- uint16_t registerCount = 0;
+ uint16_t dataCount = 0;
+ uint16_t byteCount = 0;
uint16_t startAddress = 0;
uint8_t deviceAddress = 0;
uint8_t count = 0;
@@ -148,14 +170,34 @@ struct ModbusBridge
ModbusBridge modbusBridge;
+/********************************************************************************************/
+//
+// Helper functions for data conversion between little and big endian
+//
+uint16_t swap_endian16(uint16_t num)
+{
+ return (num>>8) | (num<<8);
+}
+
+uint32_t swap_endian32(uint32_t num)
+{
+ return ((num>>24)&0xff) | // move byte 3 to byte 0
+ ((num<<8)&0xff0000) | // move byte 1 to byte 2
+ ((num>>8)&0xff00) | // move byte 2 to byte 1
+ ((num<<24)&0xff000000); // byte 0 to byte 3
+}
+
+
/********************************************************************************************/
//
// Applies serial configuration to modbus serial port
//
bool ModbusBridgeBegin(void)
{
- if ((Settings->modbus_sbaudrate < 300 / 300) || (Settings->modbus_sbaudrate > 115200 / 300)) Settings->modbus_sbaudrate = (uint8_t)((uint32_t)MBR_BAUDRATE / 300);
- if (Settings->modbus_sconfig > TS_SERIAL_8O2) Settings->modbus_sconfig = TS_SERIAL_8N1;
+ if ((Settings->modbus_sbaudrate < 1) || (Settings->modbus_sbaudrate > (115200 / 300)))
+ Settings->modbus_sbaudrate = (uint8_t)((uint32_t)MBR_BAUDRATE / 300);
+ if (Settings->modbus_sconfig > TS_SERIAL_8O2)
+ Settings->modbus_sconfig = TS_SERIAL_8N1;
int result = tasmotaModbus->Begin(Settings->modbus_sbaudrate * 300, ConvertSerialConfig(Settings->modbus_sconfig)); // Reinitialize modbus port with new baud rate
if (result)
@@ -186,7 +228,7 @@ void SetModbusBridgeBaudrate(uint32_t baudrate)
{
if ((baudrate >= 300) && (baudrate <= 115200))
{
- if (baudrate / 300 != Settings->modbus_sbaudrate)
+ if (baudrate / 300 != Settings->modbus_sbaudrate)
{
Settings->modbus_sbaudrate = baudrate / 300;
ModbusBridgeBegin();
@@ -204,15 +246,10 @@ void ModbusBridgeHandle(void)
if (data_ready)
{
uint8_t *buffer;
- buffer = (uint8_t *)malloc(5 + (modbusBridge.registerCount * 2)); // Addres(1), Function(1), Length(1), Data(1..n), CRC(2)
- uint32_t error = tasmotaModbus->ReceiveBuffer(buffer, modbusBridge.registerCount);
-
- if (error)
- {
- AddLog(LOG_LEVEL_DEBUG, PSTR("MBS: MBR Driver error %d"), error);
- free(buffer);
- return;
- }
+ if (modbusBridge.byteCount == 0) modbusBridge.byteCount = modbusBridge.dataCount * 2;
+ buffer = (uint8_t *)malloc(9 + modbusBridge.byteCount); // Addres(1), Function(1), Length(1), Data(1..n), CRC(2)
+ memset(buffer, 0, 9 + modbusBridge.byteCount);
+ uint32_t error = tasmotaModbus->ReceiveBuffer(buffer, 0, modbusBridge.byteCount);
#ifdef USE_MODBUS_BRIDGE_TCP
for (uint32_t i = 0; i < nitems(modbusBridgeTCP.client_tcp); i++)
@@ -220,27 +257,67 @@ void ModbusBridgeHandle(void)
WiFiClient &client = modbusBridgeTCP.client_tcp[i];
if (client)
{
- uint8_t MBAP_Header[7];
- MBAP_Header[0] = modbusBridgeTCP.tcp_transaction_id >> 8;
- MBAP_Header[1] = modbusBridgeTCP.tcp_transaction_id;
- MBAP_Header[2] = 0;
- MBAP_Header[3] = 0;
- MBAP_Header[4] = ((modbusBridge.registerCount * 2) + 3) >> 8;
- MBAP_Header[5] = (modbusBridge.registerCount * 2) + 3;
- MBAP_Header[6] = buffer[0]; // Send slave address
- client.write(MBAP_Header, 7);
- client.write(buffer + 1, 1); // Send Functioncode
- uint8_t bytecount[1];
- bytecount[0] = modbusBridge.registerCount * 2;
- client.write(bytecount, 1); // Send length of rtu data
- client.write(buffer + 3, (modbusBridge.registerCount * 2)); // Don't send CRC
+ uint8_t header[8];
+ uint8_t nrOfBytes = 8;
+ header[0] = modbusBridgeTCP.tcp_transaction_id >> 8;
+ header[1] = modbusBridgeTCP.tcp_transaction_id;
+ header[2] = 0;
+ header[3] = 0;
+ header[6] = buffer[0]; // Send slave address
+ header[7] = buffer[1]; // Send function code
+ if (error)
+ {
+ header[4] = 0; // Message Length Hi-Byte
+ header[5] = 3; // Message Length Low-Byte
+ header[7] = buffer[1] | 0x80; // Send function code
+ header[8] = error;
+ nrOfBytes += 1;
+ client.write(header, 9);
+ }
+ else if (buffer[1] <= 2)
+ {
+ header[4] = modbusBridge.byteCount >> 8;
+ header[5] = modbusBridge.byteCount + 3;
+ header[8] = modbusBridge.byteCount;
+ client.write(header, 9);
+ nrOfBytes += 1;
+ client.write(buffer + 3, modbusBridge.byteCount); // Don't send CRC
+ nrOfBytes += modbusBridge.byteCount;
+ }
+ else if (buffer[1] <= 4)
+ {
+ header[4] = modbusBridge.byteCount >> 8;
+ header[5] = modbusBridge.byteCount + 3;
+ header[8] = modbusBridge.byteCount;
+ client.write(header, 9);
+ nrOfBytes += 1;
+ client.write(buffer + 3, modbusBridge.byteCount); // Don't send CRC
+ nrOfBytes += modbusBridge.byteCount;
+ }
+ else
+ {
+ header[4] = 0; // Message Length Hi-Byte
+ header[5] = 6; // Message Length Low-Byte
+ client.write(header, 8);
+ client.write(buffer + 2, 4); // Don't send CRC
+ nrOfBytes += 4;
+ }
client.flush();
- AddLog(LOG_LEVEL_DEBUG, PSTR("MBS: MBRTCP from Modbus deviceAddress %d, writing %d bytes to client"), buffer[0], (modbusBridge.registerCount * 2) + 9);
+ AddLog(LOG_LEVEL_DEBUG, PSTR("MBS: MBRTCP from Modbus deviceAddress %d, writing %d bytes to client"), buffer[0], nrOfBytes);
}
}
#endif
+ if (error)
+ {
+ AddLog(LOG_LEVEL_DEBUG, PSTR("MBS: MBR Driver receive error %d"), error);
+ free(buffer);
+ return;
+ }
+
+ modbusBridge.byteCount = 0;
ModbusBridgeError errorcode = ModbusBridgeError::noerror;
+
if (modbusBridge.deviceAddress == 0)
{
#ifdef USE_MODBUS_BRIDGE_TCP
@@ -257,13 +334,30 @@ void ModbusBridgeHandle(void)
errorcode = ModbusBridgeError::wrongdeviceaddress;
else if ((uint8_t)modbusBridge.functionCode != (uint8_t)buffer[1])
errorcode = ModbusBridgeError::wrongfunctioncode;
- else if ((uint8_t)modbusBridge.registerCount * 2 != (uint8_t)buffer[2])
- errorcode = ModbusBridgeError::wrongregistercount;
- else
+ else if ((uint8_t)modbusBridge.functionCode < 5)
+ {
+ if ((uint8_t)modbusBridge.functionCode < 3)
+ {
+ if ((uint8_t)(((modbusBridge.dataCount - 1) >> 3) + 1) != (uint8_t)buffer[2])
+ errorcode = ModbusBridgeError::wrongdataCount;
+ }
+ else
+ {
+ if ((modbusBridge.type == ModbusBridgeType::mb_int8 || modbusBridge.type == ModbusBridgeType::mb_uint8) && ((uint8_t)modbusBridge.dataCount * 2 != (uint8_t)buffer[2]))
+ errorcode = ModbusBridgeError::wrongdataCount;
+ else if ((modbusBridge.type == ModbusBridgeType::mb_bit) && ((uint8_t)modbusBridge.dataCount * 2 != (uint8_t)buffer[2]))
+ errorcode = ModbusBridgeError::wrongdataCount;
+ else if ((modbusBridge.type == ModbusBridgeType::mb_int16 || modbusBridge.type == ModbusBridgeType::mb_uint16) && ((uint8_t)modbusBridge.dataCount * 2 != (uint8_t)buffer[2]))
+ errorcode = ModbusBridgeError::wrongdataCount;
+ else if ((modbusBridge.type == ModbusBridgeType::mb_int32 || modbusBridge.type == ModbusBridgeType::mb_uint32 || modbusBridge.type == ModbusBridgeType::mb_float) && ((uint8_t)modbusBridge.dataCount * 2 != (uint8_t)buffer[2]))
+ errorcode = ModbusBridgeError::wrongdataCount;
+ }
+ }
+ if (errorcode == ModbusBridgeError::noerror)
{
if (modbusBridge.type == ModbusBridgeType::mb_raw)
{
- Response_P(PSTR("{\"" D_JSON_MODBUS_RECEIVED "\":["));
+ Response_P(PSTR("{\"" D_JSON_MODBUS_RECEIVED "\":{\"RAW\":["));
for (uint8_t i = 0; i < tasmotaModbus->ReceiveCount(); i++)
{
ResponseAppend_P(PSTR("%d"), buffer[i]);
@@ -274,38 +368,115 @@ void ModbusBridgeHandle(void)
ResponseJsonEnd();
MqttPublishPrefixTopicRulesProcess_P(RESULT_OR_TELE, PSTR(D_JSON_MODBUS_RECEIVED));
}
- else if ((buffer[1] > 0) && (buffer[1] < 5)) // Read Registers, writing is not supported at this moment
+ else if (modbusBridge.type == ModbusBridgeType::mb_hex)
{
+ Response_P(PSTR("{\"" D_JSON_MODBUS_RECEIVED "\":{\"HEX\":["));
+ for (uint8_t i = 0; i < tasmotaModbus->ReceiveCount(); i++)
+ {
+ ResponseAppend_P(PSTR("0x%02X"), buffer[i]);
+ if (i < tasmotaModbus->ReceiveCount() - 1)
+ ResponseAppend_P(PSTR(","));
+ }
+ ResponseAppend_P(PSTR("]}"));
+ ResponseJsonEnd();
+ MqttPublishPrefixTopicRulesProcess_P(RESULT_OR_TELE, PSTR(D_JSON_MODBUS_RECEIVED));
+ }
+ else if ((buffer[1] > 0) && (buffer[1] < 7)) // Read Registers
+ {
+ uint8_t dataOffset = 3;
Response_P(PSTR("{\"" D_JSON_MODBUS_RECEIVED "\":{"));
ResponseAppend_P(PSTR("\"" D_JSON_MODBUS_DEVICE_ADDRESS "\":%d,"), buffer[0]);
ResponseAppend_P(PSTR("\"" D_JSON_MODBUS_FUNCTION_CODE "\":%d,"), buffer[1]);
- ResponseAppend_P(PSTR("\"" D_JSON_MODBUS_START_ADDRESS "\":%d,"), modbusBridge.startAddress);
+ if (buffer[1] < 5)
+ {
+ ResponseAppend_P(PSTR("\"" D_JSON_MODBUS_START_ADDRESS "\":%d,"), modbusBridge.startAddress);
+ }
+ else
+ {
+ ResponseAppend_P(PSTR("\"" D_JSON_MODBUS_START_ADDRESS "\":%d,"), (buffer[2] << 8) + buffer[3]);
+ dataOffset = 4;
+ }
ResponseAppend_P(PSTR("\"" D_JSON_MODBUS_LENGTH "\":%d,"), tasmotaModbus->ReceiveCount());
ResponseAppend_P(PSTR("\"" D_JSON_MODBUS_COUNT "\":%d,"), modbusBridge.count);
ResponseAppend_P(PSTR("\"" D_JSON_MODBUS_VALUES "\":["));
- for (uint8_t count = 0; count < modbusBridge.count; count++)
+ uint8_t data_count = modbusBridge.count;
+ if ((uint8_t)modbusBridge.functionCode < 3)
+ {
+ if (modbusBridge.type == ModbusBridgeType::mb_int8 || modbusBridge.type == ModbusBridgeType::mb_uint8)
+ data_count = (uint8_t)(((modbusBridge.count - 1) >> 3) + 1);
+ else if (modbusBridge.type == ModbusBridgeType::mb_int16 || modbusBridge.type == ModbusBridgeType::mb_uint16)
+ data_count = (uint8_t)(((modbusBridge.count - 1) >> 4) + 1);
+ else if (modbusBridge.type == ModbusBridgeType::mb_int32 || modbusBridge.type == ModbusBridgeType::mb_uint32 || modbusBridge.type == ModbusBridgeType::mb_float)
+ data_count = (uint8_t)(((modbusBridge.count - 1) >> 5) + 1);
+ }
+ for (uint8_t count = 0; count < data_count; count++)
{
char svalue[MBR_MAX_VALUE_LENGTH + 1] = "";
if (modbusBridge.type == ModbusBridgeType::mb_float)
{
+ // Convert next 4 bytes to float
float value = 0;
- ((uint8_t *)&value)[3] = buffer[3 + (count * 4)]; // Get float values
- ((uint8_t *)&value)[2] = buffer[4 + (count * 4)];
- ((uint8_t *)&value)[1] = buffer[5 + (count * 4)];
- ((uint8_t *)&value)[0] = buffer[6 + (count * 4)];
+ if (buffer[1] < 3)
+ {
+ // In bit mode only convert returned bytes
+ if (buffer[2] - (count * 4))
+ ((uint8_t *)&value)[0] = buffer[dataOffset + (count * 4)]; // Get float values
+ if ((buffer[2] - (count * 4)) >> 1)
+ ((uint8_t *)&value)[1] = buffer[dataOffset + 1 + (count * 4)];
+ if ((buffer[2] - (count * 4) - 1) >> 1)
+ ((uint8_t *)&value)[2] = buffer[dataOffset + 2 + (count * 4)];
+ if ((buffer[2] - (count * 4)) >> 2)
+ ((uint8_t *)&value)[3] = buffer[dataOffset + 3 + (count * 4)];
+ }
+ else
+ {
+ ((uint8_t *)&value)[3] = buffer[dataOffset + (count * 4)]; // Get float values
+ ((uint8_t *)&value)[2] = buffer[dataOffset + 1 + (count * 4)];
+ ((uint8_t *)&value)[1] = buffer[dataOffset + 2 + (count * 4)];
+ ((uint8_t *)&value)[0] = buffer[dataOffset + 3 + (count * 4)];
+ }
ext_snprintf_P(svalue, sizeof(svalue), "%*_f", 10, &value);
}
+ else if (modbusBridge.type == ModbusBridgeType::mb_bit)
+ {
+ uint8_t bits_left = modbusBridge.count - ((count/8) * 8);
+ uint8_t value = 0;
+ if (bits_left < 8)
+ {
+ uint8_t bits_skip = 8 - bits_left;
+ value = (uint8_t)(buffer[dataOffset + ((count + bits_skip) >> 3)]);
+ }
+ else
+ {
+ value = (uint8_t)(buffer[dataOffset + (count >> 3)]);
+ }
+ snprintf(svalue, MBR_MAX_VALUE_LENGTH, "%d", ((value >> (count & 7)) & 1));
+ }
else
{
if ((modbusBridge.type == ModbusBridgeType::mb_int32) ||
(modbusBridge.type == ModbusBridgeType::mb_uint32))
{
uint32_t value = 0;
- ((uint8_t *)&value)[3] = buffer[3 + (count * 4)]; // Get int values
- ((uint8_t *)&value)[2] = buffer[4 + (count * 4)];
- ((uint8_t *)&value)[1] = buffer[5 + (count * 4)];
- ((uint8_t *)&value)[0] = buffer[6 + (count * 4)];
+ if (buffer[1] < 3)
+ {
+ if (buffer[2] - (count * 4))
+ ((uint8_t *)&value)[0] = buffer[dataOffset + (count * 4)]; // Get uint values
+ if (buffer[2] - ((count * 4) - 1))
+ ((uint8_t *)&value)[1] = buffer[dataOffset + 1 + (count * 4)];
+ if (buffer[2] - ((count * 4) - 2))
+ ((uint8_t *)&value)[2] = buffer[dataOffset + 2 + (count * 4)];
+ if (buffer[2] - ((count * 4) - 3))
+ ((uint8_t *)&value)[3] = buffer[dataOffset + 3 + (count * 4)];
+ }
+ else
+ {
+ ((uint8_t *)&value)[3] = buffer[dataOffset + (count * 4)]; // Get uint values
+ ((uint8_t *)&value)[2] = buffer[dataOffset + 1 + (count * 4)];
+ ((uint8_t *)&value)[1] = buffer[dataOffset + 2 + (count * 4)];
+ ((uint8_t *)&value)[0] = buffer[dataOffset + 3 + (count * 4)];
+ }
if (modbusBridge.type == ModbusBridgeType::mb_int32)
snprintf(svalue, MBR_MAX_VALUE_LENGTH, "%d", value);
else
@@ -315,21 +486,35 @@ void ModbusBridgeHandle(void)
(modbusBridge.type == ModbusBridgeType::mb_uint16))
{
uint16_t value = 0;
- ((uint8_t *)&value)[1] = buffer[3 + (count * 2)];
- ((uint8_t *)&value)[0] = buffer[4 + (count * 2)];
+ if (buffer[1] < 3)
+ {
+ if (buffer[2] - (count * 2))
+ ((uint8_t *)&value)[0] = buffer[dataOffset + (count * 2)];
+ if (buffer[2] - ((count * 2) - 1))
+ ((uint8_t *)&value)[1] = buffer[dataOffset + 1 + (count * 2)];
+ }
+ else
+ {
+ ((uint8_t *)&value)[1] = buffer[dataOffset + (count * 2)];
+ ((uint8_t *)&value)[0] = buffer[dataOffset + 1 + (count * 2)];
+ }
if (modbusBridge.type == ModbusBridgeType::mb_int16)
snprintf(svalue, MBR_MAX_VALUE_LENGTH, "%d", value);
else
snprintf(svalue, MBR_MAX_VALUE_LENGTH, "%u", value);
}
- else if (modbusBridge.type == ModbusBridgeType::mb_bit)
+ else if ((modbusBridge.type == ModbusBridgeType::mb_int8) ||
+ (modbusBridge.type == ModbusBridgeType::mb_uint8))
{
- uint8_t value = (uint8_t)(buffer[3 + count]);
- snprintf(svalue, MBR_MAX_VALUE_LENGTH, "%d%d%d%d%d%d%d%d", ((value >> 7) & 1), ((value >> 6) & 1), ((value >> 5) & 1), ((value >> 4) & 1), ((value >> 3) & 1), ((value >> 2) & 1), ((value >> 1) & 1), (value & 1));
+ uint8_t value = buffer[dataOffset + (count * 1)];
+ if (modbusBridge.type == ModbusBridgeType::mb_int8)
+ snprintf(svalue, MBR_MAX_VALUE_LENGTH, "%d", value);
+ else
+ snprintf(svalue, MBR_MAX_VALUE_LENGTH, "%u", value);
}
}
ResponseAppend_P(PSTR("%s"), svalue);
- if (count < modbusBridge.count - 1)
+ if (count < data_count - 1)
ResponseAppend_P(PSTR(","));
}
@@ -339,6 +524,19 @@ void ModbusBridgeHandle(void)
if (errorcode == ModbusBridgeError::noerror)
MqttPublishPrefixTopicRulesProcess_P(RESULT_OR_TELE, PSTR(D_JSON_MODBUS_RECEIVED));
}
+ else if ((buffer[1] == 15) || (buffer[1] == 16)) // Write Multiple Registers
+ {
+ Response_P(PSTR("{\"" D_JSON_MODBUS_RECEIVED "\":{"));
+ ResponseAppend_P(PSTR("\"" D_JSON_MODBUS_DEVICE_ADDRESS "\":%d,"), buffer[0]);
+ ResponseAppend_P(PSTR("\"" D_JSON_MODBUS_FUNCTION_CODE "\":%d,"), buffer[1]);
+ ResponseAppend_P(PSTR("\"" D_JSON_MODBUS_START_ADDRESS "\":%d,"), (buffer[2] << 8) + buffer[3]);
+ ResponseAppend_P(PSTR("\"" D_JSON_MODBUS_LENGTH "\":%d,"), tasmotaModbus->ReceiveCount());
+ ResponseAppend_P(PSTR("\"" D_JSON_MODBUS_COUNT "\":%d"), (buffer[4] << 8) + buffer[5]);
+ ResponseAppend_P(PSTR("}"));
+ ResponseJsonEnd();
+ if (errorcode == ModbusBridgeError::noerror)
+ MqttPublishPrefixTopicRulesProcess_P(RESULT_OR_TELE, PSTR(D_JSON_MODBUS_RECEIVED));
+ }
else
errorcode = ModbusBridgeError::wrongfunctioncode;
}
@@ -445,18 +643,60 @@ void ModbusTCPHandle(void)
busy = true;
}
}
- if (buf_len == 12)
+ if (buf_len >= 12)
{
uint8_t mbdeviceaddress = (uint8_t)modbusBridgeTCP.tcp_buf[6];
uint8_t mbfunctioncode = (uint8_t)modbusBridgeTCP.tcp_buf[7];
uint16_t mbstartaddress = (uint16_t)((((uint16_t)modbusBridgeTCP.tcp_buf[8]) << 8) | ((uint16_t)modbusBridgeTCP.tcp_buf[9]));
- modbusBridge.registerCount = (uint16_t)((((uint16_t)modbusBridgeTCP.tcp_buf[10]) << 8) | ((uint16_t)modbusBridgeTCP.tcp_buf[11]));
+ uint16_t *writeData = NULL;
+ uint16_t count = 0;
+
modbusBridgeTCP.tcp_transaction_id = (uint16_t)((((uint16_t)modbusBridgeTCP.tcp_buf[0]) << 8) | ((uint16_t)modbusBridgeTCP.tcp_buf[1]));
- AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("MBS: MBRTCP to Modbus Transactionid:%d, deviceAddress:%d, functionCode:%d, startAddress:%d, Count:%d"),
- modbusBridgeTCP.tcp_transaction_id, mbdeviceaddress, mbfunctioncode, mbstartaddress, modbusBridge.registerCount);
+ if (mbfunctioncode <= 2)
+ {
+ count = (uint16_t)((((uint16_t)modbusBridgeTCP.tcp_buf[10]) << 8) | ((uint16_t)modbusBridgeTCP.tcp_buf[11]));
+ modbusBridge.byteCount = ((count - 1) >> 3) + 1;
+ modbusBridge.dataCount = ((count - 1) >> 4) + 1;
+ }
+ else if (mbfunctioncode <= 4)
+ {
+ count = (uint16_t)((((uint16_t)modbusBridgeTCP.tcp_buf[10]) << 8) | ((uint16_t)modbusBridgeTCP.tcp_buf[11]));
+ modbusBridge.byteCount = count * 2;
+ modbusBridge.dataCount = count;
+ }
+ else
+ {
+ // For functioncode 15 & 16 ignore bytecount, tasmotaModbus does calculate this
+ uint8_t dataStartByte = mbfunctioncode <= 6 ? 10 : 13;
+ uint16_t byteCount = (buf_len - dataStartByte);
+ modbusBridge.byteCount = 2;
+ modbusBridge.dataCount = 1;
- tasmotaModbus->Send(mbdeviceaddress, mbfunctioncode, mbstartaddress, modbusBridge.registerCount);
+ writeData = (uint16_t *)malloc((byteCount / 2)+1);
+
+ if ((mbfunctioncode == 15) || (mbfunctioncode == 16)) count = (uint16_t)((((uint16_t)modbusBridgeTCP.tcp_buf[10]) << 8) | ((uint16_t)modbusBridgeTCP.tcp_buf[11]));
+ else count = 1;
+
+ for (uint16_t dataPointer = 0; dataPointer < byteCount; dataPointer++)
+ {
+ if (dataPointer % 2 == 0)
+ {
+ writeData[dataPointer / 2] = (uint16_t)(((uint16_t)modbusBridgeTCP.tcp_buf[dataStartByte + dataPointer]) << 8);
+ }
+ else
+ {
+ writeData[dataPointer / 2] |= ((uint16_t)modbusBridgeTCP.tcp_buf[dataStartByte + dataPointer]);
+ }
+ }
+ }
+
+ AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("MBS: MBRTCP to Modbus TransactionId:%d, deviceAddress:%d, functionCode:%d, startAddress:%d, count:%d, recvCount:%d, recvBytes:%d"),
+ modbusBridgeTCP.tcp_transaction_id, mbdeviceaddress, mbfunctioncode, mbstartaddress, count, modbusBridge.dataCount, modbusBridge.byteCount);
+
+ tasmotaModbus->Send(mbdeviceaddress, mbfunctioncode, mbstartaddress, count, writeData);
+
+ free(writeData);
}
}
yield(); // avoid WDT if heavy traffic
@@ -470,6 +710,11 @@ void ModbusTCPHandle(void)
void CmndModbusBridgeSend(void)
{
+ uint16_t *writeData = NULL;
+ uint8_t writeDataSize = 0;
+ bool bitMode = false;
+ ModbusBridgeError errorcode = ModbusBridgeError::noerror;
+
JsonParser parser(XdrvMailbox.data);
JsonParserObject root = parser.getRootObject();
if (!root)
@@ -478,16 +723,18 @@ void CmndModbusBridgeSend(void)
modbusBridge.deviceAddress = root.getUInt(PSTR(D_JSON_MODBUS_DEVICE_ADDRESS), 0);
uint8_t functionCode = root.getUInt(PSTR(D_JSON_MODBUS_FUNCTION_CODE), 0);
modbusBridge.startAddress = root.getULong(PSTR(D_JSON_MODBUS_START_ADDRESS), 0);
+
const char *stype = root.getStr(PSTR(D_JSON_MODBUS_TYPE), "uint8");
- modbusBridge.count = root.getUInt(PSTR(D_JSON_MODBUS_COUNT), 1);
- ModbusBridgeError errorcode = ModbusBridgeError::noerror;
+ modbusBridge.count = root.getUInt(PSTR(D_JSON_MODBUS_COUNT), 1); // Number of bits or bytes to read / write
+
+ if ((functionCode == 1) || (functionCode == 2) || (functionCode == 15)) bitMode = true;
if (modbusBridge.deviceAddress == 0)
errorcode = ModbusBridgeError::wrongdeviceaddress;
- else if (modbusBridge.startAddress == 0)
- ;
- else if (functionCode > 4)
- errorcode = ModbusBridgeError::wrongfunctioncode; // Writing is not supported
+ else if ((functionCode > (uint8_t)ModbusBridgeFunctionCode::mb_writeSingleRegister) &&
+ (functionCode != (uint8_t)ModbusBridgeFunctionCode::mb_writeMultipleCoils) &&
+ (functionCode != (uint8_t)ModbusBridgeFunctionCode::mb_writeMultipleRegisters))
+ errorcode = ModbusBridgeError::wrongfunctioncode; // Invalid function code
else
{
modbusBridge.functionCode = static_cast(functionCode);
@@ -496,54 +743,223 @@ void CmndModbusBridgeSend(void)
}
modbusBridge.type = ModbusBridgeType::mb_undefined;
- if (strcmp(stype, "int16") == 0)
+ if (strcmp(stype, "int8") == 0)
+ {
+ modbusBridge.type = ModbusBridgeType::mb_int8;
+ modbusBridge.dataCount = bitMode ? modbusBridge.count : ((modbusBridge.count - 1) / 2) + 1;
+ if (bitMode) modbusBridge.byteCount = (modbusBridge.count / 8) + 1;
+ }
+ else if (strcmp(stype, "int16") == 0)
{
modbusBridge.type = ModbusBridgeType::mb_int16;
- modbusBridge.registerCount = modbusBridge.count;
+ modbusBridge.dataCount = modbusBridge.count;
}
else if (strcmp(stype, "int32") == 0)
{
modbusBridge.type = ModbusBridgeType::mb_int32;
- modbusBridge.registerCount = 2 * modbusBridge.count;
+ modbusBridge.dataCount = bitMode ? modbusBridge.count : 2 * modbusBridge.count;
}
- else if (strcmp(stype, "uint16") == 0)
+ else if ((strcmp(stype, "uint8") == 0))
+ {
+ modbusBridge.type = ModbusBridgeType::mb_uint8;
+ modbusBridge.dataCount = bitMode ? modbusBridge.count : ((modbusBridge.count - 1) / 2) + 1;
+ }
+ else if ((strcmp(stype, "uint16") == 0) || (strcmp(stype, "") == 0)) // Default is uint16
{
modbusBridge.type = ModbusBridgeType::mb_uint16;
- modbusBridge.registerCount = modbusBridge.count;
+ modbusBridge.dataCount = modbusBridge.count;
}
else if (strcmp(stype, "uint32") == 0)
{
modbusBridge.type = ModbusBridgeType::mb_uint32;
- modbusBridge.registerCount = 2 * modbusBridge.count;
+ modbusBridge.dataCount = bitMode ? modbusBridge.count : 2 * modbusBridge.count;
}
else if (strcmp(stype, "float") == 0)
{
modbusBridge.type = ModbusBridgeType::mb_float;
- modbusBridge.registerCount = 2 * modbusBridge.count;
+ modbusBridge.dataCount = bitMode ? 2 * modbusBridge.count : modbusBridge.count;
}
else if (strcmp(stype, "raw") == 0)
{
modbusBridge.type = ModbusBridgeType::mb_raw;
- modbusBridge.registerCount = modbusBridge.count;
+ modbusBridge.dataCount = bitMode ? modbusBridge.count : ((modbusBridge.count - 1) / 2) + 1;
+ }
+ else if (strcmp(stype, "hex") == 0)
+ {
+ modbusBridge.type = ModbusBridgeType::mb_hex;
+ modbusBridge.dataCount = bitMode ? modbusBridge.count : ((modbusBridge.count - 1) / 2) + 1;
}
else if (strcmp(stype, "bit") == 0)
{
modbusBridge.type = ModbusBridgeType::mb_bit;
- modbusBridge.registerCount = modbusBridge.count;
+ modbusBridge.dataCount = bitMode ? modbusBridge.count : ((modbusBridge.count - 1) / 16) + 1 ;
}
else
errorcode = ModbusBridgeError::wrongtype;
- if (modbusBridge.registerCount > MBR_MAX_REGISTERS)
+ // If functioncode is 15, the count is not the number of registers but the number
+ // of bit to write, so calculate the number data bytes to write.
+ if (modbusBridge.functionCode == ModbusBridgeFunctionCode::mb_writeMultipleCoils)
+ {
+ modbusBridge.dataCount = modbusBridge.count;
+ }
+
+ // Prevent buffer overflow due to usage of to many registers
+ if ((!bitMode) && (modbusBridge.dataCount > MBR_MAX_REGISTERS))
errorcode = ModbusBridgeError::wrongcount;
+ if ((bitMode) && (modbusBridge.dataCount > MBR_MAX_REGISTERS * 8))
+ errorcode = ModbusBridgeError::wrongcount;
+
+ // Get Json data for writing
+ JsonParserArray jsonDataArray = root[PSTR(D_JSON_MODBUS_VALUES)].getArray();
+ writeDataSize = jsonDataArray.size();
+
+ // Check if number of supplied data items is valid
+ switch (modbusBridge.functionCode)
+ {
+ case ModbusBridgeFunctionCode::mb_writeMultipleCoils:
+ // In writeMultipleCoil mode the amount of given data bits is less or equal to the count
+ switch (modbusBridge.type)
+ {
+ case ModbusBridgeType::mb_bit:
+ if (modbusBridge.count > writeDataSize) errorcode = ModbusBridgeError::wrongcount;
+ break;
+ case ModbusBridgeType::mb_uint8:
+ case ModbusBridgeType::mb_int8:
+ case ModbusBridgeType::mb_raw:
+ case ModbusBridgeType::mb_hex:
+ if (modbusBridge.count > writeDataSize * 8) errorcode = ModbusBridgeError::wrongcount;
+ break;
+ case ModbusBridgeType::mb_uint16:
+ case ModbusBridgeType::mb_int16:
+ if (modbusBridge.count > writeDataSize * 16) errorcode = ModbusBridgeError::wrongcount;
+ break;
+ case ModbusBridgeType::mb_uint32:
+ case ModbusBridgeType::mb_int32:
+ if (modbusBridge.count > writeDataSize * 32) errorcode = ModbusBridgeError::wrongcount;
+ break;
+ }
+ break;
+ case ModbusBridgeFunctionCode::mb_writeSingleRegister:
+ case ModbusBridgeFunctionCode::mb_writeSingleCoil:
+ if (modbusBridge.count != 1) errorcode = ModbusBridgeError::wrongcount;
+ break;
+ case ModbusBridgeFunctionCode::mb_writeMultipleRegisters:
+ if (modbusBridge.count != writeDataSize) errorcode = ModbusBridgeError::wrongcount;
+ break;
+ }
+
+ // If write data is specified in JSON copy it into writeData array
+ if ((errorcode == ModbusBridgeError::noerror) && (jsonDataArray.isArray()))
+ {
+ if (modbusBridge.dataCount > 40)
+ {
+ errorcode = ModbusBridgeError::tomanydata;
+ }
+ else
+ {
+ writeData = (uint16_t *)malloc(modbusBridge.dataCount);
+
+ for (uint8_t jsonDataArrayPointer = 0; jsonDataArrayPointer < writeDataSize; jsonDataArrayPointer++)
+ {
+ if (errorcode != ModbusBridgeError::noerror) break;
+ switch (modbusBridge.type)
+ {
+ case ModbusBridgeType::mb_bit:
+ {
+ // Initialize current data/register to 0
+ if (jsonDataArrayPointer % 16 == 0)
+ {
+ writeData[jsonDataArrayPointer / 15] = 0;
+ }
+ // Swap low and high bytes according to modbus specification
+ uint16_t bitValue = (jsonDataArray[jsonDataArrayPointer].getUInt(0) == 1) ? 1 : 0;
+ uint8_t bitPointer = (jsonDataArrayPointer % 16) + 8;
+ if (bitPointer > 15) bitPointer -= 16;
+
+ writeData[jsonDataArrayPointer / 16] += bitValue << bitPointer;
+ }
+ break;
+
+ case ModbusBridgeType::mb_int8:
+ if (jsonDataArrayPointer % 2)
+ writeData[jsonDataArrayPointer / 2] += (int8_t)jsonDataArray[jsonDataArrayPointer].getInt(0);
+ else
+ writeData[jsonDataArrayPointer / 2] = (int8_t)jsonDataArray[jsonDataArrayPointer / 2].getInt(0) << 8;
+ if (modbusBridge.dataCount != writeDataSize / 2) errorcode = ModbusBridgeError::wrongcount;
+ break;
+
+ case ModbusBridgeType::mb_hex:
+ case ModbusBridgeType::mb_raw:
+ case ModbusBridgeType::mb_uint8:
+ if (jsonDataArrayPointer % 2)
+ writeData[jsonDataArrayPointer / 2] += (uint8_t)jsonDataArray[jsonDataArrayPointer].getUInt(0);
+ else
+ writeData[jsonDataArrayPointer / 2] = (uint8_t)jsonDataArray[jsonDataArrayPointer].getUInt(0) << 8;
+ if (modbusBridge.dataCount != writeDataSize / 2) errorcode = ModbusBridgeError::wrongcount;
+ break;
+
+ case ModbusBridgeType::mb_int16:
+ writeData[jsonDataArrayPointer] = bitMode ? swap_endian16(jsonDataArray[jsonDataArrayPointer].getInt(0))
+ : (int16_t)jsonDataArray[jsonDataArrayPointer].getInt(0);
+ break;
+
+ case ModbusBridgeType::mb_uint16:
+ writeData[jsonDataArrayPointer] = bitMode ? swap_endian16(jsonDataArray[jsonDataArrayPointer].getUInt(0))
+ : (int16_t)jsonDataArray[jsonDataArrayPointer].getUInt(0);
+ break;
+
+ case ModbusBridgeType::mb_int32:
+ writeData[(jsonDataArrayPointer * 2)] = bitMode ? swap_endian16(jsonDataArray[jsonDataArrayPointer].getInt(0))
+ : (int16_t)(jsonDataArray[jsonDataArrayPointer].getInt(0) >> 16);
+ writeData[(jsonDataArrayPointer * 2) + 1] = bitMode ? swap_endian16(jsonDataArray[jsonDataArrayPointer].getInt(0) >> 16)
+ : (uint16_t)(jsonDataArray[jsonDataArrayPointer].getInt(0));
+ break;
+
+ case ModbusBridgeType::mb_uint32:
+ writeData[(jsonDataArrayPointer * 2)] = bitMode ? swap_endian16(jsonDataArray[jsonDataArrayPointer].getUInt(0))
+ : (uint16_t)(jsonDataArray[jsonDataArrayPointer].getUInt(0) >> 16);
+ writeData[(jsonDataArrayPointer * 2) + 1] = bitMode ? swap_endian16(jsonDataArray[jsonDataArrayPointer].getUInt(0) >> 16)
+ : (uint16_t)(jsonDataArray[jsonDataArrayPointer].getUInt(0));
+ break;
+
+ case ModbusBridgeType::mb_float:
+ // TODO
+ default:
+ errorcode = ModbusBridgeError::wrongtype;
+ break;
+ }
+ }
+ }
+
+ // Adapt data according to modbus protocol
+ if (modbusBridge.functionCode == ModbusBridgeFunctionCode::mb_writeSingleCoil)
+ {
+ writeData[0] = writeData[0] ? 0xFF00 : 0x0000; // High Byte
+ }
+ }
+
+ // Handle errorcode and exit function when an error has occured
if (errorcode != ModbusBridgeError::noerror)
{
- AddLog(LOG_LEVEL_DEBUG, PSTR("MBS: MBR Send Error %d"), (uint8_t)errorcode);
+ AddLog(LOG_LEVEL_DEBUG, PSTR("MBS: MBR Send Error %u"), (uint8_t)errorcode);
+ free(writeData);
return;
}
- tasmotaModbus->Send(modbusBridge.deviceAddress, (uint8_t)modbusBridge.functionCode, modbusBridge.startAddress, modbusBridge.registerCount);
+ // If writing a single coil or single register, the register count is always 1. We also prevent writing data out of range
+ if ((modbusBridge.functionCode == ModbusBridgeFunctionCode::mb_writeSingleCoil) || (modbusBridge.functionCode == ModbusBridgeFunctionCode::mb_writeSingleRegister))
+ modbusBridge.dataCount = 1;
+
+ uint8_t error = tasmotaModbus->Send(modbusBridge.deviceAddress, (uint8_t)modbusBridge.functionCode, modbusBridge.startAddress, modbusBridge.dataCount, writeData);
+ free(writeData);
+
+ if (error)
+ {
+ AddLog(LOG_LEVEL_DEBUG, PSTR("MBS: MBR Driver send error %u"), error);
+ return;
+ }
ResponseCmndDone();
}
diff --git a/tasmota/tasmota_xdrv_driver/xdrv_82_esp32_ethernet.ino b/tasmota/tasmota_xdrv_driver/xdrv_82_esp32_ethernet.ino
index b7cfada24..525e878cc 100644
--- a/tasmota/tasmota_xdrv_driver/xdrv_82_esp32_ethernet.ino
+++ b/tasmota/tasmota_xdrv_driver/xdrv_82_esp32_ethernet.ino
@@ -154,6 +154,31 @@ void EthernetInit(void) {
int eth_power = Pin(GPIO_ETH_PHY_POWER);
int eth_mdc = Pin(GPIO_ETH_PHY_MDC);
int eth_mdio = Pin(GPIO_ETH_PHY_MDIO);
+#if CONFIG_IDF_TARGET_ESP32
+ // fix an disconnection issue after rebooting Olimex POE - this forces a clean state for all GPIO involved in RMII
+ gpio_reset_pin((gpio_num_t)GPIO_ETH_PHY_POWER);
+ gpio_reset_pin((gpio_num_t)GPIO_ETH_PHY_MDC);
+ gpio_reset_pin((gpio_num_t)GPIO_ETH_PHY_MDIO);
+ gpio_reset_pin(GPIO_NUM_19); // EMAC_TXD0 - hardcoded
+ gpio_reset_pin(GPIO_NUM_21); // EMAC_TX_EN - hardcoded
+ gpio_reset_pin(GPIO_NUM_22); // EMAC_TXD1 - hardcoded
+ gpio_reset_pin(GPIO_NUM_25); // EMAC_RXD0 - hardcoded
+ gpio_reset_pin(GPIO_NUM_26); // EMAC_RXD1 - hardcoded
+ gpio_reset_pin(GPIO_NUM_27); // EMAC_RX_CRS_DV - hardcoded
+ switch (Settings->eth_clk_mode) {
+ case 0: // ETH_CLOCK_GPIO0_IN
+ case 1: // ETH_CLOCK_GPIO0_OUT
+ gpio_reset_pin(GPIO_NUM_0);
+ break;
+ case 2: // ETH_CLOCK_GPIO16_OUT
+ gpio_reset_pin(GPIO_NUM_16);
+ break;
+ case 3: // ETH_CLOCK_GPIO17_OUT
+ gpio_reset_pin(GPIO_NUM_17);
+ break;
+ }
+ delay(1);
+#endif // CONFIG_IDF_TARGET_ESP32
if (!ETH.begin(Settings->eth_address, eth_power, eth_mdc, eth_mdio, (eth_phy_type_t)Settings->eth_type, (eth_clock_mode_t)Settings->eth_clk_mode)) {
AddLog(LOG_LEVEL_DEBUG, PSTR("ETH: Bad PHY type or init error"));
return;
diff --git a/tasmota/tasmota_xdrv_driver/xdrv_87_tm1621_sonoff.ino b/tasmota/tasmota_xdrv_driver/xdrv_87_tm1621_sonoff.ino
index 844033f59..1feafe243 100644
--- a/tasmota/tasmota_xdrv_driver/xdrv_87_tm1621_sonoff.ino
+++ b/tasmota/tasmota_xdrv_driver/xdrv_87_tm1621_sonoff.ino
@@ -46,6 +46,8 @@ struct Tm1621 {
uint8_t pin_wr;
uint8_t state;
uint8_t device;
+ uint8_t temp_sensors;
+ uint8_t temp_sensors_rotate;
bool celsius;
bool fahrenheit;
bool humidity;
@@ -223,6 +225,37 @@ void TM1621Init(void) {
TM1621SendRows();
}
+float TM1621GetTemperatureValues(uint32_t index) {
+ char *start = ResponseData();
+ int data_start = ResponseLength();
+
+ XsnsCall(FUNC_JSON_APPEND);
+ XdrvCall(FUNC_JSON_APPEND);
+
+ if (data_start == ResponseLength()) { return NAN; }
+
+ float value = NAN;
+ uint32_t idx = 0;
+ char *data = start; // Invalid JSON ,"HTU21":{"Temperature":30.7,"Humidity":39.0,"DewPoint":15.2},"BME680":{"Temperature":30.0,"Humidity":50.4,"DewPoint":18.5,"Pressure":1009.6,"Gas":1660.52},"ESP32":{"Temperature":53.3}
+ while (data) {
+ data = strstr_P(data, PSTR(D_JSON_TEMPERATURE));
+ if (data) {
+ idx++;
+ data += 13; // strlen("Temperature") + 2;
+ if (idx == index) {
+ value = CharToFloat(data);
+ if (Tm1621.temp_sensors) {
+ break;
+ }
+ }
+ }
+ }
+ if (0 == Tm1621.temp_sensors) {
+ Tm1621.temp_sensors = idx;
+ }
+ return value;
+}
+
void TM1621Show(void) {
if (TM1621_POWR316D == Tm1621.device) {
static uint32_t display = 0;
@@ -250,8 +283,26 @@ void TM1621Show(void) {
snprintf_P(Tm1621.row[1], sizeof(Tm1621.row[1]), PSTR(" "));
if (!isnan(TasmotaGlobal.temperature_celsius)) {
float temperature = ConvertTempToFahrenheit(TasmotaGlobal.temperature_celsius);
+ if (TasmotaGlobal.humidity == 0.0f) { // No humidity so check for more temperature sensors
+ if (0 == Tm1621.temp_sensors) {
+ TM1621GetTemperatureValues(100); // Find max number of temperature sensors
+ }
+ if (Tm1621.temp_sensors > 1) {
+ if (Tm1621.temp_sensors > 2) {
+ Tm1621.temp_sensors_rotate++;
+ if (Tm1621.temp_sensors_rotate > Tm1621.temp_sensors) {
+ Tm1621.temp_sensors_rotate = 1;
+ }
+ temperature = TM1621GetTemperatureValues(Tm1621.temp_sensors_rotate);
+ ext_snprintf_P(Tm1621.row[1], sizeof(Tm1621.row[1]), PSTR("%d"), Tm1621.temp_sensors_rotate);
+ } else {
+ float temperature2 = TM1621GetTemperatureValues(2);
+ ext_snprintf_P(Tm1621.row[1], sizeof(Tm1621.row[1]), PSTR("%1_f"), &temperature2);
+ }
+ }
+ }
ext_snprintf_P(Tm1621.row[0], sizeof(Tm1621.row[0]), PSTR("%1_f"), &temperature);
- if (Settings->flag.temperature_conversion) { // SetOption8 - Switch between Celsius or Fahrenheit
+ if (Settings->flag.temperature_conversion) { // SetOption8 - Switch between Celsius or Fahrenheit
Tm1621.fahrenheit = true;
} else {
Tm1621.celsius = true;
diff --git a/tasmota/tasmota_xnrg_energy/xnrg_23_ade7880.ino b/tasmota/tasmota_xnrg_energy/xnrg_23_ade7880.ino
index bfb76079d..7583addca 100644
--- a/tasmota/tasmota_xnrg_energy/xnrg_23_ade7880.ino
+++ b/tasmota/tasmota_xnrg_energy/xnrg_23_ade7880.ino
@@ -570,7 +570,10 @@ bool Ade7880SetDefaults(const char* json) {
memcpy(json_buffer, json, len); // Keep original safe
JsonParser parser(json_buffer);
JsonParserObject root = parser.getRootObject();
- if (!root) { return false; }
+ if (!root) {
+ AddLog(LOG_LEVEL_DEBUG, PSTR("A78: Invalid JSON"));
+ return false;
+ }
// All parameters are optional allowing for partial changes
JsonParserToken val = root[PSTR("freq")];
diff --git a/tasmota/tasmota_xsns_sensor/xsns_02_analog.ino b/tasmota/tasmota_xsns_sensor/xsns_02_analog.ino
index 4ba748f86..bc475b88a 100644
--- a/tasmota/tasmota_xsns_sensor/xsns_02_analog.ino
+++ b/tasmota/tasmota_xsns_sensor/xsns_02_analog.ino
@@ -112,7 +112,7 @@
#define ANALOG_PH_DECIMAL_MULTIPLIER 100.0
// MQ-X sensor (MQ-02, MQ-03, MQ-04, MQ-05, MQ-06, MQ-07, MQ-08, MQ-09, MQ-131, MQ-135)
-//
+//
// A0 -------------------
// |
// GND ----------- |
@@ -121,7 +121,7 @@
// | | |
// 3V3 GND ADC <- (A0 for nodemcu, wemos; GPIO34,35,36,39 and other analog IN/OUT pin for esp32)
//means mq type (ex for mq-02 use 2, mq-131 use 131)
-#define ANALOG_MQ_TYPE 2
+#define ANALOG_MQ_TYPE 2
//exponential regression a params
#define ANALOG_MQ_A 574.25
//exponential regression b params
@@ -134,13 +134,13 @@
CO | 521853 | -3.821
Alcohol| 0.3934 | -1.504
Benzene| 4.8387 | -2.68
- Hexane | 7585.3 | -2.849
+ Hexane | 7585.3 | -2.849
NOx | -462.43 | -2.204
CL2 | 47.209 | -1.186
O3 | 23.943 | -1.11
*/
//ratio for alarm, NOT USED yet (RS / R0 = 15 ppm)
-#define ANALOG_MQ_RatioMQCleanAir 15.0
+#define ANALOG_MQ_RatioMQCleanAir 15.0
// Multiplier used to store pH with 2 decimal places in a non decimal datatype
#define ANALOG_MQ_DECIMAL_MULTIPLIER 100.0
// lenght of filter
@@ -387,7 +387,7 @@ void AddSampleMq(uint32_t idx){
if (Adc[idx].indexOfPointer==-1)
{
AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_APPLICATION "Init samples for mq-sensor"));
- for (int i = 0; i < ANALOG_MQ_SAMPLES; i ++)
+ for (int i = 0; i < ANALOG_MQ_SAMPLES; i ++)
Adc[idx].mq_samples[i] = _adc;
}
else
@@ -402,9 +402,9 @@ float AdcGetMq(uint32_t idx) {
float avg = 0.0;
float _RL = 10; //Value in KiloOhms
float _R0 = 10;
- for (int i = 0; i < ANALOG_MQ_SAMPLES; i ++)
+ for (int i = 0; i < ANALOG_MQ_SAMPLES; i ++)
avg += Adc[idx].mq_samples[i];
- float voltage = (avg / ANALOG_MQ_SAMPLES) * ANALOG_V33 / ((FastPrecisePow(2, ANALOG_RESOLUTION)) - 1);
+ float voltage = (avg / ANALOG_MQ_SAMPLES) * ANALOG_V33 / ANALOG_RANGE;
float _RS_Calc = ((ANALOG_V33 * _RL) / voltage) -_RL; //Get value of RS in a gas
if (_RS_Calc < 0) _RS_Calc = 0; //No negative values accepted.
diff --git a/tasmota/tasmota_xsns_sensor/xsns_127_esp32_sensors.ino b/tasmota/tasmota_xsns_sensor/xsns_127_esp32_sensors.ino
index 8ac2ee7f6..6da0baaca 100644
--- a/tasmota/tasmota_xsns_sensor/xsns_127_esp32_sensors.ino
+++ b/tasmota/tasmota_xsns_sensor/xsns_127_esp32_sensors.ino
@@ -19,7 +19,6 @@
#ifdef ESP32
#ifdef USE_ESP32_SENSORS
-#ifndef CONFIG_IDF_TARGET_ESP32S3
/*********************************************************************************************\
* ESP32 CPU Temperature and optional Hall Effect sensor
*
@@ -55,63 +54,71 @@ void Esp32SensorInit(void) {
#endif // CONFIG_IDF_TARGET_ESP32
void Esp32SensorShow(bool json) {
- static bool add_global_temp = false;
+ bool json_end = false;
- if (json) {
- add_global_temp = !ResponseContains_P(PSTR(D_JSON_TEMPERATURE));
+ if (Settings->flag6.use_esp32_temperature) { // SetOption146 - (ESP32) Show ESP32 internal temperature sensor
+ float c = CpuTemperature(); // in Celsius
+ float t = ConvertTempToFahrenheit(c);
+
+ if (json) {
+ if (!ResponseContains_P(PSTR(D_JSON_TEMPERATURE))) {
+ UpdateGlobalTemperature(c);
+ }
+ ResponseAppend_P(PSTR(",\"ESP32\":{\"" D_JSON_TEMPERATURE "\":%*_f"), Settings->flag2.temperature_resolution, &t);
+ json_end = true;
+
+#ifdef USE_DOMOTICZ
+// Instead of below code use a rule like 'on tele-esp32#temperature do dzsend1 9988,%value% endon'
+// where 9988 is the domoticz sensor Idx
+// if (0 == TasmotaGlobal.tele_period) {
+// if (!ResponseContains_P(PSTR(D_JSON_TEMPERATURE))) { // Only send if no other sensor already did
+// DomoticzFloatSensor(DZ_TEMP, t);
+// }
+// }
+#endif // USE_DOMOTICZ
+
+#ifdef USE_WEBSERVER
+ } else {
+ WSContentSend_Temp("ESP32", t);
+#endif // USE_WEBSERVER
+ }
}
- float c = CpuTemperature(); // in Celsius
- if (add_global_temp) {
- UpdateGlobalTemperature(c);
- }
- float t = ConvertTempToFahrenheit(c);
#if CONFIG_IDF_TARGET_ESP32
- int value = 0;
if (HEData.present) {
+ int value = 0;
for (uint32_t i = 0; i < HALLEFFECT_SAMPLE_COUNT; i++) {
value += hallRead();
}
value /= HALLEFFECT_SAMPLE_COUNT;
+
+ if (json) {
+ if (!json_end) {
+ ResponseAppend_P(PSTR(",\"ESP32\":{"));
+ } else {
+ ResponseAppend_P(PSTR(","));
+ }
+ ResponseAppend_P(PSTR("\"" D_JSON_HALLEFFECT "\":%d"), value);
+ json_end = true;
+
+#ifdef USE_DOMOTICZ
+// Instead of below code use a rule like 'on tele-esp32#halleffect do dzsend1 9988,%value% endon'
+// where 9988 is the domoticz sensor Idx
+// if (0 == TasmotaGlobal.tele_period) {
+// DomoticzSensor(DZ_COUNT, value);
+// }
+#endif // USE_DOMOTICZ
+
+#ifdef USE_WEBSERVER
+ } else {
+ WSContentSend_P(HTTP_SNS_HALL_EFFECT, "ESP32", value);
+#endif // USE_WEBSERVER
+ }
}
#endif // CONFIG_IDF_TARGET_ESP32
- if (json) {
- bool temperature_present = (ResponseContains_P(PSTR(D_JSON_TEMPERATURE)));
- ResponseAppend_P(PSTR(",\"ESP32\":{\"" D_JSON_TEMPERATURE "\":%*_f"), Settings->flag2.temperature_resolution, &t);
-
-#if CONFIG_IDF_TARGET_ESP32
- if (HEData.present) {
- ResponseAppend_P(PSTR(",\"" D_JSON_HALLEFFECT "\":%d"), value);
- }
-#endif // CONFIG_IDF_TARGET_ESP32
-
+ if (json_end) {
ResponseJsonEnd();
-#ifdef USE_DOMOTICZ
- if (0 == TasmotaGlobal.tele_period) {
- if (!temperature_present) { // Only send if no other sensor already did
- DomoticzFloatSensor(DZ_TEMP, t);
- }
-
-#if CONFIG_IDF_TARGET_ESP32
- if (HEData.present) {
- DomoticzSensor(DZ_COUNT, value);
- }
-#endif // CONFIG_IDF_TARGET_ESP32
-
- }
-#endif // USE_DOMOTICZ
-#ifdef USE_WEBSERVER
- } else {
- WSContentSend_Temp("ESP32", t);
-
-#if CONFIG_IDF_TARGET_ESP32
- if (HEData.present) {
- WSContentSend_P(HTTP_SNS_HALL_EFFECT, "ESP32", value);
- }
-#endif // CONFIG_IDF_TARGET_ESP32
-
-#endif // USE_WEBSERVER
}
}
@@ -139,7 +146,5 @@ bool Xsns127(uint8_t function) {
}
return result;
}
-
-#endif // Not CONFIG_IDF_TARGET_ESP32S3
#endif // USE_ESP32_SENSORS
#endif // ESP32
diff --git a/tasmota/tasmota_xsns_sensor/xsns_42_scd30.ino b/tasmota/tasmota_xsns_sensor/xsns_42_scd30.ino
index f06fddb2f..dff0e8445 100644
--- a/tasmota/tasmota_xsns_sensor/xsns_42_scd30.ino
+++ b/tasmota/tasmota_xsns_sensor/xsns_42_scd30.ino
@@ -1,5 +1,5 @@
/*
- xsns_42_scd30.ino - SC30 CO2 sensor support for Tasmota
+ xsns_42_scd30.ino - SCD30 CO2 sensor support for Tasmota
Copyright (C) 2021 Frogmore42
@@ -61,6 +61,7 @@ enum SCD30_Commands { // commands useable in console or rules
FrogmoreScd30 scd30;
+bool scd30InitOnce = false;
bool scd30Found = false;
bool scd30IsDataValid = false;
int scd30ErrorState = SCD30_STATE_NO_ERROR;
@@ -71,7 +72,7 @@ int scd30GoodMeas_count = 0;
int scd30Reset_count = 0;
int scd30CrcError_count = 0;
int scd30Co2Zero_count = 0;
-int i2cReset_count = 0;
+int scd30i2cReset_count = 0;
uint16_t scd30_CO2 = 0;
uint16_t scd30_CO2EAvg = 0;
float scd30_Humid = 0.0f;
@@ -121,7 +122,7 @@ void Scd30Update(void)
scd30CrcError_count++;
#ifdef SCD30_DEBUG
AddLog(LOG_LEVEL_ERROR, PSTR("SCD30: CRC error, CRC error: %ld, CO2 zero: %ld, good: %ld, no data: %ld, sc30_reset: %ld, i2c_reset: %ld"),
- scd30CrcError_count, scd30Co2Zero_count, scd30GoodMeas_count, scd30DataNotAvailable_count, scd30Reset_count, i2cReset_count);
+ scd30CrcError_count, scd30Co2Zero_count, scd30GoodMeas_count, scd30DataNotAvailable_count, scd30Reset_count, scd30i2cReset_count);
#endif
break;
@@ -129,7 +130,7 @@ void Scd30Update(void)
scd30Co2Zero_count++;
#ifdef SCD30_DEBUG
AddLog(LOG_LEVEL_ERROR, PSTR("SCD30: CO2 zero, CRC error: %ld, CO2 zero: %ld, good: %ld, no data: %ld, sc30_reset: %ld, i2c_reset: %ld"),
- scd30CrcError_count, scd30Co2Zero_count, scd30GoodMeas_count, scd30DataNotAvailable_count, scd30Reset_count, i2cReset_count);
+ scd30CrcError_count, scd30Co2Zero_count, scd30GoodMeas_count, scd30DataNotAvailable_count, scd30Reset_count, scd30i2cReset_count);
#endif
break;
@@ -149,7 +150,7 @@ void Scd30Update(void)
//scd30IsDataValid = false;
#ifdef SCD30_DEBUG
AddLog(LOG_LEVEL_ERROR, PSTR("SCD30: in error state: %d, good: %ld, no data: %ld, sc30 reset: %ld, i2c reset: %ld"),
- scd30ErrorState, scd30GoodMeas_count, scd30DataNotAvailable_count, scd30Reset_count, i2cReset_count);
+ scd30ErrorState, scd30GoodMeas_count, scd30DataNotAvailable_count, scd30Reset_count, scd30i2cReset_count);
AddLog(LOG_LEVEL_ERROR, PSTR("SCD30: got CRC error, try again, counter: %ld"), scd30Loop_count);
#endif
scd30ErrorState = ERROR_SCD30_NO_ERROR;
@@ -160,7 +161,7 @@ void Scd30Update(void)
//scd30IsDataValid = false;
#ifdef SCD30_DEBUG
AddLog(LOG_LEVEL_ERROR, PSTR("SCD30: in error state: %d, good: %ld, no data: %ld, sc30 reset: %ld, i2c reset: %ld"),
- scd30ErrorState, scd30GoodMeas_count, scd30DataNotAvailable_count, scd30Reset_count, i2cReset_count);
+ scd30ErrorState, scd30GoodMeas_count, scd30DataNotAvailable_count, scd30Reset_count, scd30i2cReset_count);
AddLog(LOG_LEVEL_ERROR, PSTR("SCD30: not answering, sending soft reset, counter: %ld"), scd30Loop_count);
#endif
scd30Reset_count++;
@@ -185,10 +186,10 @@ void Scd30Update(void)
//scd30IsDataValid = false;
#ifdef SCD30_DEBUG
AddLog(LOG_LEVEL_ERROR, PSTR("SCD30: in error state: %d, good: %ld, no data: %ld, sc30 reset: %ld, i2c reset: %ld"),
- scd30ErrorState, scd30GoodMeas_count, scd30DataNotAvailable_count, scd30Reset_count, i2cReset_count);
+ scd30ErrorState, scd30GoodMeas_count, scd30DataNotAvailable_count, scd30Reset_count, scd30i2cReset_count);
AddLog(LOG_LEVEL_ERROR, PSTR("SCD30: clearing i2c bus"));
#endif
- i2cReset_count++;
+ scd30i2cReset_count++;
error = scd30.clearI2CBus();
if (error) {
scd30ErrorState = SCD30_STATE_ERROR_I2C_RESET;
@@ -399,9 +400,16 @@ bool Xsns42(byte function)
bool result = false;
+ // https://github.com/arendst/Tasmota/issues/15438 and datasheet (The boot-up time is < 2 s.)
+/*
if (FUNC_INIT == function) {
Scd30Detect();
}
+*/
+ if (!scd30InitOnce && (FUNC_EVERY_SECOND == function) && (TasmotaGlobal.uptime > 2)) {
+ scd30InitOnce = true;
+ Scd30Detect();
+ }
else if (scd30Found) {
switch (function) {
case FUNC_EVERY_SECOND:
diff --git a/tasmota/tasmota_xsns_sensor/xsns_52_esp32_ibeacon_ble.ino b/tasmota/tasmota_xsns_sensor/xsns_52_esp32_ibeacon_ble.ino
index 84e6e01b1..e68cd9013 100644
--- a/tasmota/tasmota_xsns_sensor/xsns_52_esp32_ibeacon_ble.ino
+++ b/tasmota/tasmota_xsns_sensor/xsns_52_esp32_ibeacon_ble.ino
@@ -20,11 +20,11 @@
////////////////////////////////////////
// Commands:
-// iBeacon 0/1 - Enable
-// iBeaconOnlyAliased 0/1/2 -
-// 0 = all BLE devices,
+// iBeacon 0/1 - Enable
+// iBeaconOnlyAliased 0/1/2 -
+// 0 = all BLE devices,
// 1 = only devices with any BLEAlias
-// 2 = only devices which have BLEAlias starting "iB"
+// 2 = only devices which have BLEAlias starting "iB"
// iBeaconClear - clear list NOW
// iBeaconPeriod (sec) - update period - default 10s
// iBeaconTimeout (sec) - timeout period - default 30s
@@ -52,7 +52,7 @@
// for testing of BLE_ESP32, we remove xsns_52_ibeacon.ino completely, and instead add this modified xsns_52_ibeacon_BLE_ESP32.ino
// in the future this may be more fine-grained, e.g. to allow hm17 for this, and BLE-ESP32 for other
-#if CONFIG_IDF_TARGET_ESP32 || CONFIG_IDF_TARGET_ESP32C3
+#if CONFIG_IDF_TARGET_ESP32 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S3
#ifdef USE_BLE_ESP32
#define XSNS_52 52
@@ -548,7 +548,7 @@ void ibeacon_mqtt(const char *mac,const char *rssi,const char *uid,const char *m
}
} else {
if (name[0]) {
- ResponseTime_P(PSTR(",\"" D_CMND_IBEACON "\":{\"MAC\":\"%s\",\"NAME\":\"%s\",\"MAJOR\":\"%s\",\"MINOR\":\"%s\",\"MAC\":\"%s\",\"RSSI\":%d,\"STATE\":\"%s\",\"PERSEC\":%d}}"),s_uid,s_name,s_major,s_minor,s_mac,n_rssi,s_state,count);
+ ResponseTime_P(PSTR(",\"" D_CMND_IBEACON "\":{\"UID\":\"%s\",\"NAME\":\"%s\",\"MAJOR\":\"%s\",\"MINOR\":\"%s\",\"MAC\":\"%s\",\"RSSI\":%d,\"STATE\":\"%s\",\"PERSEC\":%d}}"),s_uid,s_name,s_major,s_minor,s_mac,n_rssi,s_state,count);
} else {
ResponseTime_P(PSTR(",\"" D_CMND_IBEACON "\":{\"UID\":\"%s\",\"MAJOR\":\"%s\",\"MINOR\":\"%s\",\"MAC\":\"%s\",\"RSSI\":%d,\"STATE\":\"%s\",\"PERSEC\":%d}}"),s_uid,s_major,s_minor,s_mac,n_rssi,s_state,count);
}
diff --git a/tasmota/tasmota_xsns_sensor/xsns_96_flowratemeter.ino b/tasmota/tasmota_xsns_sensor/xsns_96_flowratemeter.ino
index 4e43bd6b8..16a635e9e 100644
--- a/tasmota/tasmota_xsns_sensor/xsns_96_flowratemeter.ino
+++ b/tasmota/tasmota_xsns_sensor/xsns_96_flowratemeter.ino
@@ -25,34 +25,35 @@
#define XSNS_96 96
-
#define FLOWRATEMETER_WEIGHT_AVG_SAMPLE 20 // number of samples for smooth weigted average
#define FLOWRATEMETER_MIN_FREQ 1 // Hz
-#define D_JSON_FLOWRATEMETER_RATE "Rate"
-#define D_JSON_FLOWRATEMETER_VALUE "Value"
-#define D_JSON_FLOWRATEMETER_UNIT "Unit"
-#define D_JSON_FLOWRATEMETER_VALUE_AVG "average"
-#define D_JSON_FLOWRATEMETER_VALUE_RAW "raw"
+#define D_JSON_FLOWRATEMETER_RATE "Rate"
+#define D_JSON_FLOWRATEMETER_VALUE "Source"
+#define D_JSON_FLOWRATEMETER_UNIT "Unit"
+#define D_JSON_FLOWRATEMETER_AMOUNT_TODAY "AmountToday"
+#define D_JSON_FLOWRATEMETER_AMOUNT_UNIT "AmountUnit"
+#define D_JSON_FLOWRATEMETER_DURATION_TODAY "DurationToday"
+#define D_JSON_FLOWRATEMETER_VALUE_AVG "average"
+#define D_JSON_FLOWRATEMETER_VALUE_RAW "raw"
-
-#ifdef USE_WEBSERVER
-const char HTTP_SNS_FLOWRATEMETER[] PROGMEM =
- "{s}" D_FLOWRATEMETER_NAME "-%d{m}%*_f %s{e}"
- ;
-#endif // USE_WEBSERVER
-
+#define FLOWRATEMETER_INVALID (uint32_t)-1
int32_t flowratemeter_period[MAX_FLOWRATEMETER] = {0};
float flowratemeter_period_avg[MAX_FLOWRATEMETER] = {0};
uint32_t flowratemeter_count[MAX_FLOWRATEMETER] = {0};
-volatile uint32_t flowratemeter_last_irq[MAX_FLOWRATEMETER] = {0};
+volatile uint32_t flowratemeter_last_irq[MAX_FLOWRATEMETER] = {FLOWRATEMETER_INVALID};
+
+int32_t flowratemeter_period_sum[MAX_FLOWRATEMETER];
+int32_t flowratemeter_period_sum_dT[MAX_FLOWRATEMETER];
+int32_t flowratemeter_period_duration[MAX_FLOWRATEMETER];
+
-bool flowratemeter_valuesread = false;
bool flowratemeter_raw_value = false;
+#define FlowRateMeterIsValid(time, meter) flowratemeter_last_irq[meter] != FLOWRATEMETER_INVALID && flowratemeter_last_irq[meter] < time
void IRAM_ATTR FlowRateMeterIR(uint16_t irq)
{
@@ -62,12 +63,16 @@ void IRAM_ATTR FlowRateMeterIR(uint16_t irq)
GPIO_REG_WRITE(GPIO_STATUS_W1TC_ADDRESS, gpio_status);
#endif
if (irq < MAX_FLOWRATEMETER) {
- if ((time - flowratemeter_last_irq[irq]) < (1000000 / FLOWRATEMETER_MIN_FREQ)) {
- flowratemeter_period[irq] = time - flowratemeter_last_irq[irq];
- } else {
- flowratemeter_period[irq] = 0;
+ if (FlowRateMeterIsValid(time, irq)) {
+ if ((time - flowratemeter_last_irq[irq]) < (1000000 / FLOWRATEMETER_MIN_FREQ)) {
+ flowratemeter_period_sum_dT[irq] = millis();
+ flowratemeter_period_sum[irq]++;
+ flowratemeter_period[irq] = time - flowratemeter_last_irq[irq];
+ flowratemeter_period_duration[irq] += flowratemeter_period[irq] / 100;
+ } else {
+ flowratemeter_period[irq] = 0;
+ }
}
- flowratemeter_valuesread = true;
flowratemeter_last_irq[irq] = time;
}
}
@@ -81,20 +86,36 @@ void IRAM_ATTR FlowRateMeter2IR(void)
FlowRateMeterIR(1);
}
+void FlowRateMeterMidnightReset(void)
+{
+ uint32_t t = millis();
+ for (uint32_t i = 0; i < MAX_FLOWRATEMETER; i++) {
+ flowratemeter_period_sum[i] = 0;
+ flowratemeter_period_duration[i] = 0;
+ flowratemeter_period_sum_dT[i] = t;
+ }
+}
+
void FlowRateMeterRead(void)
{
for (uint32_t i = 0; i < MAX_FLOWRATEMETER; i++) {
- if ((micros() - flowratemeter_last_irq[i]) >= (1000000 / FLOWRATEMETER_MIN_FREQ)) {
- flowratemeter_period[i] = 0;
- flowratemeter_period_avg[i] = 0;
- }
+ uint32_t time = micros();
+ if (PinUsed(GPIO_FLOWRATEMETER_IN, i) && FlowRateMeterIsValid(time, i)) {
+ if ((time - flowratemeter_last_irq[i]) >= (1000000 / FLOWRATEMETER_MIN_FREQ)) {
+ // invalid in case of pulse outage
+ flowratemeter_period[i] = 0;
+ flowratemeter_period_avg[i] = 0;
+ flowratemeter_count[i] = 0;
+ flowratemeter_last_irq[i] = FLOWRATEMETER_INVALID;
+ }
- // exponentially weighted average
- if (flowratemeter_count[i] <= FLOWRATEMETER_WEIGHT_AVG_SAMPLE) {
- flowratemeter_count[i]++;
+ // exponentially weighted average
+ if (flowratemeter_count[i] <= FLOWRATEMETER_WEIGHT_AVG_SAMPLE) {
+ flowratemeter_count[i]++;
+ }
+ flowratemeter_period_avg[i] -= flowratemeter_period_avg[i] / flowratemeter_count[i];
+ flowratemeter_period_avg[i] += float(flowratemeter_period[i]) / flowratemeter_count[i];
}
- flowratemeter_period_avg[i] -= flowratemeter_period_avg[i] / flowratemeter_count[i];
- flowratemeter_period_avg[i] += float(flowratemeter_period[i]) / flowratemeter_count[i];
}
}
@@ -102,7 +123,7 @@ void FlowRateMeterInit(void)
{
void (* irq_service[MAX_FLOWRATEMETER])(void)= {FlowRateMeter1IR, FlowRateMeter2IR};
- flowratemeter_valuesread = false;
+ FlowRateMeterMidnightReset();
for (uint32_t i = 0; i < MAX_FLOWRATEMETER; i++) {
if (PinUsed(GPIO_FLOWRATEMETER_IN, i)) {
pinMode(Pin(GPIO_FLOWRATEMETER_IN, i), INPUT);
@@ -111,50 +132,139 @@ void FlowRateMeterInit(void)
}
}
-void FlowRateMeterShow(bool json)
+void FlowRateMeterGetValue(uint32_t meter, float *rate_float, float *amount_today)
{
- if (json) {
- ResponseAppend_P(PSTR(",\"" D_FLOWRATEMETER_NAME "\":{\"" D_JSON_FLOWRATEMETER_RATE "\":["));
- }
-
- for (uint32_t i = 0; i < MAX_FLOWRATEMETER; i++) {
- float flowratemeter_rate_avg_float = 0;
-
- if (flowratemeter_period[i]) {
- flowratemeter_rate_avg_float =
- ((Settings->SensorBits1.flowratemeter_unit ? (1000000.0 / 1000.0) : (1000000 / 60.0)) / 2.0)
- / (flowratemeter_raw_value ? flowratemeter_period[i] : flowratemeter_period_avg[i])
- * (Settings->flowratemeter_calibration[i] ? (float)Settings->flowratemeter_calibration[i] : 1000.0);
- }
-
- if (PinUsed(GPIO_FLOWRATEMETER_IN, i)) {
- if (json) {
- ResponseAppend_P(PSTR("%s%*_f"),
- i ? PSTR(",") : PSTR(""),
- Settings->flag2.frequency_resolution, &flowratemeter_rate_avg_float
- );
-
-#ifdef USE_WEBSERVER
- } else {
- WSContentSend_PD(HTTP_SNS_FLOWRATEMETER,
- i+1,
- Settings->flag2.frequency_resolution, &flowratemeter_rate_avg_float,
- Settings->SensorBits1.flowratemeter_unit ? D_UNIT_CUBICMETER_PER_HOUR : D_UNIT_LITER_PER_MINUTE
- );
-#endif // USE_WEBSERVER
- }
+ if (nullptr != rate_float) {
+ *rate_float = 0;
+ if (meter < MAX_FLOWRATEMETER && flowratemeter_period[meter]) {
+ *rate_float =
+ (1000000.0 / 60.0 / 2.0)
+ / (flowratemeter_raw_value ? flowratemeter_period[meter] : flowratemeter_period_avg[meter])
+ * (Settings->flowratemeter_calibration[meter] ? (float)Settings->flowratemeter_calibration[meter] : 1000.0);
}
}
- if (json) {
- ResponseAppend_P(PSTR("],\"" D_JSON_FLOWRATEMETER_VALUE "\":\"%s\""),
- flowratemeter_raw_value ? PSTR(D_JSON_FLOWRATEMETER_VALUE_RAW) : PSTR(D_JSON_FLOWRATEMETER_VALUE_AVG)
- );
- ResponseAppend_P(PSTR(",\"" D_JSON_FLOWRATEMETER_UNIT "\":\"%s\"}"),
- Settings->SensorBits1.flowratemeter_unit ? PSTR(D_UNIT_CUBICMETER_PER_HOUR) : PSTR(D_UNIT_LITER_PER_MINUTE)
- );
+
+ if (nullptr != amount_today) {
+ *amount_today = 0;
+ if (meter < MAX_FLOWRATEMETER && flowratemeter_period_sum[meter]) {
+ uint32_t _flowratemeter_period = (uint32_t)((float)flowratemeter_period_sum_dT[meter] / (float)flowratemeter_period_sum[meter] * 1000.0);
+ float lmin = (((1000000.0 / 60.0) / 2.0) / _flowratemeter_period * (Settings->flowratemeter_calibration[meter] ? (float)Settings->flowratemeter_calibration[meter] : 1000.0));
+ *amount_today = (lmin / 60000) * flowratemeter_period_sum_dT[meter];
+ }
}
}
+void FlowRateMeterShow(bool json)
+{
+ uint16_t flowmeter_count = 0;
+ const char* open_square_bracket;
+ const char* close_square_bracket;
+ float flowratemeter_rate_float[MAX_FLOWRATEMETER];
+ float floatrate_amount_today[MAX_FLOWRATEMETER];
+
+ for (uint32_t i = 0; i < MAX_FLOWRATEMETER; i++) {
+ FlowRateMeterGetValue(i, &flowratemeter_rate_float[i], &floatrate_amount_today[i]);
+ if (PinUsed(GPIO_FLOWRATEMETER_IN, i)) {
+ flowmeter_count++;
+ }
+ }
+ if (flowmeter_count > 1) {
+ open_square_bracket = PSTR("[");
+ close_square_bracket = PSTR("]");
+ } else {
+ open_square_bracket = PSTR("");
+ close_square_bracket = PSTR("");
+ }
+
+ if (json) {
+ ResponseAppend_P(PSTR(",\"" D_FLOWRATEMETER_NAME "\":{\"" D_JSON_FLOWRATEMETER_RATE "\":%s"), open_square_bracket);
+ for (uint32_t i = 0; i < MAX_FLOWRATEMETER; i++) {
+ if (PinUsed(GPIO_FLOWRATEMETER_IN, i)) {
+ float rate = Settings->SensorBits1.flowratemeter_unit ? flowratemeter_rate_float[i] * 60 / 1000 : flowratemeter_rate_float[i];
+ ResponseAppend_P(PSTR("%s%*_f"), i ? PSTR(",") : PSTR(""), Settings->flag2.frequency_resolution, &rate);
+ }
+ }
+ ResponseAppend_P(PSTR("%s,\"" D_JSON_FLOWRATEMETER_AMOUNT_TODAY "\":%s"), close_square_bracket, open_square_bracket);
+ for (uint32_t i = 0; i < MAX_FLOWRATEMETER; i++) {
+ if (PinUsed(GPIO_FLOWRATEMETER_IN, i)) {
+ float amount_today = Settings->SensorBits1.flowratemeter_unit ? floatrate_amount_today[i] / 1000 : floatrate_amount_today[i];
+ ResponseAppend_P(PSTR("%s%*_f"), i ? PSTR(",") : PSTR(""), Settings->flag2.frequency_resolution, &amount_today);
+ }
+ }
+ ResponseAppend_P(PSTR("%s,\"" D_JSON_FLOWRATEMETER_DURATION_TODAY "\":%s"), close_square_bracket, open_square_bracket);
+ for (uint32_t i = 0; i < MAX_FLOWRATEMETER; i++) {
+ if (PinUsed(GPIO_FLOWRATEMETER_IN, i)) {
+ ResponseAppend_P(PSTR("%s%ld"), i ? PSTR(",") : PSTR(""), flowratemeter_period_duration[i] / 10000);
+ }
+ }
+ ResponseAppend_P(PSTR("%s,\"" D_JSON_FLOWRATEMETER_VALUE "\":\"%s\""),
+ close_square_bracket,
+ flowratemeter_raw_value ? PSTR(D_JSON_FLOWRATEMETER_VALUE_RAW) : PSTR(D_JSON_FLOWRATEMETER_VALUE_AVG)
+ );
+ ResponseAppend_P(PSTR(",\"" D_JSON_FLOWRATEMETER_AMOUNT_UNIT "\":\"%s\""),
+ Settings->SensorBits1.flowratemeter_unit ? PSTR(D_UNIT_CUBIC_METER) : PSTR(D_UNIT_LITERS));
+ ResponseAppend_P(PSTR(",\"" D_JSON_FLOWRATEMETER_UNIT "\":\"%s\"}"),
+ Settings->SensorBits1.flowratemeter_unit ? PSTR(D_UNIT_CUBICMETER_PER_HOUR) : PSTR(D_UNIT_LITER_PER_MINUTE));
+#ifdef USE_WEBSERVER
+ } else {
+ // {s} = , {m} = | , {e} = |
+ if (flowmeter_count > 1) {
+ // head
+ WSContentSend_PD(PSTR("{s} | "));
+ for (uint32_t i = 0; i < MAX_FLOWRATEMETER; i++) {
+ if (PinUsed(GPIO_FLOWRATEMETER_IN, i)) {
+ WSContentSend_PD(PSTR("%d | | "),
+ Settings->flag5.gui_table_align ? PSTR("right") : PSTR("center"),
+ i+1
+ );
+ }
+ }
+ WSContentSend_PD(PSTR(" | "));
+ }
+
+ // Flowrate
+ WSContentSend_PD(PSTR("{s}" D_FLOWRATEMETER_NAME "{m} | "));
+ for (uint32_t i = 0; i < MAX_FLOWRATEMETER; i++) {
+ if (PinUsed(GPIO_FLOWRATEMETER_IN, i)) {
+ float rate = Settings->SensorBits1.flowratemeter_unit ? flowratemeter_rate_float[i] * 60 / 1000 : flowratemeter_rate_float[i];
+ WSContentSend_PD(PSTR("%*_f | | "),
+ Settings->flag5.gui_table_align ? PSTR("right") : PSTR("center"),
+ Settings->flag2.frequency_resolution, &rate
+ );
+ }
+ }
+ WSContentSend_PD(PSTR("%s{e}"), Settings->SensorBits1.flowratemeter_unit ? PSTR(D_UNIT_CUBICMETER_PER_HOUR) : PSTR(D_UNIT_LITER_PER_MINUTE));
+
+ // Amount today
+ WSContentSend_PD(PSTR("{s}" D_FLOWRATEMETER_NAME " " D_FLOWRATEMETER_AMOUNT_TODAY "{m} | "));
+ for (uint32_t i = 0; i < MAX_FLOWRATEMETER; i++) {
+ if (PinUsed(GPIO_FLOWRATEMETER_IN, i)) {
+ float amount_today = Settings->SensorBits1.flowratemeter_unit ? floatrate_amount_today[i] / 1000 : floatrate_amount_today[i];
+ WSContentSend_PD(PSTR("%*_f | | "),
+ Settings->flag5.gui_table_align ? PSTR("right") : PSTR("center"),
+ Settings->flag2.frequency_resolution, &amount_today
+ );
+ }
+ }
+ WSContentSend_PD(PSTR("%s{e}"), Settings->SensorBits1.flowratemeter_unit ? PSTR(D_UNIT_CUBIC_METER) : PSTR(D_UNIT_LITERS));
+
+ // Duration today
+ WSContentSend_PD(PSTR("{s}" D_FLOWRATEMETER_NAME " " D_FLOWRATEMETER_DURATION_TODAY "{m} | "));
+ for (uint32_t i = 0; i < MAX_FLOWRATEMETER; i++) {
+ if (PinUsed(GPIO_FLOWRATEMETER_IN, i)) {
+ float amount_today = Settings->SensorBits1.flowratemeter_unit ? floatrate_amount_today[i] / 1000 : floatrate_amount_today[i];
+ WSContentSend_PD(PSTR("%s | | "),
+ (Settings->flag5.gui_table_align)?PSTR("right"):PSTR("center"),
+ GetDuration(flowratemeter_period_duration[i] / 10000).c_str()
+ );
+ }
+ }
+ WSContentSend_PD(PSTR("{e}"));
+#endif // USE_WEBSERVER
+ }
+}
+
+
/*********************************************************************************************\
* Supported commands for Sensor96:
*
@@ -246,6 +356,9 @@ bool Xsns96(uint8_t function)
case FUNC_INIT:
FlowRateMeterInit();
break;
+ case FUNC_SAVE_AT_MIDNIGHT:
+ FlowRateMeterMidnightReset();
+ break;
case FUNC_EVERY_250_MSECOND:
FlowRateMeterRead();
break;
diff --git a/tasmota/tasmota_xsns_sensor/xsns_98_sgp40.ino b/tasmota/tasmota_xsns_sensor/xsns_98_sgp40.ino
new file mode 100644
index 000000000..093136626
--- /dev/null
+++ b/tasmota/tasmota_xsns_sensor/xsns_98_sgp40.ino
@@ -0,0 +1,170 @@
+/*
+ xsns_98_sgp40.ino - SGP40 gas and air quality sensor support for Tasmota
+
+ Copyright (C) 2021 Theo Arends
+
+ 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 .
+*/
+
+#ifdef USE_I2C
+#ifdef USE_SGP40
+/*********************************************************************************************\
+ * SGP40 - Gas (TVOC - Total Volatile Organic Compounds) and Air Quality (CO2)
+ *
+ * Source: Gerhard Mutz and Adafruit Industries
+ * Adaption for TASMOTA: Jean-Pierre Deschamps
+ *
+ * I2C Address: 0x59
+\*********************************************************************************************/
+
+#define XSNS_98 98
+#define XI2C_69 69 // See I2CDEVICES.md
+
+#define SGP40_ADDRESS 0x59
+
+#include "Adafruit_SGP40.h"
+Adafruit_SGP40 sgp40;
+
+bool sgp40_type = false;
+bool sgp40_ready = false;
+float sgp40_abshum;
+uint16_t raw_base;
+int32_t voc_index;
+
+/********************************************************************************************/
+
+void sgp40_Init(void)
+{
+ if (!I2cSetDevice(SGP40_ADDRESS)) { return; }
+
+ if (sgp40.begin()) {
+ sgp40_type = true;
+// AddLog(LOG_LEVEL_DEBUG, PSTR("SGP: Serialnumber 0x%04X-0x%04X-0x%04X"), sgp40.serialnumber[0], sgp40.serialnumber[1], sgp40.serialnumber[2]);
+ I2cSetActiveFound(SGP40_ADDRESS, "SGP40");
+ }
+}
+
+//#define POW_FUNC pow
+#define POW_FUNC FastPrecisePow
+
+float sgp40_AbsoluteHumidity(float temperature, float humidity) {
+ //taken from https://carnotcycle.wordpress.com/2012/08/04/how-to-convert-relative-humidity-to-absolute-humidity/
+ //precision is about 0.1°C in range -30 to 35°C
+ //August-Roche-Magnus 6.1094 exp(17.625 x T)/(T + 243.04)
+ //Buck (1981) 6.1121 exp(17.502 x T)/(T + 240.97)
+ //reference https://www.eas.ualberta.ca/jdwilson/EAS372_13/Vomel_CIRES_satvpformulae.html
+ float temp = NAN;
+ const float mw = 18.01534f; // molar mass of water g/mol
+ const float r = 8.31447215f; // Universal gas constant J/mol/K
+
+ if (isnan(temperature) || isnan(humidity) ) {
+ return NAN;
+ }
+
+ temp = POW_FUNC(2.718281828f, (17.67f * temperature) / (temperature + 243.5f));
+
+ //return (6.112 * temp * humidity * 2.1674) / (273.15 + temperature); //simplified version
+ return (6.112f * temp * humidity * mw) / ((273.15f + temperature) * r); //long version
+}
+
+#define SAVE_PERIOD 30
+
+void Sgp40Update(void) // Perform every second to ensure proper operation of the baseline compensation algorithm
+{
+ sgp40_ready = false;
+ if (TasmotaGlobal.global_update && (TasmotaGlobal.humidity > 0) && !isnan(TasmotaGlobal.temperature_celsius)) {
+ // abs hum in mg/m3
+ sgp40_abshum = sgp40_AbsoluteHumidity(TasmotaGlobal.temperature_celsius, TasmotaGlobal.humidity);
+ }
+ sgp40_ready = true;
+
+ // these should normally be stored permanently and used for fast restart
+ if (!(TasmotaGlobal.uptime%SAVE_PERIOD)) {
+ // store settings every N seconds
+ raw_base = sgp40.measureRaw(TasmotaGlobal.temperature_celsius, TasmotaGlobal.humidity);
+ voc_index = sgp40.measureVocIndex(TasmotaGlobal.temperature_celsius, TasmotaGlobal.humidity);
+ }
+}
+
+#ifdef USE_WEBSERVER
+const char HTTP_SNS_SGP40[] PROGMEM =
+ "{s}SGP40 " D_JSON_RAW "{m}%d " "{e}" // {s} = | , {m} = | , {e} = |
+ "{s}SGP40 " D_AIR_QUALITY "{m}%d " "{e}";
+const char HTTP_SNS_AHUM40[] PROGMEM = "{s}SGP40 Abs Humidity{m}%s g/m3{e}";
+#endif
+
+#define D_JSON_AHUM "aHumidity"
+
+void Sgp40Show(bool json)
+{
+ if (sgp40_ready) {
+ char abs_hum[33];
+ bool ahum_available = TasmotaGlobal.global_update && (TasmotaGlobal.humidity > 0) && !isnan(TasmotaGlobal.temperature_celsius);
+ if (ahum_available) {
+ // has humidity + temperature
+ dtostrfd(sgp40_abshum,4,abs_hum);
+ }
+ if (json) {
+ ResponseAppend_P(PSTR(",\"SGP40\":{\"" D_JSON_RAW "\":%d,\"" D_JSON_AIRQUALITY "\":%d"), raw_base, voc_index);
+ if (ahum_available) {
+ ResponseAppend_P(PSTR(",\"" D_JSON_AHUM "\":%s"),abs_hum);
+ }
+ ResponseJsonEnd();
+#ifdef USE_DOMOTICZ
+ if (0 == TasmotaGlobal.tele_period) DomoticzSensor(DZ_AIRQUALITY, raw_base);
+#endif // USE_DOMOTICZ
+#ifdef USE_WEBSERVER
+ } else {
+ WSContentSend_PD(HTTP_SNS_SGP40, raw_base, voc_index);
+ if (ahum_available) {
+ WSContentSend_PD(HTTP_SNS_AHUM40, abs_hum);
+ }
+#endif
+ }
+ }
+}
+
+/*********************************************************************************************\
+ * Interface
+\*********************************************************************************************/
+
+bool Xsns98(uint8_t function)
+{
+ if (!I2cEnabled(XI2C_69)) { return false; }
+
+ bool result = false;
+
+ if (FUNC_INIT == function) {
+ sgp40_Init();
+ }
+ else if (sgp40_type) {
+ switch (function) {
+ case FUNC_EVERY_SECOND:
+ Sgp40Update();
+ break;
+ case FUNC_JSON_APPEND:
+ Sgp40Show(1);
+ break;
+ #ifdef USE_WEBSERVER
+ case FUNC_WEB_SENSOR:
+ Sgp40Show(0);
+ break;
+ #endif // USE_WEBSERVER
+ }
+ }
+ return result;
+}
+
+#endif // USE_SGP40
+#endif // USE_I2C
diff --git a/tasmota/tasmota_xsns_sensor/xsns_99_luxv30b.ino b/tasmota/tasmota_xsns_sensor/xsns_99_luxv30b.ino
new file mode 100644
index 000000000..b833a5c07
--- /dev/null
+++ b/tasmota/tasmota_xsns_sensor/xsns_99_luxv30b.ino
@@ -0,0 +1,212 @@
+/*
+ xsns_99_luxv30b.ino - Driver for DFRobot V30B lux sensor
+
+ Copyright (C) 2022 Marius Bezuidenhout
+
+ 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 .
+
+*/
+
+#ifdef USE_I2C
+#ifdef USE_LUXV30B
+/**************************************************************************************************
+ * DFRobot SEN0390 V30B ambient light sensor
+ *
+ * https://wiki.dfrobot.com/Ambient_Light_Sensor_0_200klx_SKU_SEN0390
+ * https://github.com/DFRobot/DFRobot_B_LUX_V30B/
+ *
+ * I2C Address: 0x4A
+ * ================================================================================================
+ * This driver use the I2C mode. Uses address 0x94 and 0x95. Its address cannot be changed.
+ *
+ * Supply Voltage: 2.7-6V
+ * Operating Current: 0.7mA
+ * Detection Range: 0-200klx
+ * Accuracy: 0.054lx
+ * Operating Temperature Range: -40°C~+85°C
+ * I2C Address: 0x4A
+ *
+ * Pin assignments:
+ *
+ * ------------------------------------------
+ * | Num | Label | Description |
+ * 1 VCC 2.7-6V +
+ * 2 SCL I2C Serial Clock Line
+ * 3 SDA I2C Serial Data
+ * 4 GND Ground
+ * 5 EN Sensor Chip-select Enable/Disable port, High to enable, Low to disable sensor
+ *
+ *
+ * You can write the desired configuration to the configuration register(address:0x04), setting
+ * different acquisition accuracy.
+ * You can read the light intensity data from the data register(address:0x00~0x03).
+ * ------------------------------------------------------------------------------------------
+ * | b7 | b6 | b5 | b4 | b3 | b2 | b1 | b0 |
+ * ------------------------------------------------------------------------------------------
+ * | 0 | MANUAL | 0 | 0 | CDR | TIM |
+ * ------------------------------------------------------------------------------------------
+ * MANUAL :Manual configuration register.
+ * 0 represents the default automatic mode.In this mode ,CDR and TIM are automatically assigned.
+ * 1 represents the configuration of manual mode.In this mode,CDR and TIM can be set by the user.
+ * CDR :Shunt ratio register.
+ * 0 represents the default of not dividing,all the current of the photodiode into the ADC
+ * 1 represents the division of 8,as long as 1/8 of the current of the photodiode changes to ADC.
+ * This mode is used in high brightness situations.
+ * TIM[2:0]:Acquisition time.
+ * ------------------------------------------------------------------------------------------------
+ * TIM[2:0] | TIME(ms) | Introduction |
+ * ------------------------------------------------------------------------------------------------
+ * 000 | 800 | Preferred mode in low light environment |
+ * ------------------------------------------------------------------------------------------------
+ * 001 | 400 | --- |
+ * ------------------------------------------------------------------------------------------------
+ * 010 | 200 | --- |
+ * ------------------------------------------------------------------------------------------------
+ * 011 | 100 | In the strong light environment, select the mode preferentially |
+ * ------------------------------------------------------------------------------------------------
+ * 100 | 50 | Manual mode only |
+ * ------------------------------------------------------------------------------------------------
+ * 101 | 250 | Manual mode only |
+ * ------------------------------------------------------------------------------------------------
+ * 110 | 12.5 | Manual mode only |
+ * ------------------------------------------------------------------------------------------------
+ * 111 | 6.25 | Manual mode only |
+ * ------------------------------------------------------------------------------------------------
+ * Accuracy that can be set in manual mode:
+ * -------------------------------------------------------------------------------------------------------------
+ * | Light conditions | | TIM & CDR |
+ * -------------------------------------------------------------------------------------------------------------
+ * | Minimum accuracy | Maximum accuracy | Maximum | Acquisition time(ms) | TIM | CDR |
+ * —------------------------------------------------------------------------------------------------------------
+ * 0.054 11.52 2938 800 000 0
+ * 0.09 23.04 5875 400 001 0
+ * 0.18 46.08 11750 200 010 0
+ * 0.36 92.16 23501 100 011 0
+ * 0.36 92.16 23501 800 000 1
+ * 0.72 184.32 47002 50 100 0
+ * 0.72 184.32 47002 400 001 1
+ * 1.44 368.64 94003 25 101 0
+ * 1.44 368.64 94003 200 010 1
+ * 2.88 737.28 200000 12.5 110 0
+ * 2.88 737.28 200000 100 011 1
+ * 5.76 737.28 200000 6.25 111 0
+ * 5.76 737.28 200000 50 100 1
+ * 11.52 737.28 200000 25 101 1
+ * 23.04 737.28 200000 12.5 110 1
+ * 46.08 737.28 200000 6.25 111 1
+ * —------------------------------------------------------------------------------------------------------------
+ * *************************************************************************************************/
+
+#define XSNS_99 99
+#define XI2C_70 70 // See I2CDEVICES.md
+
+#define LUXV30B_ADDR 0x4A // Two wire library uses 7-bit addresses throughout
+#define LUXV30B_DATAREG 0x00 // Address of the data register
+#define LUXV30B_CONFREG 0x04 // Address of the configuration register
+
+class LuxV30b {
+ public:
+ LuxV30b();
+ void Detect();
+ bool Found();
+ void Read();
+ void Show(uint8_t function);
+ private:
+ float Lux();
+ bool _found;
+ uint32_t _lux;
+};
+
+LuxV30b::LuxV30b() {
+ _found = false;
+ _lux = 0;
+}
+
+void LuxV30b::Detect() {
+ if (!I2cSetDevice(LUXV30B_ADDR)) { return; }
+
+ _found = true;
+ I2cSetActiveFound(LUXV30B_ADDR, "LUXV30B");
+}
+
+bool LuxV30b::Found() {
+ return _found;
+}
+
+void LuxV30b::Read() {
+ _lux = I2cRead8(LUXV30B_ADDR, 0);
+ delay(8);
+ _lux |= I2cRead8(LUXV30B_ADDR, 1) << 8;
+ delay(8);
+ _lux |= I2cRead8(LUXV30B_ADDR, 2) << 16;
+ delay(8);
+ _lux |= I2cRead8(LUXV30B_ADDR, 3) << 24;
+}
+
+float LuxV30b::Lux() {
+ return ((float)_lux * 1.4) / 1000;
+}
+
+void LuxV30b::Show(uint8_t function) {
+// if (0 < Lux()) {
+ if (_lux) {
+ char lux[FLOATSZ];
+ dtostrfd(Lux(), 2, lux);
+ if (FUNC_JSON_APPEND == function) {
+ ResponseAppend_P(PSTR(",\"LUXV30B\":{\"" D_JSON_ILLUMINANCE "\":%s}"), lux);
+#ifdef USE_DOMOTICZ
+// Instead of below code use a rule like 'on tele-luxv30b#illuminance do dzsend1 9988,%value% endon'
+// where 9988 is the domoticz sensor Idx
+// if (0 == TasmotaGlobal.tele_period) { DomoticzSensor(DZ_ILLUMINANCE, lux); }
+#endif // USE_DOMOTICZ
+#ifdef USE_WEBSERVER
+ } else {
+ WSContentSend_PD(PSTR("{s}LUXV30B " D_ILLUMINANCE "{m}%s " D_UNIT_LUX "{e}"), lux);
+#endif // USE_WEBSERVER
+ }
+ }
+}
+
+LuxV30b Luxv30b;
+
+/*********************************************************************************************\
+ * Interface
+\*********************************************************************************************/
+
+bool Xsns99(uint8_t function) {
+ if (!I2cEnabled(XI2C_70)) { return false; }
+
+ bool result = false;
+
+ if (FUNC_INIT == function) {
+ Luxv30b.Detect();
+ }
+ else if (Luxv30b.Found()) {
+ switch (function) {
+ case FUNC_EVERY_SECOND:
+ Luxv30b.Read();
+ break;
+ case FUNC_JSON_APPEND:
+#ifdef USE_WEBSERVER
+ case FUNC_WEB_SENSOR:
+#endif // USE_WEBSERVER
+ Luxv30b.Show(function);
+ break;
+ }
+ }
+ return result;
+}
+
+#endif // USE_LUXV30B
+#endif // USE_I2C
\ No newline at end of file
diff --git a/tasmota/zigbee/giex_water.zb b/tasmota/zigbee/giex_water.zb
new file mode 100644
index 000000000..4f58ff46b
--- /dev/null
+++ b/tasmota/zigbee/giex_water.zb
@@ -0,0 +1,14 @@
+#Z2Tv1
+# GiEX garden watering https://www.aliexpress.com/item/1005004222098040.html
+:TS0601,_TZE200_sh1btabb
+EF00/0101,WaterMode # duration=0 / capacity=1
+EF00/0102,WaterState # off=0 / on=1
+EF00/0365,IrrigationStartTime # (string) ex: "08:12:26"
+EF00/0366,IrrigationStopTime # (string) ex: "08:13:36"
+EF00/0267,CycleIrrigationNumTimes # number of cycle irrigation times, set to 0 for single cycle
+EF00/0268,IrrigationTarget # duration in minutes or capacity in Liters (depending on mode)
+EF00/0269,CycleIrrigationInterval # cycle irrigation interval (minutes, max 1440)
+EF00/026A,CurrentTemperature # (value ignored because isn't a valid tempurature reading. Misdocumented and usage unclear)
+EF00/026C=0001/0021,mul:2 # match to BatteryPercentage
+EF00/026F,WaterConsumed # water consumed (Litres)
+EF00/0372,LastIrrigationDuration # (string) Ex: "00:01:10,0"
diff --git a/tasmota/zigbee/legacy_tuya.zb b/tasmota/zigbee/legacy_tuya.zb
new file mode 100644
index 000000000..06490e363
--- /dev/null
+++ b/tasmota/zigbee/legacy_tuya.zb
@@ -0,0 +1,31 @@
+#Z2Tv1
+# DEPRECATED
+# Legacy Tuya attributes from before Zigbee Device plugins
+# Use only if you need to legacy behavior, prefer device specific
+: # apply to all devices
+EF00/0070,TuyaScheduleWorkdays
+EF00/0071,TuyaScheduleHolidays
+EF00/0101,Power
+EF00/0102,Power2
+EF00/0103,Power3
+EF00/0104,Power4
+EF00/0107,TuyaChildLock
+EF00/0112,TuyaWindowDetection
+EF00/0114,TuyaValveDetection
+EF00/0174,TuyaAutoLock
+EF00/0202,TuyaTempTarget
+EF00/0202=0201/FFF1,mul:10 # TempTarget
+EF00/0203=0402/0000,mul:10 # Temperature
+EF00/0215,TuyaBattery
+EF00/0266,TuyaMinTemp
+EF00/0267,TuyaMaxTemp
+EF00/0269,TuyaBoostTime
+EF00/026B,TuyaComfortTemp
+EF00/026C,TuyaEcoTemp
+EF00/026D=0201/FFF0
+EF00/0272,TuyaAwayTemp
+EF00/0275,TuyaAwayDays
+EF00/0404,TuyaPreset
+EF00/0405,TuyaFanMode
+EF00/046A,TuyaForceMode
+EF00/046F,TuyaWeekSelect
diff --git a/tools/decode-status.py b/tools/decode-status.py
index b0e162f6e..465bd4712 100755
--- a/tools/decode-status.py
+++ b/tools/decode-status.py
@@ -199,9 +199,10 @@ a_setoption = [[
"(Wifi) Wait 1 second for wifi connection solving some FRITZ!Box modem issues (1)",
"(Zigbee) Disable Battery auto-probe and using auto-binding",
"(Zigbee) Include time in `ZbReceived` messages like other sensors",
- ""
+ "(MQTT) Retain on Status"
],[
- "","","","",
+ "(ESP32) Show ESP32 internal temperature sensor",
+ "","","",
"","","","",
"","","","",
"","","","",
@@ -284,7 +285,7 @@ a_features = [[
"USE_PCF85363","USE_DS3502","USE_IMPROV","USE_FLOWRATEMETER",
"USE_BP5758D","USE_HYT","USE_SM2335","USE_DISPLAY_TM1621_SONOFF"
],[
- "","","","",
+ "USE_SGP40","USE_LUXV30B","","",
"","","","",
"","","","",
"","","","",
@@ -319,7 +320,7 @@ else:
obj = json.load(fp)
def StartDecode():
- print ("\n*** decode-status.py v12.0.2.4 by Theo Arends and Jacek Ziolkowski ***")
+ print ("\n*** decode-status.py v12.1.1.1 by Theo Arends and Jacek Ziolkowski ***")
# print("Decoding\n{}".format(obj))