diff --git a/sonoff/i18n.h b/sonoff/i18n.h index 60b5880e0..766a2cd7e 100644 --- a/sonoff/i18n.h +++ b/sonoff/i18n.h @@ -69,6 +69,7 @@ #define D_JSON_FLASHCHIPID "FlashChipId" #define D_JSON_FLASHMODE "FlashMode" #define D_JSON_FLASHSIZE "FlashSize" +#define D_JSON_FLOWRATE "FlowRate" #define D_JSON_FREEMEMORY "Free" #define D_JSON_FREQUENCY "Frequency" #define D_JSON_FROM "from" @@ -143,6 +144,7 @@ #define D_JSON_TIME "Time" #define D_JSON_TODAY "Today" #define D_JSON_TOTAL "Total" +#define D_JSON_TOTAL_USAGE "TotalUsage" #define D_JSON_TOTAL_REACTIVE "TotalReactivePower" #define D_JSON_TOTAL_START_TIME "TotalStartTime" #define D_JSON_TVOC "TVOC" @@ -515,6 +517,8 @@ const char S_JSON_DRIVER_INDEX_SVALUE[] PROGMEM = "{\"" D_CMND_DRIVE const char JSON_SNS_TEMP[] PROGMEM = ",\"%s\":{\"" D_JSON_TEMPERATURE "\":%s}"; const char JSON_SNS_TEMPHUM[] PROGMEM = ",\"%s\":{\"" D_JSON_TEMPERATURE "\":%s,\"" D_JSON_HUMIDITY "\":%s}"; +const char JSON_SNS_GNGPM[] PROGMEM = "%s,\"%s\":{\"" D_JSON_TOTAL_USAGE "\":%s,\"" D_JSON_FLOWRATE "\":%s}"; + const char S_LOG_I2C_FOUND_AT[] PROGMEM = D_LOG_I2C "%s " D_FOUND_AT " 0x%x"; const char S_LOG_HTTP[] PROGMEM = D_LOG_HTTP; @@ -569,6 +573,8 @@ const char HTTP_SNS_ANALOG[] PROGMEM = "{s}%s " D_ANALOG_INPUT "%d{m}%d{e}"; const char HTTP_SNS_ILLUMINANCE[] PROGMEM = "{s}%s " D_ILLUMINANCE "{m}%d " D_UNIT_LUX "{e}"; // {s} = , {m} = , {e} = const char HTTP_SNS_CO2[] PROGMEM = "{s}%s " D_CO2 "{m}%d " D_UNIT_PARTS_PER_MILLION "{e}"; // {s} = , {m} = , {e} = const char HTTP_SNS_CO2EAVG[] PROGMEM = "{s}%s " D_ECO2 "{m}%d " D_UNIT_PARTS_PER_MILLION "{e}"; // {s} = , {m} = , {e} = +const char HTTP_SNS_GALLONS[] PROGMEM = "{s}%s " D_TOTAL_USAGE "{m}%s " D_UNIT_GALLONS " {e}"; // {s} = , {m} = , {e} = +const char HTTP_SNS_GPM[] PROGMEM = "{s}%s " D_FLOW_RATE "{m}%s " D_UNIT_GALLONS_PER_MIN" {e}"; // {s} = , {m} = , {e} = const char S_MAIN_MENU[] PROGMEM = D_MAIN_MENU; const char S_CONFIGURATION[] PROGMEM = D_CONFIGURATION; diff --git a/sonoff/language/en-GB.h b/sonoff/language/en-GB.h index d5e792462..e8ba8f3fb 100644 --- a/sonoff/language/en-GB.h +++ b/sonoff/language/en-GB.h @@ -93,6 +93,7 @@ #define D_FALLBACK_TOPIC "Fallback Topic" #define D_FALSE "False" #define D_FILE "File" +#define D_FLOW_RATE "Flow rate" #define D_FREE_MEMORY "Free Memory" #define D_FREQUENCY "Frequency" #define D_GAS "Gas" @@ -156,6 +157,7 @@ #define D_TO "to" #define D_TOGGLE "Toggle" #define D_TOPIC "Topic" +#define D_TOTAL_USAGE "Total Usage" #define D_TRANSMIT "Transmit" #define D_TRUE "True" #define D_TVOC "TVOC" @@ -576,12 +578,15 @@ #define D_SENSOR_TXD "Serial Tx" #define D_SENSOR_RXD "Serial Rx" #define D_SENSOR_ROTARY "Rotary" // Suffix "1A" - +#define D_SENSOR_HRE_CLOCK "HRE Clock" +#define D_SENSOR_HRE_DATA "HRE Data" // Units #define D_UNIT_AMPERE "A" #define D_UNIT_CENTIMETER "cm" #define D_UNIT_HERTZ "Hz" #define D_UNIT_HOUR "Hr" +#define D_UNIT_GALLONS "gal" +#define D_UNIT_GALLONS_PER_MIN "g/m" #define D_UNIT_INCREMENTS "inc" #define D_UNIT_KILOGRAM "kg" #define D_UNIT_KILOMETER_PER_HOUR "km/h" // or "km/h" diff --git a/sonoff/sonoff_template.h b/sonoff/sonoff_template.h index cf5a1af78..b50c2184b 100644 --- a/sonoff/sonoff_template.h +++ b/sonoff/sonoff_template.h @@ -178,6 +178,8 @@ enum UserSelectablePins { GPIO_ROT1B, // Rotary switch1 B Pin GPIO_ROT2A, // Rotary switch2 A Pin GPIO_ROT2B, // Rotary switch2 B Pin + GPIO_HRE_CLOCK, // Clock/Power line for HR-E Water Meter + GPIO_HRE_DATA, // Data line for HR-E Water Meter GPIO_SENSOR_END }; // Programmer selectable GPIO functionality @@ -241,6 +243,7 @@ const char kSensorNames[] PROGMEM = D_SENSOR_CSE7766_TX "|" D_SENSOR_CSE7766_RX "|" D_SENSOR_ARIRFRCV "|" D_SENSOR_TXD "|" D_SENSOR_RXD "|" D_SENSOR_ROTARY "1a|" D_SENSOR_ROTARY "1b|" D_SENSOR_ROTARY "2a|" D_SENSOR_ROTARY "2b|" + D_SENSOR_HRE_CLOCK "|" D_SENSOR_HRE_DATA "|" ; /********************************************************************************************/ @@ -584,7 +587,11 @@ const uint8_t kGpioNiceList[] PROGMEM = { GPIO_ROT1B, // Rotary switch1 B Pin GPIO_ROT2A, // Rotary switch2 A Pin GPIO_ROT2B, // Rotary switch2 B Pin - GPIO_ARIRFRCV // AliLux RF Receive input + GPIO_ARIRFRCV, // AliLux RF Receive input +#ifdef USE_HRE + GPIO_HRE_CLOCK, + GPIO_HRE_DATA +#endif }; const uint8_t kModuleNiceList[MAXMODULE] PROGMEM = { diff --git a/sonoff/xsns_91_hre.ino b/sonoff/xsns_91_hre.ino new file mode 100644 index 000000000..ac15a937a --- /dev/null +++ b/sonoff/xsns_91_hre.ino @@ -0,0 +1,299 @@ +/* + xsns_07_sht1x.ino - SHT1x temperature and sensor support for Sonoff-Tasmota + + Copyright (C) 2019 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 . +*/ + +/*********************************************************************************************\ + * HR-E LCD Water meter register interface + * + * https://www.badgermeter.com/business-lines/utility/high-resolution-lcd-encoders-hr-e-lcd/ + * Source: Jon Little, https://github.com/burundiocibu/particle/blob/master/water_meter/src/HRE_Reader.cpp + * + * This code marches the bits out the data line as ASCII characters with the form + * KG44?Q45484=0444444V;RB000000022;IB018435683 + * where the RB...; is the miligalons used + * + * Note that this sensor takes a _long_ time to read. 62 bits * 4 ms/bit for the + * sync sequence plus 46 bytes * 40 ms/byte = 2088 ms minimum. If we aren't alligned + * to the sync sequence, it could be almost twice that. + * To keep from bogging the kernel down, we read 8 bits at a time on the 50 ms callback. + * It will take seconds to discover if the device is there. + * + * In lieu of an actual schematic to describe the electrical interface, here is a description: + * + * hre_clock_pin: drives the power/clock for the water meter through a 1k resister to + * the base of a pnp transistor + * hre_data_pin: is the data and has a 1 k pulldown + * + * The pnp transitor has the collector connected to the power/clock and is pulled up + * to +5 via a 1 k resistor. + * The emitter is connected to ground + * +\*********************************************************************************************/ + +#ifdef USE_HRE + +#define XSNS_91 91 + +enum hre_states { + hre_idle, // Initial state, + hre_sync, // Start search for sync sequence + hre_syncing, // Searching for sync sequence + hre_read, // Start reading data block + hre_reading, // Reading data + hre_sleep, // Start sleeping + hre_sleeping // pausing before reading again +}; + +hre_states hre_state = hre_idle; + +float hre_usage = 0; // total water usage, in gal +float hre_rate = 0; // flow rate, in gal/min +uint32_t hre_usage_time = 0; // uptime associated with hre_usage and hre_rate + +int hre_read_errors = 0; // total number of read errors since boot +bool hre_good = false; + + +// The settling times here were determined using a single unit hooked to a scope +int hreReadBit() +{ + digitalWrite(pin[GPIO_HRE_CLOCK], HIGH); + delay(1); + int bit = digitalRead(pin[GPIO_HRE_DATA]); + digitalWrite(pin[GPIO_HRE_CLOCK], LOW); + delay(1); + return bit; +} + +// With the times in the HreReadBit routine, a characer will take +// 20 ms plus io time. +char hreReadChar(int &parity_errors) +{ + // start bit + hreReadBit(); + + unsigned ch=0; + int sum=0; + for (int i=0; i<7; i++) + { + int b = hreReadBit(); + ch |= b << i; + sum += b; + } + + // parity + if ( (sum & 0x1) != hreReadBit()) + parity_errors++; + + // stop bit + hreReadBit(); + + return ch; +} + +void hreInit(void) +{ + hre_read_errors = 0; + hre_good = false; + + pinMode(pin[GPIO_HRE_CLOCK], OUTPUT); + pinMode(pin[GPIO_HRE_DATA], INPUT); + + // Note that the level shifter inverts this line and we want to leave it + // high when not being read. + digitalWrite(pin[GPIO_HRE_CLOCK], LOW); + + hre_state = hre_sync; +} + + +void hreEvery50ms(void) +{ + static int sync_counter = 0; // Number of sync bit reads + static int sync_run = 0; // Number of consecutive '1's read + + static uint32_t curr_start = 0; // uptime when entered hre_reading for current read + static int read_counter = 0; // number of bytes in the current read + static int parity_errors = 0; // Number of parity errors in current read + static char buff[46]; // 8 char and a term + static char aux[46]; // 8 char and a term + + static char ch; + static size_t i; + + switch (hre_state) + { + case hre_sync: + if (uptime < 15) + break; + sync_run = 0; + sync_counter = 0; + hre_state = hre_syncing; + snprintf_P(log_data, sizeof(log_data), PSTR("HRE: state:syncing")); + AddLog(LOG_LEVEL_DEBUG); + break; + + case hre_syncing: + // Find the header, a string of 62 '1's + // Note that on startup, this could take a a whole block (46 bytes) + // before we start seeing the header + for (int i=0; i<8; i++) + { + if (hreReadBit()) + sync_run++; + else + sync_run = 0; + if (sync_run == 62) + { + hre_state = hre_read; + break; + } + sync_counter++; + } + // If the meter doesn't get in sync within 1000 bits, give up for now + if (sync_counter > 1000) + { + hre_state = hre_sleep; + snprintf_P(log_data, sizeof(log_data), PSTR("HRE: sync error")); + AddLog(LOG_LEVEL_DEBUG); + } + break; + + // Start reading the data block + case hre_read: + snprintf_P(log_data, sizeof(log_data), PSTR("HRE: sync_run:%d, sync_counter:%d"), sync_run, sync_counter); + AddLog(LOG_LEVEL_DEBUG); + read_counter = 0; + parity_errors = 0; + curr_start = uptime; + memset(buff, 0, sizeof(buff)); + hre_state = hre_reading; + snprintf_P(log_data, sizeof(log_data), PSTR("HRE: state:reading")); + AddLog(LOG_LEVEL_DEBUG); + // So this is intended to fall through to the hre_reading section. + // it seems that if there is much of a delay between getting the sync + // bits and starting the read, the HRE won't output the message we + // are looking for... + + case hre_reading: + //ch = hreReadChar(parity_errors); + //i = read_counter - 24; // The water usage reading starts 24 bytes into the block + //if (i>=0 && i 27) + hre_state = hre_sync; + } +} + +void hreShow(boolean json) +{ + if (!hre_good) + return; + + const char hre_types[] = "HRE"; + + char usage[33]; + char rate[33]; + dtostrfd(hre_usage, 2, usage); + dtostrfd(hre_rate, 3, rate); + + if (json) + { + snprintf_P(mqtt_data, sizeof(mqtt_data), JSON_SNS_GNGPM, mqtt_data, hre_types, usage, rate); +#ifdef USE_WEBSERVER + } + else + { + snprintf_P(mqtt_data, sizeof(mqtt_data), HTTP_SNS_GALLONS, mqtt_data, hre_types, usage); + snprintf_P(mqtt_data, sizeof(mqtt_data), HTTP_SNS_GPM, mqtt_data, hre_types, rate); +#endif // USE_WEBSERVER + } +} + + +/*********************************************************************************************\ + * Interface +\*********************************************************************************************/ +bool Xsns91(byte function) +{ + // If we don't have pins assigned give up quickly. + if (pin[GPIO_HRE_CLOCK] >= 99 || pin[GPIO_HRE_DATA] >= 99) + return false; + + switch (function) + { + case FUNC_INIT: + hreInit(); + break; + case FUNC_EVERY_50_MSECOND: + hreEvery50ms(); + break; + case FUNC_EVERY_SECOND: + break; + case FUNC_JSON_APPEND: + hreShow(1); + break; +#ifdef USE_WEBSERVER + case FUNC_WEB_SENSOR: + hreShow(0); + break; +#endif // USE_WEBSERVER + } + return false; +} + +#endif // USE_HRE