Consumo Elétrico com ESP8266 e Python
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
Olá, nessa proposta eu teria que testar em cada ponto de consumo? Existe algum medidor que tenha a capacidade de medir diretamente em cada disjuntor do quadro ? Obrigado.
Olá, Gerson. Agradeço sua participação. Essa proposta apresenta um método geral para aquisitar os dados de uma carga. Mas, você pode expandí-lo para a sua situação. Neste caso, você poderia utilizar para cada disjuntor um sensor de corrente não invasivo como o apresentado, devendo considerar a capacidade máxima de corrente que este suporta, no caso, a corrente de cada disjuntor. Existem vários sensores com diferentes correntes máximas disponíveis no site. Outro detalhe, como o ESP8266 possui apenas uma entrada analógica, você pode utilizar um ESP32 (pois possui mais entradas analógicas) ou mesmo um Arduino para obter os dados dos sensores e, em seguida, transferí-los ao ESP8266. No mais, o sistema permanece o mesmo, pode-se aproveitar o código apresentado e apenas acrescentar sensores. Ficou claro?! Para mais dúvidas estou à disposição. Bons estudos!
Daniel, ficou claro. Obrigado pelo retorno.