From 843dd17aecf37d417f302daf45512ba6aa1e9e38 Mon Sep 17 00:00:00 2001 From: Norbert Richter Date: Thu, 4 Jul 2024 09:28:01 +0200 Subject: [PATCH] NeoPool add data validation and statistics (#21721) Co-authored-by: Theo Arends <11044339+arendst@users.noreply.github.com> --- CHANGELOG.md | 1 + .../tasmota_xsns_sensor/xsns_83_neopool.ino | 191 +++++++++++++++++- 2 files changed, 186 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f61cd41e5..e6ea4bdf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ All notable changes to this project will be documented in this file. - Berry `FUNC_ANY_KEY` event calling `any_key()` (#21708) - Berry `FUNC_BUTTON_MULTI_PRESSED` event and make `FUNC_BUTTON_PRESSED` called only on state changes and once per second (#21711) - Support for Sonoff POWCT Ring (#21131) +- NeoPool add data validation and communication statistics (default enabled for ESP32 only) - `FUNC_BUTTON_PRESSED` now contains `press_counter` encoded in `XdrvMailbox.command_code` ### Breaking Changed diff --git a/tasmota/tasmota_xsns_sensor/xsns_83_neopool.ino b/tasmota/tasmota_xsns_sensor/xsns_83_neopool.ino index c13bc82d7..75dd67e26 100644 --- a/tasmota/tasmota_xsns_sensor/xsns_83_neopool.ino +++ b/tasmota/tasmota_xsns_sensor/xsns_83_neopool.ino @@ -91,6 +91,16 @@ #define NEOPOOL_LIGHT_PRG_DELAY_MAX 100 // next light prg delay max (in ms) #endif +#ifdef ESP32 // Defaults for ESP32 only + #ifndef NEOPOOL_RANGE_CHECKS + #define NEOPOOL_RANGE_CHECKS // Compile with value range checks + #endif +#endif +#ifdef NEOPOOL_RANGE_CHECKS + #ifndef NEOPOOL_CONNSTAT + #define NEOPOOL_CONNSTAT // Compile with connection statistics + #endif +#endif /*********************************************************************************************\ * Sugar Valley Modbus Register (addresses marked with * are queried with each polling cycle) @@ -707,7 +717,36 @@ enum NeoPoolModbusCode { NEOPOOL_MODBUS_ERROR_DEADLOCK }; +#ifdef NEOPOOL_RANGE_CHECKS +#define NEOPOOL_UNDEF_UINT16 0xFFFF + typedef struct { + uint16_t addr; // Modbus register addr + uint16_t min; // min valid value (or UNDEFined) + uint16_t max; // max valid value (or UNDEFined) + uint16_t prev; // previous read value + } TNeoPoolRangeCheck; + TNeoPoolRangeCheck NeoPoolRangeCheck[] = { + {MBF_ION_CURRENT, 0, 100, NEOPOOL_UNDEF_UINT16}, // Ionization level measured + {MBF_HIDRO_CURRENT, 0, NEOPOOL_UNDEF_UINT16, NEOPOOL_UNDEF_UINT16}, // Hydrolysis intensity level + {MBF_MEASURE_PH, 0, 1400, NEOPOOL_UNDEF_UINT16}, // pH level measured + {MBF_MEASURE_RX, 0, 1000, NEOPOOL_UNDEF_UINT16}, // Redox level measured + {MBF_MEASURE_CL, 0, 1000, NEOPOOL_UNDEF_UINT16}, // Chlorine level measured + {MBF_MEASURE_CONDUCTIVITY, 0, 100, NEOPOOL_UNDEF_UINT16}, // Conductivity level measured + {MBF_MEASURE_TEMPERATURE, 0, 6500, NEOPOOL_UNDEF_UINT16} // Temperature sensor measured + }; +#endif +#ifdef NEOPOOL_CONNSTAT + #define NEOPOOL_TASMOTAMODBUS_ERROR_NUM_MAX 15 // 0-14 - see TasmotaModbus.h class TasmotaModbus highest error # + // counting modbus and data error + struct { + uint32_t time; // time where counting started + uint32_t mb_requests; // request count + // result count: + uint32_t mb_results[NEOPOOL_TASMOTAMODBUS_ERROR_NUM_MAX + 1]; + uint32_t value_out_of_range; // value out of range count + } NeoPoolStats; +#endif // NPResult possible values enum NeoPoolResult { @@ -796,6 +835,48 @@ struct { #define D_NEOPOOL_JSON_BIT "Bit" #define D_NEOPOOL_JSON_NODE_ID "NodeID" +#ifdef NEOPOOL_CONNSTAT +#define D_NEOPOOL_JSON_CONNSTAT "Connection" +#define D_NEOPOOL_JSON_CONNSTAT_MB_REQUESTS "MBRequests" +#define D_NEOPOOL_JSON_CONNSTAT_MB_RESULTS_0 "MBNoError" +#define D_NEOPOOL_JSON_CONNSTAT_MB_RESULTS_1 "MBIllegalFunc" +#define D_NEOPOOL_JSON_CONNSTAT_MB_RESULTS_2 "MBIllegalDataAddr" +#define D_NEOPOOL_JSON_CONNSTAT_MB_RESULTS_3 "MBIllegalDataValue" +#define D_NEOPOOL_JSON_CONNSTAT_MB_RESULTS_4 "MBSlaveError" +#define D_NEOPOOL_JSON_CONNSTAT_MB_RESULTS_5 "MBAck" +#define D_NEOPOOL_JSON_CONNSTAT_MB_RESULTS_6 "MBSlaveBusy" +#define D_NEOPOOL_JSON_CONNSTAT_MB_RESULTS_7 "MBNotEnoughData" +#define D_NEOPOOL_JSON_CONNSTAT_MB_RESULTS_8 "MBMemParityErr" +#define D_NEOPOOL_JSON_CONNSTAT_MB_RESULTS_9 "MBCRCErr" +#define D_NEOPOOL_JSON_CONNSTAT_MB_RESULTS_10 "MBGWPath" +#define D_NEOPOOL_JSON_CONNSTAT_MB_RESULTS_11 "MBGWTarget" +#define D_NEOPOOL_JSON_CONNSTAT_MB_RESULTS_12 "MBRegErr" +#define D_NEOPOOL_JSON_CONNSTAT_MB_RESULTS_13 "MBRegData" +#define D_NEOPOOL_JSON_CONNSTAT_MB_RESULTS_14 "MBTooManyReg" +#define D_NEOPOOL_JSON_CONNSTAT_MB_RESULTS_15 "MBUnknownErr" +#define D_NEOPOOL_JSON_CONNSTAT_MB_RESULTS "MBNoResponse" +#define D_NEOPOOL_JSON_CONNSTAT_DATA_OOR "DataOutOfRange" + +const char kNeoPoolMBResults[] PROGMEM = + D_NEOPOOL_JSON_CONNSTAT_MB_RESULTS_0 "|" + D_NEOPOOL_JSON_CONNSTAT_MB_RESULTS_1 "|" + D_NEOPOOL_JSON_CONNSTAT_MB_RESULTS_2 "|" + D_NEOPOOL_JSON_CONNSTAT_MB_RESULTS_3 "|" + D_NEOPOOL_JSON_CONNSTAT_MB_RESULTS_4 "|" + D_NEOPOOL_JSON_CONNSTAT_MB_RESULTS_5 "|" + D_NEOPOOL_JSON_CONNSTAT_MB_RESULTS_6 "|" + D_NEOPOOL_JSON_CONNSTAT_MB_RESULTS_7 "|" + D_NEOPOOL_JSON_CONNSTAT_MB_RESULTS_8 "|" + D_NEOPOOL_JSON_CONNSTAT_MB_RESULTS_9 "|" + D_NEOPOOL_JSON_CONNSTAT_MB_RESULTS_10 "|" + D_NEOPOOL_JSON_CONNSTAT_MB_RESULTS_11 "|" + D_NEOPOOL_JSON_CONNSTAT_MB_RESULTS_12 "|" + D_NEOPOOL_JSON_CONNSTAT_MB_RESULTS_13 "|" + D_NEOPOOL_JSON_CONNSTAT_MB_RESULTS_14 "|" + D_NEOPOOL_JSON_CONNSTAT_MB_RESULTS_15 + ; +#endif + const char kNeoPoolMachineNames[] PROGMEM = D_NEOPOOL_MACH_NONE "|" D_NEOPOOL_MACH_HIDROLIFE "|" @@ -1212,7 +1293,9 @@ void NeoPoolPoll(void) // Poll modbus register if (nullptr != buffer) { uint8_t error = NeoPoolModbus->ReceiveBuffer(buffer, NeoPoolReg[neopool_read_state].cnt); // cnt x 16bit register - +#ifdef NEOPOOL_CONNSTAT + NeoPoolModbusErrorCount(error); +#endif if (0 == error) { neopool_failed_count = 0; neopool_error = false; @@ -1243,6 +1326,9 @@ void NeoPoolPoll(void) // Poll modbus register AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("NEO: modbus send(%d, %d, 0x%04X, %d)"), NEOPOOL_MODBUS_ADDRESS, NEOPOOL_READ_REGISTER, NeoPoolReg[neopool_read_state].addr, NeoPoolReg[neopool_read_state].cnt); #endif // DEBUG_TASMOTA_SENSOR NeoPoolModbus->Send(NEOPOOL_MODBUS_ADDRESS, NEOPOOL_READ_REGISTER, NeoPoolReg[neopool_read_state].addr, NeoPoolReg[neopool_read_state].cnt); +#ifdef NEOPOOL_CONNSTAT + NeoPoolStats.mb_requests++; +#endif } else { if (1 == neopool_send_retry) { neopool_failed_count++; @@ -1296,6 +1382,9 @@ bool NeoPoolInitData(void) neopool_error = true; neopool_power_module_version = 0; +#ifdef NEOPOOL_CONNSTAT + memset(&NeoPoolStats, 0, sizeof(NeoPoolStats)); +#endif memset(neopool_power_module_nodeid, 0, sizeof(neopool_power_module_nodeid)); for (uint32_t i = 0; i < nitems(NeoPoolReg); i++) { @@ -1362,6 +1451,19 @@ void NeoPool250msSetStatus(bool status) } } +#ifdef NEOPOOL_CONNSTAT +void NeoPoolModbusErrorCount(uint8_t error) +{ + if (NeoPoolStats.time < 86400L) { + NeoPoolStats.time = Rtc.local_time; + } + if (error < nitems(NeoPoolStats.mb_results) - 1) { + NeoPoolStats.mb_results[error]++; + } else { + NeoPoolStats.mb_results[nitems(NeoPoolStats.mb_results) - 1]++; + } +} +#endif uint8_t NeoPoolReadRegisterData(uint16_t addr, uint16_t *data, uint16_t cnt) { @@ -1373,12 +1475,18 @@ uint8_t NeoPoolReadRegisterData(uint16_t addr, uint16_t *data, uint16_t cnt) *data = 0; NeoPoolModbus->Send(NEOPOOL_MODBUS_ADDRESS, NEOPOOL_READ_REGISTER, addr, cnt); +#ifdef NEOPOOL_CONNSTAT + NeoPoolStats.mb_requests++; +#endif timeoutMS = millis() + cnt * NEOPOOL_READ_TIMEOUT; // Max delay before we timeout while (!(data_ready = NeoPoolModbus->ReceiveReady()) && millis() < timeoutMS) { delay(1); } if (data_ready) { uint8_t *buffer = (uint8_t*)malloc(5+cnt*2); if (buffer != nullptr) { uint8_t error = NeoPoolModbus->ReceiveBuffer(buffer, cnt); +#ifdef NEOPOOL_CONNSTAT + NeoPoolModbusErrorCount(error); +#endif if (error) { #ifdef DEBUG_TASMOTA_SENSOR AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("NEO: addr 0x%04X read data error %d"), addr, error); @@ -1455,6 +1563,9 @@ uint8_t NeoPoolWriteRegisterData(uint16_t addr, uint16_t *data, uint16_t cnt) NeoPoolModbus->flush(); NeoPoolModbus->write(frame, numbytes+2); +#ifdef NEOPOOL_CONNSTAT + NeoPoolStats.mb_requests++; +#endif timeoutMS = millis() + 1 * NEOPOOL_READ_TIMEOUT; // Max delay before we timeout while (!(data_ready = NeoPoolModbus->ReceiveReady()) && millis() < timeoutMS) { delay(1); } @@ -1462,6 +1573,9 @@ uint8_t NeoPoolWriteRegisterData(uint16_t addr, uint16_t *data, uint16_t cnt) if (data_ready) { uint8_t buffer[9]; uint8_t error = NeoPoolModbus->ReceiveBuffer(buffer, 1); +#ifdef NEOPOOL_CONNSTAT + NeoPoolModbusErrorCount(error); +#endif if (0 != error && 9 != error) { // ReceiveBuffer can't handle 0x10 code result #ifdef DEBUG_TASMOTA_SENSOR AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("NEO: addr 0x%04X write data response error %d"), addr, error); @@ -1539,7 +1653,7 @@ uint8_t NeoPoolWriteRegisterWord(uint16_t addr, uint16_t data) uint16_t NeoPoolGetCacheData(uint16_t addr, int32_t timeout) { - uint16_t data; + uint16_t data = 0; bool datavalid = false; uint16_t i; @@ -1599,7 +1713,55 @@ uint16_t NeoPoolGetCacheData(uint16_t addr, int32_t timeout) uint16_t NeoPoolGetData(uint16_t addr) { - return NeoPoolGetCacheData(addr, -1); + uint16_t data = NeoPoolGetCacheData(addr, -1); + +#ifdef NEOPOOL_RANGE_CHECKS + for (uint16_t i = 0; i < nitems(NeoPoolRangeCheck); i++) { + if (MBF_HIDRO_CURRENT == NeoPoolRangeCheck[i].addr && NEOPOOL_UNDEF_UINT16 == NeoPoolRangeCheck[i].max) { + // get hydrolsysis max value + uint16_t max = NeoPoolGetCacheData(MBF_PAR_HIDRO_NOM, -1); + if (0 != max) { + NeoPoolRangeCheck[i].max = max; +#ifdef DEBUG_TASMOTA_SENSOR + AddLog(LOG_LEVEL_DEBUG, PSTR("NEO: ConnStat - use hydrolysis max = %d"), NeoPoolRangeCheck[i].max); +#endif + } + } + if (NeoPoolRangeCheck[i].addr == addr) { + uint16_t prev_data = data; + // check out of range + if (data < NeoPoolRangeCheck[i].min || data > NeoPoolRangeCheck[i].max) { +#ifdef NEOPOOL_CONNSTAT + NeoPoolStats.value_out_of_range++; +#endif + // use previous value if defined + if (NEOPOOL_UNDEF_UINT16 != NeoPoolRangeCheck[i].prev) { + data = NeoPoolRangeCheck[i].prev; + } else { + // limit to min/max as long as no valid previous value is present + if (data < NeoPoolRangeCheck[i].min) { + data = NeoPoolRangeCheck[i].min; + } else { + data = NeoPoolRangeCheck[i].max; + } + } +#ifdef DEBUG_TASMOTA_SENSOR + AddLog(LOG_LEVEL_DEBUG, PSTR("NEO: ConnStat - Addr 0x%04X data out of range [%d-%d]: received %d, corrected using %d"), + NeoPoolRangeCheck[i].addr, + NeoPoolRangeCheck[i].min, + NeoPoolRangeCheck[i].max, + prev_data, + data); +#endif + } + else { + // remeber origin value + NeoPoolRangeCheck[i].prev = data; + } + } + } +#endif // NEOPOOL_RANGE_CHECKS + return data; } @@ -1986,8 +2148,25 @@ void NeoPoolShow(bool json) if (0 != NeoPoolGetData(MBF_PAR_FILTVALVE_GPIO)) { ResponseAppend_P(PSTR(",\"" D_NEOPOOL_JSON_RELAY_FILTVALVE " \":%d"), (NeoPoolGetData(MBF_RELAY_STATE) >> NeoPoolGetData(MBF_PAR_FILTVALVE_GPIO)) & 1); } + ResponseJsonEnd(); - ResponseJsonEndEnd(); +#ifdef NEOPOOL_CONNSTAT + ResponseAppend_P(PSTR(",\"" D_NEOPOOL_JSON_CONNSTAT "\":{")); + ResponseAppend_P(PSTR( "\"" D_JSON_TIME "\":\"%s\""), GetDT(NeoPoolStats.time).c_str()); + ResponseAppend_P(PSTR(",\"" D_NEOPOOL_JSON_CONNSTAT_MB_REQUESTS "\":%d"), NeoPoolStats.mb_requests); + uint32_t mb_sum = 0; + for(uint16_t i = 0; i < nitems(NeoPoolStats.mb_results); i++) { + char mbresult[32]; + GetTextIndexed(mbresult, sizeof(mbresult), i, kNeoPoolMBResults); + ResponseAppend_P(PSTR(",\"%s\":%d"), mbresult,NeoPoolStats.mb_results[i]); + mb_sum += NeoPoolStats.mb_results[i]; + } + ResponseAppend_P(PSTR(",\"" D_NEOPOOL_JSON_CONNSTAT_MB_RESULTS "\":%d"), NeoPoolStats.mb_requests - mb_sum); + ResponseAppend_P(PSTR(",\"" D_NEOPOOL_JSON_CONNSTAT_DATA_OOR "\":%d"), NeoPoolStats.value_out_of_range); + ResponseJsonEnd(); +#endif + + ResponseJsonEnd(); #ifdef USE_WEBSERVER } else { @@ -2623,7 +2802,7 @@ void CmndNeopoolLight(void) if (POWER_TOGGLE == timer_val[XdrvMailbox.payload]) { XdrvMailbox.payload = ((data >>= (neopool_light_relay - 1)) & 1) ? POWER_OFF : POWER_ON; } - NeoPoolWriteRegisterWord(MBF_PAR_TIMER_BLOCK_LIGHT_INT + MBV_TIMER_OFFMB_TIMER_ENABLE, timer_val[XdrvMailbox.payload]); + NeoPoolWriteRegisterWord((uint16_t)MBF_PAR_TIMER_BLOCK_LIGHT_INT + (uint16_t)MBV_TIMER_OFFMB_TIMER_ENABLE, timer_val[XdrvMailbox.payload]); NeoPoolWriteRegisterWord(MBF_EXEC, 1); // data >>= (neopool_light_relay - 1); ResponseCmndStateText(XdrvMailbox.payload); @@ -2693,7 +2872,7 @@ void CmndNeopoolLightPrgEnd(void) // exit manual ctrl NeoPoolWriteRegisterWord(MBF_SET_MANUAL_CTRL, 0); // switch light on to finish prg sequence - NeoPoolWriteRegisterWord(MBF_PAR_TIMER_BLOCK_LIGHT_INT + MBV_TIMER_OFFMB_TIMER_ENABLE, MBV_PAR_CTIMER_ALWAYS_ON); + NeoPoolWriteRegisterWord((uint16_t)MBF_PAR_TIMER_BLOCK_LIGHT_INT + (uint16_t)MBV_TIMER_OFFMB_TIMER_ENABLE, MBV_PAR_CTIMER_ALWAYS_ON); NeoPoolWriteRegisterWord(MBF_EXEC, 1); }