Current Cost Power Monitoring (NetSmart Bridge Replacement)

Measure power usage for your house and individual outlets with the Particle Photon or Electron.

  • Replaces Current Cost NetSmart bridge to allow you to send your data where you wish.
  • No subscription - it's Open Source.
  • Measures all channels.
  • No need to return to Current Cost for upgrade.
  • Connects using a standard 1-1 RJ45 Cable to the Current Cost base unit
  • Provides power to the base unit
  • Monitor all the Current Cost sensors
  • Publish power usage using the Particle Cloud, connect Tinamous to see power usage in real-time.
  • Can be plugged directly into a USB outlet with the USB A Plug, or to a cable using the Photons B socket.
  • Additional features can be added through the easy access holes next to the Photon/Electron.
  • Headers can connect the stick onto the Particle Asset Tracker.
  • Optional EEPROM can be used to log measurements offline.

				
				
// Pin Connection:
// Photon 3v3 -> RJ45 Pin 1
// Photon GND -> RJ45 Pin 4
// Photon RX -> RJ45 Pin 8
// Photon D3 -> 1k Resistor -> LED

// Allows for a maximum of 12 sensors.
#define MAX_SENSORS 12

// Number of appliance's defined
// these are used to determine if an appliance 
// is active or not.
#define MAX_APPLIANCES 4

// Serial receive buffer. 
// Cleared when processing begins (i.e. the end terminator is seen)
String buffer = "";

// Intermediary buffer that takes the buffer message
// and appends it, so that it can split messages up.
String messageBuffer = "";

// The message being processed.
//String message = "";

// If the start of an xml element has been found.
bool foundStart = false;

// Temperature measured by the Current Cost unit (display unit)
String temperature = "";

typedef struct {
    // Dish washer etc.
    String name;
    
    // which sensor to monitor 
    int sensorId;
    
    // What delta level is needed to trigger this appliance
    // as being on.
    long triggerLevelLower;
    long triggerLevelUpper;
    
    // When this was last triggered 
    // Not always possible to judge when applience goes off from power
    // usage so give an estimated time.
    long lastTriggeredAt;
    
    // Time this trigger can be reset after.
    long resetAfter;
    
    // How long from the last triggered at
    // the trigger should be reset after.
    long reTriggerDelaySeconds;
    
    bool isTriggered;
    
    // How many times the trigger has been 
    int triggerCount;
    
    // How long it was on for this time it was used.
    int durationSeconds;
    
    // Overall how long the appliance has been on for
    int totalDurationSeconds;
    
    // If the triggered flag can be cleared on
    // from low power usage. 
    // Some appliances (dish washer/washing machine) are on
    // even when in a low power state so fall back to timeout.
    bool clearOnPower;
} ApplianceTrigger;

// Structure used to store each sensor reading and
// some additional information 
typedef struct {
    int powerWatts;
    int lastPowerWatts;
    int deltaPower;
    long measuredAt;
    // If this measurement has been published.
    bool published = true;
} SensorState;


ApplianceTrigger applianceTriggers[MAX_APPLIANCES];

SensorState sensorStates[MAX_SENSORS];

int publishCount = 0;

int statusLed = D7;

// Buffer for publishing json data to Tinamous
// Maxes out Particle.publish at 255 characters.
String json;

void setup()
{
    // USB
    Serial.begin(57600);
    // via TX/RX pins for current cost.
    Serial1.begin(57600);      
    
    // Reserve space in the buffers.
    buffer.reserve(100);
    messageBuffer.reserve(512);
    json.reserve(256);
    
    // LED indication of RX data
    pinMode(statusLed, OUTPUT);
    digitalWrite(statusLed, LOW);
    
    // RGB indication of publish.
    RGB.control(true);
    RGB.color(255, 255, 255);
    
    // Set-up the appliances
    setupAppliances();
   
    Particle.publish("Current Cost Monitor V0.08");
}

void setupAppliances() {
    ApplianceTrigger dishWasher = ApplianceTrigger();
    dishWasher.name = "Dish washer";
    dishWasher.sensorId = 1;
    dishWasher.triggerLevelLower = 2000; 
    // Approve 2.2kW usage.
    dishWasher.triggerLevelUpper = 3000;
    dishWasher.lastTriggeredAt = 0;
    dishWasher.resetAfter = 0;
    // 2.5 hour wash cycle.
    dishWasher.reTriggerDelaySeconds = 2.75 * 60 * 60;
    dishWasher.isTriggered = false;
    dishWasher.triggerCount = 0;
    dishWasher.clearOnPower =false;
    
    
    // Washing machine
    // Consumes about 2 Watts when on, but not running.
    ApplianceTrigger washingMachine = ApplianceTrigger();
    washingMachine.name = "Washing machine (running)";
    washingMachine.sensorId = 5;
    washingMachine.triggerLevelLower = 100; 
    // Approve 2.2kW usage.
    washingMachine.triggerLevelUpper = 3000;
    washingMachine.lastTriggeredAt = 0;
    washingMachine.resetAfter = 0;
    // 2.5 hour wash cycle.
    washingMachine.reTriggerDelaySeconds = 3 * 60 * 60;
    washingMachine.isTriggered = false;
    washingMachine.triggerCount = 0;
    washingMachine.clearOnPower = false;
    
    // Kettle
    // needs to reset on -ve trigger.
    ApplianceTrigger kettle = ApplianceTrigger();
    kettle.name = "Kettle";
    kettle.sensorId = 0; // House sensor
    kettle.triggerLevelLower = 2800; 
    kettle.triggerLevelUpper = 3200;
    kettle.lastTriggeredAt = 0;
    kettle.resetAfter = 0;
    // Max 5 minutes run time.
    kettle.reTriggerDelaySeconds = 5 * 60;
    kettle.isTriggered = false;
    kettle.triggerCount = 0;
    kettle.clearOnPower = true;
    
    // Shower
    // needs to reset on -ve trigger.
    ApplianceTrigger shower = ApplianceTrigger();
    shower.name = "Shower";
    shower.sensorId = 0; // House sensor
    shower.triggerLevelLower = 7000; 
    shower.triggerLevelUpper = 8000;
    shower.lastTriggeredAt = 0;
    shower.resetAfter = 0;
    // allow upto 30 minutes for a shower before
    // forcing a reset.
    shower.reTriggerDelaySeconds = 30 * 60;
    shower.isTriggered = false;
    shower.triggerCount = 0;
    shower.clearOnPower = true;
    
    // 
    applianceTriggers[0] = dishWasher;
    applianceTriggers[1] = washingMachine;
    applianceTriggers[2] = kettle;
    applianceTriggers[3] = shower;
}

void loop() {
    
    // move the messages from the serial receive
    // to the main message buffer to be processed here.
    messageBuffer+=buffer;
    buffer = "";
    
    // Look for "" as the indication that the end of the message has been read.
    // xml end terminator found.
    int messageStartPosition = messageBuffer.indexOf("");
    int messageTerminatorPosition = messageBuffer.indexOf("");
    
    if (messageStartPosition>=0 && messageTerminatorPosition>0) {
        // This ignores the > at the end, that's left in the buffer
        // to combines with future mesages.
        messageTerminatorPosition = messageTerminatorPosition + 6;
        
        // Extract the message.
        String message = messageBuffer.substring(messageStartPosition, messageTerminatorPosition);
        
        // Remove the string we are processing and anything we're ignoring before it.
        messageBuffer.remove(0, messageTerminatorPosition);
        
        // ensure buffer contains the start terminator.
        // Ensure that we have at-least  start element
        // don't care about anything before that though.
        if (message.indexOf("tmpr") > -1) {
            processPower(message);
        } else if (message.indexOf("")) {
            processHistory(message);
        } else {
            Serial.println("Unknown message: " + message);
        }
    }
    
    // Every n seconds publish the  value of the sensors.
    // Tune this so not to publish to often but also
    // not miss a sensor reading.
    if (publishCount > 5000) {
        RGB.color(00, 00, 255);
        publishSensorsJson();
        publishCount = 0;
    }
    
    delay(10);
    publishCount+=10;
    RGB.color(0, 0, 0);
}

// 
// CC128-v1.29
// 01579
// 
// 23.7
// 2
// 02927
// 1
// 
//  00506
// 
// 
void processPower(String message) {
    Serial.println("Processing Power: " + message);
            
    RGB.color(00, 255, 00);
    
    temperature = extractTemperature(message);
    
    int sensorId = getSensor(message);
    
    // Sensor may have upto 3 Channes of data.
    // but for now, just use channel 1.
    int powerNow = extractChannelData(message, 1);
    
    // -1 indicates channel data not found.
    if (powerNow >= 0) {
        updateSensorState(sensorId, powerNow);
        checkAppliances(sensorId);
    }
}

// message will contain a history xml message.
// 
// CC128-v1.29
// 01579 
// 
// 
//  01581
//	1
//	kwhr
//	
//		0
//		2.626
//		2.708
//		2.748
//		2.291
//	
// ... to sensor 9
// 
// 
// Also...
// 3.492
// 2.892
// 5.446
// 2.558
void processHistory(String message) {
    Serial.println("History: " + message);
}

void updateSensorState(int sensorId, int powerNow) {
    SensorState sensorState = sensorStates[sensorId];
    
    if (!sensorState.published) {
        Serial.println("Missed sensor reading. Previous reading not published");
    }
    
    sensorState.deltaPower = powerNow - sensorState.powerWatts;
    sensorState.lastPowerWatts = sensorState.powerWatts;
    sensorState.powerWatts= powerNow;
    sensorState.published = false;
    sensorStates[sensorId] = sensorState;
}

// TODO: Ignore delta's on reset.
void checkAppliances(int sensorId) {
    int now = Time.now();
    
    for (int i = 0; i < MAX_APPLIANCES; i++) {
        ApplianceTrigger applianceTrigger = applianceTriggers[i];
        
        // Figure out if the appliance trigger should be reset based 
        // on time.
        if (applianceTrigger.isTriggered && now > applianceTrigger.resetAfter) {
            applianceReset(i);
        }
        
        // If the appliance is for this sensor and is not already triggered.
        if (applianceTrigger.sensorId == sensorId) {
            SensorState sensorState = sensorStates[sensorId];
            
            if (!applianceTrigger.isTriggered) {
                // If the recorderd change in power level (delta) is between the lower and upper
                // trigger levels for the appliance then trigger the appliance
                // as being on.
                if (sensorState.deltaPower > applianceTrigger.triggerLevelLower && sensorState.deltaPower < applianceTrigger.triggerLevelUpper) {
                    applianceTriggered(i, sensorState.deltaPower);
                }
            } else if (applianceTrigger.clearOnPower) {
                // If the appliance triggered can be reset from 
                // the power no longer being drawn.
                // Is triggered. Check to see if deltaPower is -ve and if within range
                // of the appliance. If so, reset the appliance.
                 if (sensorState.deltaPower < (-applianceTrigger.triggerLevelLower) && sensorState.deltaPower > (-applianceTrigger.triggerLevelUpper)) {
                    applianceReset(i);
                }
            }
        }
    }
}

void applianceTriggered(int applianceId, int powerLevel) {
    int now = Time.now();
    applianceTriggers[applianceId].isTriggered = true;
    applianceTriggers[applianceId].lastTriggeredAt = now;
    applianceTriggers[applianceId].triggerCount++;
    applianceTriggers[applianceId].resetAfter = now + applianceTriggers[applianceId].reTriggerDelaySeconds;
    
    Particle.publish("status", applianceTriggers[applianceId].name + " triggered. Power: " + String(powerLevel));
    Serial.println(applianceTriggers[applianceId].name + " triggered");
}

void applianceReset(int applianceId) {
    if (applianceTriggers[applianceId].isTriggered) {
        int durationSeconds =Time.now() - applianceTriggers[applianceId].lastTriggeredAt;
        String name = applianceTriggers[applianceId].name;
        
        applianceTriggers[applianceId].isTriggered = false;
        applianceTriggers[applianceId].durationSeconds =  durationSeconds;
        applianceTriggers[applianceId].totalDurationSeconds += durationSeconds;
        Particle.publish("status", name + " cleared. Was on for " + String(durationSeconds) + "s");
        Serial.println(name + " cleared. Was on for " + String(durationSeconds) + "s");
        // TODO: Publish json status as well.
    
        
        String json = "{'" + name + "-duration':" + String(durationSeconds);
        json.concat(", '" + name + "-totalDuration':"+ String(applianceTriggers[applianceId].totalDurationSeconds));
        json.concat(", '" + name + "-times':"+ String(applianceTriggers[applianceId].triggerCount));
        json.concat("}");
        Serial.println("Json: " + json);
        Particle.publish("json", json);
    }
}

// Extract the temperature value from the xml
String extractTemperature(String message) {
    return extractXmlElement(message, "tmpr");
}

// Get the id of the sensor being reported.
int getSensor(String message) {
    //int start = message.indexOf("sensor") + 7;
    //int endString = message.indexOf("/sensor") - 1;
    String sensor = extractXmlElement(message, "sensor");
    
    // Sensor may be 0. or, if sensor text not valid string
    // will also return 0.
    int sensorId = sensor.toInt();
    
    return sensorId;
}

// Get the channel data for the sensor.
// most will have a single channel but the base (sensor 0)
// may have more channels if more than one clamp is fitted.
// 00506
int extractChannelData(String message, int channel) {
    String channelName = "ch" + (String)channel;
    String channelData = extractXmlElement(message, channelName);
    
    //int start = message.indexOf(channelName) + 11;
    if (channelData != "") {
        //int endString = message.indexOf("/" + channelName) - 9;
        //String channelPower = message.substring(start, endString);
        String channelPower = extractXmlElement(channelData, "watts");
        
        // Each sensor has n (upto 3) channels. Ignore channels for now as 
        // typical usage is ch1.
        return channelPower.toInt();
    } 
    
    // Indicate error with -1 watts for the channel.
    Serial.println("No data found for sensor: ");
    return -1;
}

// Extract the value between two xml
// element tags
// e.g. 7
// use tag  "sensor"
// returns "7"
String extractXmlElement(String xml, String tag) {
    int startPosition = xml.indexOf("<" + tag + ">") + tag.length() + 2;
    int endPosition = xml.indexOf("");
    
    if (endPosition > startPosition) {
        return xml.substring(startPosition, endPosition);
    }
    Serial.println("End < start");
    return "";
}

// Publish the sensor values (power)
void publishSensorsJson() {
    // Build up the senml.
    bool hasData = false;
    json = "{'t': '" + temperature + "' ";
    
    for (int sensorId = 0; sensorId < MAX_SENSORS; sensorId ++) {
        SensorState sensorState = sensorStates[sensorId];
        
        // Only publish the sensor valur if it has a new value since last publish.
        if (!sensorState.published) {
            hasData = true;
            
            json.concat(",'S" + String(sensorId) + "':'" + String(sensorState.powerWatts) + "' ");
            
            //senml+= ",{'n':'S" + String(sensorId) + "', 'v':'" + String(sensorState.powerWatts) + "'}";
            
            // Include the delta if it's outside a noise range
            //if (sensorState.deltaPower > 2 || sensorState.deltaPower < -2) {
                //senml+= ",{'n':'D" + String(sensorId) + "', 'v':'" + String(sensorState.deltaPower) + "'}";
                json.concat(",'D" + String(sensorId) + "':'" + String(sensorState.deltaPower) + "' ");
            //}
            
            sensorStates[sensorId].published = true;
        }
    }
    json.concat("}");
    
    // Only publish if we have new (unpublished) measurements
    if (hasData) {
        Serial.println("json: " + json);
        Particle.publish("json", json);
    }
}

// Watch for serial data on the Tx/Rx serial pins.
void serialEvent1()
{
    digitalWrite(statusLed, HIGH);
    
    while (Serial1.available())
    {
        char c = Serial1.read();
        
        // read form TC/RX serial 
        buffer+=c;
    }
    
    digitalWrite(statusLed, LOW);
}
				
				

PCB Top Layer
PCB Bottom Layer

  • Length: 72mm
  • Width: 35mm
  • Hole 1: 3.2mm at 14,17
  • Hole 2: 3.2mm at 67.25,4.3
  • Hole 3: 3.2mm at 67.25,30