/* xsns_90-hrg15.ino - Hydreon RG-15 support for Tasmota Copyright (c) 2021 Wouter Breukink 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_HRG15 /*********************************************************************************************\ * Hydreon RG-15 * See https://rainsensors.com/products/rg-15/ * https://rainsensors.com/rg-9-15-protocol/ * * Rule for Domoticz Rain sensor index 418: * on tele-rg-15#flowrate do var1 %value% endon * on tele-rg-15#event do backlog var2 %value%; mult1 100; event sendrain endon * on event#sendrain do dzsend1 418,%var1%;%var2% endon \*********************************************************************************************/ #define XSNS_90 90 #define RG15_NAME "RG15" #define RG15_BAUDRATE 9600 #define RG15_READ_TIMEOUT 500 #define RG15_EVENT_TIMEOUT 60 #define RG15_BUFFER_SIZE 150 #include #ifdef USE_WEBSERVER const char HTTP_RG15[] PROGMEM = // {s} = , {m} = , {e} = "{s}" RG15_NAME " " D_JSON_ACTIVE "{m}%2_f " D_UNIT_MILLIMETER "{e}" "{s}" RG15_NAME " " D_JSON_EVENT "{m}%2_f " D_UNIT_MILLIMETER "{e}" "{s}" RG15_NAME " " D_JSON_TOTAL "{m}%2_f " D_UNIT_MILLIMETER "{e}" "{s}" RG15_NAME " " D_JSON_FLOWRATE "{m}%2_f " D_UNIT_MILLIMETER "/" D_UNIT_HOUR "{e}"; #endif // USE_WEBSERVER TasmotaSerial *HydreonSerial = nullptr; struct RG15 { float acc = NAN; float event = NAN; float total = NAN; float rate = NAN; uint16_t time = RG15_EVENT_TIMEOUT; uint8_t init_step; } Rg15; /*********************************************************************************************/ bool Rg15ReadLine(char* buffer) { // All lines are terminated with a carriage return (\r or 13) followed by a new line (\n or 10) uint32_t i = 0; uint32_t cmillis = millis(); while (HydreonSerial->available() ) { char c = HydreonSerial->read(); if (c == 10) { break; } // New line ends the message if ((c >= 32) && (c < 127)) { // Accept only valid characters buffer[i++] = c; if (i == RG15_BUFFER_SIZE -1) { break; } // Overflow } if ((millis() - cmillis) > RG15_READ_TIMEOUT) { AddLog(LOG_LEVEL_DEBUG, PSTR("HRG: Timeout")); return false; } } buffer[i] = '\0'; AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("HRG: Read '%s'"), buffer); return true; } bool Rg15Parse(char* buffer, const char* item, float* result) { char* start = strstr(buffer, item); if (start != nullptr) { char* end = strstr(start, " mm"); // Metric (mm or mmph) if (end == nullptr) { end = strstr(start, " i"); // Imperial (in or iph) } if (end != nullptr) { char tmp = end[0]; end[0] = '\0'; *result = CharToFloat(start + strlen(item)); end[0] = tmp; return true; } } return false; } bool Rg15Process(char* buffer) { // Process payloads like: // Acc 0.01 mm, EventAcc 2.07 mm, TotalAcc 54.85 mm, RInt 2.89 mmph // Acc 0.001 in, EventAcc 0.002 in, TotalAcc 0.003 in, RInt 0.004 iph // Acc 0.001 mm, EventAcc 0.002 mm, TotalAcc 0.003 mm, RInt 0.004 mmph, XTBTips 0, XTBAcc 0.01 mm, XTBEventAcc 0.02 mm, XTBTotalAcc 0.03 mm // check for the expected data elements // Acc should be a position 0 but note if missing we don't want to mistake it for EventAcc if (strstr(buffer, "Acc")==buffer && strstr(buffer, "EventAcc") != nullptr && strstr(buffer, "TotalAcc") != nullptr && strstr(buffer, "RInt") != nullptr) { Rg15.acc = NAN; Rg15.event = NAN; Rg15.total = NAN; Rg15.rate = NAN; float tmp; if (Rg15Parse(buffer, "Acc", &tmp)) { Rg15.acc=tmp; } else { AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("HRG: Unable to parse 'Acc' from accumulation data")); } if (Rg15Parse(buffer, "EventAcc", &tmp)) { Rg15.event=tmp; } else { AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("HRG: Unable to parse 'EventAcc' from accumulation data")); } if (Rg15Parse(buffer, "TotalAcc", &tmp)) { Rg15.total=tmp; } else { AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("HRG: Unable to parse 'TotalAcc' from accumulation data")); } if (Rg15Parse(buffer, "RInt", &tmp)) { Rg15.rate=tmp; } else { AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("HRG: Unable to parse 'RInt' from accumulation data")); } if (Rg15.acc > 0.0f) { Rg15.time = RG15_EVENT_TIMEOUT; // We have some data, so the rain event is on-going } return true; } return false; } /*********************************************************************************************/ void Rg15Init(void) { if (PinUsed(GPIO_HRG15_RX) && PinUsed(GPIO_HRG15_TX)) { // Max size message: // Acc 0.001 mm, EventAcc 0.002 mm, TotalAcc 0.003 mm, RInt 0.004 mmph, XTBTips 0, XTBAcc 0.01 mm, XTBEventAcc 0.02 mm, XTBTotalAcc 0.03 mm HydreonSerial = new TasmotaSerial(Pin(GPIO_HRG15_RX), Pin(GPIO_HRG15_TX), 2, 0, RG15_BUFFER_SIZE); if (HydreonSerial) { if (HydreonSerial->begin(RG15_BAUDRATE)) { if (HydreonSerial->hardwareSerial()) { ClaimSerial(); } #ifdef ESP32 AddLog(LOG_LEVEL_DEBUG, PSTR("HRG: Serial UART%d"), HydreonSerial->getUart()); #endif Rg15.init_step = 5; // Perform RG-15 init } } } } void Rg15Poll(void) { bool publish = false; if (!HydreonSerial->available()) { if (Rg15.time) { // Check if the rain event has timed out, reset rate to 0 Rg15.time--; if (!Rg15.time) { AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("HRG: Rg15Poll - rain event has timed out, reset acc & rate to 0 ")); Rg15.acc = 0; Rg15.rate = 0; publish = true; } } } else { char rg15_buffer[RG15_BUFFER_SIZE]; // Read what's available while (HydreonSerial->available()) { Rg15ReadLine(rg15_buffer); if (Rg15Process(rg15_buffer)) { // Do NOT use "publish = Rg15Process(rg15_buffer)" publish = true; } } } if (publish && !TasmotaGlobal.global_state.mqtt_down) { MqttPublishSensor(); } // Units: I = Imperial (in) or M = Metric (mm) // Resolution: H = High (0.001) or L = Low (0.01) // Mode: P = Request mode (Polling) or C = Continuous mode - report any change // Request: R = Read available data once char init_commands[] = "R CLM "; // Indexed by Rg15.init_step if (Rg15.init_step) { Rg15.init_step--; char cmnd = init_commands[Rg15.init_step]; if (cmnd != ' ') { HydreonSerial->println(cmnd); } } } void Rg15Show(bool json) { if (json) { // if the parsing wasn't completely successful then skip the update if( !isnan(Rg15.acc) && !isnan(Rg15.event) && !isnan(Rg15.total) && !isnan(Rg15.rate) ) { ResponseAppend_P(PSTR(",\"" RG15_NAME "\":{")); ResponseAppend_P(PSTR("\"%s\":%2_f,"), D_JSON_ACTIVE, &Rg15.acc); ResponseAppend_P(PSTR("\"%s\":%2_f,"), D_JSON_EVENT, &Rg15.event); ResponseAppend_P(PSTR("\"%s\":%2_f,"), D_JSON_TOTAL, &Rg15.total); ResponseAppend_P(PSTR("\"%s\":%2_f"), D_JSON_FLOWRATE, &Rg15.rate); ResponseAppend_P(PSTR("}")); } #ifdef USE_WEBSERVER } else { WSContentSend_PD(HTTP_RG15, &Rg15.acc, &Rg15.event, &Rg15.total, &Rg15.rate); #endif // USE_WEBSERVER } } /*********************************************************************************************\ * Commands \*********************************************************************************************/ bool Rg15Command(void) { bool serviced = true; if (XdrvMailbox.data_len == 1) { char send = XdrvMailbox.data[0] & 0xDF; // Make uppercase HydreonSerial->flush(); // Flush receive buffer HydreonSerial->println(send); if ('K' == send) { // Restart RG-15 reading DIP switches Rg15.init_step = 5; // Perform RG-15 init } ResponseCmndIdxChar(XdrvMailbox.data); } return serviced; } /*********************************************************************************************\ * Interface \*********************************************************************************************/ bool Xsns90(uint32_t function) { bool result = false; if (FUNC_INIT == function) { Rg15Init(); } else if (HydreonSerial) { switch (function) { case FUNC_EVERY_SECOND: if((TasmotaGlobal.uptime % 60) == 0) { // every minute ExecuteCommand("Sensor90 R", SRC_SENSOR); } Rg15Poll(); break; case FUNC_COMMAND_SENSOR: if (XSNS_90 == XdrvMailbox.index) { result = Rg15Command(); } break; case FUNC_JSON_APPEND: Rg15Show(1); break; #ifdef USE_WEBSERVER case FUNC_WEB_SENSOR: Rg15Show(0); break; #endif // USE_WEBSERVER } } return result; } #endif // USE_HRG15