├── 3DPrint ├── lid.jpg ├── case-lid.stl ├── insert.jpg └── case-insert.stl ├── Images ├── lid.jpg ├── insert.jpg ├── homepage.jpg ├── installed.jpg ├── mqttsetup.jpg ├── setupchart.jpg ├── watermeter2.jpg └── watermeter3.JPG ├── Circuit ├── tcrt5000.pdf ├── watermeter-cct.jpg ├── sch_d1_mini_v3.0.0.pdf ├── water-meter-reader.fzz └── watermeter-wiring.jpg ├── WaterMeter ├── UserConfig.h └── WaterMeter.ino └── README.md /3DPrint/lid.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CraigHoffmann/water-meter-reader/HEAD/3DPrint/lid.jpg -------------------------------------------------------------------------------- /Images/lid.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CraigHoffmann/water-meter-reader/HEAD/Images/lid.jpg -------------------------------------------------------------------------------- /Images/insert.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CraigHoffmann/water-meter-reader/HEAD/Images/insert.jpg -------------------------------------------------------------------------------- /3DPrint/case-lid.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CraigHoffmann/water-meter-reader/HEAD/3DPrint/case-lid.stl -------------------------------------------------------------------------------- /3DPrint/insert.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CraigHoffmann/water-meter-reader/HEAD/3DPrint/insert.jpg -------------------------------------------------------------------------------- /Circuit/tcrt5000.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CraigHoffmann/water-meter-reader/HEAD/Circuit/tcrt5000.pdf -------------------------------------------------------------------------------- /Images/homepage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CraigHoffmann/water-meter-reader/HEAD/Images/homepage.jpg -------------------------------------------------------------------------------- /Images/installed.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CraigHoffmann/water-meter-reader/HEAD/Images/installed.jpg -------------------------------------------------------------------------------- /Images/mqttsetup.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CraigHoffmann/water-meter-reader/HEAD/Images/mqttsetup.jpg -------------------------------------------------------------------------------- /Images/setupchart.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CraigHoffmann/water-meter-reader/HEAD/Images/setupchart.jpg -------------------------------------------------------------------------------- /Images/watermeter2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CraigHoffmann/water-meter-reader/HEAD/Images/watermeter2.jpg -------------------------------------------------------------------------------- /Images/watermeter3.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CraigHoffmann/water-meter-reader/HEAD/Images/watermeter3.JPG -------------------------------------------------------------------------------- /3DPrint/case-insert.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CraigHoffmann/water-meter-reader/HEAD/3DPrint/case-insert.stl -------------------------------------------------------------------------------- /Circuit/watermeter-cct.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CraigHoffmann/water-meter-reader/HEAD/Circuit/watermeter-cct.jpg -------------------------------------------------------------------------------- /Circuit/sch_d1_mini_v3.0.0.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CraigHoffmann/water-meter-reader/HEAD/Circuit/sch_d1_mini_v3.0.0.pdf -------------------------------------------------------------------------------- /Circuit/water-meter-reader.fzz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CraigHoffmann/water-meter-reader/HEAD/Circuit/water-meter-reader.fzz -------------------------------------------------------------------------------- /Circuit/watermeter-wiring.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CraigHoffmann/water-meter-reader/HEAD/Circuit/watermeter-wiring.jpg -------------------------------------------------------------------------------- /WaterMeter/UserConfig.h: -------------------------------------------------------------------------------- 1 | // **************************************************************** 2 | // WATER METER READER by Craig Hoffmann 3 | // 4 | // User configuration - UserConfig.h 5 | // 6 | // **************************************************************** 7 | 8 | 9 | // Access Point SSID name and password 10 | // hostname is also used for mDNS name ie http://watermeter.local/ 11 | // and also used as the MQTT client name 12 | #define MY_HOSTNAME "watermeter" 13 | #define AP_PASSWORD "wifisetup" // Access Point Password 14 | 15 | // Pull this pin to GND on power up to erase wifi settings & reconfigure 16 | #define RESET_WIFI_PIN D7 17 | 18 | // MQTT Settings - max string lengths for server ip, username, password 19 | #define MAX_SERVER_STR_LEN 32 20 | #define MAX_USER_STR_LEN 32 21 | #define MAX_PASSWORD_STR_LEN 32 22 | 23 | // MQTT topic strings 24 | #define meter_kLiters_topic "sensor/water-meter/kLiters" 25 | #define meter_flowrate_topic "sensor/water-meter/FlowRate" 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Water Meter Reader 2 | ## Remote reading of a South Australian mains water meter 3 | 4 | I made this water meter reader specifically for use with my home assistant monitoring system. (If you haven't heard of [home assistant](https://www.home-assistant.io/) I suggest you check it out!). I have created a discussion thread on the home ssistant [forum here](https://community.home-assistant.io/t/whole-of-house-water-meter-reader-south-australian-meter/240961). After already designing a successful electricity meter reader this was my next requirement for home monitoring and automation. 5 | I have read that some water meters have a rotating magnet inside the meter that can be detected by a hall effect sensor but couldn't find any specific information about the meter at my house (a typical South Australian mains water meter) so I decided to sense the rotating needle on the main dial. 6 | 7 | ![SA Water Meter](https://github.com/CraigHoffmann/water-meter-reader/blob/master/Images/watermeter2.jpg?raw=true) ![Meter Dial](https://github.com/CraigHoffmann/water-meter-reader/blob/master/Images/watermeter3.JPG?raw=true) 8 | 9 | This is what it looks like with the sensor fitted... 10 | 11 | ![Installed](https://github.com/CraigHoffmann/water-meter-reader/blob/master/Images/installed.jpg?raw=true) 12 | 13 | ## Electronics 14 | 15 | The meter reader uses a [WeMOS D1 Mini (R3)](https://docs.wemos.cc/en/latest/d1/d1_mini.html) but would work with any ESP8266 with a few conditions. 16 | 17 | * It uses the ADC Input for the sensor reading so a variant that has access to that pin is required. If the module doesn't have a resistor voltage divider on the ADC input you will need to add it/adjust the resistors to the sensor. 18 | * The WeMOS has a built in 3.3v regulator for the ESP8266 and the sensor runs at 5V so suitable supply electronics are required. 19 | 20 | An optical reflectance sensor (TCRT5000) is used to detect the IR reflected light and is monitored using the analog input. As the dial turns the reflectance changes as the needle passes by. Trigger levels are set on the analog signal to count the revolutions of the dial. Its simple but works reliably for my meter. 21 | 22 | I have the Wemos D1 in the garage and run a 3wire cable out to the meter with for the sensor. Alternatively all electrics could be placed at the meter and run power to the meter (or maybe use solar and batteries?) 23 | 24 | | Circuit | 25 | :-------------------------: 26 | 27 | | Wiring | 28 | :-------------------------: 29 | 30 | ## Firmware 31 | 32 | The Arduino IDE was used to develop and program the code. If you have used this before with the ESP8266 devices you should have no problems. If you're new to arduino or ESP8266's then there are hundreds of pages on the internet detailing how to setup the environment and program devices - I suggest you start by looking for one that suits your level of background knowledge to get up to speed. 33 | 34 | Simply compile and upload the **WaterMeter.ino** code using the Arduino IDE. 35 | 36 | ## Case 37 | 38 | My water meter cover had been broken off for years so I decided to make a new 3D printed cover that doubled as the case for the sensor. The lid is "keyed" with the old lid hinge mounting so that the sensor is always aligned to the rotating dial. Files are available in the github repository to download and print your own. The electronics are just hot glued to the inside plate. 39 | 40 | ![lid](https://github.com/CraigHoffmann/water-meter-reader/blob/master/Images/lid.jpg?raw=true) ![lid](https://github.com/CraigHoffmann/water-meter-reader/blob/master/Images/insert.jpg?raw=true) 41 | 42 | 43 | If you don't have access to a 3D printer you could probably use the bottom of a plastic container or tin - anything that fits snug over the meter to keep the sensor aligned to the dial. Use your imagination and maker skills. 44 | 45 | ## Setup 46 | 47 | First step is to **configure the wifi** to connect to your network. If the device wifi has not been setup previously WiFiManager will automatically setup an access point called **watermeter**. Connect to the access point with password **wifisetup** and then load the default page 192.168.4.1 and enter your network wifi details. **If you want to force the erasure of the wifi settings for some reason power up the device with pin D7 (GPIO13) pulled to ground.** This will erase the wifi settings and go stright to the WiFiManager access point config. 48 | 49 | After entering your wifi details in wifimanager restart the Wemos D1 mini and wifimanager should connect to your network. 50 | 51 | If you have a serial monitor plugged in when powering up the device it will display the assigned IP address on startup. If you don't have an easy way of finding device IP addresses on your network then I suggest allocating an IP address in your router. 52 | 53 | * The main water meter setup page can be found at http://\/ 54 | * Alternatively if your system supports multicast DNS then you can use http://watermeter.local/ 55 | 56 | The default landing page is shows the cumulative count of kLiters used as well as the usage in the past 1 minute. Both the MQTT setup and Sensor setup can be accessed from the home page. 57 | 58 | | ![Homepage](https://github.com/CraigHoffmann/water-meter-reader/blob/master/Images/homepage.jpg?raw=true) | 59 | :-------------------------: 60 | 61 | ### Setup the MQTT connection. 62 | 63 | This is done by either clicking on the **MQTT Setup** button from the home page or entering the url directly http://watermeter.local/mqttsetup - replace watermeter.local with the ip address if you don't have mDNS available. 64 | 65 | | ![MQTT Setup](https://github.com/CraigHoffmann/water-meter-reader/blob/master/Images/mqttsetup.jpg?raw=true) | 66 | :-------------------------: 67 | 68 | ### Setup the sensor trigger thresholds 69 | 70 | To access the sensor setup page in a browser click on the **Sensor Setup** button on the water meter home page or alternatively enter http://watermeter.local/sensorsetup in the browser address bar 71 | 72 | It is necessary to setup the **trigger thresholds** for the sensor for the counter to work properly. The best way to do this is to turn on a water tap full so you get a resonableflow rate then refresh the sensor setup page. You should see the sensor signal rise and fall as the meter dial rotates as shown below. Set the thresholds a bit below the maximum and a bit above the minimum as can be seen below. 73 | 74 | | ![Setup Trigger Thresholds](https://github.com/CraigHoffmann/water-meter-reader/blob/master/Images/setupchart.jpg?raw=true) | 75 | :-------------------------: 76 | 77 | Next set the **Counts per Liter**. This is the number of times the needle rotates for 1 Liter of water use. In my case the dial rotates once per liter so the value is set to 1. 78 | 79 | Set the **Meter Reading (kL)** to match the current water meter reading and **click the update button**. All settings will be saved to non-volatile memory on the Wemos. 80 | 81 | **Note:** the counter will not start "counting" until the kLiters reading is set to a value greater than zero. The kLiteres is set as "reatined" in MQTT so this is how the value is maintained through reset or power fail. 82 | 83 | 84 | 85 | ## Home Assistant 86 | 87 | Home assistant configuration is pretty straight forward, just add an MQTT sensor to the configuration.yaml file as follows: (this assumes you already have MQTT setup and working in home assistant) 88 | 89 | ```YAML 90 | sensor: 91 | # Wemos Water Meter kLiters Reading 92 | - platform: mqtt 93 | state_topic: "sensor/water-meter/kLiters" 94 | name: "Meter Reading" 95 | unit_of_measurement: "kLiters" 96 | 97 | # Wemos Water Meter Flowrate Reading 98 | - platform: mqtt 99 | state_topic: "sensor/water-meter/FlowRate" 100 | name: "Flowrate" 101 | unit_of_measurement: "L/min" 102 | ``` 103 | 104 | -------------------------------------------------------------------------------- /WaterMeter/WaterMeter.ino: -------------------------------------------------------------------------------- 1 | // **************************************************************** 2 | // WATER METER READER by Craig Hoffmann 3 | // 4 | // Written for WeMOS D1 mini R3 5 | // 6 | // Do not remove this comment block 7 | // **************************************************************** 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include //https://github.com/tzapu/WiFiManager WiFi Configuration Magic 17 | 18 | #include "UserConfig.h" 19 | 20 | 21 | // **************************************************************** 22 | // Some defines that are used 23 | // **************************************************************** 24 | 25 | #define ONE_WIRE_BUS 0 // Data wire (1 wire bus) is plugged into pin GPIO 0 26 | #define PROCESS_PERIOD_mS (30*1000) // how often to do some work 27 | #define SAMPLE_PERIOD_mS 25 // how often to sample the ADC 28 | #define MQTT_CONN_RETRY_ms (10*1000) // Only try to reconnect every 10 seconds 29 | #define MQTT_PERIOD_mS (60*1000) // Send MQTT updates every minute 30 | #define LED_ON_TIME_mS 30 // How long to flash the LED on 31 | #define MAX_CHART_POINTS 400 // Number of readings to remember for plotting 288 * 5min 32 | #define LED_PIN 2 // GPIO 2 is the LED on WeMOS D1 Mini 33 | #define CHART_LOGIC_LOW_VALUE 0 34 | #define CHART_LOGIC_LOW_TO_HIGH_DELTA 15 35 | #define VALID_DATA_CODE_ADDR 0 // eeprom address used to store int value code 36 | #define VALID_DATA_CODE ((int)12256) // just a value used to flag if eeprom has been written before 37 | #define SETUP_DATA_ADDR 4 // Setup Data Structure starting address in flash/eeprom 38 | #define ADC_BUFFER_SIZE 100 39 | 40 | 41 | // **************************************************************** 42 | // EEPROM data structure 43 | // **************************************************************** 44 | 45 | struct EEPROMDataStruct 46 | { 47 | int CountsPerLiter; // How many sensor toggles for 1 liter 48 | int MinThreshold; // Sensor threshold for a high 49 | int MaxThreshold; // Sensor threshold for a low 50 | char MQTTHost[MAX_SERVER_STR_LEN+2]; // mqtt host ip address 51 | char MQTTUser[MAX_USER_STR_LEN+2]; // mqtt user name 52 | char MQTTPassword[MAX_PASSWORD_STR_LEN+2]; // mqtt password 53 | int MQTTPort; // mqtt port number 54 | } SetupData = { 1, 45, 65, "0.0.0.0", "user", "secret", 1883 }; 55 | 56 | 57 | // **************************************************************** 58 | // Setup the global variables 59 | // **************************************************************** 60 | 61 | const int ANALOG_PIN = A0; // ESP8266 Analog Pin ADC0 = A0 62 | char TempStr[20]; 63 | float SensorLevels[MAX_CHART_POINTS + 1]; 64 | float TriggerLow[MAX_CHART_POINTS + 1]; 65 | float TriggerHigh[MAX_CHART_POINTS + 1]; 66 | char TriggerLevels[MAX_CHART_POINTS + 1]; 67 | uint16_t HistoryBufferIn; 68 | uint16_t PreviousHistoryBufferIn; 69 | float MeterReading_kL = -1.0; 70 | float LastMeterReading_kL = 0; 71 | float FlowRate = 0; 72 | 73 | volatile uint16_t ADCBuffer[ADC_BUFFER_SIZE]; 74 | volatile uint16_t ADCBufferIn; 75 | uint16_t ADCBufferOut; 76 | uint16_t NextADCBufferOut; 77 | 78 | unsigned long PreviousMillis = 0; 79 | unsigned long PreviousMQTTMillis = 0; 80 | unsigned long LastMQTTReconnectMillis = 0; 81 | unsigned long LED_OnMillis = 0; 82 | 83 | 84 | // **************************************************************** 85 | // Start some things up 86 | // **************************************************************** 87 | 88 | //multicast DNS responder 89 | MDNSResponder MDNS; 90 | 91 | //Web server object. Will be listening in port 80 (default for HTTP) 92 | ESP8266WebServer server(80); 93 | 94 | // Setup a oneWire instance to communicate with any OneWire devices (not just Maxim/Dallas temperature ICs) 95 | OneWire oneWire(ONE_WIRE_BUS); 96 | 97 | // Pass our oneWire reference to Dallas Temperature. 98 | DallasTemperature DS18B20(&oneWire); 99 | 100 | // MQTT client 101 | WiFiClient espClient; 102 | PubSubClient client(espClient); 103 | 104 | 105 | // **************************************************************** 106 | // Power on/reset setup 107 | // **************************************************************** 108 | 109 | void setup() 110 | { 111 | int i; 112 | 113 | // Start setup 114 | Serial.begin(115200); 115 | Serial.println("\n"); 116 | delay(10); 117 | pinMode(LED_PIN, OUTPUT); 118 | digitalWrite(LED_PIN, LOW); // Low LED ON 119 | 120 | // Load the water meter settings from EEPROM/Flash 121 | EEPROM.begin(512); 122 | EEPROM.get(VALID_DATA_CODE_ADDR,i); 123 | if (i==VALID_DATA_CODE) // Does it look like data has been written to the eeprom before?? 124 | { 125 | EEPROM.get(SETUP_DATA_ADDR,SetupData); 126 | if ((SetupData.CountsPerLiter < 1) || (SetupData.CountsPerLiter > 10)) 127 | { 128 | SetupData.CountsPerLiter = 1; // if out of range then reset to default 129 | } 130 | if ((SetupData.MinThreshold < 0) || (SetupData.MinThreshold > 100)) 131 | { 132 | SetupData.MinThreshold = 45; // if out of range then reset to default 133 | } 134 | if ((SetupData.MaxThreshold < 0) || (SetupData.MaxThreshold > 100)) 135 | { 136 | SetupData.MaxThreshold = 65; // if out of range then reset to default 137 | } 138 | } 139 | else 140 | { 141 | EEPROM.put(VALID_DATA_CODE_ADDR,VALID_DATA_CODE); 142 | EEPROM.put(SETUP_DATA_ADDR,SetupData); 143 | 144 | noInterrupts(); 145 | EEPROM.commit(); 146 | interrupts(); 147 | } 148 | 149 | // Make sure AP mode still not active from previous session seems to mess with mDNS 150 | WiFi.softAPdisconnect(); 151 | //WiFi.disconnect(); 152 | //WiFi.mode(WIFI_STA); 153 | delay(10); 154 | 155 | // Force wifimanager to reset config by pulling this pin to GND during startup 156 | pinMode(RESET_WIFI_PIN, INPUT_PULLUP); 157 | 158 | // WiFiManager for configuring/managing the wifi settings 159 | WiFiManager wifiManager; 160 | wifiManager.setTimeout(180); // 3min (180second) timeout for Access Point configuration 161 | wifiManager.setDebugOutput(false); // Note WiFiManager prints password on serial if debug enabled!! 162 | 163 | // Check if wifi config needs to be reset - forcing Access Point Config portal 164 | if (digitalRead(RESET_WIFI_PIN) == LOW) 165 | { 166 | Serial.println("Pin LOW - Resetting Wifi setings"); 167 | wifiManager.resetSettings(); 168 | } 169 | 170 | // Connect using wifimanager 171 | if(!wifiManager.autoConnect(MY_HOSTNAME,AP_PASSWORD)) 172 | { 173 | Serial.println("Failed to connect - resetting"); 174 | ESP.reset(); //reset and try again 175 | } 176 | 177 | // Made it past wifimanager so must be connected 178 | Serial.println("WiFi connected"); 179 | WiFi.softAPdisconnect(); // ensure softAP is stopped so mDNS works reliably 180 | delay(10); 181 | 182 | // Setup mDNS for easy address resolution 183 | if (!MDNS.begin(MY_HOSTNAME)) // NOTE: In windows need bonjour installed for mDNS to work, linux install Avahi 184 | { 185 | Serial.println("mDNS setup failed!"); 186 | } 187 | else 188 | { 189 | Serial.print("mDNS setup http://"); 190 | Serial.print(MY_HOSTNAME); 191 | Serial.println(".local/"); 192 | MDNS.addService("http","tcp",80); 193 | delay(100); // not sure why but mDNS was unreliable without this 194 | } 195 | 196 | // Start the server 197 | server.on("/", HandleRootPath); //Associate the handler function to the path 198 | server.on("/sensorsetup", HandleSetupPath); //Associate the handler function to the path 199 | server.on("/ConfirmSave", HandleSaveConfirmation); //Associate the handler function to the path 200 | server.on("/mqttsetup", HandleMQTTSetupPath); //Associate the handler function to the path 201 | server.on("/mqttConfirmSave", HTTP_POST, HandleMQTTSaveConfirmation); 202 | server.begin(); //Start the server 203 | 204 | // Print the IP address 205 | Serial.print("Use this URL to connect: "); 206 | Serial.print("http://"); 207 | Serial.print(WiFi.localIP()); 208 | Serial.println("/"); 209 | 210 | // Start the MQTT 211 | client.setServer(SetupData.MQTTHost, SetupData.MQTTPort); 212 | client.setCallback(MQTTcallback); 213 | 214 | DS18B20.begin(); // IC Default 9 bit. 215 | 216 | // Initialise some data 217 | for (i=0;i<=MAX_CHART_POINTS;i++) 218 | { 219 | SensorLevels[i]=0; 220 | TriggerLow[i]=SetupData.MinThreshold; 221 | TriggerHigh[i]=SetupData.MaxThreshold; 222 | TriggerLevels[i] = 0; 223 | } 224 | 225 | ADCBuffer[0] = 0; 226 | ADCBuffer[1] = 0; 227 | ADCBufferIn = 1; // We will always let input stay one ahead of output so no risk of reading value as get changed by ISR 228 | ADCBufferOut = 0; 229 | SetupInterrupts(); 230 | 231 | PreviousMillis = millis(); 232 | PreviousMQTTMillis = millis(); 233 | digitalWrite(LED_PIN, HIGH); // Low LED OFF 234 | } 235 | 236 | 237 | // **************************************************************** 238 | // Use Timer Interrupt to read the analog input 239 | // Initialize timer interrupt to trigger every 25ms (SAMPLE_PERIOD_mS) 240 | // **************************************************************** 241 | 242 | void ICACHE_RAM_ATTR MyTimerInterrupt(); 243 | void SetupInterrupts() 244 | { 245 | timer1_attachInterrupt(MyTimerInterrupt); // Add ISR Function 246 | timer1_enable(TIM_DIV16, TIM_EDGE, TIM_LOOP); 247 | // Dividers: 248 | // TIM_DIV1 = 0, //80MHz (80 ticks/us - 104857.588 us max) 249 | // TIM_DIV16 = 1, //5MHz (5 ticks/us - 1677721.4 us max) 250 | // TIM_DIV256 = 3 //312.5Khz (1 tick = 3.2us - 26843542.4 us max) 251 | // Reloads: 252 | // TIM_SINGLE 0 //on interrupt routine you need to write a new value to start the timer again 253 | // TIM_LOOP 1 //on interrupt the counter will start with the same value again 254 | // 255 | // Arm the Timer for our SAMPLE_PERIOD_mS Interval 256 | timer1_write(SAMPLE_PERIOD_mS * 5 * 1000); // 5 Ticks for TIM_DIV16 so use 5 * MicroSeconds 257 | } 258 | 259 | 260 | // **************************************************************** 261 | // The Timer Interrupt handler 262 | // **************************************************************** 263 | 264 | void ICACHE_RAM_ATTR MyTimerInterrupt() 265 | { 266 | ADCBufferIn++; 267 | if (ADCBufferIn >= ADC_BUFFER_SIZE) // Buffer is way bigger than it needs to be so no need to check for overruns 268 | { 269 | ADCBufferIn = 0; 270 | } 271 | ADCBuffer[ADCBufferIn] = analogRead(ANALOG_PIN); 272 | } 273 | 274 | 275 | // **************************************************************** 276 | // Reconnect MQTT if necessary 277 | // **************************************************************** 278 | 279 | void ReconnectMQTT() 280 | { 281 | // Only attempt to reconnect every MQTT_CONN_RETRY_ms 282 | if ((millis() - LastMQTTReconnectMillis) >= MQTT_CONN_RETRY_ms) 283 | { 284 | Serial.println("X"); 285 | //Serial.println(SetupData.MQTTHost); 286 | //Serial.println(SetupData.MQTTUser); 287 | //Serial.println(SetupData.MQTTPassword); 288 | // Attempt to connect 289 | if (client.connect(MY_HOSTNAME, SetupData.MQTTUser, SetupData.MQTTPassword)) 290 | { 291 | Serial.println("Y"); 292 | client.subscribe(meter_kLiters_topic); // Success connecting so subscribe to topic 293 | } 294 | else 295 | { 296 | // Failed - Reset reconnect timeout so wait a while before retrying 297 | LastMQTTReconnectMillis = millis(); 298 | } 299 | } 300 | } 301 | 302 | 303 | // **************************************************************** 304 | // Process incoming MQTT 305 | // **************************************************************** 306 | 307 | void MQTTcallback(char* topic, byte* payload, unsigned int length) 308 | { 309 | char SetkLiters[20]; 310 | 311 | if (length < 19) // if payload is too long ignore it 312 | { 313 | for (int i = 0; i < length; i++) 314 | { 315 | SetkLiters[i]=(char)payload[i]; 316 | } 317 | SetkLiters[length] = NULL; 318 | MeterReading_kL = atof(SetkLiters); 319 | LastMeterReading_kL = MeterReading_kL; 320 | } 321 | } 322 | 323 | 324 | // **************************************************************** 325 | // MAIN LOOP 326 | // **************************************************************** 327 | 328 | void loop() 329 | { 330 | int i; 331 | int analog_sample; 332 | 333 | // Check if MQTT is connected 334 | if (!client.connected()) 335 | { 336 | ReconnectMQTT(); 337 | } 338 | else 339 | { 340 | client.loop(); 341 | LastMQTTReconnectMillis = millis(); // This is to ensure the last reconnect doesnt roll over 342 | 343 | // The following block only gets processed approx every MQTT_PERIOD_mS 344 | if ((millis() - PreviousMQTTMillis) >= MQTT_PERIOD_mS) 345 | { 346 | //LastMQTTReconnectMillis = PreviousMQTTMillis; // This is to ensure the last reconnect doesnt roll over 347 | PreviousMQTTMillis += MQTT_PERIOD_mS; // Prepare for the next MQTT update time 348 | // Do MQTT Updates here 349 | 350 | if (MeterReading_kL > 0.0) // only calculate kWhrs if we already have a starting value 351 | { 352 | dtostrf(MeterReading_kL, -12, 3, TempStr); // The negative means left justify 353 | client.publish(meter_kLiters_topic, TempStr, true); // we want this retained so we update after reset/power out 354 | FlowRate = ((MeterReading_kL - LastMeterReading_kL) * 1000 / (MQTT_PERIOD_mS / 60000)); // Calculation gives Liters per minute 355 | dtostrf(FlowRate, -12, 0, TempStr); // The negative means left justify 356 | client.publish(meter_flowrate_topic, TempStr, false); // we dont need this one retained 357 | LastMeterReading_kL = MeterReading_kL; 358 | } 359 | } 360 | } 361 | 362 | // The following block only gets processed approx every PROCESS_PERIOD_mS 363 | if ((millis() - PreviousMillis) >= PROCESS_PERIOD_mS) 364 | { 365 | PreviousMillis += PROCESS_PERIOD_mS; 366 | // Do stuff here - currently not required 367 | } 368 | 369 | // Check if the LED has been on long enough and turn off 370 | if ((millis() - LED_OnMillis) >= LED_ON_TIME_mS) 371 | { 372 | digitalWrite(LED_PIN, HIGH); // High for LED OFF 373 | } 374 | 375 | // The following block only gets processed when ADC samples are ready - usually every 25ms unless que'd up 376 | NextADCBufferOut = ADCBufferOut + 1; 377 | if (NextADCBufferOut >= ADC_BUFFER_SIZE) 378 | { 379 | NextADCBufferOut = 0; 380 | } 381 | if (NextADCBufferOut != ADCBufferIn) // Process the next sample - this will always leave one sample in buffer 382 | { 383 | ADCBufferOut = NextADCBufferOut; 384 | analog_sample = ADCBuffer[ADCBufferOut]; 385 | 386 | // Save the history buffer index for later comparison/ calculation 387 | PreviousHistoryBufferIn = HistoryBufferIn; 388 | 389 | // Find the next index in the history values ring buffer 390 | HistoryBufferIn++; 391 | if (HistoryBufferIn >= MAX_CHART_POINTS) 392 | { 393 | HistoryBufferIn = 0; 394 | } 395 | 396 | SensorLevels[HistoryBufferIn] = (float)analog_sample / 10.23; // Scale 0 to 100 397 | 398 | // Check if triggered count 399 | if (SensorLevels[HistoryBufferIn] > SetupData.MaxThreshold) 400 | { 401 | TriggerLevels[HistoryBufferIn] = 1; // Use this for the high / low schmitt trigger level 402 | if (TriggerLevels[PreviousHistoryBufferIn] == 0) 403 | { 404 | digitalWrite(LED_PIN, LOW); // Low LED ON 405 | LED_OnMillis = millis(); 406 | if (MeterReading_kL > 0.0) // Only calculate kLiters if we already have a starting value 407 | { 408 | MeterReading_kL = MeterReading_kL + (0.001 / SetupData.CountsPerLiter); 409 | } 410 | } 411 | } 412 | else if (SensorLevels[HistoryBufferIn] < SetupData.MinThreshold) 413 | { 414 | TriggerLevels[HistoryBufferIn] = 0; // Use this for the high / low schmitt trigger level 415 | } 416 | else 417 | { 418 | TriggerLevels[HistoryBufferIn] = TriggerLevels[PreviousHistoryBufferIn]; // This is in the deadzone - no change 419 | } 420 | 421 | TriggerLow[HistoryBufferIn] = SetupData.MinThreshold; 422 | TriggerHigh[HistoryBufferIn] = SetupData.MaxThreshold; 423 | } 424 | MDNS.update(); 425 | server.handleClient(); // Handling of incoming web requests 426 | } 427 | 428 | 429 | // **************************************************************** 430 | // Read temperature from Dallas DS18B20 431 | // **************************************************************** 432 | 433 | float getTemperature(int Sensor) 434 | { 435 | float tempC; 436 | do { 437 | DS18B20.requestTemperatures(); 438 | tempC = DS18B20.getTempCByIndex(Sensor); 439 | delay(100); 440 | } while (tempC == 85.0 || tempC == (-127.0)); 441 | return tempC; 442 | } 443 | 444 | 445 | // **************************************************************** 446 | // Handle default web page request (Home page) 447 | // **************************************************************** 448 | 449 | void HandleRootPath() 450 | { 451 | int i; 452 | 453 | server.sendContent("HTTP/1.1 200 OK\r\n"); // start the page header 454 | server.sendContent("Content-Type: text/html\r\n"); 455 | server.sendContent("Connection: close\r\n"); // the connection will be closed after completion of the response 456 | server.sendContent("Refresh: 60\r\n"); // tell browser to refresh the page automatically every 60sec 457 | server.sendContent("\r\n"); // this separates header from content that follows 458 | server.sendContent(""); 459 | server.sendContent(""); 460 | server.sendContent("Water Meter"); 461 | server.sendContent("

Water Meter

"); 462 | server.sendContent("by Craig Hoffmann"); 463 | server.sendContent("

"); 464 | 465 | server.sendContent("



Water Usage"); 466 | dtostrf(MeterReading_kL, -12, 3, TempStr); // negative width means left justify 467 | server.sendContent(String("

") + TempStr + " kL

"); 468 | 469 | server.sendContent("

Average Flowrate last 1min"); 470 | dtostrf(FlowRate, -12, 0, TempStr); // The negative means left justify, Calculation gives Liters per minute 471 | server.sendContent(String("

") + TempStr + " L/min

"); 472 | 473 | server.sendContent("


"); 474 | server.sendContent("
"); 475 | 476 | //server.sendContent("


"); 477 | //server.sendContent("
"); 478 | 479 | server.sendContent("
"); 480 | server.sendContent(""); 481 | server.sendContent("\r\n"); 482 | server.client().stop(); // Stop is needed because we sent no content length 483 | } 484 | 485 | 486 | // **************************************************************** 487 | // Handle default web page request (Setup Page) 488 | // **************************************************************** 489 | 490 | void HandleSetupPath() 491 | { 492 | int i; 493 | uint16_t HistoryBufferOut; 494 | 495 | server.sendContent("HTTP/1.1 200 OK\r\n"); // start the page header 496 | server.sendContent("Content-Type: text/html\r\n"); 497 | server.sendContent("Connection: close\r\n"); // the connection will be closed after completion of the response 498 | // server.sendContent("Refresh: 60\r\n"); // tell browser to refresh the page automatically every 60sec 499 | server.sendContent("\r\n"); // this separates header from content that follows 500 | server.sendContent(""); 501 | server.sendContent(""); 502 | server.sendContent("Sensor Setup"); 503 | server.sendContent(" "); 504 | server.sendContent(" "); 563 | server.sendContent(""); 564 | server.sendContent("

Water Meter Sensor Setup

"); 565 | server.sendContent("by Craig Hoffmann"); 566 | server.sendContent("
"); // was fixed at 900px and 400px 567 | 568 | server.sendContent("

"); 569 | dtostrf(SetupData.MaxThreshold, -4, 0, TempStr); // negative width means left justify 570 | server.sendContent(String("Max Threshold: "); 571 | 572 | dtostrf(SetupData.MinThreshold, -4, 0, TempStr); // negative width means left justify 573 | server.sendContent(String("

Min Threshold: "); 574 | 575 | dtostrf(SetupData.CountsPerLiter, -4, 0, TempStr); // negative width means left justify 576 | server.sendContent(String("

Counts per Liter: "); 577 | 578 | dtostrf(MeterReading_kL, -12, 3, TempStr); // negative width means left justify 579 | server.sendContent(String("

Meter Reading (kL): "); 580 | server.sendContent("

"); 581 | server.sendContent("

"); 582 | 583 | server.sendContent("
"); 584 | server.sendContent(""); 585 | server.sendContent("\r\n"); 586 | server.client().stop(); // Stop is needed because we sent no content length 587 | } 588 | 589 | 590 | // **************************************************************** 591 | // Present a web page to confirm settings saved 592 | // **************************************************************** 593 | 594 | void HandleSaveConfirmation() 595 | { 596 | int i; 597 | float j; 598 | String ArgsString = ""; 599 | 600 | if (server.arg("MinThreshold") != "") 601 | { 602 | ArgsString = server.arg("MinThreshold"); //Gets the value of the query parameter 603 | i = ArgsString.toInt(); 604 | if ((i>=0) && (i<=100)) 605 | { 606 | SetupData.MinThreshold = i; 607 | } 608 | } 609 | 610 | if (server.arg("MaxThreshold") != "") 611 | { 612 | ArgsString = server.arg("MaxThreshold"); //Gets the value of the query parameter 613 | i = ArgsString.toInt(); 614 | if ((i>=0) && (i<=100)) 615 | { 616 | SetupData.MaxThreshold = i; 617 | } 618 | } 619 | 620 | if (server.arg("CountsPerLiter") != "") 621 | { 622 | ArgsString = server.arg("CountsPerLiter"); //Gets the value of the query parameter 623 | i = ArgsString.toInt(); 624 | if ((i>=1) && (i<=10)) 625 | { 626 | SetupData.CountsPerLiter = i; 627 | } 628 | } 629 | 630 | if (server.arg("MeterReading_kL") != "") 631 | { 632 | ArgsString = server.arg("MeterReading_kL"); //Gets the value of the query parameter 633 | j = ArgsString.toFloat(); 634 | if ((j>=1) && (j<=999999)) 635 | { 636 | MeterReading_kL = j; // **** NOTE: we don't save this one to FLASH/EEPROM 637 | } 638 | } 639 | 640 | EEPROM.put(SETUP_DATA_ADDR,SetupData); 641 | 642 | noInterrupts(); 643 | EEPROM.commit(); 644 | interrupts(); 645 | 646 | server.sendContent("HTTP/1.1 200 OK\r\n"); 647 | server.sendContent("Content-Type: text/html\r\n"); 648 | server.sendContent("Connection: close\r\n"); // the connection will be closed after completion of the response 649 | server.sendContent("\r\n"); // this separates header from content that follows 650 | server.sendContent(""); 651 | server.sendContent(""); 652 | server.sendContent("Sensor Setup Saved"); 653 | server.sendContent("

Save Complete

"); 654 | server.sendContent("

"); 655 | 656 | dtostrf(SetupData.MaxThreshold, -4, 0, TempStr); // negative width means left justify 657 | server.sendContent(String("Max Threshold: "); 658 | 659 | dtostrf(SetupData.MinThreshold, -4, 0, TempStr); // negative width means left justify 660 | server.sendContent(String("

Min Threshold: "); 661 | 662 | dtostrf(SetupData.CountsPerLiter, -4, 0, TempStr); // negative width means left justify 663 | server.sendContent(String("

Counts per Liter: "); 664 | 665 | dtostrf(MeterReading_kL, -12, 3, TempStr); // negative width means left justify 666 | server.sendContent(String("

Meter Reading (kL): "); 667 | 668 | server.sendContent("

"); 669 | server.sendContent("
"); 670 | server.sendContent(""); 671 | server.sendContent("\r\n"); 672 | server.client().stop(); // Stop is needed because we sent no content length 673 | } 674 | 675 | 676 | // **************************************************************** 677 | // Handle MQTT Setup Web Page Request 678 | // **************************************************************** 679 | 680 | void HandleMQTTSetupPath() 681 | { 682 | server.sendContent("HTTP/1.1 200 OK\r\n"); // start the page header 683 | server.sendContent("Content-Type: text/html\r\n"); 684 | server.sendContent("Connection: close\r\n"); // the connection will be closed after completion of the response 685 | server.sendContent("\r\n"); // this separates header from content that follows 686 | server.sendContent(""); 687 | server.sendContent(""); 688 | server.sendContent("MQTT Setup"); 689 | server.sendContent("

Water Meter MQTT Setup

"); 690 | server.sendContent("by Craig Hoffmann

"); 691 | 692 | server.sendContent("

"); 693 | server.sendContent("

"); 694 | 695 | server.sendContent(String("

Server:
"); 696 | server.sendContent(String("

User:
"); 697 | server.sendContent(String("

Password:
"); 698 | //server.sendContent(String("

") + SetupData.MQTTPassword + "

"); // ** test code only ** 699 | dtostrf(SetupData.MQTTPort, -4, 0, TempStr); // negative width means left justify 700 | server.sendContent(String("

Port:
"); 701 | 702 | server.sendContent("

"); 703 | server.sendContent("

"); 704 | server.sendContent("
"); 705 | 706 | server.sendContent(""); 707 | server.sendContent(""); 708 | server.sendContent("\r\n"); 709 | server.client().stop(); // Stop is needed because we sent no content length 710 | } 711 | 712 | 713 | // **************************************************************** 714 | // Handle MQTT Save Confirmation POST Request 715 | // **************************************************************** 716 | 717 | void HandleMQTTSaveConfirmation() 718 | { 719 | int i; 720 | String ArgsString = ""; 721 | 722 | if (server.hasArg("Server")) 723 | { 724 | ArgsString = server.arg("Server"); //Gets the value of the query parameter 725 | i = ArgsString.length()+1; 726 | if (i < MAX_SERVER_STR_LEN) 727 | { 728 | ArgsString.toCharArray(SetupData.MQTTHost,i); 729 | } 730 | } 731 | 732 | if (server.hasArg("User")) 733 | { 734 | ArgsString = server.arg("User"); //Gets the value of the query parameter 735 | i = ArgsString.length()+1; 736 | if (i < MAX_USER_STR_LEN) 737 | { 738 | ArgsString.toCharArray(SetupData.MQTTUser,i); 739 | } 740 | } 741 | 742 | if (server.hasArg("Password")) 743 | { 744 | ArgsString = server.arg("Password"); //Gets the value of the query parameter 745 | i = ArgsString.length()+1; 746 | if ((i < MAX_PASSWORD_STR_LEN) && (ArgsString != "XXXXXX")) 747 | { 748 | ArgsString.toCharArray(SetupData.MQTTPassword,i); 749 | } 750 | } 751 | 752 | 753 | if (server.hasArg("Port")) 754 | { 755 | if (server.arg("Port") != "") 756 | { 757 | ArgsString = server.arg("Port"); //Gets the value of the query parameter 758 | i = ArgsString.toInt(); 759 | if ((i>=1) && (i<=9999)) 760 | { 761 | SetupData.MQTTPort = i; 762 | } 763 | } 764 | } 765 | 766 | EEPROM.put(SETUP_DATA_ADDR,SetupData); 767 | 768 | noInterrupts(); 769 | EEPROM.commit(); 770 | interrupts(); 771 | 772 | server.sendContent("HTTP/1.1 200 OK\r\n"); 773 | server.sendContent("Content-Type: text/html\r\n"); 774 | server.sendContent("Connection: close\r\n"); // the connection will be closed after completion of the response 775 | server.sendContent("\r\n"); // this separates header from content that follows 776 | server.sendContent(""); 777 | server.sendContent(""); 778 | server.sendContent("MQTT Setup Saved"); 779 | server.sendContent("

Save Complete

"); 780 | server.sendContent("

"); 781 | 782 | server.sendContent(String("

Server:
"); 783 | server.sendContent(String("

User:
"); 784 | server.sendContent(String("

Password:
"); 785 | //server.sendContent(String("

") + SetupData.MQTTPassword + "

"); // ** test code only ** 786 | dtostrf(SetupData.MQTTPort, -4, 0, TempStr); // negative width means left justify 787 | server.sendContent(String("

Port:
"); 788 | 789 | server.sendContent("

"); 790 | server.sendContent("
"); 791 | server.sendContent(""); 792 | server.sendContent("\r\n"); 793 | server.client().stop(); // Stop is needed because we sent no content length 794 | } 795 | --------------------------------------------------------------------------------