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