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 forAutumn-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!
Heating
By Robert Saric
Heating
A home HVAC automation project
- 3,092