Skip to content

A User-Oriented Energy Monitor to Enhance Energy Efficiency in Households

License

Notifications You must be signed in to change notification settings

klemenjak/YoMoPie

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

79 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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:

  • 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.

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 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

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:

  • 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

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:

  • 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

read_8bit

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]

write_16bit

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

read_16bit

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

read_24bit

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

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:

  • value - 8 bit of data representing the OPMODE

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:

  • value - 8 bit of data representing the MMODE

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:

  • 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

do_n_measurements

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

do_metering

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

change_factors

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

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:

  • 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

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.