Skip to content

Instantly share code, notes, and snippets.

@andersevenrud
Last active January 15, 2024 20:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save andersevenrud/bfba208f0901887f3bf2b7b9ee8a6f97 to your computer and use it in GitHub Desktop.
Save andersevenrud/bfba208f0901887f3bf2b7b9ee8a6f97 to your computer and use it in GitHub Desktop.
DIY Z-Wave Power Metering Pulse Sensor

DIY Z-Wave Power Metering Pulse Sensor

I created this because my meter doesn't have an accessible HAN port, and I was not able to find any basic Z-Wave sensor to monitor pulses that did not require the use of some (packaged) proprietary solution or a subscription (and mobile apps). Also, kinda expensive.

This cost me $50 in total (2023), which was considerably cheaper and very beginner friendly.

Accuracy is ~99% (when compared to billing information from my provider over a period of 90 days).

Parts Used

Assembly

The circuit is very simple:

  • Attach wires to the photoresistor and put on shrink tubing
  • One wire on the phororesistor goes onto 5V (pin 2 from top)
  • The other wire to A0 (pin 6 from bottom)
  • And A0 to GND with the resistor (pin 3 from top)

20230310_190441_2

Flashing

Requires Z-Uno2 SDK Version 3.0.10 or higher and latest bootloader (which can be flashed).

The code assumes a meter with 1000 imp/kWh. Change this if required. It's also possible to adjust thresholds for the diode, etc. Everything is configured via the #define directives.

Use the attached sensor.ino code and flash it via Arduino IDE.

⚠️ There seems to be some kind of bug ? in the SDK causing malformed publish messages. Z-Wave JS will drop these and you won't get any updates, which means you have to set up polling. Try enabling ZUNO_SDK_BUG_WORKAROUND to work around this... however this is a hacky and unreliable solution. I'm looking into this. Changing this with re-flashing will require you to exclude and include the device again.

Installation

The photoresistor now just have to be placed onto the blinking LED on the power meter and secured with some tape or anything you want. Just make sure no light bleeds in and that it's making direct contact for best performance.

The white light on the Z-Uno should flash in sync with the meter.

Usage

You can now include the device into your Z-Wave network by pressing the Service button three times. Reports are sent every 60s by default.

Long press the service button to reset the counter.

S0 Security works out of the box and values are stored to EEPROM so the incrementing value is not reset between power cycles.

Please note that this was not made with the intention of running the Z-Uno off a battery, however the operational power usage should be fairly low even if it runs without any main loop delays.

//
// Basic Z-Uno Power Metering Sensor
//
// Counts light pulses on a power meter and
// delivers it over z-wave as a sensor metered in kWh.
//
// Anders Evenrud <andersevenrud@gmail.com>
//
// https://gist.github.com/andersevenrud/bfba208f0901887f3bf2b7b9ee8a6f97
//
// License: MIT
//
#define WITH_CC_METER
#include <EEPROM.h>
#include <ZUNO_Buttons.h>
/* Enable Serial debugging */
#define DEBUG 0
/* Debugger Serial baud rate */
#define SERIAL_BAUD 115200
/* Debugger Serial device */
#define SERIAL_DEVICE Serial
/* Pin of the photoresistor */
#define ZUNO_SENSOR_PIN A0
/* Pin of the service button */
#define ZUNO_BUTTON_PIN 23
/* Location in EEPROM where history is stored */
#define ZUNO_EEPROM_ADDR 0x0
/* Enable hack to get publish to work...
There seems to be a bug in the SDK related to Meter type controllers
which corrupts CC reports. Z-Wave JS will drop these! The "solution"
is to occupy the first channel with some basic sensor. This seems to
create a Meter without a version (undefined) that does not get the same
checks as v4+ (v6 is standard in Z-Uno) and the value comes through.
An unfortunate side effect of this is that the reset feature disappears. */
#define ZUNO_SDK_BUG_WORKAROUND 0
/* The channel used for emitting updates */
#define ZUNO_CHANNEL_NUMBER_POWER ZUNO_SDK_BUG_WORKAROUND + 1
/* How often we announce and store value */
#define CONFIG_UPDATE_INTERVAL 1000 * 60
/* The light threshold where we tick the meter (0-100) */
#define CONFIG_LIGHT_THRESHOLD 20
/* Store data in EEPROM to keep it between power cycles */
#define CONFIG_ENABLE_STORAGE 1
/* Disable publishing of values if you're doing polling exclusively.
This changes behavior of the getter for consistency. */
#define CONFIG_DISABLE_PUBLISH 0
/* We have 4 bytes here and a precision of two.
My meter counts 1Wh per pulse (1000imp/kWh), so if we ex
have 100 counts, it needs to be divided by 10 to get 0.10 kWh. */
#define PULSE_VALUE_CONVERTER(VALUE) VALUE / 10
/* Current meter increment value */
DWORD meterValue = 0;
// Internal States
bool high = false;
unsigned long lastScheduleMs = 0;
unsigned int lastLightLevel = 0;
#if CONFIG_DISABLE_PUBLISH
DWORD announceValue = 0;
#endif
// Set up Z-Wave features
ZUNO_SETUP_SLEEPING_MODE(ZUNO_SLEEPING_MODE_ALWAYS_AWAKE);
#if ZUNO_SDK_BUG_WORKAROUND
bool meterBugfixVariable = false;
ZUNO_SETUP_CHANNELS(
ZUNO_SENSOR_BINARY(ZUNO_SENSOR_BINARY_TYPE_GENERAL_PURPOSE, meterBugfixVariable),
ZUNO_METER(ZUNO_METER_TYPE_ELECTRIC, METER_RESET_ENABLE, ZUNO_METER_ELECTRIC_SCALE_KWH, METER_SIZE_FOUR_BYTES, METER_PRECISION_TWO_DECIMALS, meterGetter, meterReseter));
#else
ZUNO_SETUP_CHANNELS(
ZUNO_METER(ZUNO_METER_TYPE_ELECTRIC, METER_RESET_ENABLE, ZUNO_METER_ELECTRIC_SCALE_KWH, METER_SIZE_FOUR_BYTES, METER_PRECISION_TWO_DECIMALS, meterGetter, meterReseter));
#endif
/* Z-Wave reset method */
void meterReseter() {
eepromReset();
}
/* Z-Wave getter method */
DWORD meterGetter(void) {
#if CONFIG_DISABLE_PUBLISH
return PULSE_VALUE_CONVERTER(announceValue);
#else
return PULSE_VALUE_CONVERTER(meterValue);
#endif
}
/* Save current state to EEPROM */
void eepromSave() {
#if CONFIG_ENABLE_STORAGE
SERIAL_DEVICE.println("Save EEPROM...");
EEPROM.put(ZUNO_EEPROM_ADDR, &meterValue, sizeof(meterValue));
#endif
}
/* Reset EEPROM values */
void eepromReset() {
meterValue = 0;
#if CONFIG_DISABLE_PUBLISH
announceValue = 0;
#endif
eepromSave();
}
/* Restore previous value from EEPROM */
void eepromRestore() {
#if CONFIG_ENABLE_STORAGE
SERIAL_DEVICE.println("Restpre EEPROM...");
DWORD tmpValue;
EEPROM.get(ZUNO_EEPROM_ADDR, &tmpValue, sizeof(meterValue));
if (tmpValue > meterValue) {
SERIAL_DEVICE.println("Got value: " + String(tmpValue));
meterValue = tmpValue;
}
#endif
#if CONFIG_DISABLE_PUBLISH
announceValue = meterValue;
#endif
}
/* Reads photoresistor value and clamps the value to a perctage range */
unsigned int sensorRead() {
unsigned long raw = analogRead(ZUNO_SENSOR_PIN);
unsigned int value = map(raw, 0, 800, 0, 100);
return value;
}
/* Emit our current value onto the network */
void announce() {
SERIAL_DEVICE.println("Announce");
#if CONFIG_DISABLE_PUBLISH
announceValue = meterValue;
#else
zunoSendReport(ZUNO_CHANNEL_NUMBER_POWER);
#endif
}
/* Increment metering value */
void increment() {
meterValue++;
SERIAL_DEVICE.println("Tick (" + String(lastLightLevel) + "L) > " + String(meterValue) + "Wh");
}
/* Arduino bootstrapping */
void setup() {
#if DEBUG
SERIAL_DEVICE.begin(SERIAL_BAUD);
#endif
SERIAL_DEVICE.println("Starting Meter...");
// Activate internal light
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, LOW);
// Activate internal button
Btn.addButton(ZUNO_BUTTON_PIN);
// Then make sure our state is up to date
eepromRestore();
announce();
}
/* Arduino main loop */
void loop() {
unsigned long now = millis();
unsigned int lightLevel = sensorRead();
if (lightLevel != lastLightLevel) {
SERIAL_DEVICE.println(lightLevel);
}
lastLightLevel = lightLevel;
if (Btn.isLongClick(ZUNO_BUTTON_PIN)) {
SERIAL_DEVICE.println("Reset");
eepromReset();
}
if (lightLevel > CONFIG_LIGHT_THRESHOLD) {
if (!high) {
digitalWrite(LED_BUILTIN, HIGH);
increment();
}
high = true;
} else {
if (high) {
digitalWrite(LED_BUILTIN, LOW);
}
high = false;
}
if ((now - lastScheduleMs) > CONFIG_UPDATE_INTERVAL) {
lastScheduleMs = now;
// Store before announce to prevent any cases of value going down
eepromSave();
announce();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment