-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.js
338 lines (288 loc) · 10.2 KB
/
main.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
/*
Program: Pi Air Conditioner Controller
Author: Logan (GitHub: EthyMoney)
Version: 1.0.0b (Beta)
Started: 5/13/2020
*/
//##################################################################
// Setup and Declarations
//##################################################################
// Instance constants
const MQTT_CLIENT_IDENTIFIER = `${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 5)}`;
const FAN_RELAY_PIN = 23;
const COMPRESSOR_RELAY_PIN = 24;
const MQTT_CONTROL_CHANNEL = 'home/shop/aircon';
const MQTT_TEMPERATURE_CHANNEL = MQTT_CONTROL_CHANNEL + '/temp';
const MQTT_BROKER_ENDPOINT = 'mqtt://192.168.1.55';
// Imports
import chalk from 'chalk';
import mqtt from 'mqtt';
import schedule from 'node-schedule';
import rpio from 'rpio';
// System runtime values
let currentTemp = 70; //needs to get set by sensor
let setTemp = 76; //default
let compressorOn = false;
let fanOn = false;
let cycleTime = '';
let systemEnabled = false;
let currentDuty = 'OFF';
let startTime = new Date(); //start counting the cycle time
// Configure rpio and set relay pins
rpio.init({ gpiomem: false });
rpio.init({ mapping: 'gpio' });
rpio.open(FAN_RELAY_PIN, rpio.OUTPUT);
rpio.open(COMPRESSOR_RELAY_PIN, rpio.OUTPUT);
// Configure the LCD output (20 columns by 4 rows)
const init = new Buffer.from([0x03, 0x03, 0x03, 0x02, 0x28, 0x0c, 0x01, 0x06]);
const LCD_LINE1 = 0x80, LCD_LINE2 = 0xc0, LCD_LINE3 = 0x94, LCD_LINE4 = 0xD4;
const LCD_ENABLE = 0x04, LCD_BACKLIGHT = 0x08;
// Connect to MQTT broker and start listening on the control channel
const client = mqtt.connect(MQTT_BROKER_ENDPOINT);
client.on('connect', function () {
client.subscribe([MQTT_CONTROL_CHANNEL, MQTT_TEMPERATURE_CHANNEL], function (err) {
if (err) {
console.error('Failed to subscribe to MQTT channels:', err);
}
});
});
// Handle MQTT connection errors
client.on('error', function (error) {
console.error('MQTT connection error:', error);
});
// Set the system updater to run on a schedule of every 1 minutes
schedule.scheduleJob('*/1 * * * *', update);
// Update once right away at startup
update();
// Report that we are ready for action!
console.log(`AirCon Client ${MQTT_CLIENT_IDENTIFIER} is running and listening for commands! :)`);
//##################################################################
// Unit Control Management
//##################################################################
// Perform an elegant startup of the unit
async function cool() {
//start the fan first, then compressor after delay, then update status
fanStart();
publishReport();
setTimeout(function () {
motorCheckup();
//make sure the fan is on AND the current status hasn't been changed back to idle/off while we waited
if (fanOn) {
compressorStart();
publishReport();
}
}, 15000); //15sec delay between fan and compressor startup
}
// Perform an elegant shutdown of the unit
async function shutdown() {
//stop the compressor first, then fan after a defrost delay, then update status
compressorStop();
publishReport();
setTimeout(function () {
motorCheckup();
//make sure the current status hasn't been changed back to cooling while we waited
if (!compressorOn) {
fanStop();
publishReport();
}
}, 180000); //3min defrost
}
// Retrieve the status of the relays to know what our fan and compressor motors are doing
function motorCheckup() {
compressorOn = rpio.read(COMPRESSOR_RELAY_PIN) ? true : false;
fanOn = rpio.read(FAN_RELAY_PIN) ? true : false;
// Emergency compressor safety (prevents compressor from running without airflow from the fan)
// This should never really happen in this code, but just in case something leads to this condition...
if (compressorOn && !fanOn) {
compressorStop();
systemEnabled = false;
client.publish('------------ COMPRESSOR E-STOP TRIGGERED! System shutting down to prevent damage! ------------');
console.log(chalk.red('COMPRESSOR E-STOP SHUTDOWN TRIGGERED! SYSTEM REVIEW REQUIRED'));
}
}
// Update all system values to current and set the operational modes
function update() {
// Check in on our compressor and fan motors
motorCheckup();
// See if we're enabled to run, then verify running operation
if (systemEnabled) {
// Check temp and adjust operation if needed
if (currentDuty == 'Idle') {
if (currentTemp > setTemp + .5) {
currentDuty = 'Cool';
startTime = new Date();
cool();
}
// Otherwise stay idle
else {
if (compressorOn) {
shutdown(); // Sanity check in case we are not fully idle
}
publishReport();
}
}
// Check temp and adjust operation if needed
else if (currentDuty == 'Cool') {
if (currentTemp <= setTemp - 2) {
currentDuty = 'Idle';
startTime = new Date();
shutdown();
}
// Otherwise stay cooling
else {
if (!compressorOn) {
cool(); // Sanity check in case we are not fully up and cooling
}
publishReport();
}
}
}
// System is disabled! Lets make sure that it's set correctly
else {
// If disabled, stay disabled and update stats
if (currentDuty == 'OFF') {
publishReport();
}
// Otherwise switch to disabled state
else {
currentDuty = 'OFF';
startTime = new Date();
shutdown();
publishReport();
}
}
}
// Report our current overall system status over MQTT and refresh the LCD on the front of the AC unit
function publishReport() {
motorCheckup();
// Check the time spent in the current mode
cycleTime = (new Date() - startTime);
// Make timestamp in normal human readable format
const date = new Date();
const timeOptions = {
hour: 'numeric',
minute: 'numeric',
hour12: true
};
const formattedTime = date.toLocaleDateString() + ' ' + date.toLocaleTimeString(undefined, timeOptions);
// Build JSON status report
let statusJSON = {
'Enabled': systemEnabled,
'Task': currentDuty,
'Runtime': cycleTime,
'FanON': fanOn,
'CompON': compressorOn,
'Temp': currentTemp,
'SetTemp': setTemp,
'Timestamp': formattedTime
};
// Log and display the status
console.log(statusJSON);
updateLCD(statusJSON);
client.publish(MQTT_CONTROL_CHANNEL, JSON.stringify(statusJSON));
}
//##################################################################
// Unit Relay Control
//##################################################################
// Note: High is ON, Low is OFF!
// SET THESE TO WHAT IS COMMENTED FOR THE AC!!!
function compressorStart() {
rpio.write(COMPRESSOR_RELAY_PIN, rpio.HIGH); // Sets to HIGH (ON)
}
function compressorStop() {
rpio.write(COMPRESSOR_RELAY_PIN, rpio.LOW); // Resets to LOW (OFF)
}
function fanStart() {
rpio.write(FAN_RELAY_PIN, rpio.HIGH); // Sets to HIGH (ON)
}
function fanStop() {
rpio.write(FAN_RELAY_PIN, rpio.LOW); // Resets to LOW (OFF)
}
//##################################################################
// LCD Control
//##################################################################
// Data is written 4 bits at a time with the lower 4 bits containing the mode.
function lcdwrite4(data) {
rpio.i2cWrite(Buffer.from([(data | LCD_BACKLIGHT)]));
rpio.i2cWrite(Buffer.from([(data | LCD_ENABLE | LCD_BACKLIGHT)]));
rpio.i2cWrite(Buffer.from([((data & ~LCD_ENABLE) | LCD_BACKLIGHT)]));
}
function lcdwrite(data, mode) {
lcdwrite4(mode | (data & 0xF0));
lcdwrite4(mode | ((data << 4) & 0xF0));
}
// Write a string to the specified LCD line.
function lineout(str, addr) {
lcdwrite(addr, 0);
str.split('').forEach(function (c) {
lcdwrite(c.charCodeAt(0), 1);
});
}
// Write the status data to the LCD
function updateLCD(data) {
rpio.i2cBegin();
rpio.i2cSetSlaveAddress(0x27);
rpio.i2cSetBaudRate(10000);
for (var i = 0; i < init.length; i++)
lcdwrite(init[i], 0);
lineout(`Status: ${data.Task}`, LCD_LINE1);
lineout(`For: ${msToTime(data.Runtime)}`, LCD_LINE2);
lineout(`Current: ${data.Temp}`, LCD_LINE3);
lineout(`Set: ${data.SetTemp}`, LCD_LINE4);
rpio.i2cEnd();
}
//##################################################################
// Event Handlers
//##################################################################
// Initial "hello" to the MQTT channel
client.publish(MQTT_CONTROL_CHANNEL, `AirCon Controller Client ${MQTT_CLIENT_IDENTIFIER} is online!`);
// Listen for messages on MQTT channel and process them accordingly
client.on('message', function (topic, message) {
try {
if (topic == MQTT_CONTROL_CHANNEL) {
//set temp
if (message.toString().includes('set')) {
setTemp = Number(message.toString().slice(message.toString().lastIndexOf('-') + 1));
update();
}
//enable system
if (message.toString() === 'on') {
systemEnabled = true;
currentDuty = 'Idle';
update();
}
//disable system
if (message.toString() === 'off') {
systemEnabled = false;
update();
}
//report current status
if (message.toString() === 'status') {
publishReport();
}
}
else if (topic == MQTT_TEMPERATURE_CHANNEL) {
// check if the message is a number
if (!isNaN(message.toString())) {
//if it is a number, update the current temp
currentTemp = Number(message.toString());
}
}
} catch (error) {
console.error('Error while processing MQTT message:', error);
}
});
//##################################################################
// Supporting Functions
//##################################################################
// Takes the cycle time in milliseconds and converts it to a human readable time format of hours and minutes
function msToTime(duration) {
let milliseconds = parseInt((duration % 1000) / 100),
seconds = Math.floor((duration / 1000) % 60),
minutes = Math.floor((duration / (1000 * 60)) % 60),
hours = Math.floor(duration / (1000 * 60 * 60));
hours = (hours < 10) ? '0' + hours : hours;
minutes = (minutes < 10) ? '0' + minutes : minutes;
seconds = (seconds < 10) ? '0' + seconds : seconds;
return hours + ':' + minutes;
}