...
 
Commits (3)
## v2.2.0 - 2020-05-13
- Added temperature-dependent switch to control a fan or heating
## v2.1.1 - 2020-04-23
- Fixed bug with *delay after send*
......
/*
* Automatic Watering System Control App
* Automatic Watering System - Control-App
*
* Frontend
*
......@@ -39,9 +39,34 @@ class WateringClient {
}
document.getElementById('pause').onclick = this.apiPause;
document.getElementById('resume').onclick = this.apiResume;
document.getElementById('tempSwitchOn').onclick = this.apiTempSwitch.bind(this);
document.getElementById('deButton').onclick = () => {
this.i18n.lang = 'de';
this.i18n.translateAll();
}
document.getElementById('enButton').onclick = () => {
this.i18n.lang = 'en';
this.i18n.translateAll();
}
this.apiGetInfo = this.apiGetInfo.bind(this);
// enable info icons
const elems = document.querySelectorAll('[data-info]');
for (let i = 0; i < elems.length; i++) {
let key = elems[i].dataset.info;
elems[i].onclick = (event) => {
const infoElem = document.getElementById(event.target.dataset.info);
if (infoElem.style.display === 'block') {
infoElem.style.display = '';
} else {
infoElem.style.display = 'block';
}
};
}
this.settingsTime = 0;
this.softwareVersion = null;
this.logCount = 0;
......@@ -50,6 +75,8 @@ class WateringClient {
this.apiGetPorts();
this.apiGetInfo();
this.i18n = new I18n();
// start interval to get the info every second
window.setInterval(this.apiGetInfo, 1000);
}
......@@ -101,6 +128,19 @@ class WateringClient {
document.getElementById('nodeAddress').disabled = true;
document.getElementById('delayAfterSend').disabled = true;
}
// temperature switch only available in >= v2.2.0
if (checkVersionGe(this.softwareVersion, '2.2.0')) {
document.getElementById('tempSwitchTriggerValue').disabled = false;
document.getElementById('tempSwitchHyst').disabled = false;
document.getElementById('tempSwitchInverted').disabled = false;
document.getElementById('tempSwitchOn').disabled = false;
} else {
document.getElementById('tempSwitchTriggerValue').disabled = true;
document.getElementById('tempSwitchHyst').disabled = true;
document.getElementById('tempSwitchInverted').disabled = true;
document.getElementById('tempSwitchOn').disabled = true;
}
}
if (info.settings) {
......@@ -132,6 +172,15 @@ class WateringClient {
document.getElementById('nodeAddress').value = '';
document.getElementById('delayAfterSend').value = 10;
}
if (checkVersionGe(this.softwareVersion, '2.2.0')) {
document.getElementById('tempSwitchTriggerValue').value = info.settings.tempSwitchTriggerValue;
document.getElementById('tempSwitchHyst').value = info.settings.tempSwitchHyst;
document.getElementById('tempSwitchInverted').checked = info.settings.tempSwitchInverted;
} else {
document.getElementById('tempSwitchTriggerValue').value = 0;
document.getElementById('tempSwitchHyst').value = 0;
document.getElementById('tempSwitchInverted').checked = false;
}
}
} else {
document.getElementById('settings').style.display = 'none';
......@@ -140,7 +189,8 @@ class WateringClient {
}
for (let i = 0; i < 4; i++) {
document.getElementById('onoff' + i).innerHTML = info.status.on[i] ? 'On' : 'Off';
document.getElementById('onoff' + i).innerHTML = info.status.on[i] ? this.i18n.__('on') : this.i18n.__('off');
document.getElementById('onoff' + i).dataset.translate = info.status.on[i] ? 'on' : 'off';
if (info.status.on[i]) {
document.getElementById('onoff' + i).classList.add('on');
} else {
......@@ -153,6 +203,16 @@ class WateringClient {
document.getElementById('battery').innerHTML = info.status.batPercent + ' %';
document.getElementById('battery2').innerHTML = info.status.batVolt + ' V (' + info.status.batRaw + ')';
if (info.status.tempSwitchOn) {
document.getElementById('tempSwitchOn').innerHTML = this.i18n.__('on');
document.getElementById('tempSwitchOn').dataset.translate = 'on';
document.getElementById('tempSwitchOn').classList.add('on');
} else {
document.getElementById('tempSwitchOn').innerHTML = this.i18n.__('off');
document.getElementById('tempSwitchOn').dataset.translate = 'off';
document.getElementById('tempSwitchOn').classList.remove('on');
}
if (this.logCount != info.log.length) {
document.getElementById('log').innerHTML = info.log.map((l) => { return l.time + ' ' + l.text }).reverse().join('\n');
this.logCount = info.log.length;
......@@ -262,7 +322,10 @@ class WateringClient {
pushDataEnabled: document.getElementById('pushDataEnabled').checked,
serverAddress: document.getElementById('serverAddress').value,
nodeAddress: document.getElementById('nodeAddress').value,
delayAfterSend: document.getElementById('delayAfterSend').value
delayAfterSend: document.getElementById('delayAfterSend').value,
tempSwitchTriggerValue: document.getElementById('tempSwitchTriggerValue').value,
tempSwitchHyst: document.getElementById('tempSwitchHyst').value,
tempSwitchInverted: document.getElementById('tempSwitchInverted').checked,
}),
headers: {
'content-type': 'application/json'
......@@ -293,7 +356,7 @@ class WateringClient {
* Called by pressing a button.
*/
apiOnoff (event) {
let data = {
const data = {
channel: event.target.dataset.chan || 0,
on: false // turn off by default
};
......@@ -315,6 +378,28 @@ class WateringClient {
});
}
/**
* Method to turn the temperature switch on/off.
* Called by pressing a button.
*/
apiTempSwitch (event) {
const data = {
on: !this.info.status.tempSwitchOn
};
fetch('/api/tempSwitch', {
body: JSON.stringify(data),
headers: {
'content-type': 'application/json'
},
method: 'POST'
})
.then((res) => {
if (res.status != 200) {
alert('Error! ' + res.status + '\n' + res.body);
}
});
}
/**
* Method to send the 'pause' command to the watering system.
*/
......
This diff is collapsed.
This diff is collapsed.
/*
* Automatic Watering System Control App
* Automatic Watering System - Control-App
*
* Frontend styles
*
......@@ -48,6 +48,10 @@
width: 80px;
}
.cell button.wide {
width: auto;
}
#port {
width: 150px;
}
......@@ -82,3 +86,40 @@ h1 {
font-size: 1.2em;
font-weight: bold;
}
[data-info] {
color: #009;
cursor: pointer;
font-weight: bold;
user-select: none;
float: right;
display: inline-block;
width: 20px;
margin: 0 3px;
text-align: center;
border-radius: 8px;
background: #ddd;
}
[data-info]:hover {
background: #eee;
color: #00a;
}
.description {
padding: 10px;
left: 50px;
min-width: 50%;
display: none;
position: absolute;
background: #eeeeeef0;
box-shadow: 5px 5px #aaaaaa55;
border: 1px dotted #aaa;
}
code {
font-family: monospace;
font-size: 1.2em;
font-weight: bold;
}
......@@ -38,6 +38,7 @@ const RH_MSG_RESUME = 0x64;
const RH_MSG_TURN_CHANNEL_ON_OFF = 0x65; // >= v2.0.0 only
const RH_MSG_POLL_DATA = 0x66; // >= v2.0.0 only
const RH_MSG_PAUSE_ON_OFF = 0x67; // >= v2.0.0 only
const RH_MSG_TURN_TEMP_SWITCH_ON_OFF = 0x68; // >= v2.2.0 only
const RH_MSG_GET_VERSION = 0xF0;
const RH_MSG_VERSION = 0xF1;
......@@ -79,6 +80,7 @@ class Watering {
this.apiPause = this.apiPause.bind(this);
this.apiResume = this.apiResume.bind(this);
this.apiOnoff = this.apiOnoff.bind(this);
this.apiTempSwitch = this.apiTempSwitch.bind(this);
this.log = this.log.bind(this);
this.rhsSend = this.rhsSend.bind(this);
this.rhsReceived = this.rhsReceived.bind(this);
......@@ -108,6 +110,7 @@ class Watering {
this.app.post('/api/connect', this.apiConnect);
this.app.get('/api/disconnect', this.apiDisconnect);
this.app.post('/api/onoff', this.apiOnoff);
this.app.post('/api/tempSwitch', this.apiTempSwitch);
this.app.post('/api/setSettings', this.apiSetSettings);
// let the server listen on the configured host and port
......@@ -187,7 +190,7 @@ class Watering {
this.connected = true;
this.log('connected to the serial-radio gateway');
this.log(`connected to the serial-radio gateway via ${req.body.port}, baud ${req.body.baud}`);
// request the software version from the watering system
// use an interval to retry until we got a version
......@@ -295,8 +298,16 @@ class Watering {
this.settings.delayAfterSend = parseInt(req.body.delayAfterSend, 10);
}
if (semver.satisfies(this.softwareVersion, '>=2.2.0')) {
this.settings.tempSwitchTriggerValue = parseInt(req.body.tempSwitchTriggerValue, 10);
this.settings.tempSwitchHyst = parseFloat(req.body.tempSwitchHyst);
this.settings.tempSwitchInverted = req.body.tempSwitchInverted;
}
let buf;
if (semver.satisfies(this.softwareVersion, '>=2.1.0')) {
if (semver.satisfies(this.softwareVersion, '>=2.2.0')) {
buf = Buffer.alloc(28);
} else if (semver.satisfies(this.softwareVersion, '>=2.1.0')) {
buf = Buffer.alloc(26);
} else {
buf = Buffer.alloc(22);
......@@ -311,13 +322,14 @@ class Watering {
buf.writeUInt16LE(this.settings.adcTriggerValue[chan], 2+chan*2);
buf.writeUInt16LE(this.settings.wateringTime[chan], 10+chan*2);
}
if (this.settings.sendAdcValuesThroughRH) {
bools |= (1 << 7);
}
if (semver.satisfies(this.softwareVersion, '>=2.0.0') && this.settings.pushDataEnabled) {
bools |= (1 << 6);
}
buf[1] = bools;
buf.writeUInt16LE(this.settings.checkInterval, 18);
buf.writeUInt16LE(this.settings.tempSensorInterval, 20);
......@@ -327,6 +339,17 @@ class Watering {
buf.writeUInt16LE(this.settings.delayAfterSend, 24);
}
if (semver.satisfies(this.softwareVersion, '>=2.2.0')) {
buf.writeInt8(this.settings.tempSwitchTriggerValue, 26);
const tempSwitchHystTenth = Math.floor(this.settings.tempSwitchHyst * 10);
buf.writeUInt8(tempSwitchHystTenth, 27);
if (this.settings.tempSwitchInverted) {
bools |= (1 << 5);
}
}
buf[1] = bools;
this.rhsSend(buf);
res.send('Ok');
......@@ -374,6 +397,19 @@ class Watering {
res.send('Ok');
}
/**
* API endpoint for turning the temperature switch on or off at the watering system.
*/
apiTempSwitch (req, res, next) {
const buf = Buffer.alloc(2);
buf[0] = RH_MSG_TURN_TEMP_SWITCH_ON_OFF;
buf[1] = req.body.on ? 0x01 : 0x00;
this.rhsSend(buf);
res.send('Ok');
}
/**
* API endpoint for sending a 'pause' command to the watering system.
*/
......@@ -506,6 +542,16 @@ class Watering {
} else {
this.status.humidity = '-';
}
this.status.tempSwitchOn = false;
if (semver.satisfies(this.softwareVersion, '>=2.2.0')) {
// byte 5 or 6 is tempSwitchOn
if (msg.data.length === 6) {
this.status.tempSwitchOn = (msg.data[5] >= 0x01) ? true : false;
} else if (msg.data.length === 10) {
this.status.tempSwitchOn = (msg.data[9] >= 0x01) ? true : false;
}
}
break;
case RH_MSG_CHANNEL_ON: // < v2.0.0
......@@ -553,6 +599,11 @@ class Watering {
this.settings.nodeAddress = msg.data[23];
this.settings.delayAfterSend = msg.data.readUInt16LE(24);
}
if (semver.satisfies(this.softwareVersion, '>=2.2.0')) {
this.settings.tempSwitchTriggerValue = msg.data.readInt8(26);
this.settings.tempSwitchHyst = msg.data.readUInt8(27) / 10;
this.settings.tempSwitchInverted = ((msg.data[1] & (1 << 5)) != 0);
}
break;
case RH_MSG_VERSION:
......
{
"name": "auto-watering-control",
"version": "2.1.1",
"version": "2.2.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
......
{
"name": "auto-watering-control",
"version": "2.1.1",
"version": "2.2.0",
"description": "Control tool for the automatic watering system",
"main": "index.js",
"scripts": {
......
......@@ -18,13 +18,3 @@ lib_deps =
PinChangeInterrupt@1.2.6
RadioHead@1.82
SPI
#[env:nanoatmega328]
#platform = atmelavr
#board = nanoatmega328
#framework = arduino
#lib_deps =
# DHTStable@0.2.4
# PinChangeInterrupt@1.2.6
# RadioHead@1.82
# SPI
......@@ -25,10 +25,11 @@
#define SENSORS_ACTIVE_PIN 10
#define RH_TX_PIN 11
#define RH_RX_PIN 12
#define RH_PTT_PIN 17 // unused but needed
#define RH_PTT_PIN 18 // unused but needed
#define LED_PIN 13
#define TEMP_SENSOR_PIN 14
#define EEPROM_RESET_PIN 15
#define TEMP_SWITCH_PIN 17
/*
* Analog pins
......
......@@ -22,6 +22,9 @@ uint16_t adcValues[4] = {0, 0, 0, 0};
float temperature = -99;
float humidity = -99;
uint16_t batteryRaw;
bool tempSwitchOn = false;
float tempSwitchTriggerValueHigh = 32;
float tempSwitchTriggerValueLow = 28;
bool pauseAutomatic;
......
......@@ -20,11 +20,11 @@
// version number of the software
#define SOFTWARE_VERSION_MAJOR 2
#define SOFTWARE_VERSION_MINOR 1
#define SOFTWARE_VERSION_PATCH 1
#define SOFTWARE_VERSION_MINOR 2
#define SOFTWARE_VERSION_PATCH 0
// version of the eeporm data model; must be increased if the data model changes
#define EEPROM_VERSION 4
#define EEPROM_VERSION 5
// eeprom addresses
#define EEPROM_ADDR_VERSION 0 // 1 byte
......@@ -48,6 +48,9 @@ struct Settings {
uint8_t serverAddress; // the address of the server in the RadioHead network
uint8_t ownAddress; // the address of this node in the RadioHead network
uint16_t delayAfterSend; // time in milliseconds to delay after each data send
int8_t tempSwitchTriggerValue; // value where to trigger the temperature switch
uint8_t tempSwitchHystTenth; // hysteresis of the temperature switch in tenth of the value (10 = 0,1)
bool tempSwitchInverted; // if the switch will be inverted (default temp>value = on)
};
/**
......@@ -72,6 +75,10 @@ extern float temperature;
extern float humidity;
extern uint16_t batteryRaw;
extern bool tempSwitchOn;
extern float tempSwitchTriggerValueHigh;
extern float tempSwitchTriggerValueLow;
extern bool pauseAutomatic;
#if TEMP_SENSOR_TYPE == 11 || TEMP_SENSOR_TYPE == 12 || TEMP_SENSOR_TYPE == 22
......
......@@ -42,9 +42,6 @@ void loop () {
// sensor read ok
temperature = dhtSensor.temperature;
humidity = dhtSensor.humidity;
// send RadioHead message
memcpy(&rhBufTx[1], &temperature, 4);
memcpy(&rhBufTx[5], &humidity, 4);
sensorReadOk = true;
} else {
temperature = -99;
......@@ -57,11 +54,6 @@ void loop () {
temperature = ds1820.getTempCByIndex(0);
if (temperature != DEVICE_DISCONNECTED_C) {
memcpy(&rhBufTx[1], &temperature, 4);
rhBufTx[5] = 0x00;
rhBufTx[6] = 0x00;
rhBufTx[7] = 0x00;
rhBufTx[8] = 0x00;
sensorReadOk = true;
} else {
temperature = -99;
......@@ -72,6 +64,26 @@ void loop () {
#endif
if (sensorReadOk) {
// check temperature switch
if (tempSwitchTriggerValueLow != 0.0 && tempSwitchTriggerValueHigh != 0.0) {
// automatic switching enabled
if (!tempSwitchOn && (
(temperature >= tempSwitchTriggerValueHigh && !settings.tempSwitchInverted)
|| (temperature <= tempSwitchTriggerValueLow && settings.tempSwitchInverted)
)) {
// turn on the temperature switch
digitalWrite(TEMP_SWITCH_PIN, HIGH);
tempSwitchOn = true;
} else if (tempSwitchOn && (
(temperature <= tempSwitchTriggerValueLow && !settings.tempSwitchInverted)
|| (temperature >= tempSwitchTriggerValueHigh && settings.tempSwitchInverted)
)) {
// turn off the temperature switch
digitalWrite(TEMP_SWITCH_PIN, LOW);
tempSwitchOn = false;
}
}
// send data
rhSendData(RH_MSG_TEMP_SENSOR_DATA);
} else {
// sensor read error
......
......@@ -73,19 +73,25 @@ void rhRecv () {
if (settings.pushDataEnabled) {
rhBufTx[1] |= (1 << 6);
}
// bit 5 indecate if temperature switch is inverted
if (settings.tempSwitchInverted) {
rhBufTx[1] |= (1 << 5);
}
memcpy(&rhBufTx[18], &settings.checkInterval, 2);
memcpy(&rhBufTx[20], &settings.tempSensorInterval, 2);
rhBufTx[22] = settings.serverAddress;
rhBufTx[23] = settings.ownAddress;
memcpy(&rhBufTx[24], &settings.delayAfterSend, 2);
rhBufTx[26] = settings.tempSwitchTriggerValue;
rhBufTx[27] = settings.tempSwitchHystTenth;
rhSend(RH_MSG_SETTINGS, 26, rhRxFrom);
rhSend(RH_MSG_SETTINGS, 28, rhRxFrom);
break;
case RH_MSG_SET_SETTINGS:
// got new settings
if (rhRxLen < 26) {
if (rhRxLen < 28) {
return;
}
for (uint8_t chan = 0; chan < 4; chan++) {
......@@ -95,6 +101,7 @@ void rhRecv () {
}
settings.sendAdcValuesThroughRH = ((rhBufRx[1] & (1 << 7)) != 0);
settings.pushDataEnabled = ((rhBufRx[1] & (1 << 6)) != 0);
settings.tempSwitchInverted = ((rhBufRx[1] & (1 << 5)) != 0);
memcpy(&settings.checkInterval, &rhBufRx[18], 2);
memcpy(&settings.tempSensorInterval, &rhBufRx[20], 2);
settings.serverAddress = rhBufRx[22];
......@@ -107,6 +114,12 @@ void rhRecv () {
memcpy(&settings.delayAfterSend, &rhBufRx[24], 2);
settings.tempSwitchTriggerValue = rhBufRx[26];
settings.tempSwitchHystTenth = rhBufRx[27];
// calc temperature switch high/low trigger values
calcTempSwitchTriggerValues();
// calc new read times
// temperature sensor read is 5 seconds before adc read to avoid both readings at the same time
tempSensorNextReadTime = millis() - 5000 + ((uint32_t)settings.tempSensorInterval * 1000);
......@@ -139,6 +152,21 @@ void rhRecv () {
}
break;
case RH_MSG_TURN_TEMP_SWITCH_ON_OFF:
// temperature switch on/off
if (rhRxLen < 2) {
return;
}
if (rhBufRx[1] == 0x01) {
digitalWrite(TEMP_SWITCH_PIN, HIGH);
tempSwitchOn = true;
} else {
digitalWrite(TEMP_SWITCH_PIN, LOW);
tempSwitchOn = false;
}
rhSendData(RH_MSG_TEMP_SENSOR_DATA, RH_FORCE_SEND, rhRxFrom);
break;
case RH_MSG_PAUSE:
// enable pause
pauseAutomatic = true;
......@@ -265,10 +293,12 @@ bool rhSendData(uint8_t msgType, bool forceSend, uint8_t sendTo, uint16_t delayA
#if TEMP_SENSOR_TYPE == 11 || TEMP_SENSOR_TYPE == 12 || TEMP_SENSOR_TYPE == 22
memcpy(&rhBufTx[1], &temperature, 4);
memcpy(&rhBufTx[5], &humidity, 4);
len = 9;
rhBufTx[9] = (tempSwitchOn) ? 0x01 : 0x00;
len = 10;
#elif TEMP_SENSOR_TYPE == 1820
memcpy(&rhBufTx[1], &temperature, 4);
len = 5;
rhBufTx[5] = (tempSwitchOn) ? 0x01 : 0x00;
len = 6;
#else
// nothing to do if no sensor is enabled
return true;
......
......@@ -29,6 +29,7 @@
#define RH_MSG_TURN_CHANNEL_ON_OFF 0x65
#define RH_MSG_POLL_DATA 0x66
#define RH_MSG_PAUSE_ON_OFF 0x67
#define RH_MSG_TURN_TEMP_SWITCH_ON_OFF 0x68
#define RH_MSG_GET_VERSION 0xF0
#define RH_MSG_VERSION 0xF1
......@@ -37,8 +38,8 @@
// buffer for RadioHead messages
// rhBuf?x[0] - message type
#define RH_BUF_TX_LEN 26
#define RH_BUF_RX_LEN 26
#define RH_BUF_TX_LEN 28
#define RH_BUF_RX_LEN 28
extern uint8_t rhBufTx[RH_BUF_TX_LEN];
extern uint8_t rhBufRx[RH_BUF_RX_LEN];
......
......@@ -27,6 +27,11 @@ void loadDefaultSettings () {
settings.serverAddress = RH_SERVER_ADDR; // RadioHead remote node address
settings.ownAddress = RH_OWN_ADDR; // RadioHead address of this node
settings.delayAfterSend = 10; // milliseconds to delay after each send
settings.tempSwitchTriggerValue = 30; // turn on temperature switch if > 30°C
settings.tempSwitchHystTenth = 20; // 2°C hysteresis -> 32°C on, 28°C off
settings.tempSwitchInverted = false; // don't invert - turn on if greater
calcTempSwitchTriggerValues();
}
/**
......@@ -34,6 +39,8 @@ void loadDefaultSettings () {
*/
void loadSettings () {
EEPROM.get(EEPROM_ADDR_SETTINGS, settings);
calcTempSwitchTriggerValues();
}
/**
......@@ -43,3 +50,11 @@ void saveSettings () {
// write to eeprom
EEPROM.put(EEPROM_ADDR_SETTINGS, settings);
}
/**
* Calculate temperature switch high/low trigger values.
*/
void calcTempSwitchTriggerValues () {
tempSwitchTriggerValueHigh = settings.tempSwitchTriggerValue + (float)settings.tempSwitchHystTenth/10;
tempSwitchTriggerValueLow = settings.tempSwitchTriggerValue - (float)settings.tempSwitchHystTenth/10;
}
......@@ -11,5 +11,6 @@
void loadDefaultSettings ();
void loadSettings ();
void saveSettings ();
void calcTempSwitchTriggerValues();
#endif
......@@ -28,6 +28,7 @@ void setup () {
pinMode(SENSORS_ACTIVE_PIN, OUTPUT);
pinMode(LED_PIN, OUTPUT);
pinMode(EEPROM_RESET_PIN, INPUT_PULLUP);
pinMode(TEMP_SWITCH_PIN, OUTPUT);
// blink the LED to indecate starting
blinkCode(BLINK_LONG);
......@@ -40,6 +41,7 @@ void setup () {
digitalWrite(valvePins[chan], LOW);
}
digitalWrite(SENSORS_ACTIVE_PIN, LOW);
digitalWrite(TEMP_SWITCH_PIN, LOW);
pauseAutomatic = false;
// enable PCINT for the buttons
......