Close

Measuring Water Consumption

A project log for analog.io - A full stack IoT platform

A full stack project dedicated to easily collecting, analyzing and sharing IoT sensor data.

Luke Beno 08/17/2015 at 20:470 Comments

Just a very quick note on a sensor that I have up and running in my home. It was inspired by John Schuch's Remote Water Consumption project last year. I'm using his same concept with using the MAG3110 Digital compass to measure the spinning magnet in my homes water meter. Here's a picture with some data.

The basic operation of the device is that it measures the oscillating magnetic field inside the meter and counts the number of oscillations. The through a calibration cycle I calculated the number of oscillations per gallon and that is how water consumption is measured.

The technique could be used to monitor for leaks or in general just to help home owners understand what activities that they are doing which drive water usage.

The hardware setup uses one of these sensors that I sell on Tinde:

Here's the Electric Imp Device code:

// Define a class for a ring buffer object with some analysis functions
class ring_buffer {
  _depth = 0
  _index = 0
  _data = 0
  _buff = 0
  _fill = 0
  
  constructor(depth) {
    _depth = depth
    _data = array(depth,0)
    _index = 0
  }
  

  function insert(value) {
    if (_index < _depth)
    {
      _data[_index] = value
      _index++
    }
    else
    {
      _data[0] = value
      _index = 1
    }
    if (_fill<=_depth){
      _fill++
    }
  }
  
  function z(relative_index){
    if ((_index-1-relative_index)>=0)
    {
      return _data[_index-1-relative_index]
    }
    else
    {
      return _data[_index-1-relative_index+_depth]
    }
  }
  
  function dump(){
    local i = 0
    local buff = array(_fill,0)
    
    for (i=0;i<_fill;i++) {
      buff[i] = z(i)
    }
    return buff
  }
  
  function stringify(){
    local d = dump()
    local string = ""
    local i=0
    for (i=0;i<d.len();i++) {
       string += d[i]+" "
    }
    return string
  }
  
  function stats(d){
    local min = 32767
    local max = -32767
    local i
    for (i=0;i<d;i++) {
      local t = z(i)
      if (t>max){max=t}
      if (t<min){min=t}
    }
    return [min,max,max-min]
  }

  function fill(){
    return _fill
  }
  
  function rolled_over(){
    return _fill>_depth
  }

  function reset(){
    _index = 0
    _fill = 0
  }
}

// Define the Sensor Address
const ADDR = 0x1C;
 
// Define register map
const OUT_X_MSB = "\x01";
const CTRL_REG1 = "\x10";
const CTRL_REG2 = "\x11";

// Some useful bitmasks
const ACTIVE = "\x01";
const AUTO_MRST_EN = "\x80";

// Define the i2c periphrial being used
i2c <- hardware.i2c89;

// Interupt pin
int <- hardware.pin7;

// Config the i2c periph
i2c.configure(CLOCK_SPEED_400_KHZ);

// Set to auto reset int flag on read
i2c.write(ADDR,CTRL_REG2+AUTO_MRST_EN)

// Activate the MAG3110, sample at 80Hz
i2c.write(ADDR,CTRL_REG1+ACTIVE)

// Read the initial data to clear the isr flag
local data = i2c.read(ADDR,OUT_X_MSB,6);

// create a buffer for storing data
data_buffer <- ring_buffer(800);

// We're going to do some global min and max calculations
// So init the variables
min <- 32767
max <- -32767

// Initialize the threshriold
thresh <- 0
spread_thresh <- 100

// Debouncing parameter
debounce <- 120

// Some bools for oneshots
x_lf <- false
x_hf <- false

// Counters for counting the number of triggers
trigs <- 0
x_trigs_lf <- 0
x_trigs_hf <- 0
k <- 0

x_int <- 0
x_int_neg <- 0

// Interupt routine for getting MAG3110 measurements
function mag_isr() {
  // Check for the rising edge
  if (int.read()==1)
  {
    // Read data from i2c
    local data = i2c.read(ADDR,OUT_X_MSB,6);  

    // Combine the MSB and LSB into a 16bit value
    local x = data[0]<<8 | data[1]
    
    // Convert 2's compliment
    if (x & 0x8000) {x = -((~x & 0x7FFF) + 1);}
    
    // Detect for min and max, then calculate a new threshold
    if (x<min) {min = x; thresh = min + (max-min)/2}
    if (x>max) {max = x; thresh = min + (max-min)/2}
    
    // Put data in buffer
    data_buffer.insert(x)
    
    // Get the count spread from the last 20 items
    local stats = data_buffer.stats(20)
    local spread = stats[2]
    
    // Detect if measured value is above or below the threshold
    if (x>thresh) {
      // Logic for oneshot to count the crossing
      if (!x_hf){
        x_hf = true
        
        // Increment the high frequency counter
        x_trigs_hf++
        
        if (spread > spread_thresh){trigs++}
      }
      
      // Integrate the measurement, this is for the debounce algorithim
      x_int += (x-thresh)
      
      // If integral exceeds the debounce parameter, time for the lf count
      if (x_int>debounce) {
        // Logic for oneshot to count the crossing
        if (!x_lf) {
          x_lf=true
          
            // Increment the high frequency counter
            x_trigs_lf++
            
            if (spread <= spread_thresh){trigs++}
            
            // Reset the negative integral
            x_int_neg = 0
          }
        }
      }
    else {
      // Reset the hf oneshot
      x_hf = false
      
      // Integrate the measurement under the threshold 
      x_int_neg += (thresh-x)
      
      // If integral exceeds the threshold, time to reset the lf oneshot
      if (x_int_neg>debounce) {
        x_lf = false
        
        // Reset the positive integral
        x_int = 0
      }
    }
    
    //determine if buffer has reached the end
    if (data_buffer.rolled_over())
    {
      
      if (spread>spread_thresh) {
        k = x_trigs_hf
      }
      else {
        k = x_trigs_lf
      }
      
      // Make sure min, max and threshold are stabilized before sending data
      if ((max-min)>spread_thresh)
      {
        // buffer is at end, send the data back to the agent
        agent.send("buffer",[data_buffer.dump(),[trigs,x_trigs_hf,x_trigs_lf,k,stats[1],stats[0],stats[2],max,min,thresh]])
      }
      
      // Reset the trigger counters
      x_trigs_hf = 0
      x_trigs_lf = 0
      trigs = 0
      
      //reset the buffer and start all over again
      data_buffer.reset()
    }
  }
}

// Configure the interrupt pin to run the mag_isr callback
int.configure(DIGITAL_IN, mag_isr)
And the Agent Code:
// Create a data stream at data.sparkfun.com, then enter your keys here:
local publicKey = "dAb2YJaZkKhz4kV53qla"
local privateKey = "...................."

data_array <- array(800,[0,0,0,0])
name_array <- ["x","y","z"]

total <- 0
conversion <- 4.95/476

local req = http.get("https://data.sparkfun.com/output/"+publicKey+"/latest.json");
local response = req.sendsync()
local data = http.jsondecode(response.body)
total = data[0].total_gal.tointeger()
server.log(total)


function bufferRx(data) {
  data_array <- data

  local gpm = data_array[1][0].tofloat()*conversion*6.0
  local total_gal = total.tofloat()*conversion
   // Prepare the phant headers
  local phantHeaders = {"Phant-Private-Key": privateKey, "connection": "close"};
  
  // Create a post request
  local req = http.post("http://data.sparkfun.com/input/"+publicKey, phantHeaders,"gpm="+gpm+"&total_gal="+total_gal);
  
  
  total += data_array[1][0]
  //Send out the request!
  server.log(gpm+" | "+total_gal+" | "+total+" | "+data_array[1][0]+" | "+data_array[1][1]+" | "+data_array[1][2]+" | "+req.sendsync().statuscode);
}

device.on("buffer",bufferRx);

Discussions