Medir consumo de corriente con Arduino (parte 2)

Este módulo ESP8266 compatible con Arduino realiza una lectura de los amperes con el consumo de corriente de mi casa, cada un segundo.

lector de consumo watts con ArduinoEn el artículo anterior hay una explicación del hardware, así como enlaces a mi tienda online favorita, Amazon.com, para que vean sus precios y compra.

El ESP8266  almacena el dato obtenido segundo a segundo en un array de 180 valores que va rotando para tener siempre los últimos 3 minutos de consumo y así poder graficarlos.

A su vez, almacena en otro array el promedio de consumo cada 3 minutos, para tener así una segunda gráfica con el consumo de las últimas 9 horas.

El ESP8266 muestra en la pantalla LCD los amperes y watts que va registrando, así como una barra de “consumo de pontencia” que va de 0 a 30 amperes (el máximo que puede leer la pinza amperimétrica que utilizamos como sensor de consumo).

Nuestro ESP8266 tiene un servidor web que nos muestra las gráficas que comentamos al principio. Para ello necesita conectarse a nuestra red WIFI.

Este ESP8266 incluía originalmente una función extra, que se dedicaba a buscar a un segundo ESP8266 que pudiera estar conectado en la red WIFI. Este segundo módulo oficia de concentrador de datos y servidor web, presentando un panorama visual completo de todos los módulos ESP8266 que se encuentran repartidos en mi casa, recopilando distinta información.

Esa parte más compleja del código ha sido anulada ya que formará parte de otro tutorial de Arduino, más adelante.

Detalles de funcionamiento de este proyecto Arduino

Al inicio, el ESP8266 intentará conectarse a nuestra red WIFI.

Si es la primera vez que se enciende, o no la encuentra, cambiará su modo a ACCESS POINT, generando su propio punto de acceso wifi de nombre ELECTROMAIN.

Una vez nos conectemos al ssid ELECTROMAIN, podremos navegar a una página web de configuración, tremendamente sencilla, que se encuentra en http://192.168.4.1

Allí veremos las redes WIFI que el ESP8266 encuentra en su vecindad, para poder verificar que estamos dentro del alcance de nuestro propio router WIFI, y tendremos dos casilleros para ingresar el SSID y la CLAVE para conectarnos.

armando-el-arduino-para-medir-consumo-corriente-de-la-casa-y-reportar-via-wifiUna vez ingresados ambos datos, el ESP8266 intentará conectarse a nuestra red WIFI.

Si lo logra, desconectará el ACCESS POINT, y en la pantalla LCD indicará la IP asignada, para que podamos navegar hasta él normalmente.

Si nuestro sistema operativo soporta el protocolo mDNS, también podremos navegar hasta este módulo ESP8266 utilizando la url http://electro.local

Pinza amperimétrica y Arduino

La pinza amperimétrica entregará un resultado de -0.5 a 0.5 volts (1v de amplitud) en relación directa a la corriente que pasa por el cable que está midiendo.

Por ese motivo, utilizaremos un circuito muy sencillo con dos resistencias y un capacitor de 10uf para mover medio volt hacia arriba el retorno de la pinza amperimétrica, y de paso, filtrar un poco su señal (que incluye picos espúreos, originalmente).

La lógica de este pequeñito circuito se explica en la página del proyecto open energy monitor.

Considerar que en el esquema allí presentado, los voltajes de alimentación de nuestro ESP8266 en nuestro caso son 3.3v en vez de 5v. Además la pinza amperimétrica que utilizan como ejemplo entrega otros rangos de voltaje.

circuito offset esp8266 AD 0.5vLo importante es que para nuestro caso, el C1 es de 10uf, R1 y R2 son de 10k y 56k respectivamente (cuidado, en el dibujo original se utilizaron valores erróneos).

El conector tipo “spica” de la pinza amperimétrica tiene dos contactos, la punta y la tierra. La punta va conectada al pin del conversor analógico del ESP8266.

La tierra va al pequeño circuito que entrega ese offset de medio volt.   La imagen del costado  izquierdo fue recortada del diseño de TINKERMAN, para su MOTEINO EMON SHIELD, quien dicho sea de paso me orientó en esta parte de mi proyecto, con esta solución.

Código Fuente de este proyecto Arduino

/* 
TAREAS PARA ESTE MÓDULO:
1) MEDIR WATTS POR PINZA AMPERIMETRICA
2) ACTUALIZAR EN TIEMPO REAL EN LCD
3) CONTROLAR WIFI
4) (no se hace en este ejemplo) MANDAR PAQUETES CON CONSUMO CADA 10 SEGUNDOS APROX AL ESP8266 "MAESTRO"

LCD: 20x4 con serial module lcd.Initialize(0x27, 20, 4) 'el lcd 20x4 es de 5v y usa el 0x27 o 0x3f para conectarse 
USO DE LA PINZA AMPERIMETRICA: https://bitbucket.org/xoseperez/emonliteesp
*/

#include <Wire.h>             // Asi podemos definir los pines del i2c para el ESP8266
#include <LiquidCrystal_I2C.h>
#include "EmonLiteESP.h"
#include <ESP8266WiFi.h>	  // Librería con las funciones básicas para el wifi
#include <ESP8266WebServer.h>
#include <ESP8266mDNS.h>

#define ejeygraf 150 //Altura de las barras en las gráficas via web
#define ejexgraf 180 //Ancho de las barras en las gráficas via web

const unsigned long millisxhora = 1000 * 60 * 60;
const unsigned long millisx25seg = 1000 * 25;
const unsigned long millisxseg = 1000;
const unsigned int sensorute = A0; // el sensor de corriente va al A0 (punta) y GND (malla)
const unsigned int voltaje = 230; //voltaje promedio de la red eléctrica en mi país.

LiquidCrystal_I2C lcd(0x27, 20, 4);
EmonLiteESP power;
MDNSResponder mdns;
ESP8266WebServer server(80);
WiFiClient client;

unsigned long mspasados25seg = 0;	//Vamos a llevar cuenta de los milisegundos que van transcurriendo para interrumpir el loop cada 25 segundos
unsigned long mspasadosseg = 0;		//Vamos a llevar cuenta de los milisegundos que van transcurriendo para interrumpir el loop cada segundo
unsigned long mspasadoshora = 0; // Vamos a llevar la cuenta de milisegundos que pasan para contar la hora
uint horas = 0; //contador de horas.  Al ser INT, 65535 horas sería el máximo ... más de 7 años sin apagarse debería ser suficiente.

int watts = 0; //Aqui guarda los watts
double amps = 0; //Y guarda los amps
int graf[2][ejexgraf]; //Guardo los ultimos 53 minutos de información en watts y las últimas 9 horas 
byte pos[2];  //Se guarda la posición para recibir el nuevo dato en los arrays de arriba
long acumulador = 0; //Voy sumando los valores de los 5 minutos de la gráfica instantánea, asi cuando se llena, saco promedio para guardar un dato mas en la histórica
long acumuladorweb = 0; //Acumulador para sumar watts y promediar, enviando valor a servidor
uint segundos = 0; //Contador de segundos pasados
bool configurandoAP = false;

unsigned int currentCallback()  // Un callback que le da a la libreria EmonLiteESP de corriente el valor del sensor de corriente
{ 
	return analogRead(sensorute);
}

void setup()
{
	Wire.begin(2, 14); //GPIO2 es D4 y GPIO14 es D5 en el ESP8266
	lcd.begin(20, 4);
	lcd.init();
	lcd.backlight();
	lcd.setCursor(0, 0);
	lcd.print("SENSOR CORRIENTE 1.0");
	mspasadosseg = millis(); //arranco contadores de milisegundos para actuar cada segundo y hora
	mspasadoshora = millis();
	mspasados25seg = millis();
	power.initCurrent(currentCallback, 10, 1.0, 30);  //ADC 10 bits, escala de voltaje 1V=30 amperes
	power.setPrecision(2); //dos lugares decimales de precisión
	//limpio los arrays de los watts y punteros:
	for (byte i = 0; i < ejexgraf; i++) { graf[0][i] = 0; graf[1][i] = 0; } 
pos[0] = pos[1] = 0; 
power.warmup(); // Que la rutina de EmonLite se balancee para arrancar con un valor real 
power.warmup(); // Lo hago dos veces, porque con una no alcanza. 
conectarwifi(true); //Activamos wifi (si no detecta red, activa el Access Point de configuración) 
server.on("/", root); //Configuramos el servidor web para que la página root vaya a la función root 
server.on("/setap", webconfiguracionguardar); //Y para guardar parametros de wifi en caso de estar configurando 
server.begin(); } 
/* ==== Loop ==== */ 
void loop() { 
        server.handleClient(); //manejo el servidor web 
        mdns.update(); //respondo eventuales peticiones mdns 
       yield(); 
     if ((millis() - mspasadosseg) > millisxseg)  //Si ya pasó un segundo ...
	{
		mspasadosseg = millis();
		actualizodatos(); //actualizo datos del sensor y la pantalla LCD cada segundo
		//Veo si pasaron 25 segundos, para -si no hay conectividad- reintentar conectar
		if ((millis() - mspasados25seg) > millisx25seg) { //cada 25 segundos me fijo si la conectividad sigue en pie y además veo si pasó una hora
			mspasados25seg = millis();
			if ((configurandoAP == false) && (WiFi.status() != WL_CONNECTED)) conectarwifi(false); //reintento conexión a la red WIFI salvo que esté en modo AP
			if ((millis() - mspasadoshora) > millisxhora) { //vemos si pasó una hora
				mspasadoshora = millis();
				horas++; //incremento contador de horas que lleva el ESP8266 encendido
			}
		}
	}
}

void actualizodatos()
{
	//LEVANTO DATO CRUDO, CORRIJO Y CALCULO WATTS Y AMPS: 
	amps = power.getCurrent(1000);
	//corrijo por falta de precisión de pinza amperimétrica
	if (amps < 0.04) {
		amps = 0;
	}
	else if (amps > 30) {
		amps = 30;
	}
	watts = amps * voltaje;
	//Le muestro watts y amps
	lcd.clear(); 
	lcd.print("  " + (String)watts + " W / " + (String)amps + " A");
	for (int i = 0; i < (amps / 1.55); i++)
	{
		lcd.setCursor(i, 1);
		lcd.write((uint8_t)255);
	}
	yield();
	lcd.setCursor(0, 2);
	lcd.print("min<------------>max");
	graf[0][pos[0]] = watts;  //Guardo los watts
	acumulador += watts; //guardo en el acumulador para promediar como histórico
	acumuladorweb += watts; // y guardo en el acumulador para informar al server
	segundos++; //Incremento contador de valores en acumuladores
	pos[0]++;
	//Ya llené el array entero (300 segundos, vuelvo al segundo 0, y guardo el promedio de estos 5 minutos en el histórico)
	if (pos[0] == ejexgraf) {
		pos[0] = 0;
		acumulador = acumulador / ejexgraf; //Hago el promedio
		graf[1][pos[1]] = acumulador; //guardo un dato más en la graf histórica
		acumulador = 0; //reseteo acumulador para promedio de nuevos 5 minutos
		pos[1]++;
		if (pos[1] == ejexgraf) pos[1] = 0;
	}
}


void conectarwifi(bool configurar)
{
	lcd.clear();
	lcd.print("Buscando red WIFI");
	if (testearwifi()) {
		activarserver();
	}
	else if (configurar) //Si no pudo conectarse al WIFI, y viene de ser recién encendido o reseteado, lo dejamos configurar un wifi:
	{
		activarserverconfig();
	}
}

//Testeo si hay conexión WIFI al SSID / PASSWORD que el ESP8266 tiene guardado
bool testearwifi()
{
	for (byte i = 0; i < 20; i++)
	{
		if (WiFi.status() == WL_CONNECTED) return true;
		delay(500);
		lcd.setCursor(i, 1);
		lcd.print(".");
	}
	return false;
}


void activarserver()
{
	//EL ESP8266 se conectó al SSID que venia en su memoria, asi que le aviso y arranco el servidor web con nuestra página
	WiFi.mode(WIFI_STA); //Colocamos al ESP8266 en modo WIFI STATION, asumiendo que ya tiene configurada la SSID y la clave.
	delay(100);
	yield();
	configurandoAP = false;
	lcd.clear();
	lcd.setCursor(0, 0);
	lcd.print("Conectado a:");
	lcd.setCursor(0, 1); 
	lcd.print((String)WiFi.SSID());
	delay(100);
	lcd.setCursor(0, 2);
	lcd.print("IP: " + WiFi.localIP().toString());
	WiFi.hostname("electro");
	if (mdns.begin("electro", WiFi.localIP()))
	{  //Pude activar el mDNS para que -en algunos OS- pueda encontrar el sensor en http://electro.local
            lcd.setCursor(0, 3);
	    lcd.print("http://electro.local");
        }
	delay(3000);
}

void activarserverconfig()
{
	configurandoAP = true;
	WiFi.mode(WIFI_STA);
	WiFi.disconnect();
	delay(100);
	WiFi.mode(WIFI_AP_STA);  //Activamos el modo access point del ESP8266
	WiFi.softAP("ELECTROMAIN");
	lcd.clear();
	lcd.setCursor(0, 0);
	lcd.print("Configurar WIFI");
	lcd.setCursor(0, 1);
	lcd.print("ssid ELECTROMAIN");
	lcd.setCursor(0, 2);
	lcd.print("IP " + WiFi.softAPIP().toString()); //Le muestro como puede conectarse al AP
	delay(5000);
}

//La página web raíz dependerá de si estamos configurando con AP mode el wifi, o mostrando los datos normalmente
void root()
{
	if (configurandoAP)
	{
		webconfiguracion();
	}
	else {
		webdatos();
	}
}

//Genera gráfica para la web con divs de diversa altura. 
void graficoweb(byte grafica)
{
	String retorno = "";
	int puntero = 0; //en la grafica web, se usa este puntero para dibujar hacia atrás cada barrita.
	uint valor,maximo = 0;
	uint minimo = 8000;
	//Primero hallo maximo y mínimo en este array delta de 41 valores
	for (byte i = 0; i<ejexgraf; i++) {
		if (maximo < graf[grafica][i]) { maximo = graf[grafica][i]; } else if (minimo > graf[grafica][i]) { minimo = graf[grafica][i]; }
	}
	//tengo el máximo y el mínimo: Armo max - min en costado izquierdo onda ejey
	server.sendContent("<div id=e>" + (String)maximo + "<div style=\"height:130px;\"></div>" + (String)minimo + "</div>"); //mando al cliente x web la escala max min vertical
	maximo = maximo - minimo; // ahora minimo representa CERO y máximo está ajustado a esta nueva escala
	if (maximo == 0) maximo=1;
	puntero = pos[grafica];
	for (byte i = ejexgraf; i > 0; i--) {
		//incluso en el primer caso, puntero tiene que ser puntero-1 ya que puntero siempre apunta a proxima posicion "en limpio" a almacenar en los arrays
		if (puntero > 0) { puntero--; }
		else { puntero = ejexgraf - 1; }
		//El dato mas reciente viene en la var puntero: Grafico ese punto y de ahi para atrás en el array. Es un sinfin: Si ya leí valor cero,continúo por el tope
		valor = graf[grafica][puntero];
		valor = valor - minimo; //shifteo el valor al nuevo CERO
		//Regla de tres contra la altura máxima del Eje y el valor máximo que se va a necesitar graficar
		valor = (valor * ejeygraf) / (maximo);
		yield();
		retorno = "<div id=b title=" + (String)graf[grafica][puntero] + "w style=\"height:" + (String)valor + ";\"></div>" + retorno;
	}
	server.sendContent(retorno); //mando al cliente x web la gráfica
}

//Envía header y estilos al usuario via web ATENCIÓN, no cierra el style ni el header, eso queda para hacer en cada página, por si hay que agregar mas styles y headers.
void arranqueweb() 
{
	server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
	server.sendHeader("Pragma", "no-cache");
	server.sendHeader("Expires", "-1");
	server.setContentLength(CONTENT_LENGTH_UNKNOWN);
	server.sendContent("<html><head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" /><meta name=\"apple-mobile-web-app-capable\" content=\"yes\" /><meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black-translucent\" /><style>body {font-family:arial;} h1 {font-size:20px;}  h3, hr{clear:both;font-size:16px;} h6 {clear:both;border-top:1px solid #000;font-size:9px;width:100%;text-align:center;}"); //inicializo cabezera página contemplando navegación móvil y aplico font ARIAL y otros detalles
}

//Envía cierre de página al usuario via web incluyendo pié de página
void cierreweb() 
{
	server.sendContent("<h6>(C)Enrique Avalle 12/2016 v1</h6></body></html>");
	server.client().stop();
}

void webconfiguracion()
{
	arranqueweb();
	server.sendContent("</style></head><body><h1>Nodo Micro El&eacute;ctrico</h1><h2>Configuraci&oacute;n WiFi</h2>");
	String listassids = "";
	int n = WiFi.scanNetworks();
	if (n > 0)
	{ //Hay SSDID en la vuelta, genero lista
		server.sendContent("<h3>Lista de redes WIFI accesibles:</h3>");
		server.sendContent("<ol>");
		for (int i = 0; i < n; ++i)
		{
			server.sendContent("<li>");
			server.sendContent(WiFi.SSID(i));
			server.sendContent(" (");
			server.sendContent((String)WiFi.RSSI(i));
			server.sendContent(")");
			server.sendContent((WiFi.encryptionType(i) == ENC_TYPE_NONE) ? " " : "*");
			server.sendContent("</li>");
		}
		server.sendContent("</ol>");
	}
	server.sendContent("<form method='get' action='setap'><label>SSID: </label><input name='ssid' length=32><input name='pass' length=64><input type='submit'></form>");
	cierreweb();
}

//Pagina con la información de consumo eléctrico
void webdatos() 
{
	arranqueweb();
	server.sendContent(" .d{float:left;text-align:center;height:80px;width:92;margin:2px 2px 2px 2px;border:1px solid #777;}"); //Rectángulos para datos numericos temp hum y pres 
	server.sendContent(" #v{background-color:#bfb;} #a{background-color:#ffb;} #r{background-color:#fbb;}"); //Fondo verde, amarillo y rojo para los recuadros de temperatura
	server.sendContent("#x {font-size:12px;margin:9px auto 9px auto;} "); //textito para las etiquetas y unidades de los rectángulos temp, hum y pres
	server.sendContent("#y {font-size:18px;margin:2px auto 2px auto;font-weight:bold;} "); // textito para los valores de los rectángulos temp, hum y pres
	server.sendContent(" #g {float:left;margin:10px;width:300px;height:180px;background-color:#eee;}"); // recuadro para cada gráfica y titulo en general
	server.sendContent(" #c{height:100px;width:275px;margin:0 auto 0 auto;}"); //contenedor para las barras de la gráfica
	server.sendContent(" #b {width:3px;border-bottom:1px solid blue;display:inline-block;position:relative;background-color:blue;vertical-align:baseline;}"); //barras de cada gráfica
	server.sendContent(" #e {width:20px;height:150px;text-align:right;padding-right:2px;border-right:1px solid black;font-size:10px;float:left;}"); //costado izq con max y min para cada gráfica
	IPAddress ip = WiFi.localIP();
	server.sendContent("</style></head><body><h1>Nodo Micro El&eacute;ctrico</h1><h3>IP " + String(ip[0]) + '.' + String(ip[1]) + '.' + String(ip[2]) + '.' + String(ip[3]) + "</h3>");
	//Presento cuadradito de horas de uso:
	server.sendContent("<div id=v class=d><div id=x>Activo</div><div id=y>" + String(horas / 24) + "</div><div id=x>D&iacute;as</div></div>");
	String color = "v";
	if (watts > 3999) 
		{
		color = "r";
		}
	else if (watts > 1999)
		{
			color = "a";
		}
	//Presento cuadradito de Watts:
	server.sendContent("<div id=" + color + " class=d><div id=x>Consumo</div><div id=y>" + String(watts) + "</div><div id=x>Watts</div></div>");
	server.sendContent("<hr>");
	//Presento grafica de los últimos CINCO minutos (300 valores):
	server.sendContent("<h3>Watts reales p/seg &uacute;ltimos 3 minutos:</h3>");
	graficoweb(0);
	server.sendContent("<hr>");
	server.sendContent("<h3>Watts promedio c/3 mins. en &uacute;ltimas 9 horas:</h3>");
	//Presento gráficas históricas
	graficoweb(1);
	cierreweb();
}

void webconfiguracionguardar()
{
	lcd.clear();
	lcd.setCursor(0, 0);
	lcd.print("Actualizando WIFI");
	String qsid = server.arg("ssid");
	String qpass = server.arg("pass");
	if (qsid.length() > 0 && qpass.length() > 0) {
		for (int i = 0; i < qsid.length(); i++)
		{
			// Deal with (potentially) plus-encoded ssid
			qsid[i] = (qsid[i] == '+' ? ' ' : qsid[i]);
		}
		for (int i = 0; i < qpass.length(); i++)
		{
			// Deal with (potentially) plus-encoded password
			qpass[i] = (qpass[i] == '+' ? ' ' : qpass[i]);
		}
		//WiFi.mode(WIFI_AP_STA);  //de nuevo, lo ponemos en modo AP  + STATION (esta linea es innecesaria, ya está en modo AP_STA)
		WiFi.begin(qsid.c_str(), qpass.c_str());
		if (testearwifi())
		{
			IPAddress ip = WiFi.localIP();
			arranqueweb();
			server.sendContent("</style></head><body><h1>Nodo Micro El&eacute;ctrico</h1><h3>Conexi&oacute;n a red " + qsid + " exitosa</h3><p>Por favor, espere unos segundos mientras se resetea este nodo y  recon&eacute;ctese a esa red para poder visualizar la informaci&oacute;n del sistema.<br><br>Podr&aacute; hacerlo desde http://electro.local &oacute; si esto no funciona, utilizando la IP asignada: http://" + String(ip[0]) + '.' + String(ip[1]) + '.' + String(ip[2]) + '.' + String(ip[3]) + "</p>");
			cierreweb();
			lcd.clear();
			lcd.setCursor(0, 0);
			lcd.print("WIFI Actualizado");
			delay(900);
			activarserver();
		}
		else
		{
			lcd.clear();
			lcd.setCursor(0, 0);
			lcd.print("ERROR SSID clave");
			delay(1000);
			arranqueweb();
			server.sendContent("</style></head><body><h1>Nodo Micro El&eacute;ctrico</h1><h3>Fall&oacute; conexi&oacuten a red " + qsid + "</h3><p>Por favor intente nuevamente, revisando y escribiendo correctamente el SSID y la clave wifi</p>");
			cierreweb();
			delay(900);
		}
	}
	else
	{
		arranqueweb();
		server.sendContent("</style></head><body><h1>Nodo Micro El&eacute;ctrico</h1><h3>Error en SSID y clave</h3><p>Por favor intente nuevamente, revisando y escribiendo correctamente el SSID y la clave wifi</p>");
		cierreweb();
		delay(900);
	}
}

Espero como siempre que este código sirva de ejemplo y ayuda a alguno de ustedes. Me encantará escuchar sus comentarios, y por supuesto, si encuentran una mejor forma de resolver algún aspecto del código, por favor, comenten también así todos podemos aprender!

Deja un comentario

Tu dirección de correo electrónico no será publicada.