Computational methods for the enhancement of energy efficiency rely on a measurement process with sufficient accuracy and number of measurements. Networked energy meters, energy monitors, serve as a vital link between the energy consumption of households and key insights that reveal strategies to achieve significant energy savings. With YoMoPie, we propose a user-oriented energy monitor for the Raspberry Pi platform that aims to enable intelligent energy services in households. YoMoPie measures active as well as apparent power, stores data locally, and integrates a user-friendly Python library. Furthermore, the presented energy monitor allows users to run self-designed services in their home to enhance energy efficiency. Potential services are (but not limited to) residential demand response, immediate user feedback, smart meter data analytics, or energy disaggregation.
All design files and pieces of software are available free of charge. However, in case you use the PCB design, code, or other material for research purposes, we kindly ask you to cite our peer-reviewed research paper:
- Title: YoMoPie: A User-Oriented Energy Monitor to Enhance Energy Efficiency in Households
- Authors: Mr. Christoph Klemenjak, Mr. Stefan Jost and Dr. Wilfried Elmenreich
- Conference: 2018 IEEE Conference on Technologies for Sustainability (SusTech)
Recommended Citation:
@INPROCEEDINGS{klemenjak2018yomopie,
author={C. Klemenjak and S. Jost and W. Elmenreich},
booktitle={2018 IEEE Conference on Technologies for Sustainability (SusTech)},
title={Yo{M}o{P}ie: {A} User-Oriented Energy Monitor to Enhance Energy Efficiency in Households},
year={2018},
volume={},
number={},
pages={},
keywords={},
doi={},
ISSN={},
month={Nov}}
YoMoPie | |
---|---|
Communication | WiFi, Ethernet, RF |
Measurement | P, Q, S, I, U |
Number of connections | 1 or 3 |
Integrated relay | yes |
Sampling frequency | tba. |
Data update rate | tba. |
Power calculation | Hardware |
Open-Source | yes |
RaspberryPi-compatible | yes |
Costs | approx. 50€ |
Beside a current and a voltage sensor, the board integrates an energy metering chip, the ADE7754. Our library is designed in a way to offer single-phase as well as multi-phase metering.
The YoMoPie Python package is available on Python Package Index (PyPI), a repository of software for the Python programming language, and can be installed by issuing one command:
pip3 install YoMoPie
Additionally, the entire source code and a manual can be obtained from our YoMoPie Github repository.
After a successful installation process, the YoMoPie package is available system-wide and can be accessed by a simple import command:
import YoMoPie as yomopie
yp = yomopie.YoMoPie()
During initialisation, the number of line conductors has to be set (single or polyphase metering):
yomo.set_lines(1)
To test the operation, we recommend to call the function do_n_measurements. Based on the number of samples and the sampling period, the function will return first measurement values and saves it into the target file:
yomo.do_n_measurements(number of samples, sampling period, target file)
Active power, apparent power, current, and voltage samples can be read with commands such as:
[t, I] = yp.get_irms()
[t, U] = yp.get_vrms()
[t, P] = yp.get_active_energy()
[t, S] = yp.get_apparent_energy()
In the same vein, users can activate continuous data logging or perform a fixed amount of subsequent measurements:
yp.do_metering()
yp.do_n_measurements(quantity, rate, file)
The operational mode (OPMODE) register defines the general configuration of the integrated measurement chip ADE7754. By writing to this register, A/D converters can be turned on/off, sleep mode can be activated, or a software chip reset can be triggered. For further information, we refer to the datasheet of the measurement chip.
yp.set_operational_mode(OPMODE)
Imports
Classvariables
Methods
-init
-init_yomopie
-set_lines
-enable_board
-disable_board
-chip_reset
-write_8bit
-read_8bit
-write_16bit
-read_16bit
-read_24bit
-get_temp
-get_laenergy
-get_lappenergy
-get_period
-set_operational_mode
-set_measurement_mode
-close_SPI_connection
-get_aenergy
-get_active_energy
-get_apparent_energy
-get_sample
-get_sampleperperiod
-get_vrms
-get_irms
-do_n_measurements
-do_metering
-change_factors
-reset_factors
-init_nrf24
-write_nrf24
-read_nrf24
OPMODE
MMODE
YomoPie requires some additional libraries:
time: The time package is required to obtain timestamps.
math: YoMoPie requires the math lib for calculations such as reactive energy.
spidev: The YoMoPie integrates an energy monitor IC, which communicates via SPI to the RPi. To enable this communication, YoMoPie exploits the spidev lib.
sys: This module provides access to some variables used or maintained by the interpreter and to functions that interact strongly with the interpreter.
RPi.GPIO: In order to allow further extensions of the YoMoPie eco-system, our package integrates the RPi.GPIO. Also, the reset pin is controlled via GPIO.
NRF24: This package allows to utilize the RF module for 2.4 GHz communication.
import time
import math
import spidev
import sys
import RPi.GPIO as GPIO
from lib_nrf24 import NRF24
To correctly access the internal registers of the energy monitor IC, several custom variables are required to adjust the register values.
-
read and write: These variables hold the mask that defines the operational mode. Therefore, for reading a register the given address and the read variable are the inputs of a bitwise AND operation. On the other hand, for writing to a register the register address and the write variable are the inputs of a bitwise OR operation.
-
spi, active_lines and debug: These variables hold the SPI object, save the number of active lines, and enable/disable the debug mode.
-
radio: This object will be used for the RF communication and its functions.
-
sample interval and max_f_sample: defines the time between two samples (with respect to the start_sampling method) in seconds and the maximum sampling frequency (with respect to the sampling error).
-
active_power_LSB, apparent_power_LSB, vrms_factor and irms_factor: Convert register values to physical quantities and represent permanent conversion factors.
read = 0b00111111
write = 0b10000000
spi=0
radio=0
active_lines = 1
debug = 1
sample_intervall = 1
max_f_sample = 10
active_power_LSB= 0.000013292
apparent_power_LSB= 0.00001024
vrms_factor = 0.000047159
irms_factor = 0.000010807
In this Section, we describe every method of our package. A description of function parameters and return values is given.
Description: This method represents the constructor and creates a new YoMoPie object.
Parameters: None.
Returns: Nothing.
def __init__(self):
self.spi=spidev.SpiDev()
self.init_yomopie()
return
Description: Initialises the YoMoPie object. Sets the GPIO mode, disables GPIO warnings and defines pin 19 as output. Also opens a new SPI connection via the SPI device (0,0), sets the SPI speed to 62500 Hz and sets the SPI mode to 1. Finally, the function set_lines is called to set the MMODE, WATMODE and VAMODE.
Parameters: None.
Returns: Nothing.
def init_yomopie(self):
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
GPIO.setup(19,GPIO.OUT)
self.spi.open(0,0)
self.spi.max_speed_hz = 62500
self.spi.mode = 0b01
self.set_lines(self.active_lines)
self.sampleintervall = 1
return
Description: This function sets the number of active phases that will be measured.
Parameters:
- lines - Number of phases
Returns: Nothing.
def set_lines(self, lines):
if (lines != 1) and (lines != 3):
print("Incompatible number of power lines")
return
else:
self.active_lines = lines
if self.active_lines == 3:
self.write_8bit(0x0D, 0x3F)
self.write_8bit(0x0E, 0x3F)
self.set_measurement_mode(0x70)
elif self.active_lines == 1:
self.write_8bit(0x0E, 0x24)
self.set_measurement_mode(0x10)
self.write_8bit(0x0D, 0x24)
return
Description: Enables the board by pulling pin 19 to HIGH.
Parameters: None.
Returns: Nothing.
def enable_board(self):
GPIO.output(19, GPIO.HIGH)
return
Description: Disables the board by pulling pin 19 to LOW.
Parameters: None.
Returns: Nothing.
def disable_board(self):
GPIO.output(19, GPIO.LOW)
return
Description: Resets the chip to the manufacturer settings
Parameters: None.
Returns: Nothing.
def chip_reset(self):
self.write_8bit(0x0A, 0x40)
time.sleep(1);
return
Description: Writes 8 bit to the given address.
Parameters:
-
register - 8 bit address of the register (see ADE7754 register table)
-
value - 8 bit of value that will be written into the register
Returns: Nothing.
def write_8bit(self, register, value):
self.enable_board()
register = register | self.write
self.spi.xfer2([register, value])
return
Description: Reads 8 bit of data from the given address.
Parameters:
- register - 8 bit address of the register (see ADE7754 register table)
Returns: the 8 bit of data in the register as decimal
def read_8bit(self, register):
self.enable_board()
register = register & self.read
result = self.spi.xfer2([register, 0x00])[1:]
return result[0]
Description: Writes 16 bit to the given address.
Parameters:
-
register - 8 bit address of the register (see ADE7754 register table)
-
value - 16 bit of value that will be written into the register
Returns: Nothing.
def write_16bit(self, register, value):
self.enable_board()
register = register | self.write
self.spi.xfer2([register, value[0], value[1]])
return
Description: Reads 16 bit of data from the given address.
Parameters:
- register - 8 bit address of the register (see ADE7754 register table)
Returns: the 16 bit of data in the register as decimal
def read_16bit(self, register):
self.enable_board()
register = register & self.read
result = self.spi.xfer2([register, 0x00, 0x00])[1:]
dec_result = (result[0]<<8)+result[1]
return dec_result
Description: Reads 24 bit of data from the given address.
Parameters:
- register - 8 bit address of the register (see ADE7754 register table)
Returns: the 24 bit of data in the register as decimal
def read_24bit(self, register):
self.enable_board()
register = register & self.read
result = self.spi.xfer2([register, 0x00, 0x00, 0x00])[1:]
dec_result = (result[0]<<16)+(result[1]<<8)+(result[2])
return dec_result
Description: Reads the temperature register (0x08).
Parameters: None.
Returns: A list [timestamp, temperature in °C]
def get_temp(self):
reg = self.read_8bit(0x08)
temp = [time.time(),(reg-129)/4]
return temp
Description: Reads the active energy register (0x03).
Parameters: None.
Returns: A list [timestamp, value of the register]
def get_laenergy(self):
laenergy = [time.time(), self.read_24bit(0x03)]
return laenergy
Description: Reads the apparent energy register (0x06).
Parameters: None.
Returns: A list [timestamp, value of the register]
def get_lappenergy(self):
lappenergy = [time.time(), self.read_24bit(0x06)]
return lappenergy
Description: Reads the period register (0x07).
Parameters: None.
Returns: A list [timestamp, value of the register]
def get_period(self):
period = [time.time(), self.read_16bit(0x07)]
return period
Description: Sets the OPMODE. For more information see section OPMODE.
Parameters:
- value - 8 bit of data representing the OPMODE
Returns: Nothing.
def set_operational_mode(self, value):
self.write_8bit(0x0A, value)
return
Description: Sets the MMODE. For more information see section MMODE.
Parameters:
- value - 8 bit of data representing the MMODE
Returns: Nothing.
def set_measurement_mode(self, value):
self.write_8bit(0x0B, value)
return
Description: Closes the SPI connection.
Parameters: None.
Returns**: 0 if connection is closed.
def close_SPI_connection(self):
self.spi.close()
return 0
Description: Reads the active energy register (0x01).
Parameters: None.
Returns: A list [timestamp, value of register converted to real value]
def get_aenergy(self):
aenergy = [time.time(), self.active_power_LSB * self.read_24bit(0x01) * 3600/self.sample_intervall]
return aenergy
Description: Reads the active energy register (0x02) and resets the register value.
Parameters: None.
Returns: A list [timestamp, value of register converted to real value]
def get_active_energy(self):
aenergy = [time.time(), self.active_power_LSB * self.read_24bit(0x02) * 3600/self.sample_intervall]
return aenergy
Description: Reads the apparent energy register (0x05) and resets the register vlaue.
Parameters: None.
Returns: A list [timestamp, value of register converted to real value]
def get_apparent_energy(self):
appenergy = [time.time(), self.apparent_power_LSB * self.read_24bit(0x05)* 3600/self.sample_intervall]
return appenergy
Description: Takes one sample and calculates the active energy, apparent energy, reactive energy, VRMS and IRMS.
Parameters: None.
Returns: A list of 7 elements [timestamp, active energy, apparent energy, reactive energy, period, VRMS, IRMS]
def get_sample(self):
aenergy = self.get_aenergy()[1] *self.active_factor
appenergy = self.get_appenergy()[1] *self.apparent_factor
renergy = math.sqrt(appenergy*appenergy - aenergy*aenergy)
if self.debug:
print"Active energy: %f W, Apparent energy: %f VA, Reactive Energy: %f var" %(aenergy, appenergy, renergy)
print"VRMS: %f IRMS: %f" %(self.get_vrms()[1]*self.vrms_factor,self.get_irms()[1]*self.irms_factor)
sample = []
sample.append(time.time())
sample.append(aenergy)
sample.append(appenergy)
sample.append(renergy)
sample.append(self.get_period()[1])
sample.append(self.get_vrms()[1]*self.vrms_factor)
sample.append(self.get_irms()[1]*self.irms_factor)
return sample
Description: Reads the VRMS register depending.
Parameters: None.
Returns: A list of 2 elements [timestamp, Phase A VRMS] or 4 elements [timestamp, Phase A VRMS, Phase B VRMS, Phase C VRMS]
def get_vrms(self):
if self.active_lines == 1:
avrms = [time.time(), self.read_24bit(0x2C)]
return avrms
elif self.active_lines == 3:
vrms = []
vrms.append(time.time())
vrms.append(self.read_24bit(0x2C))
vrms.append(self.read_24bit(0x2D))
vrms.append(self.read_24bit(0x2E))
return vrms
return 0
Description: Reads the IRMS register.
Parameters: None.
Returns: A list of 2 elements [timestamp, Phase A IRMS] or 4 elements [timestamp, Phase A IRMS, Phase B IRMS, Phase C IRMS]
def get_irms(self):
if self.active_lines == 1:
airms = [time.time(), self.read_24bit(0x29)]
return airms
elif self.active_lines == 3:
irms = []
irms.append(time.time())
irms.append(self.read_24bit(0x29))
irms.append(self.read_24bit(0x2A))
irms.append(self.read_24bit(0x2B))
return vrms
return 0
Description: Reads multiple register and returns the values adjusted to the sampling frequency. This function will be used in the do_n_measurements function.
Parameters:
- samplerate - the actual samplerate
Returns: A list of 7 elements [timestamp, active energy, apparent energy, reactive energy, period, VRMS, IRMS]
def get_sampleperperiod(self, samplerate):
aenergy = self.get_aenergy()[1] *self.active_factor * 3600/samplerate
appenergy = self.get_appenergy()[1] *self.apparent_factor * 3600/samplerate
renergy = math.sqrt(abs(appenergy*appenergy - aenergy*aenergy))
vrms = self.get_vrms()[1]*self.vrms_factor
irms = self.get_irms()[1]*self.irms_factor
if self.debug:
print("Active energy: %f W, Apparent energy: %f VA, Reactive Energy: %f var" % (aenergy, appenergy, renergy))
print("VRMS: %f IRMS: %f" %(vrms,irms))
sample = []
sample.append(time.time())
sample.append(aenergy)
sample.append(appenergy)
sample.append(renergy)
sample.append(self.get_period()[1])
sample.append(vrms)
sample.append(irms)
return sample
Description: Takes nr_samples with sampling period samplerate and saves the measurements into the give file.
Parameters:
- nr_samples - this are the number of samples that will be taken, integer greater then 0
- samplerate - this is the time between each sample, integer greater then 0
- file - this will be the path and name of the file, where the date will be stored
Returns: A list of samples (each sample is a list of 7 elements)
def do_n_measurements(self, nr_samples, samplerate, file):
if (samplerate<1) or (nr_samples<1):
return 0
self.sample_intervall = samplerate
samples = []
for i in range(0, nr_samples):
for j in range(0, samplerate):
time.sleep(1)
sample = self.get_sampleperperiod(samplerate)
samples.append(sample)
logfile = open(file, "a")
for value in sample:
logfile.write("%s; " % value)
logfile.write("\n")
logfile.close()
return samples
Description: Starts a sampling process and saves the data into a file.
Parameters:
- f_sample - the sampling frequency (<= 10, but if max_f_sample is changed, this value can be up to 100 with a higher error)
- file - this will be the path and name of the file, where the date will be stored. If this parameter is empty the data will be stored in "smart_meter_output.csv".
Returns: Nothing.
def do_metering(self, f_sample, file):
if (f_sample > max_f_sample):
print('Incompatible sampling frequency!')
return 1
if (file == ''):
file = 'smart_meter_output.csv'
for i in range(0,86400):
sample = []
sample.append(time.time())
sample.append(i)
sample.append(self.get_active_energy())
sample.append(self.get_apparent_energy())
data_file = open(file,'a')
for value in sample:
logfile.write("%s; " % value)
logfile.write("\n")
##print(sample)
time.sleep(1/f_sample);
return 0
Description: Changes the multiplication factors for the register values.
Parameters:
- active_f - this is the factor for the active energy calculation
- apparent_f - this is the factor for the apparent energy calculation
- vrms_f - this is the factor for the vrms calculation
- irms_f this is the factor for the irms calculation
Returns: Nothing.
def change_factors(self, active_f, apparent_f, vrms_f, irms_f):
self.active_power_LSB = active_f
self.apparent_power_LSB = apparent_f
self.vrms_factor = vrms_f
self.irms_factor = irms_f
return
Description: Resets the multiplication factors to the default values. The default values are calculated by our measurements with calibrated equipment.
Parameters: None.
Returns: Nothing.
def reset_factors(self):
self.active_power_LSB= 0.000013292
self.apparent_power_LSB= 0.00001024
self.vrms_factor = 0.000047159
self.irms_factor = 0.000010807
return
Description: Initializes the RF communication via the NRF24 chip.
Parameters: None.
Returns: Nothing.
def init_nrf24(self):
pipes = [[0xe7, 0xe7, 0xe7, 0xe7, 0xe7], [0xc2, 0xc2, 0xc2, 0xc2, 0xc2]]
self.radio = NRF24(GPIO, self.spi)
self.radio.begin(1, 13)
self.radio.setPayloadSize(32)
self.radio.setChannel(0x60)
self.radio.setDataRate(NRF24.BR_2MBPS)
self.radio.setPALevel(NRF24.PA_MIN)
self.radio.setAutoAck(True)
self.radio.enableDynamicPayloads()
self.radio.enableAckPayload()
self.radio.openWritingPipe(pipes[1])
self.radio.openReadingPipe(1, pipes[0])
self.radio.printDetails()
return
Description: Sends a message via the RF communication.
Parameters:
- command - the message that will be written via RF
Returns: Nothing.
def write_nrf24(self, command):
message = []
message = list(command)
self.radio.write(message)
print("Send: {}".format(message))
if self.radio.isAckPayloadAvailable():
pl_buffer = []
self.radio.read(pl_buffer, self.radio.getDynamicPayloadSize())
print(pl_buffer)
print("Translating the acknowledgment to unicode chars...")
string = ""
for n in pl_buffer:
if(n >= 32 and n <= 126):
string += chr(n)
print(string)
return
Description: Reads a message via the RF communication.
Parameters: None.
Returns: Nothing.
def read_nrf24(self):
print("Ready to receive data...")
self.radio.startListening()
pipe = [0]
while not self.radio.available(pipe):
time.sleep(1/100)
receivedMessage = []
self.radio.read(receivedMessage, self.radio.getDynamicPayloadSize())
print("Translating the receivedMessage to unicode chars...")
string = ""
for n in receivedMessage:
if (n >= 32 and n <= 126):
string += chr(n)
print("Our sensor sent us: {}".format(string))
self.radio.stopListening()
return
The operational mode (OPMODE) register defines the general configuration of the ADE7754 chip. For detailed information about the individual bits of this register we refer to Table IX.
The configuration of period and peak measurements are defined by writing to the MMODE register (0x0B). For more information about the register we refer to Table XII.