Compare commits

...

57 Commits

Author SHA1 Message Date
Skylar Grant 9499d6756e Add timestamp to startup and shutdown commands 2024-09-04 19:53:44 -04:00
Skylar Grant 53b1be6f59 Call corect functions 2024-08-22 22:01:28 -04:00
Skylar Grant 7f8c090781 Fix imports again 2024-08-22 21:59:55 -04:00
Skylar Grant c07c31ab0c Import functions 2024-08-22 21:58:55 -04:00
Skylar Grant cc5dca9628 Add startup and shutdown buttons 2024-08-22 21:53:56 -04:00
Skylar Grant 6ac88305a6 Fix styling 2024-08-22 21:02:18 -04:00
Skylar Grant 14e9a4b932 Add pof and vac places 2024-08-22 20:23:17 -04:00
Skylar Grant 2fd5239094 Ignore package-lock 2024-08-22 20:16:06 -04:00
Skylar Grant 1aa56bb8dd Almost MVP 2024-08-15 12:03:11 -04:00
Skylar Grant b62eebbe0f Only set the power state, not the entire state code lol 2024-08-15 11:44:10 -04:00
Skylar Grant 1c37b63314 Spaghetti at wall. 2024-08-15 11:31:31 -04:00
Skylar Grant 432e2e7dfd Fix what gets sent to MQTT 2024-08-15 11:22:05 -04:00
Skylar Grant ab89e2e256 Begin setup for receiving state changes 2024-08-15 11:20:24 -04:00
Skylar Grant f09f4dcf24 Add array to iterate over for elements 2024-08-15 11:18:06 -04:00
Skylar Grant 5af6ca77ba Fix first call 2024-08-15 11:14:24 -04:00
Skylar Grant 259700a1b5 Unconsolidate generating of states 2024-08-15 11:12:54 -04:00
Skylar Grant 2ef1e9320a Reveal and restyle bottom buttons 2024-08-15 11:12:32 -04:00
Skylar Grant d0940d7bc1 Maybe? 2024-08-15 10:58:31 -04:00
Skylar Grant 2a8734eec8 screm 2024-08-15 10:56:41 -04:00
Skylar Grant a49e0f61b2 Export 2024-08-15 10:55:56 -04:00
Skylar Grant 6cf9d622cb I think I need to give full domain? 2024-08-15 10:51:53 -04:00
Skylar Grant 4ff6404958 Rearrange 2024-08-15 10:49:50 -04:00
Skylar Grant 9b4ea6f576 Try new power toggle name 2024-08-15 10:47:12 -04:00
Skylar Grant 4485d14c5d Move buttons outside of form 2024-08-15 10:45:04 -04:00
Skylar Grant 31da872ef6 Test buttons and state refresh 1 2024-08-15 10:44:10 -04:00
Skylar Grant 7ef748ae0d Try domain again with wss 2024-08-15 10:41:21 -04:00
Skylar Grant 180606e552 Try 9001 2024-08-15 10:31:12 -04:00
Skylar Grant dd3c7e26c3 Try mqtt://mqtt.3411.one 2024-08-15 10:30:45 -04:00
Skylar Grant 2f4714e2e5 Switch min to max 2024-08-15 10:28:11 -04:00
Skylar Grant 82cb858894 Go back from domain 2024-08-15 10:26:59 -04:00
Skylar Grant 97c2343993 Added some debug logging, moved config to main file 2024-08-15 10:24:49 -04:00
Skylar Grant 358136a6ef Move to subdomain for mqtt 2024-08-15 10:13:46 -04:00
Skylar Grant 93ae038a7e Switch from ws to mqtt TCP 2024-08-15 10:05:40 -04:00
Skylar Grant 02db7e36e0 Change ws to wss 2024-08-15 10:01:27 -04:00
Skylar Grant 1975be4375 Testing auth connection mqtt 2024-08-15 10:00:57 -04:00
Skylar Grant 89ccd032a9 Didn't need to declare, needed to hand over config 2024-08-15 09:49:24 -04:00
Skylar Grant 9f42cc6240 Forgot to declare before using 2024-08-15 09:48:26 -04:00
Skylar Grant 024d5d1058 Misnamed variable 2024-08-15 09:47:34 -04:00
Skylar Grant 63053d6a87 Gonna go testing 2024-08-15 09:46:45 -04:00
Skylar Grant 5633c2340a Classes Testing 2024-08-15 09:31:01 -04:00
Skylar Grant c7ffc0c296 Update styling 2024-08-14 20:34:31 -04:00
Skylar Grant 470d00f468 Consolidate buttons 2024-08-14 20:31:22 -04:00
Skylar Grant 538775d726 Hoping and praying 2024-08-14 20:29:22 -04:00
Skylar Grant 6fab8e4101 7 2024-08-14 20:27:24 -04:00
Skylar Grant 1e55c058b9 6 2024-08-14 20:26:40 -04:00
Skylar Grant b30caa1c60 Testing 5 2024-08-14 19:18:01 -04:00
Skylar Grant ca30189c64 Testing 4 2024-08-14 19:15:55 -04:00
Skylar Grant 26c2f0a87d revert config 2024-08-14 19:12:49 -04:00
Skylar Grant 1ce103d39c silly brackets 2024-08-14 19:12:01 -04:00
Skylar Grant 8bea9b55e4 I want to scream 2024-08-14 19:11:09 -04:00
Skylar Grant e24a70053d Add config.json, nothing is sensitive in it, use .env 2024-08-14 19:08:06 -04:00
Skylar Grant 76e66c8818 Fix mqtt import 2024-08-14 19:05:55 -04:00
Skylar Grant 6ba5766a30 Re-fix imports 2024-08-14 19:03:32 -04:00
Skylar Grant b8038e7847 Fix import I hope 2024-08-14 19:02:03 -04:00
Skylar Grant 63ef0da043 Testing 2 2024-08-14 19:00:58 -04:00
Skylar Grant 180723e4f5 Testing 1 2024-08-14 16:25:15 -04:00
Skylar Grant 928b1d36da Init, blank 2024-08-14 14:32:46 -04:00
23 changed files with 328 additions and 5020 deletions

2
.gitignore vendored
View File

@ -9,7 +9,6 @@ lerna-debug.log*
.VSCodeCounter
.vscode
.vscode/*
config.json
log.txt
nohup.out
data/config.db
@ -111,3 +110,4 @@ dist
# TernJS port file
.tern-port
package-lock.json

110
README.md
View File

@ -1,106 +1,6 @@
# Hestia
Node.js Raspberry Pi Pellet Stove Controller, named after the Greek virgin goddess of the hearth.
# Hestia Control Panel Frontend
This is the web frontend for the Hestia Control Panel. Checkout `v2-back` for the backend.
# About
This project seeks to replace the OEM control panel on a Lennox Winslow PS40 pellet stove with a Raspberry Pi. I will be utilizing a Raspberry Pi Zero W, relays, and switches already installed on the pellet stove. I chose a Pi Zero W for its small form factor, the lack of pre-installed headers, its wireless connectivity, and familiarity with the platform. I had previously used an Arduino Nano for a much more rudimentary version of this project and found great difficulty in making adjustments to the code. Additionally the Raspberry Pi platform will allow for expansion in the future to include IoT controls and logging of usage utilizing networked databases. The project will be written using Node.js and the rpi-gpio Node module. I've chosen Node.js for its familiarity as well as ease of implementation of a web server for future expansion.
# GPIO
Three GPIO pins are used along with a common ground to control three relays, supplying 120VAC power to the igniter and combustion blower when appropriate, and supplying power to the auger motor in pulses. Two more GPIO pins are used to detect open/closed status of a temperature-controlled snap switch and a vacuum switch. Another temperature-controlled snap switch is used to supply power to the convection motor when the pellet stove has reached a suitable temperature. A final temperature-controlled snap switch us used to interrupt the circuit for the auger motor to shut the stove off when an over-temperature condition is met. I will be utilizing a OneWire DS18B20 temperature sensor to detect the temperature of air exiting the stove vents.
| Pi Pin | Function | Direction | Wire Color |
| ------:| -------- | --------- | ---------- |
7 | Auger Relay | Out | Blue
13 | Igniter Relay | Out | Blue/White
15 | Combustion Blower Relay | Out | Orange
16 | Proof of Fire Switch | In | Orange/White
18 | OneWire Temp Sensor | In | Brown
22 | Vacuum Switch | In | Brown/White
4 | +5VDC for Switches | N/A | Green
6 | GND for Relays | N/A | Green/White
# Schematics
## The Current Setup
![Current Schematic](/assets/currentschem.png)
## The End Goal
![Future Schematic](/assets/futureschem.png)
# Oddities
For ease of adaption, connection, and prototyping I've decided to use Cat 5 ethernet cabling and RJ45 connectors to connect the Raspberry Pi to the stove, and to a breadboard mockup of the sensors and switches for testing.
# Environment Variables
* ONTIME - How long to turn the auger on, in milliseconds.
* OFFTIME - How long to wait between turning the auger on, in milliseconds.
* PAUSETIME - How long to pause when a `pause` file is detected, in milliseconds.
* DEBUG - Displays extra log information when set to `true`
# Controls
* Run with `node main.js > log.txt &` to launch it in the background, piping output to a file `log.txt` which can be read from later.
* Pause the script by creating a file named `pause` in the root directory.
* Reload the environment variables by creating a file named `reload` in the root directory.
* Quit the script by creating a file named `quit` in the root directory.
# Roadmap
* v0.1 - Get the pellet stove operating at a basic level. Only implements the auger relay and no safeties.
* v0.2 - Implement safety switches and put the igniter and combustion blowers on relays controlled by the Pi.
* v0.3 - Implement the HTTP module to allow controlling the stove from the LAN.
* v0.4 - Implement usage logging with a SQL database.
# Testing Procedure
1. Launch app, check startup messages, check that it idles and pauses properly.
2. Provide ignite command, observe if the igniter, blower, and auger get turned on. Make sure the igniter turns off after the pre-set time.
3. Test that the following conditions cause a shutdown:
* Proof of Fire Switch OPEN after igniter shutoff.
* Vacuum Switch OPEN after igniter start.
4. Test manipulation of feed rates.
5. Test shutdown sequence.
# SQLite Database Tables
## status
| Field | Type | Null | Key | Default | Extra |
| ----- | ---- | ---- | --- | ------- | ----- |
| id | int(10) | No | PRI | NULL | auto_increment |
| key | varchar(100) | No | | |
| value | varchar(1000) | No | | |
| id | key | value |
| -- | --- | ----- |
0 | igniter | 0
1 | blower | 0
2 | auger | 0
3 | igniter_finished | false
4 | shutdown_initiated | 0
5 | vacuum | 0
6 | proof_of_fire | 0
7 | shutdown_next_cycle | 0
## timestamps
| Field | Type | Null | Key | Default | Extra |
| ----- | ---- | ---- | --- | ------- | ----- |
| id | int(10) | No | PRI | NULL | auto_increment |
| key | varchar(100) | No | | |
| value | varchar(1000) | No | | |
| id | key | value |
| -- | --- | ----- |
0 | process_start | 0
1 | blower_on | 0
2 | blower_off | 0
3 | igniter_on | 0
4 | igniter_off | 0
## intervals
| Field | Type | Null | Key | Default | Extra |
| ----- | ---- | ---- | --- | ------- | ----- |
| id | int(10) | No | PRI | NULL | auto_increment |
| key | varchar(100) | No | | |
| value | varchar(1000) | No | | |
| id | key | value |
| -- | --- | ----- |
0 | auger_on | 600
1 | auger_off | 1400
2 | pause | 5000
3 | igniter_start | 420000
4 | blower_stop | 600000
# Features
* Realtime state updates and communication via MQTT broker
* Easily hosted separate from the backend

8
TODO
View File

@ -1,8 +0,0 @@
Logic Goal:
1. Start Script & Web Server
2. Await a command from the user.
3. Startup enables auger
4. If auger is enabled, run the cycle command.
5. Once per cycle, read then rewrite the config file for interoperability
6. Set feed rate based on config.

10
TODO.md Normal file
View File

@ -0,0 +1,10 @@
# In Progress
1. Commenting
# Immediate To-Do
1. Add logic for the feed rate
2. Add a debug mode to the config and make console logs conditional
# Roadmap
1. Add backend logic to handle an automatic startup and shutdown
2. Temperature reading

View File

@ -1,5 +0,0 @@
{
"database": {
"createConfigTable": ""
}
}

View File

@ -1,93 +0,0 @@
#!/bin/bash
#####################################################
# Interactive script for managing Hestia Web Portal #
#####################################################
# Formatting Tips:
# https://misc.flogisoft.com/bash/tip_colors_and_formatting
#
# Formatting:
# \e[1m - Bold
# \e[2m - Dim
# \e[8m - Hidden (passwords)
#
# Reset:
# \e[0m - Reset All Attributes
# \e[21m - Reset Bold/Bright
# \e[22m - Reset Dim
# \e[28m - Reset Hidden
#
# Colors:
# \e[39m - Default Foreground Color
# \e[30m - Black
# \e[31m - Red
# \e[32m - Green
# \e[34m - Blue
#####################################################
# Some initial variables to work with
timestamp=$(date "+%Y%m%d_%H%M")
filename="backup_$timestamp.tar.gz"
# Initial Prompt
# Bash allows for linebreaks in string literals and will
# break lines accordingly in the shell
echo -e "
[ Hestia Control Panel ]
This script is being run from: '$(pwd)'
Active Nodes: $(ps ax -o pid,user,command | grep 'node main.js' | grep -v grep)
Please enter an option from below:
[1] Launch Hestia Web Portal
[2] Quit Hestia Web Portal
[3] View the logs
[4] Update Hestia
[5] Set up database
[0] Quit Control Panel"
# Wait for input
read -p "Option: " opt
# Execute the correct commands based on input.
case "$opt" in
1)
# Launch Hestia Web Portal
clear
echo "Launching Hestia Web Portal"
nohup node main.js > log.txt &
;;
2)
# Quit Hestia Web Portal
clear
echo "Quitting Hestia Web Portal Gracefully"
touch quit
;;
3)
# View logs
clear
less +F log.txt
;;
4)
# Update Hestia
rm data/config.db
git pull
;;
5)
# Set up database
node modules/_setupdb.js
;;
0)
# Exit the script
clear
echo "Quitting..."
exit
;;
*)
clear
echo "Invalid Option!"
;;
esac
exec ./hestia.sh

91
main.js
View File

@ -1,91 +0,0 @@
const fn = require('./modules/functions.js').functions;
// Import the config file
var config = require('./templates/config.json');
// Database Functions
const dbfn = require('./modules/database.js');
// Web Portal
const portal = require('./modules/_server.js');
portal.start();
dbfn.run(`UPDATE timestamps SET value = ${Date.now()} WHERE key = 'process_start'`).catch(err => console.error(`Error setting process start time: ${err}`));
fn.commands.refreshConfig().then(res => {
if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res.status}`);
config = res.config;
// Setup for use with the Pi's GPIO pins
switch (process.env.ONPI) {
case 'true':
console.log(`== Running on a Raspberry Pi.`);
var gpio = require('rpi-gpio');
fn.init(gpio).then((res) => {
console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res}`);
main(gpio);
}).catch(rej => {
console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] E: Error during initialization: ${rej}`);
process.exit(1);
});
break;
case 'false':
console.log(`I: Not running on a Raspberry Pi.`);
var gpio = 'gpio';
fn.init(gpio).then(res => {
console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res}`);
main(gpio);
}).catch(rej => {
console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] E: Error during initialization: ${rej}`);
process.exit(1);
});
break;
default:
console.log(`[${Date.now() - config.timestamps.procStart}] E: Problem with ENV file.`);
process.exit(1);
break;
}
}).catch(rej => {
console.error(`[${(Date.now() - config.timestamps.procStart)/1000}] E: Problem refreshing the config: ${rej}`);
process.exit(1);
});
function main(gpio) {
// If the auger is enabled
if (config.status.auger == 1) {
// Run a cycle of the auger
fn.auger.cycle(gpio).then(res => {
if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res}`);
fn.checkForQuit().then(n => {
fn.commands.refreshConfig().then(res => {
if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res.status}`);
config = res.config;
// Recursion ecursion cursion ursion rsion sion ion on n
main(gpio);
}).catch(rej => {
console.error(`[${(Date.now() - config.timestamps.procStart)/1000}] E: Problem refreshing the config: ${rej}`);
// Recursion ecursion cursion ursion rsion sion ion on n
main(gpio);
});
});
}).catch(err => {
if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] E: ${err}`);
});
} else {
// If the auger is disabled
fn.commands.pause().then(res => {
fn.checkForQuit().then(n => {
fn.commands.refreshConfig().then(res => {
if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res.status}`);
config = res.config;
// Recursion ecursion cursion ursion rsion sion ion on n
main(gpio);
}).catch(rej => {
console.error(`[${(Date.now() - config.timestamps.procStart)/1000}] E: Problem refreshing the config: ${rej}`);
// Recursion ecursion cursion ursion rsion sion ion on n
main(gpio);
});
});
}).catch(err => {
if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] E: ${err}`);
// Recursion ecursion cursion ursion rsion sion ion on n
main(gpio);
});
}
}

View File

@ -1,83 +0,0 @@
/* Pellet Stove Control Panel
* Web Configuration Server
* v0.0.0 by Skylar Grant
*
* TODOs:
* Implement Express to make it easier
* Add actual data into the responses
*/
const express = require('express');
const http = require('http');
const fn = require('./functions.js').functions;
var config;
fn.commands.refreshConfig().then(newConfig => {
config = newConfig.config;
});
const { dbfn } = require('./functions.js');
const app = express();
const server = http.createServer(app);
app.use(express.urlencoded());
// Our root directory for the public web files
app.use(express.static(__dirname + '/../www/public'));
// Our directory for views used to render the pages
app.set('views', __dirname + '/../www/views');
// Set .html as the file extension for views
app.engine('html', require('ejs').renderFile);
app.set('view engine', 'html');
// A normal load of the root page
app.get('/', (req, res) => {
// if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${JSON.stringify(config)}`);
res.render('index', { config: JSON.stringify(config) });
});
// A POST form submission to the root page
app.post('/', (req, response) => {
if (req.body.start != undefined) {
fn.commands.startup();
fn.commands.refreshConfig().then(res => {
config = res.config;
response.render('index', { config: JSON.stringify(config) });
return;
});
}
if (req.body.shutdown != undefined) {
fn.commands.shutdown();
fn.commands.refreshConfig().then(res => {
config = res.config;
response.render('index', { config: JSON.stringify(config) });
return;
});
}
if (req.body.reload != undefined) {
const updateAugerOffIntervalQuery = `UPDATE intervals SET value = '${2000 - req.body.feedRate}' WHERE key = 'auger_off'`;
const updateAugerOnIntervalQuery = `UPDATE intervals SET value = '${req.body.feedRate}' WHERE key = 'auger_on'`;
dbfn.run(updateAugerOffIntervalQuery).then(res => {
if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: Auger off interval updated: ${res.data.changes}`);
dbfn.run(updateAugerOnIntervalQuery).then(res => {
if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: Auger on interval updated: ${res.data.changes}`);
fn.commands.refreshConfig().then(res => {
config = res.config;
response.render('index', { config: JSON.stringify(config) });
return;
});
}).catch(err => console.log(`E: ${err}`));
}).catch(err => console.log(`E: ${err}`));
}
if (req.body.quit != undefined) {
fn.commands.quit();
fn.commands.refreshConfig().then(res => {
config = res.config;
response.render('index', { config: JSON.stringify(config) });
return;
});
}
});
module.exports = {
start: () => {
server.listen(8080, "0.0.0.0");
}
};

View File

@ -1,158 +0,0 @@
const dbfn = require('../modules/database.js');
// Create `status` table
/*
+ ----- + ------------- + ---- + --- + ------- + -------------- +
| Field | Type | Null | Key | Default | Extra |
+ ----- + ------------- + ---- + --- + ------- + -------------- +
| key | varchar(100) | No | | | |
| value | varchar(1000) | No | | | |
+ ----- + ------------- + ---- + --- + ------- + -------------- +
+ ------------------- +
| igniter |
| blower |
| auger |
| igniter_finished |
| shutdown_initiated |
| vacuum |
| proof_of_fire |
| shutdown_next_cycle |
+ ------------------- +
CREATE TABLE IF NOT EXISTS status (
key varchar(100) NOT NULL,
value varchar(1000) NOT NULL
);
*/
const createStatusTableQuery = "CREATE TABLE IF NOT EXISTS status (key varchar(100) NOT NULL,value varchar(1000) NOT NULL);";
dbfn.run(createStatusTableQuery).then(res => {
console.log(res.status);
const statusEntries = {
igniter: 0,
blower: 0,
auger: 0,
igniter_finished: false,
shutdown_initiated: 0,
vacuum: 0,
proof_of_fire: 0,
shutdown_next_cycle: 0
};
for ( key in statusEntries ){
const insertStatusEntryQuery = `INSERT INTO status (key, value) VALUES ("${key}", "${statusEntries[key]}")`;
dbfn.run(insertStatusEntryQuery).then(res => {
console.log(`${res.status}: ${res.data.lastID}: ${res.data.changes} changes`);
}).catch(err => console.error(err));
}
const selectAllStatusEntriesQuery = "SELECT * FROM status";
dbfn.all(selectAllStatusEntriesQuery).then(res => {
console.log(res.status);
}).catch(err => console.error(err));
}).catch(err => {
console.error(err);
});
// Create `timestamps` table
/*
+ ----- + ------------- + ---- + --- + ------- + -------------- +
| Field | Type | Null | Key | Default | Extra |
+ ----- + ------------- + ---- + --- + ------- + -------------- +
| key | varchar(100) | No | | | |
| value | varchar(1000) | No | | | |
+ ----- + ------------- + ---- + --- + ------- + -------------- +
+ ------------- +
| process_start |
| blower_on |
| blower_off |
| igniter_on |
| igniter_off |
+ ------------- +
CREATE TABLE IF NOT EXISTS timestamps (
key varchar(100) NOT NULL,
value varchar(1000) NOT NULL
);
*/
const createTimestampsTableQuery = "CREATE TABLE IF NOT EXISTS timestamps (key varchar(100) NOT NULL,value varchar(1000) NOT NULL);";
dbfn.run(createTimestampsTableQuery).then(res => {
console.log(res.status);
const timestampsEntries = {
process_start: 0,
blower_on: 0,
blower_off: 0,
igniter_on: 0,
igniter_off: 0
};
for ( key in timestampsEntries ){
const insertTimestampsEntryQuery = `INSERT INTO timestamps (key, value) VALUES ("${key}", "${timestampsEntries[key]}")`;
dbfn.run(insertTimestampsEntryQuery).then(res => {
console.log(`${res.status}: ${res.data.lastID}: ${res.data.changes} changes`);
}).catch(err => console.error(err));
}
const selectAllTimestampsEntriesQuery = "SELECT * FROM timestamps";
dbfn.all(selectAllTimestampsEntriesQuery).then(res => {
console.log(res.status);
}).catch(err => console.error(err));
}).catch(err => {
console.error(err);
});
// Create `intervals` table
/*
+ ----- + ------------- + ---- + --- + ------- + -------------- +
| Field | Type | Null | Key | Default | Extra |
+ ----- + ------------- + ---- + --- + ------- + -------------- +
| key | varchar(100) | No | | | |
| value | varchar(1000) | No | | | |
+ ----- + ------------- + ---- + --- + ------- + -------------- +
+ ------------- +
| auger_on |
| auger_off |
| pause |
| igniter_start |
| blower_stop |
+ ------------- +
CREATE TABLE IF NOT EXISTS intervals (
key varchar(100) NOT NULL,
value varchar(1000) NOT NULL
);
*/
const createIntervalsTableQuery = "CREATE TABLE IF NOT EXISTS intervals (key varchar(100) NOT NULL,value varchar(1000) NOT NULL);";
dbfn.run(createIntervalsTableQuery).then(res => {
console.log(res.status);
const intervalsEntries = {
auger_on: 600,
auger_off: 1400,
pause: 5000,
igniter_start: 420000,
blower_stop: 600000
};
for ( key in intervalsEntries ){
const insertIntervalsEntryQuery = `INSERT INTO intervals (key, value) VALUES ("${key}", "${intervalsEntries[key]}")`;
dbfn.run(insertIntervalsEntryQuery).then(res => {
console.log(`${res.status}: ${res.data.lastID}: ${res.data.changes} changes`);
}).catch(err => console.error(err));
}
const selectAllIntervalsEntriesQuery = "SELECT * FROM intervals";
dbfn.all(selectAllIntervalsEntriesQuery).then(res => {
console.log(res.status);
}).catch(err => console.error(err));
}).catch(err => {
console.error(err);
});
// Show the tables to confirm they were created properly:
dbfn.showTables().then(res => {
res.rows.forEach(row => {
console.log("Table: " + JSON.stringify(row));
});
}).catch(err => {
console.error(err);
});

View File

@ -1,53 +0,0 @@
const sqlite3 = require('sqlite3').verbose();
const db = new sqlite3.Database('./data/config.db', (err) => {
if (err) throw `E: DB Connection: ${err.message}`;
console.log(`I: Connected to the database.`);
});
module.exports = {
run(query) {
return new Promise((resolve, reject) => {
db.serialize(() => {
db.run(query, function(err) {
if (err) {
reject("Problem executing the query: " + err.message);
return;
}
resolve( { "status": "Query executed successfully: " + query, "data": this });
});
});
});
},
all(query) {
return new Promise((resolve, reject) => {
db.serialize(() => {
db.all(query, (err, rows) => {
if (err) {
reject("Problem executing the query: " + err.message);
return;
}
// [ { key: 'key_name', value: '0' }, { key: 'key_name', value: '0' } ]
let organizedRows = {};
rows.forEach(row => {
organizedRows[row.key] = row.value;
});
resolve({ "status": "Query executed successfully: " + query, "rows": organizedRows });
});
});
});
},
showTables() {
return new Promise((resolve, reject) => {
db.serialize(() => {
db.all("SELECT name FROM sqlite_master WHERE type='table'", (err, rows) => {
if (err) {
reject("Problem executing the query: " + err.message);
return;
}
resolve({ "status": "Tables retreived successfully", "rows": rows });
});
});
});
}
};

View File

@ -1,292 +0,0 @@
// TODOs: Add tests for PoF and Vacuum switches, add delays for shutting down blower, test logic for igniter
// TODO: Move these to config
// Physical Pin numbers for GPIO
const augerPin = 7; // Pin for controlling the relay for the pellet auger motor.
// Require the package for pulling version numbers
const package = require('../package.json');
// Database Functions
const dbfn = require('./database.js');
// Get environment variables
const dotenv = require('dotenv').config();
// Module for working with files
const fs = require('fs');
const { exec } = require('child_process');
var config = require('../templates/config.json');
// The functions we'll export to be used in other files
const functions = {
auger: {
// Gets called once the Auger Pin has been setup by rpi-gpio
ready(err) {
if (err) throw err;
console.log('Auger GPIO Ready');
return;
},
// Turns the auger on (Pin 7 high)
on(gpio) {
return new Promise((resolve) => {
if (process.env.ONPI == 'true') {
gpio.write(augerPin, true, function (err) {
if (err) throw err;
resolve('Auger turned on.');
});
} else {
resolve('Simulated auger turned on.');
}
});
},
// Turns the auger off (pin 7 low)
off(gpio) {
return new Promise((resolve) => {
if (process.env.ONPI == 'true') {
gpio.write(augerPin, false, function (err) {
if (err) throw err;
resolve('Auger turned off.');
});
} else {
resolve('Simulated auger turned off.');
}
});
},
// Cycles the auger using the two functions above this one (functions.auger.on() and functions.auger.off())
// Sleeps in between cycles using functions.sleep()
cycle(gpio) {
return new Promise((resolve) => {
// Turn the auger on
this.on(gpio).then((res) => {
// Log action if in debug mode
// if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res}`);
// Sleep for the time set in env variables
functions.sleep(config.intervals.augerOn).then((res) => {
// Log action if in debug mode
// if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res}`);
// Turn the auger off
this.off(gpio).then((res) => {
// Log action if in debug mode
// if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res}`);
// Sleep for the time set in env variables
functions.sleep(config.intervals.augerOff).then((res) => {
// Log action if in debug mode
// if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res}`);
// Resolve the promise, letting the main script know the cycle is complete
resolve(`Auger cycled (${config.intervals.augerOn}/${config.intervals.augerOff})`);
});
});
});
});
});
},
},
commands: {
// Prepare the stove for starting
startup() {
// Basic startup just enables the auger
const enableAugerQuery = "UPDATE status SET value = 1 WHERE key = 'auger'";
dbfn.run(enableAugerQuery).then(res => {
console.log(`[${(Date.now() - config.timestamps.procStart) / 1000}] I: Auger enabled.`);
return;
}).catch(err => console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] E: ${err}`));
},
shutdown() {
// Basic shutdown only needs to disable the auger
const disableAugerQuery = "UPDATE status SET value = 0 WHERE key = 'auger'";
dbfn.run(disableAugerQuery).then(res => {
if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res.status}`);
console.log(`[${(Date.now() - config.timestamps.procStart) / 1000}] I: Auger disabled.`);
return;
}).catch(err => console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] E: ${err}`));
},
// Pauses the script for the time defined in env variables
pause() {
return new Promise((resolve) => {
if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart) / 1000}] I: Pausing for ${config.intervals.pause}ms`);
functions.sleep(config.intervals.pause).then((res) => {
if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart) / 1000}] I: Pause finished.`);
resolve();
});
});
},
// Reload the environment variables on the fly
reload(envs) {
return new Promise((resolve) => {
// Re-require dotenv because inheritance in js sucks
const dotenv = require('dotenv').config({ override: true });
// Delete the reload file
fs.unlink('./reload', (err) => {
if (err) throw err;
if (process.env.DEBUG) console.log('Deleted reload file.');
});
// Print out the new environment variables
// This should be printed regardless of debug status, maybe prettied up TODO?
console.log('Reloaded environment variables.');
console.log(`ONTIME=${config.intervals.augerOn}\nOFFTIME=${config.intervals.augerOff}\nPAUSETIME=${config.intervals.pause}\nDEBUG=${process.env.DEBUG}\nONPI=${process.env.ONPI}`);
// Resolve the promise, letting the main script know we're done reloading the variables and the cycle can continue
resolve();
});
},
refreshConfig() {
return new Promise((resolve, reject) => {
// When the reload button is pressed, the call to this function will contain new config values
// {
// augerOff: 500,
// augerOn: 1500,
// pause: 5000
// }
// if (newSettings != undefined) {
// config.intervals.augerOff = newSettings.augerOff;
// config.intervals.augerOn = newSettings.augerOn;
// console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: Intervals updated: (${newSettings.augerOn}/${newSettings.augerOff})`);
// }
// fs.writeFile('./config.json', JSON.stringify(config), (err) => {
// if (err) reject(err);
// resolve();
// });
// Get status
const selectStatusQuery = "SELECT * FROM status";
dbfn.all(selectStatusQuery).then(res => {
let { status } = config;
let { rows } = res;
status.auger = rows.auger;
status.blower = rows.blower;
status.igniter = rows.igniter;
status.igniterFinished = rows.igniter_finished;
status.pof = rows.proof_of_fire;
status.shutdownNextCycle = rows.shutdown_next_cycle;
status.vacuum = rows.vacuum;
// Get timestamps
const selectTimestampsQuery = "SELECT * FROM timestamps";
dbfn.all(selectTimestampsQuery).then(res => {
let { timestamps } = config;
let { rows } = res;
timestamps.blowerOff = rows.blower_off;
timestamps.blowerOn = rows.blower_on;
timestamps.igniterOff = rows.igniter_off;
timestamps.igniterOn = rows.igniter_on;
timestamps.procStart = rows.process_start;
// Get intervals
const selectIntervalsQuery = "SELECT * FROM intervals";
dbfn.all(selectIntervalsQuery).then(res => {
let { intervals } = config;
let { rows } = res;
intervals.augerOff = rows.auger_off;
intervals.augerOn = rows.auger_on;
intervals.blowerStop = rows.blower_stop;
intervals.igniterStart = rows.igniter_start;
intervals.pause = rows.pause;
resolve({ "status": "Refreshed the config", "config": config });
}).catch(err => {
reject(err);
return;
});
}).catch(err => {
reject(err);
return;
});
}).catch(err => {
reject(err);
return;
});
});
},
quit() {
functions.commands.shutdown();
functions.auger.off(gpio).then(res => {
console.log(`[${(Date.now() - config.timestamps.procStart) / 1000}] I: Exiting app...`);
process.exit(0);
}).catch(err => {
console.log(`[${(Date.now() - config.timestamps.procStart) / 1000}] E: Unable to shut off auger, rebooting Pi!`);
exec('shutdown -r 0');
});
}
},
// Sleeps for any given milliseconds
sleep(ms) {
return new Promise((resolve) => {
// if (process.env.DEBUG) console.log(`Sleeping for ${ms}ms`);
// Function to be called when setTimeout finishes
const finish = () => {
// Resolve the promise
resolve(`Slept for ${ms}ms`);
};
// The actual sleep function, sleeps for ms then calls finish()
setTimeout(finish, ms);
});
},
// Initializes rpi-gpio, or resolves if not on a raspberry pi
init(gpio) {
fs.readFile('./templates/config.json', (err, data) => {
fs.writeFile('./config.json', data, (err) => {
if (err) throw err;
config = require('../config.json');
})
})
// TODO this boot splash needs updating
return new Promise((resolve, reject) => {
// Boot/About/Info
console.log(`== Lennox Winslow PS40
== Pellet Stove Control Panel
== Author: Skylar Grant
== Version: v${package.version}
==
== Startup Time: ${new Date().toISOString()}
==
== Environment variables:
== == ONTIME=${config.intervals.augerOn}
== == OFFTIME=${config.intervals.augerOff}
== == PAUSETIME=${config.intervals.pause}
== == DEBUG=${process.env.DEBUG}
== == ONPI=${process.env.ONPI}`);
// Set up GPIO 4 (pysical pin 7) as output, then call functions.auger.ready()
if (process.env.ONPI == 'true') {
// Init the Auger pin
gpio.setup(augerPin, gpio.DIR_OUT, (err) => {
if (err) reject(err);
if (process.env.DEBUG) console.log('== Auger pin initialized.');
// Resolve the promise now that all pins have been initialized
resolve('== GPIO Initialized.');
});
} else {
// Resolve the promise
resolve('== GPIO Not Available');
}
});
},
checkForQuit() {
if (config.status.shutdownNextCycle == 1) {
console.log(`[${(Date.now() - config.timestamps.procStart) / 1000}] I: Exiting Process!`);
process.exit();
}
return new Promise((resolve, reject) => {
if (fs.existsSync('./quit')) {
fs.unlink('./quit', err => {
if (err) console.log('Error removing the quit file: ' + err);
config.status.shutdownNextCycle = 1;
config.status.auger = 0;
resolve();
});
} else {
resolve('Not shutting down');
}
});
},
time(stamp) {
const time = new Date(stamp);
return `${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}`;
}
}
// Export the above object, functions, as a module
module.exports = { functions, dbfn };

3770
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +0,0 @@
{
"name": "pscontrolpanel",
"version": "0.2.1",
"requires": true,
"packages": {},
"dependencies": {
"body-parser": "^1.20.1",
"dotenv": "^16.0.3",
"ejs": "^3.1.8",
"express": "^4.18.2",
"rpi-gpio": "^2.1.7",
"sequelize": "^6.28.0",
"sqlite3": "^5.1.4"
}
}

133
src/assets/HestiaClasses.js Normal file
View File

@ -0,0 +1,133 @@
export class State {
constructor(config) {
this.igniter = {
on: false,
name: "igniter",
topic: config.mqtt.topics.igniter,
publisher: 'front',
power: (communicator) => {
// This *should* toggle the state, asks if state is true, if it is set it false, otherwise set it true
this.igniter.on ? this.igniter.on = false : this.igniter.on = true;
communicator.send(config.mqtt.topics.igniter, JSON.stringify(this.igniter));
}
};
this.exhaust = {
on: false,
name: "exhaust",
topic: config.mqtt.topics.exhaust,
publisher: 'front',
power: (communicator) => {
// This *should* toggle the state, asks if state is true, if it is set it false, otherwise set it true
this.exhaust.on ? this.exhaust.on = false : this.exhaust.on = true;
communicator.send(config.mqtt.topics.exhaust, JSON.stringify(this.exhaust));
}
};
this.auger = {
on: false,
name: "auger",
feedRate: 500,
topic: config.mqtt.topics.auger,
publisher: 'front',
power: (communicator) => {
// This *should* toggle the state, asks if state is true, if it is set it false, otherwise set it true
this.auger.on ? this.auger.on = false : this.auger.on = true;
communicator.send(config.mqtt.topics.auger, JSON.stringify(this.auger));
}
};
this.pof = {
on: false,
name: "pof",
topic: config.mqtt.topics.pof,
publisher: 'backend',
power: (communicator) => {
// This *should* toggle the state, asks if state is true, if it is set it false, otherwise set it true
this.pof.on ? this.pof.on = false : this.pof.on = true;
communicator.send(config.mqtt.topics.pof, JSON.stringify(this.pof));
}
};
this.vacuum = {
on: false,
name: "vacuum",
topic: config.mqtt.topics.vacuum,
publisher: 'backend',
power: (communicator) => {
// This *should* toggle the state, asks if state is true, if it is set it false, otherwise set it true
this.vacuum.on ? this.vacuum.on = false : this.vacuum.on = true;
communicator.send(config.mqtt.topics.vacuum, JSON.stringify(this.vacuum));
}
};
console.log(`State initialized.`)
};
};
export class Communicator {
constructor(state) {
this.publisher = state.publisher;
return this;
}
init(state, config) {
// Connect to the MQTT Broker
console.log(`Attempting MQTT connection to broker: ${config.mqtt.address}, with username: ${config.mqtt.username}`);
this.client = mqtt.connect(config.mqtt.address, {
username: config.mqtt.username,
password: config.mqtt.password
});
const { client } = this;
client.on('connect', () => {
console.log('Connected to MQTT broker');
// Subscribe to status topics
config.states.elements.forEach(element => {
client.subscribe(state[element].topic, (err) => {
if (!err) {
console.log(`Subscribed to ${state[element].topic}`);
}
});
});
});
// Handle when the Broker sends us a message
client.on('message', (topic, message) => {
const msgStr = message.toString();
const msgJson = JSON.parse(msgStr);
console.log(`Message received on topic ${topic}: ${msgStr}`);
console.log(msgJson);
state[msgJson.name].on = msgJson.on;
window.refreshState(window.document, state);
});
}
// Publish a message to the MQTT Broker
send(topic, message) {
// Publish with retain flag set to true
this.client.publish(topic, message, { retain: true }, (err) => {
if (err) {
console.error('Failed to publish message:', err);
} else {
console.log('Message published and retained on topic:', topic);
}
});
}
// Send startup command
startup() {
this.send('hestia/command/startup', JSON.stringify({
"publisher": "front",
"timestamp": Date.now()
}));
}
// Send shutdown command
shutdown() {
this.send('hestia/command/shutdown', JSON.stringify({
"publisher": "front",
"timestamp": Date.now()
}));
}
}

View File

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 163 KiB

75
src/assets/hestia.js Normal file
View File

@ -0,0 +1,75 @@
import { Communicator, State } from './HestiaClasses.js';
const config = {
"mqtt": {
"address": "wss://mqtt.3411.one",
"username": "hestia",
"password": "hestia",
"topics": {
"igniter": "hestia/status/igniter",
"exhaust": "hestia/status/exhaust",
"auger": "hestia/status/auger",
"pof": "hestia/status/pof",
"vacuum": "hestia/status/vacuum"
}
},
"states": {
"elements": [
"igniter",
"exhaust",
"auger",
"pof",
"vacuum"
]
}
};
const psState = new State(config);
const comms = new Communicator(psState);
export function refreshState(doc, state) {
const igniterStatus = doc.getElementById("igniter-status");
const exhaustStatus = doc.getElementById("exhaust-status");
const augerStatus = doc.getElementById("auger-status");
const pofStatus = doc.getElementById("pof-status");
const vacuumStatus = doc.getElementById("vacuum-status");
let statusString;
statusString = '';
if (state.igniter.on) statusString = "On"; else statusString = "Off";
igniterStatus.innerHTML = statusString;
statusString = '';
if (state.exhaust.on) statusString = "On"; else statusString = "Off";
exhaustStatus.innerHTML = statusString;
statusString = '';
if (state.auger.on) statusString = "On"; else statusString = "Off";
augerStatus.innerHTML = statusString;
statusString = '';
if (state.pof.on) statusString = "On"; else statusString = "Off";
pofStatus.innerHTML = statusString;
statusString = '';
if (state.vacuum.on) statusString = "On"; else statusString = "Off";
vacuumStatus.innerHTML = statusString;
console.log('State refreshed.');
}
export function togglePower(element) {
psState[element].power(comms);
refreshState(window.document, psState);
}
export function startup() {
comms.startup();
}
export function shutdown() {
comms.shutdown();
}
window.onload = function() {
comms.init(psState, config);
refreshState(window.document, psState);
};

104
src/index.html Normal file
View File

@ -0,0 +1,104 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hestia</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/mqtt/dist/mqtt.js"></script>
<script type="module">
import { refreshState, togglePower, startup, shutdown } from './assets/hestia.js';
window.togglePower = togglePower;
window.startup = startup;
window.shutdown = shutdown;
window.refreshState = refreshState;
</script>
</head>
<body class="bg-gray-900 text-white font-sans">
<marquee style="color: yellow; background-color: red; font-weight: bold;">
PELLET STOVE FREE TRIAL HAS ENDED, PLEASE PURCHASE A LICENSE NOW!
</marquee>
<div class="container mx-auto py-8 max-w-md">
<h1 class="text-3xl text-center text-purple-400 font-bold mb-8">Hestia Control Panel</h1>
<div class="controls-container bg-gray-800 p-6 rounded-lg shadow-lg space-y-4">
<table class="min-w-full bg-gray-900 text-white rounded-lg shadow-lg">
<thead>
<tr class="border-b border-gray-700">
<th class="py-2 px-4 text-left text-purple-400">Element</th>
<th class="py-2 px-4 text-left text-purple-400">Status</th>
</tr>
</thead>
<tbody>
<tr class="border-b border-gray-700">
<td class="py-2 px-4">Igniter</td>
<td class="py-2 px-4" id="igniter-status">Placeholder</td>
</tr>
<tr class="border-b border-gray-700">
<td class="py-2 px-4">Exhaust</td>
<td class="py-2 px-4" id="exhaust-status">Placeholder</td>
</tr>
<tr class="border-b border-gray-700">
<td class="py-2 px-4">Auger</td>
<td class="py-2 px-4" id="auger-status">Placeholder</td>
</tr>
<!-- PoF -->
<tr class="border-b border-gray-700">
<td class="py-2 px-4">Proof of Fire</td>
<td class="py-2 px-4" id="pof-status">Placeholder</td>
</tr>
<!-- Vacuum -->
<tr>
<td class="py-2 px-4">Vacuum</td>
<td class="py-2 px-4" id="vacuum-status">Placeholder</td>
</tr>
</tbody>
</table>
<div class="button-container flex">
<input class="btn bg-gray-700 hover:bg-purple-600 text-white py-2 w-full rounded mx-auto" type="submit" id="igniter-toggle-btn" value="Toggle Igniter" name="igniterToggle" onclick="togglePower('igniter')">
</div>
<div class="button-container flex mt-4">
<input class="btn bg-gray-700 hover:bg-purple-600 text-white py-2 w-full rounded mx-auto" type="submit" id="exhaust-toggle-btn" value="Toggle Exhaust" name="exhaustToggle" onclick="togglePower('exhaust')">
</div>
<div class="button-container flex mt-4">
<input class="btn bg-gray-700 hover:bg-purple-600 text-white py-2 w-full rounded mx-auto" type="submit" id="auger-toggle-btn" value="Toggle Auger" name="augerToggle" onclick="togglePower('auger')">
</div>
<div class="button-container flex mt-4">
<input class="btn bg-gray-700 hover:bg-purple-600 text-white py-2 w-full rounded mx-auto" type="submit" id="start-btn" value="Startup" name="startButton" onclick="startup()">
</div>
<div class="button-container flex mt-4">
<input class="btn bg-gray-700 hover:bg-purple-600 text-white py-2 w-full rounded mx-auto" type="submit" id="stop-btn" value="Shutdown" name="stopButton" onclick="shutdown()">
</div>
<form action="/" method="post">
<!-- Set feed rates -->
<div class="form-group mt-4">
<label for="feedRate" class="block text-sm font-medium text-purple-400">Feed Rate:</label>
<select name="feedRate" class="form-control bg-gray-700 text-white mt-1 block w-full rounded border-gray-600 focus:border-purple-500 focus:ring-purple-500 py-2 px-4">
<option value="500">Low</option>
<option value="625">Medium-Low</option>
<option value="750">Medium</option>
<option value="875">Medium-High</option>
<option value="1000">High</option>
</select>
</div>
<div class="button-container flex justify-end mt-4">
<input class="btn bg-purple-600 hover:bg-purple-700 text-white py-2 px-4 rounded w-full" type="submit" id="reload" value="Set Feed Rate" name="reload">
</div>
</form>
</div>
<div class="text-center my-8">
<img src="./assets/dancing_jesus.gif" class="img-fluid mx-auto rounded-lg shadow-lg" alt="Dancing Jesus">
</div>
<div class="controls-container bg-gray-800 p-6 rounded-lg shadow-lg" id="quitContainer">
<input class="btn bg-green-200 hover:bg-green-400 text-black font-bold py-2 px-4 rounded w-full" type="submit" id="reload" value="Refresh State" name="refresh">
<input class="btn bg-red-600 hover:bg-red-700 text-white py-2 font-bold rounded w-full mt-4" type="submit" id="quit" value="Quit!!" name="quit">
</div>
</div>
</body>
</html>

View File

@ -1,30 +0,0 @@
{
"status": {
"igniter": 0,
"blower": 0,
"auger": 0,
"igniterFinished": false,
"shutdown": 0,
"vacuum": 0,
"pof": 0,
"shutdownNextCycle": 0
},
"timestamps": {
"procStart": 0,
"blowerOn": 0,
"blowerOff": 0,
"igniterOn": 0,
"igniterOff": 0
},
"intervals": {
"augerOn": "600",
"augerOff": "1400",
"pause": "3000",
"igniterStart": "5000",
"blowerStop": "5000"
},
"web": {
"port": 8080,
"ip": "0.0.0.0"
}
}

BIN
www/.DS_Store vendored

Binary file not shown.

View File

@ -1,98 +0,0 @@
html, body {
padding: 0;
margin: 0;
background-color: #333;
color: aqua;
font-family: sans-serif;
}
#title {
text-align: center;
font-size: 35px;
margin-top: 10px;
padding-bottom: 15px;
}
#title a {
text-decoration: none;
color: inherit;
}
#safeties {
text-align: center;
}
.controls-container {
text-align: center;
}
#buttons button {
margin: 20px 5px;
font-size: 20px;
width: 150px;
height: 30px;
}
.controls-container input {
margin: 5px;
}
.subheading {
font-size: 20px;
font-weight: bold;
text-align: center;
}
#log-container {
text-align: center;
}
#log-area {
width: 100%;
height: 500px;
background-color: aqua;
color: aqua;
}
#trial {
display:;
color: yellow;
font-size: 20px;
background-color: red;
}
.button-container {
padding: 0;
margin-top: 20px;
margin-bottom: 20px;
text-align: center;
}
.btn {
background-color: #333;
color: aqua;
}
.button-selected {
margin: 5px;
border-width: 0px;
font-family: times;
font-size: 20px;
height: 35px;
width: 100px;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
}
table {
margin: 0 auto;
color: aqua !important;
}
table, th, td {
border: 1px solid;
border-collapse: collapse;
padding: 3px;
}

View File

@ -1,150 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Hestia Web Portal</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js" integrity="sha384-w76AqPfDkMBDXo30jS1Sgez6pr3x5MlQ1ZAGC+nuZB+EYdgRZgiwxhTBTkF7CXvN" crossorigin="anonymous"></script>
<link rel="stylesheet" href="/main.css">
</head>
<body onload="refreshData()" class="container">
<script>
// Get the config file
const config = <%- config %>;
console.log(<%- config %>);
</script>
<%- include('trial.html') -%>
<div id="title" class="text-center mb-4">
<a href='./'>Hestia Web Portal</a>
</div>
<div id="status" class="row">
<!--
| Auger | rows[0].cells[1] | On Time | rows[0].cells[3] |
| Feed Rate | rows[1].cells[1] | Off Time | rows[1].cells[3] |
-->
<table id="status-table" class="table table-bordered col-sm-12 col-md-6">
<tr>
<td>Auger</td>
<td></td>
<td>On Time</td>
<td></td>
</tr>
<tr>
<td>Feed Rate</td>
<td></td>
<td>Off Time</td>
<td></td>
</tr>
</table>
</div>
<div class="controls-container">
<form action="/" method="post">
<!-- Start | Shutdown | Reload Settings -->
<div class="button-container d-flex justify-content-between">
<input class="btn btn-outline-secondary" type="submit" id="ignite" value="Enable Auger" name="start">
<input class="btn btn-outline-secondary" type="submit" id="shutdown" value="Disable Auger" name="shutdown">
</div>
<!-- Set feed rates -->
<div class="form-group">
<label for="feedRate">Feed Rate: </label>
<select name="feedRate" class="form-control" id="feed-rate-select">
<option value="600">Low</option>
<option value="800">Medium</option>
<option value="1000">High</option>
</select>
</div>
<div class="button-container d-flex justify-content-end">
<input class="btn btn-outline-secondary" type="submit" id="reload" value="Set Feed Rate" name="reload">
</div>
</form>
</div>
<!-- <div class="text-center my-4">
<img src="./dancing_jesus.gif" class="img-fluid">
</div> -->
<div class="controls-container">
<form action="/" method="POST">
<input class="btn btn-danger" type="submit" id="quit" value="Quit!!" name="quit" style="visibility: hidden;">
</form>
</div>
<!-- <script src="./main.js"></script> -->
<script>
function sleep(ms) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, ms);
});
}
function readJSON(path) {
var request = new XMLHttpRequest();
request.open("GET", path, false);
request.send(null)
var JSONObj = JSON.parse(request.responseText);
return JSONObj;
}
function parseStatus(data) {
switch (data) {
case "0":
return "Off";
break;
case "1":
return "On";
break
default:
return "Error: " + data;
break;
}
}
function refreshData() {
// const log = document.getElementById('log-area');
// log.contentWindow.location.reload();
// sleep(100).then(() => {
// document.getElementById('log-area').contentWindow.scrollTo(0, 9999999999);
// });
// Get the elements we need to update
const statusTable = document.getElementById('status-table');
const augerStatus = statusTable.rows[0].cells[1];
const augerOn = statusTable.rows[0].cells[3];
const augerOff = statusTable.rows[1].cells[3];
const feedRate = statusTable.rows[1].cells[1];
const feedRateSelect = document.getElementById('feed-rate-select');
// console.log(config);
augerStatus.innerHTML = parseStatus(config.status.auger);
augerOn.innerHTML = config.intervals.augerOn;
augerOff.innerHTML = config.intervals.augerOff;
switch (config.intervals.augerOn) {
case '600':
feedRate.innerHTML = 'Low';
feedRateSelect.selectedIndex = 0;
break;
case '800':
feedRate.innerHTML = 'Medium';
feedRateSelect.selectedIndex = 1;
break;
case '1000':
feedRate.innerHTML = 'High';
feedRateSelect.selectedIndex = 2;
break;
default:
feedRate.innerHTML = 'Unknown';
break;
}
feedRate.value = config.intervals.augerOn;
};
</script>
</body>
</html>

View File

@ -1,65 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Hestia Web Portal</title>
<link rel="stylesheet" href="/main.css">
</head>
<body onload="refreshData()">
<%- include('trial.html') -%>
<div id="title"><a href='./'>Hestia Web Portal</a></div>
<div id="status">
<!--
| Auger | rows[0].cells[1] | On Time | rows[0].cells[3] |
| Feed Rate | rows[1].cells[1] | Off Time | rows[1].cells[3] |
-->
<table id="status-table">
<tr>
<td>Auger</td>
<td></td>
<td>On Time</td>
<td></td>
</tr>
<tr>
<td>Feed Rate</td>
<td></td>
<td>Off Time</td>
<td></td>
</tr>
</table>
</div>
<div class="controls-container">
<form action="/" method="post">
<!-- Start | Shutdown | Reload Settings -->
<div class="button-container">
<input class="button-unselected" type="submit" id="ignite" value="Enable Auger" name="start"><input class="button-unselected" type="submit" id="shutdown" value="Disable Auger" name="shutdown"><br>
</div>
<!-- Set feed rates -->
<label for="feedRate">Feed Rate: </label>
<select name="feedRate">
<option value="600">Low</option>
<option value="800">Medium</option>
<option value="1000">High</option>
</select>
<div class="button-container">
<input class="button-unselected" type="submit" id="reload" value="Reload" name="reload">
</div>
</form>
</div>
<div class="button-container">
<img src="./dancing_jesus.gif">
</div>
<div id="log-container">
<iframe id="log-area" src="log.txt"></iframe>
</div>
<div class="controls-container">
<form action="/" method="POST">
<input class="button-unselected" type="submit" id="quit" value="Quit!!" name="quit" style="visibility: hidden;">
</form>
</div>
<script src="./main.js"></script>
</body>
</html>

View File

@ -1,3 +0,0 @@
<body>
<marquee id="trial">YOUR FREE TRIAL HAS ENDED, PLEASE PURCHASE A PELLET STOVE SUBSCRIPTION</marquee>
</body>