Hackster is hosting Hackster Holidays, Ep. 6: Livestream & Giveaway Drawing. Watch previous episodes or stream live on Monday!Stream Hackster Holidays, Ep. 6 on Monday!
Stefaan Vandevelde
Published © MIT

Monitoring and Controlling a Heat Recovery Ventilation Unit

We had an very hot summer this year in Belgium. Can the heat-recovery ventilation unit help to reduce the temperature in the house?

IntermediateShowcase (no instructions)24 hours2,104
Monitoring and Controlling a Heat Recovery Ventilation Unit

Things used in this project

Story

Read more

Schematics

PCB Schematics

Code

Arduino Firmware

C/C++
#include "SoftwareI2C.h" 
SoftwareI2C wire[4];

#include "Adafruit_Si7021.h"
Adafruit_Si7021 sensor[4];

#define filter_in     16
#define bypass_in     10
#define bypass_led    14
#define filter_led    19
#define flowrateA_out 18
#define flowrateB_out 15

char outBuffer[64];
int flowRate = 1;

void setup() {
  Serial.begin(57600);
  Serial.println("Starting ...");
  
  Serial1.begin(57600);
  while (!Serial1)
    delay(10);
  Serial1.println("Starting ...");

  pinMode(filter_in, INPUT);
  pinMode(bypass_in, INPUT_PULLUP);
  pinMode(bypass_led, OUTPUT);
  pinMode(filter_led, OUTPUT);
  pinMode(flowrateA_out, OUTPUT);
  pinMode(flowrateB_out, OUTPUT);
  
  for(int i=0; i<4; i++)
  {
    sensor[i] = Adafruit_Si7021();
    sensor[i].initSoftwareI2C(&(wire[i]), 2*(i+1), 2*(i+1)+1); 
  }
  delay(100);
}

int cycle=0;

void loop() {
  memset(outBuffer,0,sizeof(outBuffer));
  if (cycle<4)
  {
    sprintf(outBuffer, "S");
    addIntToCharArray(outBuffer, cycle+1);  
    addTandRhMeasurementToCharArray(outBuffer, &(sensor[cycle]));   
    Serial1.println(outBuffer);
  }
  
  if (cycle == 4)
  {
    Serial1.print ("FB ");
    Serial1.println(flowRate);
  }

  if (cycle == 5)
  {
    Serial1.print ("DI ");
    Serial1.print(digitalRead(bypass_in));
    Serial1.println(digitalRead(filter_in));
  }

  while (Serial1.available()>1)
  {
    String s = Serial1.readStringUntil('\n');
    Serial.print("received from UART:");
    Serial.println(s);
    flowRate = s.toInt();
    if (flowRate <1 || flowRate>3)
      flowRate = 1;
  }
  
  digitalWrite(filter_led, digitalRead(filter_in));
  digitalWrite(bypass_led, digitalRead(bypass_in));

  switch(flowRate){
    case 2:
      digitalWrite(flowrateA_out, LOW);
      digitalWrite(flowrateB_out, HIGH);
      break;
    case 3:
      digitalWrite(flowrateA_out, HIGH);
      digitalWrite(flowrateB_out, LOW);
      break;
    default:
      digitalWrite(flowrateA_out, LOW);
      digitalWrite(flowrateB_out, LOW);
      break;
  }

  cycle++;
  if (cycle>5) 
    cycle = 0;
  delay(500);
}

void addTandRhMeasurementToCharArray(char* string, Adafruit_Si7021* sensor){
  float t = sensor->readTemperature();
  float rh = sensor->readHumidity(); 
  addSpaceAndFloatToCharArray(string, t);
  addSpaceAndFloatToCharArray(string, rh);
}

void addSpaceAndFloatToCharArray(char* string, float value)
{
  string[strlen(string)] = ' ';
  dtostrf(value, 5, 3, &string[strlen(string)]);
}

void addIntToCharArray(char* string, byte value)
{
  string[strlen(string)] = (char)(48+value);
}

Node.js code running on the Raspberry Pi

JavaScript
'use strict';

const config = require('./config.json');
const SerialPort = require("serialport");
const Readline = require('@serialport/parser-readline');
const schedule = require('node-schedule');
const Influx = require('influx');
const influx = new Influx.InfluxDB({
    host: config.influx.server,
    database: config.influx.database,
    schema: [
        {
            measurement: config.influx.measurement,
            fields: {
                vanBinnenTemp: Influx.FieldType.FLOAT,
                vanBinnenHum: Influx.FieldType.FLOAT,
                vanBuitenTemp: Influx.FieldType.FLOAT,
                vanBuitenHum: Influx.FieldType.FLOAT,
                naarBinnenTemp: Influx.FieldType.FLOAT,
                naarBinnenHum: Influx.FieldType.FLOAT,
                naarBuitenTemp: Influx.FieldType.FLOAT,
                naarBuitenHum: Influx.FieldType.FLOAT,
            },
            tags: [
                'workingLevel'
            ]
        }
    ]
});

var port = new SerialPort(config.serialPort.portName, {
    baudRate: config.serialPort.baudRate
});
const parser = port.pipe(new Readline({ delimiter: config.serialPort.delimiter }));

const mqtt = require('mqtt');
const mqttClient = mqtt.connect(config.mqtt.broker);
var mqttConnected = false;

const sensorRegex = /S(\d)\s([-+]?[0-9]*\.?[0-9]*)\s([-+]?[0-9]*\.?[0-9]*)/;
const inputRegex = /DI\s(\d\d)/;
const workingLevelRegex = /FB\s(\d)/;

var measurements = [];
var filterStatus = 0;
var bypassStatus = 0;
var workingLevelActual;
var WorkingLevelRequested;
var printCaptures = config.general.printCaptures;

config.sensorNames.forEach((sensor) => measurements.push(
    {
        name: sensor.name,
        id: sensor.id,
        temperature: 0.0,
        humidity: 0.0,
        timestamp: Date.now()
    }    
));

parser.on('data', function (message) {
    let sensorMatch = sensorRegex.exec(message);
    if (sensorMatch !== null) {
        let sensorId = parseInt(sensorMatch[1]);
        let temp = parseFloat(sensorMatch[2]);
        let hum = parseFloat(sensorMatch[3]);
        let measurement = measurements.find((m) => m.id == sensorId);
        if (measurement !== undefined) {
            measurement.temperature = temp;
            measurement.humidity = hum;
            measurement.timestamp = Date.now();
        };
        if (printCaptures) {
            process.stdout.write("Sensor capture: ");
            console.log(measurement);
        }
    }

    let inputMatch = inputRegex.exec(message);
    if (inputMatch !== null && printCaptures) {
        console.log(`Captured input status : ${inputMatch[1].toString()}`);
    }

    let workingLevelMatch = workingLevelRegex.exec(message);
    if (workingLevelMatch !== null) {
        workingLevelActual = parseInt(workingLevelMatch[1].toString());
        if (printCaptures)
            console.log(`Working level : actual=${workingLevelActual}, requested=${WorkingLevelRequested}`);
        if (workingLevelActual != WorkingLevelRequested) {
            //TODO: working level aanpassen
        }
    }

    if (sensorMatch == null && inputMatch == null && workingLevelMatch == null)
        console.log(`Unknown message: ${message}`);
});

function printMeasurementHeaders() {
    console.log(`\t${measurements[0].name}\t|\t${measurements[1].name}\t|\t${measurements[2].name}\t|\t${measurements[3].name}\t|\tfilter`);
}
function printAndPostMeasurements() {
    process.stdout.write("|");
    measurements.forEach((measurement) => {
        process.stdout.write(
            `  ${measurement.temperature.toFixed(2)} gr `
            + ` ${measurement.humidity.toFixed(0)}% `
            + ` (${((Date.now() - measurement.timestamp) / 1000).toFixed(0)}s)\t|`);
        if (mqttConnected) {
            mqttClient.publish(
                config.mqtt.temperatureTopic.replace('$sensorName', measurement.name),
                measurement.temperature.toString());
            mqttClient.publish(
                config.mqtt.humidityTopic.replace('$sensorName', measurement.name),
                measurement.humidity.toString());
        }
    });
    
    console.log("\t<filter status>");
    if (mqttConnected) {
        mqttClient.publish(
            config.mqtt.filterTopic,
            "TODO".toString());
    };

    measurements.forEach((measurement) => {
        if (mqttConnected) {
            mqttClient.publish(
                config.mqtt.temperatureTopic.replace('$sensorName', measurement.name),
                measurement.temperature.toString());
            mqttClient.publish(
                config.mqtt.humidityTopic.replace('$sensorName', measurement.name),
                measurement.humidity.toString());
        }
    });

   // Write the data to influx database
    var vanBinnen = measurements.find(m => m.name == 'vanBinnen');
    var vanBuiten = measurements.find(m => m.name == 'vanBuiten');
    var naarBinnen = measurements.find(m => m.name == 'naarBinnen');
    var naarBuiten = measurements.find(m => m.name == 'naarBuiten');
    influx.writePoints([
        {
            measurement: config.influx.measurement,
            fields: {
                vanBinnenTemp: vanBinnen.temperature,
                vanBinnenHum: vanBinnen.humidity,
                vanBuitenTemp: vanBuiten.temperature,
                vanBuitenHum: vanBuiten.humidity,
                naarBinnenTemp: naarBinnen.temperature,
                naarBinnenHum: naarBinnen.humidity,
                naarBuitenTemp: naarBuiten.temperature,
                naarBuitenHum: naarBuiten.humidity
            },
            tags: {
                workingLevel: workingLevelActual.toString()
            }
        }
    ]).then(() => {
        console.log("Data successfully written to influx DB");
        }).catch(err => {
            console.error(`Error writing data to Influx database!`);
            console.error(err);
    })
}

mqttClient.on('connect', function () {
    mqttConnected = true;
    mqttClient.subscribe(config.mqtt.setLevelTopic);
});

mqttClient.on('message', function (topic, message) {
    if (topic === config.mqtt.setLevelTopic) {
        // message is Buffer
        let requestedLevel = parseInt(message.toString());
        if (!Number.isNaN(requestedLevel)) {
            let levelObj = config.operatingLevelDefinitions.find(obj => obj.level === requestedLevel);
            if (levelObj != undefined) {
                WorkingLevelRequested = levelObj.level;
                port.write(levelObj.pattern + "\n");
                console.log(`Working level set to ${levelObj.level}`);
            }
        }
    }
})

printMeasurementHeaders();

var j = schedule.scheduleJob(config.general.reportSchedule, function () {
    printAndPostMeasurements();
});

Node.js config file

JSON
{
  "general": {
    "reportSchedule": "*/5 * * * *",
    "printCaptures": false
  },
  "serialPort": {
    "portName": "/dev/ttyS0",
    "baudRate": 57600,
    "delimiter": "\r\n"
  },
  "mqtt": {
    "broker": "mqtt://home",
    "temperatureTopic": "home/ventilatie/$sensorName/temperature",
    "humidityTopic": "home/ventilatie/$sensorName/humidity",
    "filterTopic": "home/ventilatie/filter",
    "setLevelTopic": "home/ventilatie/setLevel"
  },
  "influx": {
    "server": "home",
    "database": "openhab",
    "measurement": "ventilatie"
  },
  "sensorNames": [
    {
      "id": 1,
      "name": "vanBuiten"
    },
    {
      "id": 2,
      "name": "vanBinnen"
    },
    {
      "id": 3,
      "name": "naarBuiten"
    },
    {
      "id": 4,
      "name": "naarBinnen"
    }
  ],
  "operatingLevelDefinitions": [
    {
      "level": 1,
      "pattern": "1"
    },
    {
      "level": 2,
      "pattern": "2"
    },
    {
      "level": 3,
      "pattern": "3"
    }
  ]
}

Node.js package file

JSON
{
  "name": "Venti-controller",
  "version": "0.0.0",
  "description": "Controller app for the venti-controller setup",
  "main": "app.js",
  "author": {
    "name": "Stefaan Vandevelde"
  },
  "dependencies": {
    "influx": "^5.0.7",
    "mqtt": "^2.18.3",
    "node-schedule": "^1.3.0",
    "serialport": "^6.2.2"
  }
}

Credits

Stefaan Vandevelde

Stefaan Vandevelde

2 projects • 29 followers
Software developer and embedded devices enthousiast

Comments