Servus!

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.

Research Paper on YomoPie

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:

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.

Installation

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.

Examples of use

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)

YoMoPie Python package documentation

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

Imports

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

Class variables

To correctly access the internal registers of the energy monitor IC, several custom variables are required to adjust the register values.

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

Methods

In this Section, we describe every method of our package. A description of function parameters and return values is given.

init

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

init_yomopie

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

set_lines

Description: This function sets the number of active phases that will be measured.

Parameters:

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

enable_board

Description: Enables the board by pulling pin 19 to HIGH.

Parameters: None.

Returns: Nothing.

def enable_board(self):
	GPIO.output(19, GPIO.HIGH)
	return

disable_board

Description: Disables the board by pulling pin 19 to LOW.

Parameters: None.

Returns: Nothing.

def disable_board(self):
	GPIO.output(19, GPIO.LOW)
	return

chip_reset

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

write_8bit

Description: Writes 8 bit to the given address.

Parameters:

Returns: Nothing.

def write_8bit(self, register, value):
	self.enable_board()
	register = register | self.write
	self.spi.xfer2([register, value])
	return

read_8bit

Description: Reads 8 bit of data from the given address.

Parameters:

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]

write_16bit

Description: Writes 16 bit to the given address.

Parameters:

Returns: Nothing.

def write_16bit(self, register, value):
	self.enable_board()
	register = register | self.write
	self.spi.xfer2([register, value[0], value[1]])
	return

read_16bit

Description: Reads 16 bit of data from the given address.

Parameters:

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

read_24bit

Description: Reads 24 bit of data from the given address.

Parameters:

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

get_temp

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

get_laenergy

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

get_lappenergy

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

get_period

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

set_operational_mode

Description: Sets the OPMODE. For more information see section OPMODE.

Parameters:

Returns: Nothing.

def set_operational_mode(self, value):
	self.write_8bit(0x0A, value)
	return

set_measurement_mode

Description: Sets the MMODE. For more information see section MMODE.

Parameters:

Returns: Nothing.

def set_measurement_mode(self, value):
	self.write_8bit(0x0B, value)
	return

close_SPI_connection

Description: Closes the SPI connection.

Parameters: None.

Returns**: 0 if connection is closed.

def close_SPI_connection(self):
	self.spi.close()
	return 0

get_aenergy

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

get_active_energy

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

get_apparent_energy

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

get_sample

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

get_vrms

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

get_irms

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

get_sampleperperiod

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:

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

do_n_measurements

Description: Takes nr_samples with sampling period samplerate and saves the measurements into the give file.

Parameters:

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

do_metering

Description: Starts a sampling process and saves the data into a file.

Parameters:

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

change_factors

Description: Changes the multiplication factors for the register values.

Parameters:

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

reset_factors

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

init_nrf24

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

write_nrf24

Description: Sends a message via the RF communication.

Parameters:

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

read_nrf24

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

OPMODE

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.

MMODE

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.