Fala galera, tudo bem?! Neste artigo irei lhes apresentar um projeto que realiza a análise de consumo elétrico residencial, utilizando o ESP8266 e scripts Python. A ideia é fornecer uma solução para o controle de energia, permitindo estratégias de economia em sua casa. Gostou da ideia?! Então, vamos nessa!
Entendendo o Cálculo do Consumo Elétrico
As distribuidoras de energia elétrica realizam o cálculo do consumo de acordo com a quantidade de potência gasta por equipamentos elétricos e suas horas de uso, na unidade de medida kWh. O valor da conta é dado por uma tarifa aplicada por cada kWh consumido. Para exemplificar, iremos utilizar os seguintes dados de uma lâmpada incandescente:
| Potência Elétrica (W) | Horas de Uso Diário | Consumo Mensal (kWh) | Valor (R$) |
| 40 | 5 | 6 | Tarifa*6 |
Assim, o cálculo para estimar o consumo mensal, utilizando a potência em W, é realizado com a seguinte fórmula:
Consumo = (Potência * Horas * 30)/1000 = (40 * 5 * 30)/1000 = 6 kWh
Dessa forma, conseguimos estimar uma média do consumo mensal e prever o gasto da conta no final do mês. Como pode ser observado, nesta fórmula estimamos um tempo médio diário de uso que nem sempre conseguimos determinar. Mas, e se conseguíssemos analisar em tempo real o consumo de certo equipamento e acompanhar, de forma mais precisa, quanto esse equipamento está gastando?!
Essa é a proposta do projeto apresentado a seguir. Então, vem comigo!
Funcionamento do Sistema
Como mencionado anteriormente, nosso objetivo é conseguir acompanhar o consumo elétrico de determinado equipamento. Para isso, utilizaremos o dispositivo ESP8266 que enviará os dados da potência obtidos do equipamento para o banco de dados relacional MySQL através do script Python sendDatas e o broker MQTT Eclipse.
Em sequência, usaremos outro script Python, analizeDatas, para consultar os dados armazenados e visualizar as atualizações de novas medidas em tempo real. Assim, o esquema descrito pode ser resumido na ilustração a seguir:
Dessa forma, para realização do projeto, necessitaremos de um dispositivo ESP8266 que executará a aquisição dos dados a partir de um sensor de corrente não invasivo conectado ao equipamento.
De acordo com a corrente rms obtida, é calculada a potência elétrica aproximada do equipamento. Assim, o dispositivo enviará as informações aquisitadas para o banco de dados, onde serão armazenadas e processadas pelos scripts em Python.
Com base no esquema apresentado, desenvolveremos, nas próximas seções, o passo a passo para a construção do projeto. A seguir, apresento a lista de itens necessários.
Componentes Necessários
- Placa de Desenvolvimento NodeMCU
- Transformador de Corrente AC ZMCT124A Não Invasivo
- Protoboard (opcional)
- Jumpers
Estes e muitos outros materiais podem ser encontrados em nossa loja.
Montagem do Circuito
Com posse dos componentes essenciais, podemos iniciar a montagem de nosso circuito. Basta realizarmos as seguintes conexões:
Código Detalhado do Dispositivo
/*
Projeto: Análise de Consumo Elétrico com ESP8266
Autor: Daniel Fiuza - AutoCore Robótica
Data: 03/04/2020
Código de Dominio Público.
*/
//============== Declaração de Bibliotecas ====================
#include <NTPClient.h> // Biblioteca do NTP.
#include <WiFiUdp.h> // Biblioteca do UDP.
#include <ESP8266WiFi.h> // Biblioteca do WiFi.
#include "EmonLib.h" // Biblioteca do Sensor de Corrente AC.
#include <PubSubClient.h> // Biblioteca para enviar dados MQTT.
#include <ArduinoJson.h> // Biblioteca para criar mensagem Json.
//============== Definição de Constantes ======================
// Calibração da Corrente. Este valor deve ser obtido com o auxílio de um multímetro,
// modificando o parâmetro CURRENT_CAL até a coincidência entre o valor medido e o mensurado pela IDE.
#define CURRENT_CAL 8.23
#define WIFI_SSID "sua_rede_wifi"
#define WIFI_PASS "sua_senha"
//============== Criação de Instâncias ========================
WiFiUDP udp; // Cria um objeto "UDP".
NTPClient ntp(udp, "0.br.pool.ntp.org", -3 * 3600, 60000); // Cria um objeto "NTP".
WiFiClient wifiClient; // Cria o objeto wifiClient
PubSubClient client(wifiClient); // Cria objeto MQTT client.
EnergyMonitor emon1; // Cria objeto emon1 para obter dados do sensor.
//============= Declaração de Variáveis Globais ===============
int hour; // Armazena o horário obtido do NTP.
int last_hour; // Armazena horário da última medida realizada.
int diff_hour; // Armazena diferença em segundos entre duas medidas.
unsigned long last_time; // Armazena intervalo de tempo entre medidas.
int status = WL_IDLE_STATUS; // Obtém status de conexão Wifi.
//============ Configuração da função void setup() ============
void setup()
{
Serial.begin(115200);//Inicia a comunicação serial.
emon1.current(A0, CURRENT_CAL); // Inicializa parâmetros do sensor de corrente.
client.setServer("mqtt.eclipse.org", 1883); // Configura conexão MQTT.
WiFi.begin(WIFI_SSID, WIFI_PASS);
while ( WiFi.status() != WL_CONNECTED ) {
delay ( 500 );
Serial.print ( "." );
}
ntp.begin(); // Inicia o cliente NTP.
ntp.forceUpdate(); // Força o Update.
last_hour = ntp.getEpochTime(); // Armazena primeira medida com a hora atual.
}
//============ Configuração da função void loop() ============
void loop(){
if ( !client.connected() ) {
reconnect(); // Conecta-se ao broker MQTT.
}
client.loop(); // Mantém conexão MQTT aberta.
// Atualiza novas medidas a cada 1s.
if ( millis() - last_time > 1000 ) {
emon1.calcVI(40,500); // Calcula Corrente AC.
float Current = emon1.Irms; // Obtém valor da corrente calculado.
float Power = Current * 220.00; // Calcula valor aproximado da Potência Aparente.
ntp.update();
hour = (int) ntp.getEpochTime(); // Obtém horário em épocas.
// Atualiza diferença entre horários em segundos.
if(last_hour != hour){
diff_hour = hour - last_hour;
last_hour = hour;
}
// Devido as oscilações iniciais, limitamos a corrente em uma margem.
if(Current < 0.019) {
Current = 0.00;
Power = 0.00;
}
// Envia dados do sensor ao Broker MQTT no formato Json
StaticJsonDocument<200> datas;
datas["Irms"] = Current;
datas["Power"] = Power;
datas["Hour"] = hour;
datas["Diff_hour"] = diff_hour;
String payload;
serializeJson(datas, payload);
client.publish("energymeter/send", payload.c_str()); // Publica mensagem MQTT
last_time = millis();
}
}
//============ Configuração da função void reconnect() ============
void reconnect() {
while (!client.connected()) {
status = WiFi.status();
// Caso não esteja conectado ao wifi, inicia conexão
if ( status != WL_CONNECTED) {
WiFi.begin(WIFI_SSID, WIFI_PASS);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("Conectado ao AP");
}
Serial.print("Conectando-se ao Broker MQTT ...");
// Inicia conexão MQTT
if ( client.connect("ESP8266 Device") ) {
Serial.println( "[MQTT DONE]" ); // Conexão MQTT estabelecida.
} else {
Serial.print( "[MQTT FAILED] [ rc = " ); // Conexão MQTT falhou.
Serial.print( client.state() );
Serial.println( " : retrying in 5 seconds]" );
// Aguarda 5 segundos antes de se reconectar
delay( 5000 );
}
}
}
Configurando Banco de Dados
Como informado anteriormente, iremos salvar os dados obtidos do dispositivo no banco de dados relacional MySQL. Para isso, você precisará tê-lo instalado localmente ou em algum serviço em nuvem.
Em seguida, iremos criar o banco e a tabela onde iremos armazenar os dados. Dessa forma, execute o comando:
CREATE DATABASE energy_meter;
Como pode ser observado, ele cria o banco de dados que iremos utilizar. Após isso, adicionaremos a tabela datas de acordo com o comando a seguir:
CREATE TABLE datas(
id int(4) AUTO_INCREMENT,
current float,
power float,
diff_hour int(4),
hour datetime,
PRIMARY KEY (id)
);
Feito isso, teremos criado uma tabela com cinco colunas. A coluna current e power armazenaremos os dados medidos da corrente rms e da potência aparente, respectivamente.
O campo diff_hour armazena a diferença de horário entre a medida atual e a anterior. Por fim, o campo hour contém o horário atual em que a medida foi realizada.
Agora, podemos inserir o dados em nosso banco. Para isso, apresentarei a seguir o script em Python que receberá as mensagens do dispositivo por uma conexão MQTT e as enviará ao MySQL.
Enviando Dados Aquisitados ao Banco de Dados
Nesta etapa, criaremos o script sendDatas.py com o seguinte código:
import paho.mqtt.client as mqtt # Módulo MQTT
import time # Módulo para trabalhar com tempo e conversões
import pymysql.cursors # Módulo Mysql (Banco de Dados)
import json # Módulo Json
################################ FUNÇÕES MYSQL ###############################
# Insere mensagens recebidas no banco de dados.
def send_mysql(msg_vector):
# Inicia conexão com o Banco de Dados Mysql
try:
connection = pymysql.connect(host='localhost',
user='nome-de-usuario',
password='senha-do-usuario',
db='energy_meter',
charset='utf8mb4',
cursorclass=pymysql.cursors.DictCursor)
except:
print("Não foi possível conectar-se ao banco de dados.")
return 0
try:
with connection.cursor() as cursor:
sql = "INSERT INTO datas (current, power, diff_hour, hour) VALUES(%s, %s, %s, %s)"
# Executa query Insert
cursor.execute(sql,( msg_vector[0], msg_vector[1], msg_vector[2], msg_vector[3] ))
# Confirma a transação realizada
connection.commit()
print("Dados inseridos: current = "+str(msg_vector[0])+", power = "+str(msg_vector[1])+" , diff_hour = "+str(msg_vector[2])+", hour = "+str(msg_vector[3]) )
except:
print("Falha ao inserir dados no banco de dados.")
finally:
# Encerra conexão com Mysql
connection.close()
################################ FUNÇÕES MQTT ################################
# Define função de retorno de chamada ao conectar-se com o Broker.
def on_connect(client, userdata, flags, rc):
print("Conectado ao broker.")
# Inscreve-se no tópico para receber mensagens.
client.subscribe("energymeter/send")
# Define função de retorno de chamada ao receber mensagem.
def on_message(client, userdata, msg):
# Converte mensagem em bytes para string
msg_string=str(msg.payload.decode("utf-8","ignore"))
# Desserializa string Json para dicionário Python
dict_json=json.loads(msg_string)
# Arredonda corrente para duas casas decimais
Irms = round(dict_json["Irms"], 2)
# Arredonda Potencia para duas casas decimais
Power = round(dict_json["Power"],2)
# Armazena diff_hour
Diff_hour = int(dict_json['Diff_hour'])
# Obs: Esta hora está no padrão época
Hour_epoch = int(dict_json["Hour"])
# Converte a hora do padrão época para o padrão data e hora do Mysql
Hour = time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(Hour_epoch))
# Armazena dados formatados no vetor msg_formated
msg_formated = [Irms, Power, Diff_hour, Hour]
# Função que insere os dados no Mysql
send_mysql(msg_formated)
# Define função de retorno de chamada após uma desconexão.
def on_disconnect(client, userdata, rc):
if rc != 0:
print("Desconexão MQTT Inesperada.")
print("Reconectando-se ao Broker em 3 segundos...")
time.sleep(3)
client.connect("mqtt.eclipse.org", 1883, 60)
# Instancia cliente MQTT.
client = mqtt.Client()
client.on_connect = on_connect # Define como callback a função on_connect
client.on_message = on_message # Define como callback a função on_message
client.on_disconnect = on_disconnect # Define como callback a função on_disconnect
# Inicia conexão MQTT com o Broker Mosquitto.
client.connect("mqtt.eclipse.org", 1883, 60)
client.loop_forever()
Entendendo o Script de Envio dos Dados
Primeiramente é necessário importar os seguintes módulos para o correto funcionamento do script:
import paho.mqtt.client as mqtt # Módulo MQTT import time # Módulo para trabalhar com tempo e conversões import pymysql.cursors # Módulo Mysql (Banco de Dados) import json # Módulo Json
Inicialmente, o script instancia o cliente MQTT, definindo suas funções de callback e, em seguida, inicia a conexão. A função client.loop_forever() mantém a conexão aberta, permitindo receber e processar os eventos de callback, como pode ser visto no trecho a seguir:
# Instancia cliente MQTT.
client = mqtt.Client()
client.on_connect = on_connect # Define como callback a função on_connect
client.on_message = on_message # Define como callback a função on_message
client.on_disconnect = on_disconnect # Define como callback a função on_disconnect
# Inicia conexão MQTT com o Broker Mosquitto.
client.connect("mqtt.eclipse.org", 1883, 60)
client.loop_forever()
Ao conectar-se com o broker, a função on_connect() é executada, a qual realiza a inscrição no tópico “energymeter/send”, permitindo receber as mensagens do dispositivo. Essa função é demonstrada a seguir:
def on_connect(client, userdata, flags, rc):
print("Conectado ao broker.")
# Inscreve-se no tópico para receber mensagens.
client.subscribe("energymeter/send")
Após a inscrição no tópico descrito, novas mensagens são recebidas e processadas pela função on_message() que as converte em um dicionário, estrutura equivalente ao vetor em outras linguagens de programação, da seguinte forma:
# Converte mensagem em bytes para string
msg_string=str(msg.payload.decode("utf-8","ignore"))
# Desserializa string Json para dicionário Python
dict_json=json.loads(msg_string)
Depois, os dados formatados são armazenados no vetor msg_formated, o qual é passado como parâmetro da função send_mysql() que enviará os dados para o banco MySQL através da query insert, como visto a seguir:
# Arredonda corrente para duas casas decimais
Irms = round(dict_json["Irms"], 2)
# Arredonda Potencia para duas casas decimais
Power = round(dict_json["Power"],2)
# Armazena diff_hour
Diff_hour = int(dict_json['Diff_hour'])
# Obs: Esta hora está no padrão época
Hour_epoch = int(dict_json["Hour"])
# Converte a hora do padrão época para o padrão data e hora do Mysql
Hour = time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(Hour_epoch))
# Armazena dados formatados no vetor msg_formated
msg_formated = [Irms, Power, Diff_hour, Hour]
# Função que insere os dados no Mysql
send_mysql(msg_formated)
Entendido o fluxo do script apresentado, demonstrarei, a seguir, o código de visualização dos dados do dispositivo. Sua estrutura é semelhante ao primeiro script, facilitando bastante sua compreensão.
Visualizando os Dados do Consumo Elétrico
Para a visualização dos dados, crie o script analisysMeters.py com o seguinte código:
import matplotlib.pyplot as plt # Módulo para imprimir gráfico
import numpy as np # Módulo para manipular vetores
import paho.mqtt.client as mqtt # Módulo MQTT
import time # Módulo para trabalhar com tempo e conversões
import pymysql.cursors # Módulo Mysql (Banco de Dados)
import json # Módulo Json
import datetime # Módulo para manipular datas e strings
# Define estilo do gráfico
plt.style.use('ggplot')
########################## FUNÇÃO PLOTAR GRÁFICO #############################
def live_plotter(x_vec,y1_data,line1,pause_time=0.1):
if line1==[]:
# Esta chamada permite plotar o gráfico dinamicamente
plt.ion()
fig = plt.figure(figsize=(13,6))
ax = fig.add_subplot(111)
# Cria uma variável referente aos dados do gráfico para, posteriormente, atualizá-la
line1, = ax.plot(x_vec,y1_data,'-o',alpha=0.8)
# Atualiza label/title
plt.ylabel('Potência (W)')
plt.title('Consumo Elétrico em Tempo Real')
plt.show()
# Após a criação da figura, eixo e linha, nós precisamos atualizar o dados do eixo y
line1.set_ydata(y1_data)
# Ajusta os limites se novos dados estiverem além destes
if np.min(y1_data)<=line1.axes.get_ylim()[0] or np.max(y1_data)>=line1.axes.get_ylim()[1]:
plt.ylim([np.min(y1_data)-np.std(y1_data),np.max(y1_data)+np.std(y1_data)])
# Pausa os dados para que a figura / eixo possam recuperar o atraso
plt.pause(pause_time)
# Retorna a variável line1 para que ela seja atualizada na próxima iteração
return line1
################################ FUNÇÕES MYSQL ###############################
# Insere mensagens recebidas no banco de dados.
def get_data_mysql(size, query=''):
# Inicia conexão com o Banco de Dados Mysql
try:
connection = pymysql.connect(host='localhost',
user='nome-de-usuario',
password='senha-de-usuario',
db='energy_meter',
charset='utf8mb4',
cursorclass=pymysql.cursors.DictCursor)
except:
print("Não foi possível conectar-se ao banco de dados.")
return 0
try:
with connection.cursor() as cursor:
if query == 'get_value': # Obtém o valor em real total até o momento.
sql = "SELECT SUM(power*diff_hour*0.79857/(1000*3600)) AS valor_total FROM datas;"
else:
sql = "SELECT power, hour FROM datas ORDER BY hour DESC LIMIT "+str(size)+";"
# Executa query Select
cursor.execute(sql)
# Confirma a transação realizada
connection.commit()
result = cursor.fetchall()
# Retorna resultado
return result
except:
print("Falha ao inserir dados no banco de dados.")
finally:
# Encerra conexão com Mysql
connection.close()
#####################################################################################
################################ FUNÇÕES MQTT ################################
# Define função de retorno de chamada ao conectar-se com o Broker.
def on_connect(client, userdata, flags, rc):
print("Conectado ao broker.")
# Inscreve-se no tópico para receber mensagens.
client.subscribe("energymeter/send")
# Define função de retorno de chamada ao receber mensagem.
def on_message(client, userdata, msg):
# Converte mensagem em bytes para string
msg_string=str(msg.payload.decode("utf-8","ignore"))
# Desserializa string Json para dicionário Python
dict_json=json.loads(msg_string)
# Arredonda corrente para duas casas decimais
Irms = round(dict_json["Irms"], 2)
# Arredonda Potencia para duas casas decimais
Power = round(dict_json["Power"],2)
# Armazena diff_hour
Diff_hour = int(dict_json["Diff_hour"])
# Obs: Esta hora está no padrão época
Hour_epoch = int(dict_json["Hour"])
# Converte a hora do padrão época para o padrão data e hora do Mysql
Hour = datetime.datetime.fromtimestamp(float(str(Hour_epoch)))
# Informa que as seguintes variáveis são globais
global y_vec, x_vec, valor_total, taxa
# Elimina primeiro elemento do array e adiciona 0 como último elemento
y_vec = np.append(y_vec[1:],0.0)
x_vec = np.append(x_vec[1:],0.0)
# Substitui último elemento do array pelos valores atualizados
y_vec[-1] = Power
x_vec[-1] = Hour
# Atualiza Gasto total
valor_total += Power*Diff_hour*taxa
print("Valor Gasto Atual: R$ "+str(round(valor_total,6)))
# Define função de retorno de chamada após uma desconexão.
def on_disconnect(client, userdata, rc):
if rc != 0:
print("Desconexão MQTT Inesperada.")
print("Reconectando-se ao Broker em 3 segundos...")
time.sleep(3)
client.connect("mqtt.eclipse.org", 1883, 60)
############################# TRECHO INICIAL ######################################
print("Iniciando script ")
# Instancia cliente MQTT.
client = mqtt.Client()
client.on_connect = on_connect # Define como callback a função on_connect
client.on_message = on_message # Define como callback a função on_message
client.on_disconnect = on_disconnect # Define como callback a função on_disconnect
# Variáveis Globais.
size = 50 # Tamanho da amostra de dados
valor_total = 0 # Valor em real total
line1, y_vec, x_vec = [], [], [] # Arrays usados para elaborar o gráfico
taxa = 0.79857/(1000*3600) # cálculo de conversão considerando 1s cada amostra
# Obtém os últimos 50 dados do Banco de dados.
result = get_data_mysql(size)
# Insere os dados obtidos do Banco de Dados nos arrays y_vec e x_vec
for x in reversed(result):
y_vec.append(float(x['power']))
x_vec.append(x['hour'])
# Obtém o valor total em real até o momento do Banco de dados.
result_valor = get_data_mysql(size,'get_value')
# Atribui o valor recebido do banco de dados à variável valor_total.
for x in result_valor:
valor_total = float(x['valor_total'])
# Inicia conexão MQTT com o Broker.
client.connect("mqtt.eclipse.org", 1883, 60)
while True:
# Atualiza constantemente o gráfico
line1 = live_plotter(x_vec,y_vec,line1)
# Mantém conexão MQTT aberta
client.loop_start()
Explicação Detalhada do Script de Visualização
Se você entendeu o script anterior, esse será tranquilo. Além disso, considero esta a parte mais interessante. Então, vamos lá!
Nosso código inicia instanciando o cliente MQTT e definindo suas funções de callback. Até agora, nada de novo. Em seguida, são declarada algumas variáveis globais que serão utilizadas ao longo do nosso script, apresentadas no seguinte trecho:
# Variáveis Globais. size = 50 # Tamanho da amostra de dados valor_total = 0 # Valor em real total line1, y_vec, x_vec = [], [], [] # Arrays usados para elaborar o gráfico taxa = 0.79857/(1000*3600) # cálculo de conversão considerando 1s cada amostra
Como pode observar, a variável taxa é uma constante que resulta da tarifa da concessionária de energia dividida pelo valor (1000*3600). Esse termo apenas converte a potência obtida em Ws para KWh, unidade de medida utilizada pelas distribuidoras.
Então, essa taxa será aplicada à potência recebida do dispositivo, obtendo-se o valor em real do consumo elétrico.
Prosseguindo no código, é solicitada, duas vezes, a função get_data_mysql(). A primeira chamada realiza uma query select para obter os últimos 50 valores da potência e seus respectivos horários, armazenando-os nos vetores x_vec e y_vec.
Em seguida, é solicitada novamente, onde se obtém o valor total do consumo elétrico e o armazena na variável valor_total. Isso é referido no trecho:
# Obtém os últimos 50 dados do Banco de dados. result = get_data_mysql(size) # Insere os dados obtidos do Banco de Dados nos arrays y_vec e x_vec for x in reversed(result): y_vec.append(float(x['power'])) x_vec.append(x['hour']) # Obtém o valor total em real até o momento do Banco de dados. result_valor = get_data_mysql(size,'get_value') # Atribui o valor recebido do banco de dados à variável valor_total. for x in result_valor: valor_total = float(x['valor_total'])
Logo após, é iniciada a conexão MQTT e definido o loop while. Então, o código atualizará constantemente o gráfico, mantendo a conexão MQTT aberta.
Ao receber uma nova mensagem do dispositivo, a função on_message() formata os dados e atualiza os vetores do gráfico, assim como, a variável valor_total. Esse trecho é observado a seguir:
# Informa que as seguintes variáveis são globais
global y_vec, x_vec, valor_total, taxa
# Elimina primeiro elemento do array e adiciona 0 como último elemento
y_vec = np.append(y_vec[1:],0.0)
x_vec = np.append(x_vec[1:],0.0)
# Substitui último elemento do array pelos valores atualizados
y_vec[-1] = Power
x_vec[-1] = Hour
# Atualiza Gasto total
valor_total += Power*Diff_hour*taxa
print("Valor Gasto Atual: R$ "+str(round(valor_total,6)))
Dessa forma, é apresentado o consumo elétrico, em tempo real, do equipamento permitindo monitorá-lo e, até mesmo, realizar estratégias de economia.
Resultado do Projeto
A seguir apresento um vídeo demonstrando o resultado obtido do projeto:
Então, gostou do projeto? Espero que ele possa estimular novas propostas e colaborar com o seu desenvolvimento acerca do tema. Caso haja alguma dúvida, comente abaixo. Confira também mais conteúdos em nosso Blog
