Heating

A home HVAC automation project:

(a working title)

Robert Šarić, 2015.

In the beginning...

© vngrp.deviantart.com

Getting colder...

© www.universetoday.com

1st Home Heating Project

© LupusUVA1Phototherapy.com

And a "few" years later...

© Nest Labs

The situation

✓ Central heating

✗ Thermostat

✗ Remote

The motivation

  • Convenience
  • Savings
  • Learning
  • Conversation
    material

© Inti St Clai / Getty Images

Convenience

Savings

Learning

Conversation Material :-)

  • Automatic regulation
  • Remote control
  • Mild
    temperatures
  • Improved
    efficiency

     
  • Reduced
    overheating
  • Electronics
  • Coding
  • Integration
  • Project Management

Prospect

Standard

Time

All things are created twice; first mentally; then physically. The key to creativity is to begin with the end in mind, with a vision and a blue print of the desired result.

— Stephen Covey

The scheme

Proof of concept

#!/bin/bash

# turn heater on
echo 11 > /sys/class/gpio/export
echo out > /sys/class/gpio/gpio11/direction
echo 1 > /sys/class/gpio/gpio11/value

# turn pump on
echo 12 > /sys/class/gpio/export
echo out > /sys/class/gpio/gpio12/direction
echo 1 > /sys/class/gpio/gpio12/value

✗ Convenient

✓ Remote controled

✗ Automatized

What else do I want?

  • Room temp. feedback
  • Automation

What else do I want?

  • Room temp. feedback
  • Program logic
  • Run as a service
  • Web app
  • Automation
  • Profiles/presets
  • Always-on
  • More convenient UI

Software Requirements:

  • Communicate with GPIO
  • Access 1-wire/OWFS
  • Access to Round Robin Database
  • Provide web access

Architecture decision:

  • Python
  • Java
  • JavaScript
  • too novice for it
  • too much overhead
  • bingo! :-)

Requirements:

  • Communicate with GPIO
  • Access 1-wire/OWFS
  • Access to Round Robin Database
  • Provide web access

Node.js:

  • pi-gpio (3rd pary module)
  • child_process.exec
  • child_process.exec
     
  • express (3rd pary module)

Bootstrapping

  • Node.js modules
  • Twitter bootstrap
  • code snippets
  • Android Studio
    mobile+wear project
  • Retrofit (REST client)
  • Otto (event bus)

System parts

  • Backend
  • Frontend

The code!

  • dependencies
  • deployment
  • installation
  • configuration

README.md

// imports

var app = express();

app.use(express.static(config.public_dir));

// <some context initialization here>

// routes
app.get('/get_state', function(req, res) {
  console.log('/get_state');

  res.json(getState());
});

// <more routes>

app.get('/', function(req, res) {
  res.render('index.html');
});

// Express route for any other unrecognised incoming requests
app.get('*', function(req, res) {
  res.status(404).send('Unrecognised API call');
});

// <some error handling here>

app.listen(3000);
console.log('App Server running at port 3000');
app/app.js (web app & routes setup)
function getState() {
  var state = {
    "temp_preset": last_temp_preset,
    "temp_living": last_temp_living,
    "temp_osijek": last_temp_osijek,
    "overrideSwitch": config.overrideSwitch,
    "heatingSwitch": config.heatingSwitch,
    "holidaySwitch": config.holidaySwitch,
    "updated": new Date().getTime()
  };

  return state;
}
var tempRegulateInterval;
var tempCollectInterval;

function initTimers() {
  console.log('initTimers()');

  tempRegulateInterval = setInterval(function() {
    collectAndRegulateTemp();
  }, config.regulate_interval); // 30 s

  tempCollectInterval = setInterval(function() {
    collectAndRecordCurrTemps();
  }, config.collect_record_interval); // 2 min
}
app/app.js (start periodic updates)
function collectAndRegulateTemp() {
  var ts = Math.round(new Date().getTime() / 1000);

  gpio_tools.getTempLiving(last_temp_living, function(value) {
    last_temp_living = value;

    if (!config.overrideSwitch) {
      last_temp_preset = config.getTimeTableTemp();
    }

    // regulate on/off
    gpio_tools.regulateHeating(
      config.shouldStartHeating(undefined, last_temp_preset, last_temp_living, last_temp_osijek)
    );
  });

  gpio_tools.getHeaterState(function(state) {
    rrdb_tools.insertState(ts, state ? 1 : 0);
  });
}
app/app.js (periodically: read temp. & regulate heating)
var getTempLiving = function(last_temp_living, cb) {
  var readStr = "cat /sys/bus/w1/devices/" + tempLivingSensorId + "/w1_slave";

  var tempLiving;
  execute(readStr, function(out, err) {
    if (err) {
      console.error(err);
      tempLiving = last_temp_living;
    } else {
      var lines = out.replace(/\r\n/g, "\n").split("\n");

      if (lines == null || lines.length != 3) {
        console.error('  ERROR: Wrong sensor readout format.');
        tempLiving = last_temp_living;
      } else {
        // 46 01 4b 46 5f ff 0a 10 f5 : crc=f5 YES
        // 46 01 4b 46 5f ff 0a 10 f5 t=20375

        var crc_OK = lines[0].substring(lines[0].length - 3) == "YES";

        // logging and error handling removed for brevity

        var temp = lines[1].substring(lines[1].indexOf('t=') + 2);

        tempLiving = parseFloat(temp / 1000).toFixed(1);
      }
    }

    cb(tempLiving);
  });
};
app/gpio-tools.js (read the temperature)
function collectAndRegulateTemp() {
  var ts = Math.round(new Date().getTime() / 1000);

  gpio_tools.getTempLiving(last_temp_living, function(value) {
    last_temp_living = value;

    if (!config.overrideSwitch) {
      last_temp_preset = config.getTimeTableTemp();
    }

    // regulate on/off
    gpio_tools.regulateHeating(
      config.shouldStartHeating(undefined, last_temp_preset, last_temp_living, last_temp_osijek)
    );
  });

  gpio_tools.getHeaterState(function(state) {
    rrdb_tools.insertState(ts, state ? 1 : 0);
  });
}
app/app.js (periodically: read temp. & regulate heating)
var shouldStartHeating = function(millis, temp_preset, temp_living, temp_osijek) {
  var should = false;

  if (temp_preset > temp_living) {
    should = true;
  } else {
    // ???
  }

  return should;
};
app/config-tools.js (should I stay or should I go burn?)
// we're above preset temp, but next preset could require heating --> check it
var target = getNextTimeTable(now);

if (target.temp > temp_living && target.temp > temp_preset) {
  // see how much we should heat for
  var tempDiffToReach = parseFloat((target.temp - temp_living).toFixed(2));

  var delta = parseFloat((target.temp - temp_osijek).toFixed(2));

  var Cph = 3 - (0.1 * delta);
  if (Cph < 0.5) {
    Cph = 0.5;
  }

  // see how long to reach that temp
  var timeToReachTempDiff = parseFloat((tempDiffToReach / Cph).toFixed(2));

  var hourMillis = 60 * 60 * 1000;
  var timeToReachTempDiffMillis = Math.round(timeToReachTempDiff * hourMillis);

  var target_hour = target.from[0];
  var target_minute = target.from[1];
  // if target is tomorrow, add 1 day
  var target_date = new Date(now.getFullYear(), now.getMonth(), target_hour < now.getHours() ?
    now.getDate() + 1 : now.getDate(), target_hour, target_minute, 0, 0);
  var shouldStartAt = target_date.getTime() - timeToReachTempDiffMillis;

  if (shouldStartAt < now.getTime()) {
    should = true;
  }
}
app/config-tools.js (the "smart" part in smart heating)
function collectAndRegulateTemp() {
  var ts = Math.round(new Date().getTime() / 1000);

  gpio_tools.getTempLiving(last_temp_living, function(value) {
    last_temp_living = value;

    if (!config.overrideSwitch) {
      last_temp_preset = config.getTimeTableTemp();
    }

    // regulate on/off
    gpio_tools.regulateHeating(
      config.shouldStartHeating(undefined, last_temp_preset, last_temp_living, last_temp_osijek)
    );
  });

  gpio_tools.getHeaterState(function(state) {
    rrdb_tools.insertState(ts, state ? 1 : 0);
  });
}
app/app.js (periodically: read temp. & regulate heating)
var regulateHeating = function(turnOn) {
  if (!config.heatingSwitch) {
    getHeaterState(function(heaterState) {
      if (heaterState) {
        setHeaterState(0);
      }
    });

    return;
  }
  getHeaterState(function(heaterState) {
    if (turnOn) {
      if (!heaterState) {
        clearTimeout(pumpOffTimeout);

        setPumpState(1);

        setTimeout(function() {
          setHeaterState(1);
        }, 3000); // 3s
      } else {
        getPumpState(function(pumpState) {
          if (!pumpState) {
            setPumpState(1);
          }
        });
      }
    } else {
      if (heaterState) {
        setHeaterState(0);

        pumpOffTimeout = setTimeout(function() {
          setPumpState(0);
        }, config.delay_pump_off);
      }
    }
  });
};
app/gpio-tools.js
var getHeaterState = function(cb) {
  gpio.read(gpioPinHeater, function(err, value) {
    if (err) throw err;
    if (typeof(cb) == "function") {
      cb(value);
    }
  });
};

var setHeaterState = function(value, cb) {
  gpio.write(gpioPinHeater, value, function(err) {
    if (err) throw err;

    if (typeof(cb) == "function") {
      cb();
    }
  });
};
#!/bin/bash

# turn heater on
echo ${gpioPinHeater} > /sys/class/gpio/export
echo out > /sys/class/gpio/gpio${gpioPinHeater}/direction
echo ${value} > /sys/class/gpio/gpio${gpioPinHeater}/value

Simple User Interface

Browser: accessible from everywhere

Native Android App

Project Management

Requirements Management

Change Management

Version Control

Release Management

An ounce of action is worth a ton of theory.

Friedrich Engels

Convenience

Savings

Learning

  • Improved
    efficiency

     
  • Reduced
    overheating

​Considering only 4 months (November - February).

Deducted the (average for​Autumn-Winter) sanitary water and cooking gas consumption: ~90 m3.

Manual: 2011/12 & 12/13

Auto.: 2013/14 & 14/15

Heating gas savings: 25%!

Saved in 2 years: ~1860 kn (~260 USD)

Gas consumption readings for last 4 years.

All code presented here © Robert Šarić

and licensed under Apache License 2.0

Thank You!