Skip to main content

The ERV Data Acquisition System

 Progress over the weekend with the ERV DAS



The 999's are me testing the sensor disconnected response.









I have a Pitot tube left over from an RC plane I think I might put it in the inlet air stream




Pin assignment in the Arduino sketch


DHT dht[4] = {DHT(4, DHTTYPE),DHT(0, DHTTYPE),DHT(2, DHTTYPE),DHT(14, DHTTYPE)};


There are 4 DHT22's inside heat exchanger lid, shown below.



Sensor locations


Arduino code

#include <Arduino_JSON.h>
#include <ESP8266WiFi.h>
#include <MQTT.h>

#define LED_PIN LED_BUILTIN //LED_BUILTIN is built in LED

#include "DHT.h"
// Uncomment whatever type you're using!
#define DHTTYPE DHT11   // DHT 11
//#define DHTTYPE DHT22   // DHT 22  (AM2302), AM2321
//#define DHTTYPE DHT21   // DHT 21 (AM2301)

WiFiClient net;
MQTTClient MQTTclient; 


const char ssid[] = "Network";
const char pass[] = "DF@#$%";

// the IP address for the MQTT server
char MQTTip[] = "192.168.1.71";   

unsigned long UpdateCount = 12;
unsigned long TickCount = UpdateCount-1;
unsigned long PacketCount = 0;
unsigned long lastMillis = 0;

DHT dht[4] = {DHT(4, DHTTYPE),DHT(0, DHTTYPE),DHT(2, DHTTYPE),DHT(14, DHTTYPE)};

DHT loft(12, DHT11);

void setup() {

  pinMode(LED_PIN, OUTPUT);
  
  Serial.begin(115200);

  WiFi.begin(ssid, pass);

  // Note: Local domain names (e.g. "Computer.local" on OSX) are not supported by Arduino.
  // You need to set the IP address directly.
  MQTTclient.begin(MQTTip, net);
  MQTTclient.onMessage(messageReceived);

  WiFiconnect();


  MQTTclient.publish("/ervdas/status", "start");

  for(int  i = 0;i < 4;i++)
  {
	dht[i].begin();
  }

  loft.begin();

  MQTTclient.publish("/ervdas/status", "run");
}

void loop() {
  
  MQTTclient.loop();
  
  delay(10);  // <- fixes some issues with WiFi stability

  if (!MQTTclient.connected()) {
	WiFiconnect();
  }

	
  // publish a message roughly every second.
  if (millis() - lastMillis > (UpdateCount * 1000)) 
  { 
	 lastMillis = millis();
	 SendMQTT();
  }
  
  delay(100);

}

void WiFiconnect() {
  
  Serial.print("checking wifi...");
  Serial.println(ssid);
  
  while (WiFi.status() != WL_CONNECTED) {
	Serial.print(".");
	delay(500);
	digitalWrite(LED_PIN, LOW);
	delay(500);
	digitalWrite(LED_PIN, HIGH);
  }
  
  Serial.println("\nWiFi Connected! ");
  Serial.println( WiFi.localIP());

  Serial.print("\nMQTT connecting...");
  while (!MQTTclient.connect(WiFi.localIP().toString().c_str() , "try", "try")) {
	Serial.print(".");
	delay(800);
	digitalWrite(LED_PIN, LOW);
	delay(200);
	digitalWrite(LED_PIN, HIGH);
  }

  Serial.println("\nMQTT Connected!");

  digitalWrite(LED_PIN, LOW);
  
  MQTTclient.subscribe("/ervdas/control");  
  MQTTclient.publish("/ervdas/status", "start");
  MQTTclient.publish("/ervdas/localip", "/ervdas/localip/" + WiFi.localIP().toString());

}

void SendMQTT()
{
  digitalWrite(LED_PIN, LOW);
	
  JSONVar myArray;

  //STALE
  myArray[0] = (!isnan(dht[0].readTemperature()) ? dht[0].readTemperature() : 999);
  myArray[1] = (!isnan(dht[0].readHumidity()) ? dht[0].readHumidity() : 999);

  //FRESH
  myArray[2] = (!isnan(dht[1].readTemperature()) ? dht[1].readTemperature() : 999);
  myArray[3] = (!isnan(dht[1].readHumidity()) ? dht[1].readHumidity() : 999);


  //INLET
  myArray[4] = (!isnan(dht[2].readTemperature()) ? dht[2].readTemperature() : 999);
  myArray[5] = (!isnan(dht[2].readHumidity()) ? dht[2].readHumidity() : 999);
 

  //EXHAUST
  myArray[6] = (!isnan(dht[3].readTemperature()) ? dht[3].readTemperature() : 999);
  myArray[7] = (!isnan(dht[3].readHumidity()) ? dht[3].readHumidity() : 999);

  
  //LOFT
  myArray[8] = (!isnan(loft.readTemperature()) ? loft.readTemperature() : 999);
  myArray[9] = (!isnan(loft.readHumidity()) ? loft.readHumidity() : 999); 
  
  //MISC
  myArray[10] = (PacketCount++);
  myArray[11] = WiFi.RSSI();
  myArray[12] = analogRead(0);

   
  String jsonString = JSON.stringify(myArray);
  
  MQTTclient.publish("/ervdas/sensors", jsonString);
  Serial.print("/ervdas/sensors " );
  Serial.print(jsonString );
  
  Serial.println();

  digitalWrite(LED_PIN, HIGH);
}

float toFixed(float f, int dp)
{
  char buf[40] = {0, };
  sprintf(buf, "%03d", f);
  return atof(buf);
}

void messageReceived(String &topic, String &payload) {

  digitalWrite(LED_PIN, LOW);
  
  Serial.println("incoming: " + topic + " - " + payload);
  
  if(topic == "/ervdas/control")
  {  
	if(payload.startsWith("UpdateCount"))
	{
	  UpdateCount = (long)payload.substring(12).toInt();
	  TickCount = UpdateCount -1;
	}
	else if(payload.startsWith("read"))
	{
	  SendMQTT();
	   MQTTclient.publish("/ervdas/localip", "/ervdas/localip/" + WiFi.localIP().toString());
	}
  
	MQTTclient.publish("/ervdas/payload", topic + "/" + payload);

  }

  digitalWrite(LED_PIN, HIGH);
 
}

Device driver

metadata {
	definition(name: "Greenway MQTT ERV DAS Driver", namespace: "Greenway", author: "Nick Goodey") {
		capability "Initialize"
		capability "Sensor"
		capability "Polling"
		capability "Battery"


		//ERV DAS

		attribute "StaleTile", "String"
		attribute "StaleTemp", "Number"
		attribute "StaleHumidity", "Number"

		attribute "FreshTile", "String"
		attribute "FreshTemp", "Number"
		attribute "FreshHumidity",  "Number"

		attribute "InletTile", "String"
		attribute "InletTemp", "Number"
		attribute "InletHumidity", "Number"

		attribute "ExhaustTile", "String"
		attribute "ExhaustTemp", "Number"
		attribute "ExhaustHumidity",  "Number"

		attribute "LoftTile", "String"
		attribute "LoftTemp", "Number"
		attribute "LoftHumidity",  "Number"
		
		attribute "Packet",  "Number"
		attribute "RSSI",  "Number"
		attribute "Analog",  "Number"
		
		attribute "ExhaustStaleTile", "String"
		attribute "ExhaustStaleTemp", "Number"
		attribute "ExhaustStaleHumidity",  "Number"

		attribute "FreshInletTile", "String"
		attribute "FreshInletTemp", "Number"
		attribute "FreshInletHumidity",  "Number"


	}

	preferences {


		input name: "MQTTBroker", type: "text", title: "MQTT Broker Address:", required: true, displayDuringSetup: true
		input name: "username", type: "text", title: "MQTT Username:", description: "(blank if none)", required: false, displayDuringSetup: true
		input name: "password", type: "password", title: "MQTT Password:", description: "(blank if none)", required: false, displayDuringSetup: true
		input name: "topicSub", type: "text", title: "Topic to Subscribe:", description: "Example Topic (topic/device/#)", required: false, displayDuringSetup: true
		input name: "topicCon", type: "text", title: "Topic to control:", description: "Example Topic (topic/device/#)", required: false, displayDuringSetup: true
		input name: "topicPoll", type: "text", title: "Topic to poll device:", description: "Example Topic (topic/device/#)", required: false, displayDuringSetup: true
   
		input("logEnable", "bool", title: "Enable logging", required: true, defaultValue: true)
		

	}

}


def installed() {
	log.info "installed..."
}



def poll() {

	 displayDebugLog("poll")
	
	try {
		topic = settings?.topicPoll
		if (logEnable) log.debug "Poll: $topic/read"
		interfaces.mqtt.publish(topic, "read", 1, false)

	} catch (e) {
		log.error "Device Poll error: ${e.message}"
	}

}

// Parse incoming device messages to generate events
def parse(String description) {


	try {

		displayDebugLog("Parse")
		displayDebugLog(description);
		displayDebugLog(did);
		
		did = device.getId()

		state.DeviceID = did
		
		// parse message
		mqtt = interfaces.mqtt.parseMessage(description)

		if (logEnable) log.debug mqtt.topic
		if (logEnable) log.debug mqtt.payload

		state.topic = mqtt.topic

		json = new groovy.json.JsonSlurper().parseText(mqtt.payload)
			  
		/* 
		  //STALE
		  myArray[0] = dht[0].readTemperature();
		  myArray[1] = dht[0].readHumidity(); 

		  //FRESH
		  myArray[2] = dht[1].readTemperature();
		  myArray[3] = dht[1].readHumidity(); 

		  //INLET
		  myArray[4] = dht[2].readTemperature();
		  myArray[5] = dht[2].readHumidity(); 

		  //EXHAUST
		  myArray[6] = dht[3].readTemperature();
		  myArray[7] = dht[3].readHumidity(); 
		  
		  //LOFT
		  myArray[8] = loft.readTemperature();
		  myArray[9] = loft.readHumidity(); 

		  //MISC
		  myArray[9] = (PacketCount++);
		  myArray[10] = WiFi.RSSI();
		  myArray[11] = analogRead(0);
		*/
		//Double.parseDouble(
		
		state.StaleTemp = (float)(json[0] * 100)/100.0
		state.StaleHumidity = (float)(json[1] * 100)/100.0
		sendEvent(name: "StaleTemp", value: state.StaleTemp)        
		sendEvent(name: "StaleHumidity", value: state.StaleHumidity)
		sendEvent(name: "StaleTile", value: "Stale Air<br>$state.StaleTemp °C<BR>$state.StaleHumidity %")

		state.FreshTemp = (float)(json[2] * 100)/100.0
		state.FreshHumidity = (float)(json[3] * 100)/100.0
		sendEvent(name: "FreshTemp", value: state.FreshTemp)
		sendEvent(name: "FreshHumidity", value: state.FreshHumidity)
		sendEvent(name: "FreshTile", value: "Fresh Air<br>$state.FreshTemp °C<BR>$state.FreshHumidity %")

		state.InletTemp = (float)(json[4] * 100)/100.0
		state.InletHumidity = (float)(json[5] * 100)/100.0
		sendEvent(name: "InletTemp", value: state.InletTemp)
		sendEvent(name: "InletHumidity", value: state.InletHumidity)
		sendEvent(name: "InletTile", value: "Intake Air<br>$state.InletTemp °C<BR>$state.InletHumidity %")
 
		state.ExhaustTemp = (float)(json[6] * 100)/100.0
		state.ExhaustHumidity = (float)(json[7] * 100)/100.0
		sendEvent(name: "ExhaustTemp", value: state.ExhaustTemp)
		sendEvent(name: "ExhaustHumidity", value: state.ExhaustHumidity)
		sendEvent(name: "ExhaustTile", value: "Exhaust Air<br>$state.ExhaustTemp °C<BR>$state.ExhaustHumidity %")
		
		state.LoftTemp = (float)(json[8] * 100)/100.0
		state.LoftHumidity = (float)(json[9] * 100)/100.0
		sendEvent(name: "LoftTemp", value: state.ExhaustTemp)
		sendEvent(name: "LoftHumidity", value: state.ExhaustHumidity)
		sendEvent(name: "LoftTile", value: "Loft Air<br>$state.LoftTemp °C<BR>$state.LoftHumidity %")
  
		
		sendEvent(name: "Packet", value: json[10])        
		sendEvent(name: "RSSI", value: json[11])
		sendEvent(name: "Analog", value: json[12])
		
		state.ExhaustStaleTemp = (float)( (state.StaleTemp - state.ExhaustTemp) * 1000)/1000.0 
		state.ExhaustStaleHumidity = (float)( (state.StaleHumidity - state.ExhaustHumidity) * 100)/100.0         
		sendEvent(name: "ExhaustStaleTemp", value: state.ExhaustStaleTemp)
		sendEvent(name: "ExhaustStaleHumidity", value: state.ExhaustStaleHumidity)
		sendEvent(name: "ExhaustStaleTile", value: "Exhaust - Stale Air<br>$state.ExhaustStaleTemp °C<BR>$state.ExhaustStaleHumidity %")
		
		state.FreshInletTemp = (float)( (state.FreshTemp - state.InletTemp) * 1000)/1000.0 
		state.FreshInletHumidity = (float)( (state.StaleHumidity - state.ExhaustHumidity) * 100)/100.0        
		sendEvent(name: "FreshInletTemp", value: state.FreshInletTemp)
		sendEvent(name: "FreshInletHumidity", value: state.FreshInletHumidity)
		sendEvent(name: "FreshInletTile", value: "Fresh - Inlet Air<br>$state.FreshInletTemp °C<BR>$state.FreshInletHumidity %")
		
		

	}
	catch (Exception e) 
	{
		log.error "Parse error: ${e.message}"
	}

}


def updated() {
	if (logEnable) log.info "Updated..."
	initialize()
}

def uninstalled() {
	if (logEnable) log.info "Disconnecting from mqtt"
	interfaces.mqtt.disconnect()
}


def initialize() {
	
	
	displayDebugLog("initialize")

	if (logEnable) runIn(900, logsOff)

	state.ThisCharge = 0
	state.ChargeLimit = 0
	state.Relay = -1

	MQTTconnect()
	
	unschedule()
	
	schedule("0/10 * * * * ? *", MQTTconnect)
	

}


def MQTTconnect() {

	try {

		def mqttInt = interfaces.mqtt

		if (mqttInt.isConnected()) {
			displayDebugLog( "Allready Connected to: $MQTTBroker $topicSub")
			return
		}

		def clientID = "hubitat-" + device.deviceNetworkId
		state.clientID = clientID
		
		 displayDebugLog( "Allready Connected to: $MQTTBroker $settings?.MQTTBroker/$settings?.topicSub")

		//open connection
		mqttbroker = "tcp://" + settings?.MQTTBroker + ":1883"
		mqttInt.connect(mqttbroker, clientID, settings?.username, settings?.password)

		//give it a chance to start
		pauseExecution(500)

		mqttInt.subscribe(settings?.topicSub)

		log.info "Connection established: $MQTTBroker $topicSub"
		log.info "clientID: $clientID"
		log.info "Subscribed to: $topicSub"


	} catch (e) {
		log.error "MQTTconnect error: ${e.message}"
	}
}


def mqttClientStatus(String status) {
	log.error "MQTTStatus- error: ${status}"
}

def logsOff() {
	log.warn "Debug logging disabled."
	device.updateSetting("logEnable", [value: "false", type: "bool"])
}




private def displayDebugLog(message) {
	if (logEnable) log.debug "${device.displayName}: ${message}"
}

private def displayInfoLog(message) {

	log.info "${device.displayName}: ${message}"
}

Not sure who I cribbed the above off originally I think it was a community driver


 Greenway Live Sensor Data


Live data feed from Hubitat Cloud 


 






Popular Posts

The Heat Exchanger

  Under construction It took around 6 hours to layer the core Based on a 60 Litre Sistema box with a 400 X 400 X 350 6 mm Coroplast core. The fan speed controller is mounted on the end so it takes a 12V 10 Amp supply from the power system and 2 analogue 0 to 5 V signals for speed demand. The pink foam is a NZ$12 exercise matt which insulates it for both sound and heat quite nicely. 60 Litre Plenum on the vent side See  https://greenwayerv.blogspot.com/2020/10/we-added-60-litre-plenum-on-vent-side.html

Home Ventilation Heat Exchanger

 

The Experiment

First things first an experiment. I had been looking around at various homemade ERV core designs and eventually decided to try a Coroplast design. The experimental core was tiny 12 X 12 X 12 cm  using 3 mm Coroplast Nice isn't it :) 20 80 mm PC cooling fans at a fixed speed four temperature probes. Here is the full scale one around 40 X 40 X 35 cm using 6 mm Coroplast A Wemos D1/R2 for instrumentation sending temperatures via MQTT to Node-Red. I'm was not totally convinced by the numbers but it did definitely exchange significant amounts of heat. I checked the calibration and it was good compared to my multi-meter TC probe. In any case it seems to work even at such a small scale. Never did understand why it seems to gain more heat that it lost from the exhaust steam, checked for leaks there were none. Any way  a hairdryer experiment showed it worked and heat was transferred too so I was happy that it would probably work at a larger scale.

Heat Exchanger Low Ambient 86% Heat Recovery

  Drop on exiting air 2.9C Heating of incoming air 2.5C So 86% of the heat recovered

Drivers and Arduino Sketches for ERV Fans Control Testing

  The Hookup For proof of concept seems to run reliably in the lab. I don't want to be up and down ladders to make update I intend keeping the control system in Hubitat so I can tinker to my Hearts content. Boost fan PWM control as well as start relays. The valve position is a separate driver and device in Hubitat, This will use one of the PWM Four boost fans in the ducts for each room inlet. One fan in the two way turbo exhaust valve. The valve position is a separate driver and device in Hubitat, This will use one of the PWM pins on the Wemos and its analogue input for the position pot Hubitat Valve Position Driver import groovy.json.JsonSlurper metadata { definition(name: "Greenway ERV Turbo Valve", namespace: "Greenway", author: "Nick Goodey") { capability "Initialize" capability "Switch" capability "Switch Level" command "on" command "off" } preferences { section("Device&qu