diff --git a/README.md b/README.md index 9904c41..d7b7f19 100644 --- a/README.md +++ b/README.md @@ -15,12 +15,38 @@ This is a NodeJS based software for raspberry pi to control my heater at home. M Right now the software just activates a relay when the temperature gets too low. In theory it should be possible to extend it in order to support digital thermostats. ## Requirements -- NodeJS 6 -- MongoDB +- RaspberryPi or similar +- relay module (I used the Foxnovo 2-channel relay) +- temperature sensor (I used a DS18B20 sensor) +- some wiring +- NodeJS 6 (tested with 6.9.x) +- MongoDB (tested with 3.4.x) +- optional: +-- `forever` a node deamonizer, OR: +-- `pm2` and avanced process manager, which consumes quite a lot of ressouces (wasn't able to run it on a raspberry 1, you should use `forever` instead +-- 1602 LCD display module ## Setup -- checkout this repository -- run `npm install` +- setup your RaspberryPi (or something similar) like in the matching guidelines +- connect your relay to GND and a GPIO port +- connect and setup your temerature sensor according to it's manual +- install NodeJS like this: + ```shell + $ wget http://node-arm.herokuapp.com/node_latest_armhf.deb + $ sudo dpkg -i node_latest_armhf.deb + ``` +- `node -v` should now return something like this: + ```shell + $ node -v + v6.9.1 + ``` +- optionally install `pm2` or `forever` like this: + ```shell + $ sudo npm install pm2 -g + $ sudo npm install forever -g + ``` +- checkout this repository, probably in `~/raspi-heater/` or `/etc/raspi-heater/` +- run `npm install` in that directory - create environment config file: - create new file `.env` - add the mongodb location like `MONGODB='localhost/heater'` @@ -29,14 +55,46 @@ Right now the software just activates a relay when the temperature gets too low. - Change the GPIO ports to your ones and your sensor name - Change the desired days, times and temperatures - if no temerature is set for a status, the default temperatures will be used - Change the locale - - If you want to use multiple raspi-heaters with the same database, give each one a different zone id + - If you want to use multiple raspi-heaters: + -- use the same central database + -- give each one a different zone id + -- deactivate the home- and holiday status accessories on every instance exept the main one - start the processes you need: - - `control.js` for the general controller - - `homekit.js` for homekit support - - `display.js` if you've connected a display + - `bin/control.js` for the general controller + - `bin/homekit.js` for homekit support + - `bin/display.js` if you've connected a display - you can start all three with the pm2 process manager with the added process.json +- if you want to start the processes via a custom `/etc/init.d`-Script, you probably want to install `raspi-heater` into a public location like `/etc/raspi-heater` instead of one users home folder + - then you can add the processes to your custom `/etc/init.d`-script (assuming you want to use `forever`): + ```shell + cd /etc/raspi-heater && sudo forever bin/control.js + cd /etc/raspi-heater && sudo forever bin/homekit.js + cd /etc/raspi-heater && sudo forever bin/display.js + ``` + - if you want to use `pm2` you can simply add: + ```shell + cd /etc/raspi-heater && sudo pm2 start process.json + ``` +- to check the current status: + ```shell + $ node bin/status.js + ######################## + #### raspi-heater Status + ######################## + heaterOn true since 8 minutes + targetHeaterOn false since a few seconds + cooldownOn false since 25 minutes + heatingMode auto changed 8 minutes ago + ###################### + isHome true + isHoliday false + ###################### + current temerature: 19.9 + target temerature: 20.0 + ###################### + ``` -## Future features +## Future features (maybe) - browser interface - server based, with own database and replication - real support for multiple instances in the same home network diff --git a/accessories/Thermostat_accessory.js b/accessories/Thermostat_accessory.js index 3331a7b..9a4d8ab 100644 --- a/accessories/Thermostat_accessory.js +++ b/accessories/Thermostat_accessory.js @@ -1,5 +1,5 @@ var HAP = require('hap-nodejs'); -var app = require('../lib/global'); +var app = require('../controller/app.js'); var cfg = require('../config.json'); var Accessory = HAP.Accessory; @@ -10,6 +10,8 @@ var uuid = HAP.uuid; var Zone = require('../models/zone.js'); var Status = require('../models/status.js'); +var heater = require('../controller/heater.js'); + var thermostat = exports.accessory = new Accessory('Thermostat', uuid.generate('hap-nodejs:accessories:thermostat')); thermostat.username = "C1:5D:3A:AE:5E:F3"; @@ -33,27 +35,23 @@ thermostat .addService(Service.Thermostat, 'Thermostat') .getCharacteristic(Characteristic.CurrentHeatingCoolingState) .on('get', function(callback) { - Status.findOne({ key: 'heaterOn' }).exec((err, data) => { + heater.get((status) => { // return our current value var state, stateName; - // maybe i should find a more suitable solution here, but it works for now - it actually shows in homekit if the heater is turned on now - // - which isn't that bad, right? - switch(data.value) { - case 'false': + // shows if the heater is on or off - no cooling for now possible + switch(status.heater) { + case false: stateName = 'off'; state = Characteristic.CurrentHeatingCoolingState.OFF; break; - case 'true': + case true: stateName = 'heat'; state = Characteristic.CurrentHeatingCoolingState.HEAT; - break; - default: - stateName = 'off'; - state = Characteristic.CurrentHeatingCoolingState.OFF; + break; } - app.log('get: CurrentHeatingCoolingState', stateName, data.value); + app.log('get: CurrentHeatingCoolingState', stateName, status.heater); callback(null, state); }); }); @@ -69,20 +67,17 @@ thermostat // same as above, should be changed maybe switch(data.value) { - case '0': - stateName = 'off'; + case 'off': state = Characteristic.TargetHeatingCoolingState.OFF; break; - case '1': - stateName = 'heat'; + case 'on': state = Characteristic.TargetHeatingCoolingState.HEAT; break; default: - stateName = 'auto'; state = Characteristic.TargetHeatingCoolingState.AUTO; } - app.log('get: TargetHeatingCoolingState', stateName, data.value); + app.log('get: TargetHeatingCoolingState', data.value); callback(null, state); }) }); @@ -92,18 +87,21 @@ thermostat .getService(Service.Thermostat) .getCharacteristic(Characteristic.TargetHeatingCoolingState) .on('set', function(value, callback) { + var state; // ignore state "2" which would be "cooling", and set to "auto" instead - if (value === 2 || value === '2') value === 3; + switch(value) { + case Characteristic.TargetHeatingCoolingState.OFF: + state = 'off'; + break; + case Characteristic.TargetHeatingCoolingState.HEAT: + state = 'on'; + break; + default: + state = 'auto'; + } + // save the state - Status.findOneAndUpdate({ key: 'heatingMode' }, { value: value }, { new: true, upsert: true }).exec((err, data) => { - setTimeout(function() { - // reset the state to off after 15 minutes again -> 15 minutes of heating - // - the fun part is: we don't need to do more than setting it. the actual changes happens, because this one gets called again - and control.js sees the new status - thermostat - .getService(Service.Thermostat) - .setCharacteristic(Characteristic.TargetHeatingCoolingState, Characteristic.TargetHeatingCoolingState.OFF); - }, 1000 * 60 * 60) // 1h - + Status.findOneAndUpdate({ key: 'heatingMode' }, { value: state }, { new: true, upsert: true }).exec((err, data) => { app.log('set: TargetHeatingCoolingState', value); callback(); }) diff --git a/control.js b/bin/control.js similarity index 82% rename from control.js rename to bin/control.js index 4b8cedd..01dbe20 100644 --- a/control.js +++ b/bin/control.js @@ -1,17 +1,13 @@ -var cfg = require('./config.json'); -var _ = require('lodash'); +var cfg = require('../config.json'); var mongoose = require('mongoose'); var moment = require('moment'); -var gpio = require('./lib/gpio_wrapper'); +var gpio = require('../lib/gpio_wrapper'); -var sensor = require('./lib/sensor.js'); -var heater = require('./controller/heater.js'); +var heater = require('../controller/heater.js'); +var app = require('../controller/app.js'); -var Zone = require('./models/zone.js'); -var Status = require('./models/status.js'); - -var app = require('./lib/global'); +var Status = require('../models/status.js'); var displayTimeout; @@ -63,14 +59,18 @@ gpio.setup(cfg.hardware.button, 'in').then((data) => { //////////////////////////////////// // first check after launch -Status.findOneAndUpdate({ key: 'heatingMode' }, { value: 0 }, { new: true, upsert: true }).exec((err, data) => { - checkTemperatures(); - _set(); +Status.findOneAndUpdate({ key: 'heatingMode' }, { value: 'auto' }, { new: true, upsert: true }).exec((err, data) => { + Status.findOneAndUpdate({ key: 'heaterOn' }, { value: false }, { new: true, upsert: true }).exec((err, data) => { + checkTemperatures(); + _set(); + heater.checkHeaterStatus(); + }); }); // interval checks setInterval(checkTemperatures, 1000 * 30); // every 30s setInterval(_set, 1000 * 60); // every 1min +setInterval(heater.checkHeaterStatus, 1000 * 60); // every 1min ///////////// // on kill // diff --git a/display.js b/bin/display.js similarity index 68% rename from display.js rename to bin/display.js index 2cbbcd7..21afe13 100644 --- a/display.js +++ b/bin/display.js @@ -1,70 +1,84 @@ -var status, lines; -var mongoose = require('mongoose'); -var moment = require('moment'); -var _ = require('lodash'); - -var lcd = require('./lib/lcd.js'); -var app = require('./lib/global'); - -var Status = require('./models/status.js'); -var Zone = require('./models/zone.js'); - -require('dotenv').load(); -var cfg = require('./config.json'); - -// set the mongoose promise library to the nodejs one, required by mongoose now -mongoose.Promise = global.Promise; -// connect to the mongodb -mongoose.connect(process.env.MONGODB); -// output an error if the connection fails - kill the app -mongoose.connection.on('error', () => { - console.error('ERROR - MongoDB Connection Error. Please make sure that MongoDB is running.'); - process.exit(1); -}); - -lcd.on('ready', () => { - app.log('LCD READY!!'); -}); - -// update display every 10s now -setInterval(updateDisplay, 1000 * 10); - -function updateDisplay() { - app.log('update display....'); - lines = []; - - app.log('...get the current status...'); - app.getCurrentStatus((status, data, statuses) => { - app.log('...current status:', status); - app.log('...get temperatures...'); - - // get current temperatures for display - Zone.findOne({ number: cfg.zone }).exec((err, zone) => { - app.log('...got temperatures!'); - - // set the first line - lines.push( - zone.currentTemperature.toFixed(1) + - 'C > ' + - zone.targetTemperature.toFixed(1) + 'C ' + (statuses.heaterOn.value === 'true' ? '#': ' ') - ); - - // put the status in the second line, fill it up with spaces to prevent display bugs - no idea why they keep appearing - // - looking funny though - lines.push(_.padEnd(status, 16)); - - // print those lines to the display - lcd.clear((err) => { - if (err) { - throw err; - } - - lcd.printLines(lines).then(() => { - app.log('DISPLAY printed all lines'); - }); - // debug output - app.log('print lines:', lines); - }); - }); - }) +var status, lines; +var mongoose = require('mongoose'); +var moment = require('moment'); +var _ = require('lodash'); + +var lcd; +var app = require('../controller/app.js'); + +var Status = require('../models/status.js'); +var Zone = require('../models/zone.js'); + +require('dotenv').load(); +var cfg = require('../config.json'); + +// set the mongoose promise library to the nodejs one, required by mongoose now +mongoose.Promise = global.Promise; +// connect to the mongodb +mongoose.connect(process.env.MONGODB); +// output an error if the connection fails - kill the app +mongoose.connection.on('error', () => { + console.error('ERROR - MongoDB Connection Error. Please make sure that MongoDB is running.'); + process.exit(1); +}); + +// call start display +reopenDisplay(); +updateDisplay(); +// update display every 10s now +setInterval(reopenDisplay, 1000 * 60) +setInterval(updateDisplay, 1000 * 31); + +function reopenDisplay() { + if(lcd && lcd.close) lcd.close(); + lcd = require('../lib/lcd.js'); + + lcd.on('ready', () => { + app.log('LCD READY!!'); + }); +} + +function getHeaterOn(statuses) { + app.log('DISPLAY getHeaterOn', Object.keys(statuses)); + return (statuses.heaterOn.value === 'true' ? '#' : (statuses.cooldownOn.value === 'true' ? '0' : ' ')); +} + +function updateDisplay() { + app.log('update display....'); + lines = []; + + app.log('...get the current status...'); + app.getCurrentStatus((status, data, statuses) => { + app.log('...current status:', status); + app.log('...get temperatures...'); + + // get current temperatures for display + Zone.findOne({ number: cfg.zone }).exec((err, zone) => { + app.log('...got temperatures!'); + + // set the first line + lines.push( + zone.currentTemperature.toFixed(1) + + 'C > ' + + zone.targetTemperature.toFixed(1) + 'C ' + getHeaterOn(statuses) + ); + + // put the status in the second line, fill it up with spaces to prevent display bugs - no idea why they keep appearing + // - looking funny though + lines.push(_.padEnd(status, 16)); + + // print those lines to the display + lcd.clear((err) => { + if (err) { + app.log('DISPLAY got ERROR while clearing:', err); + } + + lcd.printLines(lines).then(() => { + app.log('DISPLAY printed all lines'); + }); + // debug output + app.log('print lines:', lines); + }); + }); + }) } \ No newline at end of file diff --git a/homekit.js b/bin/homekit.js similarity index 54% rename from homekit.js rename to bin/homekit.js index e9417e2..de5caec 100644 --- a/homekit.js +++ b/bin/homekit.js @@ -1,19 +1,13 @@ var mongoose = require('mongoose'); -var path = require('path'); var HAP = require('hap-nodejs'); -var app = require('./lib/global'); -var Zone = require('./models/zone.js'); +var app = require('../controller/app.js'); +var homekit = require('../controller/homekit.js'); require('dotenv').load(); -var cfg = require('./config.json'); +var cfg = require('../config.json'); -// init the HAP-server -HAP.init(); -var storage = require('node-persist'); -var uuid = HAP.uuid; -var Accessory = HAP.Accessory; -var accessoryLoader = require('hap-nodejs/lib/AccessoryLoader'); +app.log("HAP-NodeJS starting..."); // set the mongoose promise library to the nodejs one, required by mongoose now mongoose.Promise = global.Promise; @@ -25,30 +19,20 @@ mongoose.connection.on('error', function() { process.exit(1); }); +// init the HAP-server +HAP.init(); +var storage = require('node-persist'); +var uuid = HAP.uuid; + // Initialize our storage system storage.initSync(); // Our Accessories will each have their own HAP server; we will assign ports sequentially var targetPort = 51826; -// Load up all accessories in the /accessories folder -var dir = path.join(__dirname, "accessories"); -var accessories = accessoryLoader.loadDirectory(dir); - -app.log("HAP-NodeJS starting..."); // Publish them all separately (as opposed to BridgedCore which publishes them behind a single Bridge accessory) -accessories.forEach(function(accessory) { - - // To push Accessories separately, we'll need a few extra properties - if (!accessory.username) - throw new Error("Username not found on accessory '" + accessory.displayName + - "'. Core.js requires all accessories to define a unique 'username' property."); - - if (!accessory.pincode) - throw new Error("Pincode not found on accessory '" + accessory.displayName + - "'. Core.js requires all accessories to define a 'pincode' property."); +homekit.accessories.forEach(function(accessory) { - // app.log('test', accessory.services[0].characteristics); // publish this Accessory on the local network accessory.publish({ port: targetPort++, diff --git a/bin/status.js b/bin/status.js new file mode 100644 index 0000000..118149b --- /dev/null +++ b/bin/status.js @@ -0,0 +1,60 @@ + +var mongoose = require('mongoose'); +var moment = require('moment'); +var table = require('text-table'); + +var app = require('../controller/app.js'); + +var Status = require('../models/status.js'); +var Zone = require('../models/zone.js'); + +require('dotenv').load(); +process.env.LOGGING = false; +var cfg = require('../config.json'); + +// set the mongoose promise library to the nodejs one, required by mongoose now +mongoose.Promise = global.Promise; +// connect to the mongodb +mongoose.connect(process.env.MONGODB); +// output an error if the connection fails - kill the app +mongoose.connection.on('error', () => { + console.error('ERROR - MongoDB Connection Error. Please make sure that MongoDB is running.'); + process.exit(1); +}); + +function onoff(val) { + if (typeof val === 'boolean') return val ? 'on' : 'off'; + else return val === 'true' ? 'on' : 'off'; +} + +var lines = []; + +app.getCurrentStatus((status, data, statuses) => { + console.log(); + console.log('########################'); + console.log('#### raspi-heater Status'); + console.log('########################'); + if (statuses.heaterOn) lines.push([ 'heaterOn', onoff(statuses.heaterOn.value), 'since ' + moment(statuses.heaterOn.updatedAt).fromNow(true) ]); + if (statuses.targetHeaterOn) lines.push([ 'targetHeaterOn', onoff(statuses.targetHeaterOn.value), 'since ' + moment(statuses.targetHeaterOn.updatedAt).fromNow(true) ]); + if (statuses.cooldownOn) lines.push([ 'cooldownOn', onoff(statuses.cooldownOn.value), 'since ' + moment(statuses.cooldownOn.updatedAt).fromNow(true) ]); + if (statuses.heatingMode) lines.push([ 'heatingMode', statuses.heatingMode.value, 'changed ' + moment(statuses.heatingMode.updatedAt).fromNow() ]); + console.log(table(lines)); + lines = []; + console.log('######################'); + lines.push([ 'status', status ]); + if (statuses.isHome) lines.push([ 'isHome', statuses.isHome.value ]); + if (statuses.isHoliday) lines.push([ 'isHoliday', statuses.isHoliday.value ]); + console.log(table(lines)); + lines = []; + console.log('######################'); + + Zone.findOne({ number: cfg.zone }).exec((err, zone) => { + lines.push([ 'current temerature:', zone.currentTemperature.toFixed(2) + '°C' ]); + lines.push([ 'target temerature:', zone.targetTemperature.toFixed(2) + '°C' ]); + console.log(table(lines)); + console.log('######################'); + console.log(); + + process.exit(1); + }); +}); \ No newline at end of file diff --git a/config.json b/config.json index 7f09129..e10eb49 100644 --- a/config.json +++ b/config.json @@ -12,7 +12,10 @@ "data": [5, 6, 17, 18] } }, - "defaults": { + "maxOnDuration": 30, + "maxCooldownDuration": 10, + "manualModeDuration": 120, + "defaultTemperatures": { "home": 20, "away": 18, "holiday": 17 diff --git a/lib/global.js b/controller/app.js similarity index 69% rename from lib/global.js rename to controller/app.js index 3f4e45c..ccedb7f 100644 --- a/lib/global.js +++ b/controller/app.js @@ -2,7 +2,7 @@ var moment = require('moment'); var _ = require('lodash'); var cfg = require('../config.json'); -var sensor = require('./sensor'); +var sensor = require('../lib/sensor'); var Status = require('../models/status.js'); var Zone = require('../models/zone.js'); @@ -22,15 +22,18 @@ function getCurrentStatus(cb) { status = 'home'; } - _getCurrentConfig().then((data) => { - if (statuses.heatingMode && statuses.heatingMode.value === '1') { - status = 'timer'; + _getCurrentConfig(null, statuses.heatingMode).then((data) => { + // manual mode may be globally active, but not in this zone + // => use the actual status if not + // => maybe not the best solution, but the best way for my own heater setup + if (data.manual && data.temperatures['manual']) { + status = 'manual'; } - + clog('CONTROL current status:', status); cb(status, data, statuses); - }); + }).catch((err) => { clog('APP caught error', err); }); } }); } @@ -54,7 +57,7 @@ function hex2String(input) { function clog() { var args = Array.prototype.slice.call(arguments); args.unshift('[' + moment().format('YYYY-MM-DD HH:mm:ss') + ']'); - console.log.apply(null, args); + if(process.env.LOGGING !== 'false') console.log.apply(null, args); } function updateCurrentTemperature(temp, cb) { @@ -112,10 +115,11 @@ function updateTargetTemperature(cb) { * @param {object} now moment.js Obj for setting the current time - if undefined use real now * @return {object} object containing data and the temperature set */ -function _getCurrentConfig(now) { +function _getCurrentConfig(now, heatingMode) { now = now || moment(); var dateArray = []; var foundTime; + var manualMode = false; for (let day of cfg.days) { for(let time of day.times) { @@ -131,6 +135,11 @@ function _getCurrentConfig(now) { dateArray.push({ datetime: now._d, now: true }); + if (heatingMode && heatingMode.value !== 'auto') { + manualMode = true; + dateArray.push({ on: (heatingMode.value === 'on'), datetime: heatingMode.updatedAt, manual: true }); + } + dateArray.sort((a, b) => { if (moment(a.datetime).unix() > moment(b.datetime).unix()) { return 1; @@ -150,15 +159,31 @@ function _getCurrentConfig(now) { foundIndex = foundIndex === 0 ? dateArray.length : foundIndex; foundTime = dateArray[foundIndex - 1]; - return Zone.findOne({ number: cfg.zone }) - .then((data) => { + return Zone.findOne({ number: cfg.zone }).then((zoneData) => { + if (foundTime.manual && now.diff(moment(foundTime.datetime), 'minutes') < cfg.manualModeDuration) { + // manual mode is not overwritten by config AND not older than config (120min) + return { - day: foundTime.day, - dayIndex: foundTime.dayIndex, - time: foundTime.time, - temperatures: _.extend(cfg.defaults, foundTime.temperatures, { timer: data.customTemperature }) - }; - }); + manual: true, + temperatures: { manual: zoneData.customTemperature || 20 } + } + } else if (manualMode) { + // manualMode tryes to be still active, although it already too old + // => reset it + Status.findOneAndUpdate({ key: 'heatingMode' }, { value: 'auto' }, { new: true, upsert: true }).exec(); + + foundIndex = foundIndex === 0 ? dateArray.length : foundIndex; + foundTime = dateArray[foundIndex - 1]; + } + + return { + day: foundTime.day, + dayIndex: foundTime.dayIndex, + time: foundTime.time, + temperatures: _.extend(cfg.defaultTemperatures, foundTime.temperatures) + } + }) + } // exports diff --git a/controller/heater.js b/controller/heater.js index 45df632..c41ce24 100644 --- a/controller/heater.js +++ b/controller/heater.js @@ -1,49 +1,211 @@ +var moment = require('moment'); + +var app = require('../controller/app.js'); +var cfg = require('../config.json'); + var Status = require('../models/status.js'); -var heater = require('../lib/heater.js'); -module.exports = (function() { - return { - on, - off, - toggle - }; +var heaterHrdwr = require('../lib/heater.js'); - function on() { - return toggle(true); - } +var cooldownTimer; +var onTimer; - function off() { - return toggle(false); - } +function on() { + return toggle(true); +} - function toggle(state) { - Status.findOne({ 'key': 'heaterOn' }).select('key value').exec((err, oldStatus) => { - var newSetting = false; +function off() { + return toggle(false); +} - console.log('HEATER: old status:', oldStatus.value); - - if (!oldStatus && typeof oldStatus.value !== 'string') { - newSetting = true; - var oldStatus = new Status({ - key: 'heaterOn', - value: state - }); - } +function get(cb) { + var heater = false; + var targetHeater = false; + var cooldown = false; + + app.getCurrentStatus((status, data, statuses) => { + if (statuses.heaterOn && statuses.heaterOn.value === 'true') { + heater = true; + } + + if (statuses.targetHeaterOn && statuses.targetHeaterOn.value === 'true') { + targetHeater = true; + } + + if (statuses.cooldownOn && statuses.cooldownOn.value === 'true') { + cooldown = true; + } + + cb({ + heater, + targetHeater, + cooldown, + statuses + }); + }); +} + +function toggleOLD(state) { + Status.findOne({ 'key': 'heaterOn' }).select('key value').exec((err, oldStatus) => { + var newSetting = false; + + app.log('HEATER: old status:', oldStatus.value); + + if (!oldStatus && typeof oldStatus.value !== 'string') { + newSetting = true; + var oldStatus = new Status({ + key: 'heaterOn', + value: state + }); + } + + app.log('HEATER new heater status?:', state); + + if (state !== (oldStatus.value === 'true') || newSetting) { + app.log('HEATER really toggle heater!'); - console.log('HEATER new heater status?:', state); + oldStatus.value = state; + oldStatus.save((err, status) => { + if (status && status.value !== 'false') { + heater.on(); + } else { + heater.off(); + } + }); + } + }) +} + +function toggle(newState) { + Status.findOneAndUpdate({ key: 'targetHeaterOn' }, { value: newState }, { upsert: true }).select('key value').exec((err, oldState) => { + app.log('HEATER trying to toggle status...'); + app.log('HEATER ...old status:', oldState.value || false); + app.log('HEATER ...toggle status to:', newState); + }); +} + +function checkHeaterStatus() { + var heater = false; + var targetHeater = false; + var cooldown = false; + + app.log('HEATER checkHeaterStatus'); + + get((data) => { + var statuses = data.statuses; + cooldown = data.cooldown; + targetHeater = data.targetHeater; + heater = data.heater; + + app.log('HEATER checkHeaterStatus - cooldown:', cooldown); + app.log('HEATER checkHeaterStatus - heater:', heater); + app.log('HEATER checkHeaterStatus - targetHeater:', targetHeater); + + if (cooldown && (moment().unix() - moment(statuses.cooldownOn.updatedAt).unix() > (cfg.maxCooldownDuration * 60))) { + // cooldown state is too old - turn cooldown off + app.log('HEATER checkHeaterStatus - cooldown state too old, turn it off'); + Status.findOneAndUpdate({ key: 'cooldownOn' }, { value: false }, { upsert: true }).exec(); + cooldown = false; + } + + if (targetHeater) { + // heater is supposed to be on + if (heater && (moment().unix() - moment(statuses.heaterOn.updatedAt).unix() > (cfg.maxOnDuration * 60))) { + // heater is on for too long => start cooldown + app.log('HEATER checkHeaterStatus - heater is on for too long, turn it off'); + Status.findOneAndUpdate({ key: 'heaterOn' }, { value: false }, { upsert: true }).exec(); + Status.findOneAndUpdate({ key: 'cooldownOn' }, { value: true }, { upsert: true }).exec(); + heater = false; + cooldown = true; + } - if (state !== (oldStatus.value === 'true') || newSetting) { - console.log('HEATER really toggle heater!'); - - oldStatus.value = state; - oldStatus.save((err, status) => { - if (status && status.value !== 'false') { - heater.on(); - } else { - heater.off(); - } - }); + if(!heater && !cooldown) { + // heater is off, should be on AND cooldown off => turn it on! + app.log('HEATER checkHeaterStatus - heater is off for too long, turn it on'); + Status.findOneAndUpdate({ key: 'heaterOn' }, { value: true }, { upsert: true }).exec(); + heater = true; + } + } else { + // heater is supposed to be off + if (heater) { + //...but actually its on :( + heater = false; } - }) + } + + app.log('HEATER checkHeaterStatus - ACTUALLY cooldown:', cooldown); + app.log('HEATER checkHeaterStatus - ACTUALLY heater:', heater); + + + if (heater && !cooldown) { + // heater on - but no cooldown + heaterHrdwr.on(); + } + + if (!heater) { + heaterHrdwr.off(); + } + }); +} + + +function toggleHeater(state) { + Status.findOne({ key: 'cooldownOn' }).exec((err, cooldownState) => { + if (cooldownState.value === 'true') { + // cooldown active + if (moment().unix() - moment(cooldownState.updatedAt).unix() > (cfg.maxCooldownDuration * 60)) { + // cooldown too old + } + } + }); + + if (newState === false) { + // turn off + if (!timeout) { + // without timeout + app.log('HEATER turn OFF heater - clearing cooldown timeout'); + + clearTimeout(cooldownTimer); + } else { + // with timeout + app.log('HEATER ...starting timer for turning it back on.'); + + cooldownTimer = setTimeout(function() { + app.log('HEATER ... heater cooled down, turn it on again!'); + Status.findOneAndUpdate({ key: 'cooldownOn' }, { value: false }, { upsert: true }).exec(); + + toggle(true, true); + }, 1000 * 60 * cfg.maxCooldownDuration); + } + + app.log('HEATER - ACTUALLY turn OFF now'); + heater.off(); + } else { + // turn on + if (!timeout) { + app.log('HEATER turn ON heater - starting new timeout...'); + } else { + app.log('HEATER turn heater back on from cooldown...'); + } + + clearTimeout(onTimer); + onTimer = setTimeout(function() { + app.log('HEATER ... heater was on for too long, cooldown turns it off!'); + Status.findOneAndUpdate({ key: 'cooldownOn' }, { value: true }, { upsert: true }).exec(); + + toggle(false, true); + }, 1000 * 60 * cfg.maxOnDuration); + + app.log('HEATER - ACTUALLY turn ON now'); + heater.on(); } -})(); \ No newline at end of file +} + + +module.exports = { + on, + off, + toggle, + get, + checkHeaterStatus +}; \ No newline at end of file diff --git a/controller/homekit.js b/controller/homekit.js new file mode 100644 index 0000000..953196b --- /dev/null +++ b/controller/homekit.js @@ -0,0 +1,22 @@ +var path = require('path'); + +var accessoryLoader = require('hap-nodejs/lib/AccessoryLoader'); + +// Load up all accessories in the /accessories folder +var dir = path.join(__dirname, '../accessories'); +var accessories = accessoryLoader.loadDirectory(dir); + +// Publish them all separately (as opposed to BridgedCore which publishes them behind a single Bridge accessory) +accessories.forEach(function(accessory) { + + // To push Accessories separately, we'll need a few extra properties + if (!accessory.username) + throw new Error('Username not found on accessory "' + accessory.displayName + '". Core.js requires all accessories to define a unique "username" property.'); + + if (!accessory.pincode) + throw new Error('Pincode not found on accessory "' + accessory.displayName + '". Core.js requires all accessories to define a "pincode" property.'); +}); + +module.exports = { + accessories: accessories +} \ No newline at end of file diff --git a/lib/heater.js b/lib/heater.js index 0c06c01..974cd3f 100644 --- a/lib/heater.js +++ b/lib/heater.js @@ -1,20 +1,35 @@ -var app = require('./global'); var gpio = require('./gpio_wrapper'); + +var app = require('../controller/app.js'); var cfg = require('../config.json'); +function on() { + app.log('HARDWARE turned heater on'); + gpio.setFalse(cfg.hardware.relay); +} -module.exports = { - on: function() { - app.log('HARDWARE turned heater on'); - gpio.setFalse(cfg.hardware.relay); - }, - off: function() { - app.log('HARDWARE turned heater off'); - gpio.setTrue(cfg.hardware.relay); - }, - get: function() { - return gpio.get(cfg.hardware.relay).then((status) => { - return !!status; - }); +function off() { + app.log('HARDWARE turned heater off'); + gpio.setTrue(cfg.hardware.relay); +} + +function get() { + return gpio.get(cfg.hardware.relay).then((status) => { + return !!status; + }); +} + +function set(state) { + if (state) { + on(); + } else { + off(); } +} + +module.exports = { + on: on, + off: off, + get: get, + set: set } \ No newline at end of file diff --git a/lib/lcd.js b/lib/lcd.js index 0386573..f1f218d 100644 --- a/lib/lcd.js +++ b/lib/lcd.js @@ -1,5 +1,6 @@ -var app = require('./global'); var _ = require('lodash'); + +var app = require('../controller/app.js'); var cfg = require('../config.json'); var Lcd; @@ -28,7 +29,7 @@ if (process.platform === 'win32' || process.platform === 'darwin') { } }; - Lcd.prototype.close = function() {}; + Lcd.prototype.close = function() { app.log('LCD closed!'); }; Lcd.prototype.clear = function(cb) { app.log('LCD DEBUG: #### Display cleared ####'); cb(); }; Lcd.prototype.setCursor = function(x, y) { this.line = y; diff --git a/lib/sensor.js b/lib/sensor.js index e8bbcd1..9d3f231 100644 --- a/lib/sensor.js +++ b/lib/sensor.js @@ -7,7 +7,7 @@ if (process.platform === 'win32' || process.platform === 'darwin') { readDevice: function(sensor) { return new Promise((resolve, reject) => { console.log('FAKE sensor read!', sensor); - resolve({ value: randomIntFromInterval(12, 25) }); + resolve({ value: randomIntFromInterval(15, 21) }); }); } } diff --git a/package.json b/package.json index 0922186..3b2f395 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "raspi-heater", - "version": "2.0.2", + "version": "2.1.0", "description": "", "main": "index.js", "scripts": { @@ -15,12 +15,13 @@ "dependencies": { "dotenv": "^2.0.0", "ds1820-temp": "^1.0.0", - "hap-nodejs": "github:khaost/HAP-NodeJS", + "hap-nodejs": "^0.4.21", "lcd": "^1.1.4", "lodash": "^4.16.4", "moment": "^2.15.2", "mongoose": "^4.6.4", "node-persist": "^2.0.7", - "rpi-gpio": "^0.7.0" + "rpi-gpio": "^0.7.0", + "text-table": "^0.2.0" } } diff --git a/process.json b/process.json index 60f577f..c90da97 100644 --- a/process.json +++ b/process.json @@ -2,17 +2,17 @@ "apps" : [ { "name" : "Control Server", - "script" : "./control.js", + "script" : "./bin/control.js", "watch" : true }, { "name" : "HomeKit Server", - "script" : "./homekit.js", + "script" : "./bin/homekit.js", "watch" : true }, { "name" : "Display Server", - "script" : "./display.js", + "script" : "./bin/display.js", "watch" : true } ]