DIY Smart Meter

Pretty much all Digital Electricity meters (smart or not) have a light that blinks every time a certain amount of energy is used - often once for every Watt-hour (Usually labelled as 1000 imp/kWh).

You can easily detect this with a simple Light Dependent Resistor and use it to measure and record your energy usage over time.

You'll Need

  • A Puck.js
  • A standard Light Dependent Resistor (eg. GL5537) - see the end for information about other options.
  • Some double sided tape (VHB tape works great)


Just solder the LDR between pins D1 and D2, so that it can go through a hole in the back of the Puck.js case.

See the video for more information.

Simple Pulse Counter

This is the code used in the video - it measures the number of pulses and reports that over Bluetooth LE Advertising.

Simply copy & paste this to the right-hand side of the Web IDE and click the 'Upload' button.

var counter = 0;

// Update BLE advertising
function update() {
  var a = new ArrayBuffer(4);
  var d = new DataView(a);
  d.setUint32(0, counter, false/*big endian*/);
    name: "Puck.js \xE2\x9A\xA1",
    interval: 600 // default is 375 - save a bit of power

// Set up pin states
// Watch for pin changes
setWatch(function(e) {
 digitalPulse(LED1,1,1); // show activity
}, D2, { repeat:true, edge:"falling" });

Pulse Counter and usage history

However, you can easily go a bit further and can store historical energy usage data on the Puck.js itself.

This code uses exactly the same detection code as before, but adds a class called Counter that stores historical energy usage data.

Once connected to a Web Bluetooth website, the data can then be read and displayed on a graph.

To use this:

  • Go into the Web IDE settings, Communications and ensure that Set Current Time is ticked.
  • Copy & paste the code below to the right-hand side of the Web IDE
  • Click the 'Upload' button
  • Type save() on the left-hand side of the IDE. You're now ready to go.
/** Create a new counter. Contains the following:
  totals : {
    count // Overall count
    year  // each month of the year (0..11)
    week  // each day of the week (0..6)
    month // each day of the month (0..31)
    day : // each hour (0..23)
  history // last 96 hours, newest last (0..95)
function Counter() {
/// Clear the counters back to zero
Counter.prototype.clear = function() {
  this.totals = {
    count : 0,                // Overall count
    year : new Uint32Array(12), // each month of the year (0..11)
    week : new Uint32Array(7), // each day of the week (0..6)
    month : new Uint32Array(31), // each day of the month (0..31)
    day : new Uint32Array(24) // each hour (0..23)
  this.historyPeriod = 60*60*1000; // 1 hour in milliseconds
  this.history = new Uint32Array(96); // last 96 hours
  this.historyTime = 0;
  this.lastUpdate = new;
/// = function(n) {
  if (n===undefined) n=1;
  var d = new Date();
  var t = this.totals;
  // Totals by time period
  // Rolling history
  this.historyTime += d.getTime()-this.lastUpdate;
  this.lastUpdate = d.getTime();
  var steps = Math.floor(this.historyTime / this.historyPeriod);
  if (steps>=0) {
    this.historyTime -= steps*this.historyPeriod;
    var h = this.history;
    h.set(new Uint32Array(h.buffer, 4*steps), 0);
    h.fill(0, h.length-steps);
var c = new Counter();

// Update BLE advertising
function update() {
  var a = new ArrayBuffer(4);
  var d = new DataView(a);
  d.setUint32(0, c.totals.count, false/*big endian*/);
    name: "Puck.js \xE2\x9A\xA1",
    interval: 600 // default is 375 - save a bit of power

function onInit() {
  setWatch(function(e) {;
    digitalPulse(LED1,1,1); // show activity
  }, D2, { repeat:true, edge:"falling" });

Now you have the Puck.js set up, you can use the Web Bluetooth website below (or can modify it for your needs).

To try it, just click the Try Me! button in the bottom right of the code sample below. To make your own version, follow the instructions for creating a GitHub page at the top of the Web Bluetooth tutorial

  <meta name="viewport" content="width=620, initial-scale=1">
 <body style="width:620px;height:800px">
  <link href="" rel="stylesheet">
  <script src=""></script>
  <script src=""></script>  
  function connectDevice() {    
    // connect, issue Ctrl-C to clear out any data that might have been left in REPL
    Puck.write("\x03", function() {
      setTimeout(function() {
        // After a short delay ask for the battery percentage
        Puck.eval("{bat:E.getBattery()}", function(d,err) {
          if (!d) {
            alert("Web Bluetooth connection failed!\n"+(err||""));
          // remove the 'connect' window
          // update battery meter
          // Get all our data - it can take a while
          // so we do it in stages
          Puck.eval("c.totals", function(d,err) {
            // Get the 96 hour history last
            Puck.eval("c.history", function(d) {
        }, 500);
  // Set up the controls we see on the screen    
  var elements = {
    heading : TD.label({x:10,y:10,width:190,height:50,label:"Electricity meter"}),
    total : TD.value({x:10,y:70,width:190,height:60,label:"Total Usage",value:0}),
    bat : TD.gauge({x:10,y:140,width:190,height:220,label:"Battery Level",value:0,min:0,max:100}),
    history : TD.graph({x:210,y:10+160*0,width:400,height:150,label:"96 hr history"}),
    day : TD.graph({x:210,y:10+160*1,width:400,height:150,label:"Day"}),
    month : TD.graph({x:210,y:10+160*2,width:400,height:150,label:"Month"}),
    week : TD.graph({x:210,y:10+160*3,width:400,height:150,label:"Week"}),
    year : TD.graph({x:210,y:10+160*4,width:400,height:150,label:"Year"}),
    modal: TD.modal({x:10,y:10,width:600,height:790,label:"Click to connect",onchange:connectDevice})
  for (var i in elements)


  • While this example doesn't do it, your current power consumption can be calculated by measuring the time between pulses - it'd be a good addition to the advertising data.
  • Web Bluetooth websites can't read advertising data (yet) so cannot display any electricity data without connecting. However another Espruino device like Pixl.js could do that easily.
  • The resistance of the LDR varies with the light on it. On a standard LDR like a GL5537 complete darkness has a resistance of around 2 M Ohms, so the power usage from the coin cell (against the 40k internal pullup) is very low. However the more ambient light that gets into the sensor the lower the resistance and so the lower the battery life - so it's worth trying to fit the sensor as snugly as possible.

Do I need to use an LDR?

The Puck does have the ability to measure light via Puck.light(), and it does this using the fact the red LED makes a small voltage when exposed to some wavelengths of light.

If you're lucky and your electricity meter's flashing LED is of a wavelength that will trigger the red (or green) LEDs then you could try and use some code like this:

// D5 (LED1) is used for sensing
// D1 is used as an output which is also watched with setWatch to detect a state change
var ll = require("NRF52LL");
// set up D1 as an output
// create a 'toggle' task for pin D1
var tog = ll.gpiote(7, {type:"task",pin:D1,lo2hi:1,hi2lo:1,initialState:0});
// compare D5 against vref/16  (vref:8 would be vref/2)
var comp = ll.lpcomp({pin:D5,vref:1,hyst:true});
// use a PPI to trigger the toggle event
ll.ppiEnable(0, comp.eCross, tog.tOut);
// Detect a change on D1
setWatch(function() {
  // called twice per 'flash' (for light on and off)
  print("Light level changed");
}, D1, {repeat:true});

You would then need to mount the Puck the opposite way around with the LEDs directly over the top of the meter's light.

This is in some ways better than the LDR arrangement as there is no resistance draining the Puck's battery.

This page is auto-generated from GitHub. If you see any mistakes or have suggestions, please let us know.