mirror of https://github.com/arendst/Tasmota.git
290 lines
9.3 KiB
C++
290 lines
9.3 KiB
C++
/*
|
|
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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#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 "RG-15"
|
|
#define RG15_BAUDRATE 9600
|
|
#define RG15_READ_TIMEOUT 500
|
|
#define RG15_EVENT_TIMEOUT 60
|
|
#define RG15_BUFFER_SIZE 150
|
|
|
|
#include <TasmotaSerial.h>
|
|
|
|
#ifdef USE_WEBSERVER
|
|
const char HTTP_RG15[] PROGMEM =
|
|
// {s} = <tr><th>, {m} = </th><td>, {e} = </td></tr>
|
|
"{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
|