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

Added a 60 Litre Plenum air-distribution box on the fresh air side

A bit or research and it seems a plenum on the outlet of the heat exchanger seemed worth a try. 60 Litre Plenum on the vent side. Seems to make the vents more balanced measuring the flow I can regulate the rate more evenly with one vent having less effect on the others, I'm going to add a presure sensor to the chamber see if there is any pressure in it I noticed coupling between room vents. e.g. I set one room and move to the next then recheck the first vent and its flow rate was changed. I was never happy with all those Y junctions, a very poor manifold design I suspect. As an added bonus its made the vent even quieter too. It works well within a range I can set the vent flows independently of one another, also the living room and office flows are much improved which have the longest ducts. The flow at the office and living room are 50% higher if needed now which is great for working at home. The office got warm and sticky in the past, not anymore. More Photos Sound test My refere...