Hardware components | ||||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 8 | ||||
| × | 1 | ||||
| × | 5 | ||||
| × | 1 | ||||
| × | 2 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 7 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 2 | ||||
| × | 1 | ||||
Software apps and online services | ||||||
| ||||||
Hand tools and fabrication machines | ||||||
|
EDIT 2 (Nov 10, 2023): I added code to deal with occasional temperature reading errors, up to and including a system reset. It appears this has helped, as seen in the picture below. Note that after 10 or more bad temperature readings (N_TErrors), I instead add 10 to the N_Reboots, and reboot the system, to see if it fixes the issue.
EDIT 1: I added some changes to the code to add more Particle variables and functions, and changed the room setpoint variables to EPROM variables so temperature changes are kept during power failures.
When my Keen Home smart vents stopped working, I decided to replace them with something that will always work. I chose the Photon because it primarily runs only the software that I program, has secure IoT access built-in, and I can expect it to properly operate HVAC dampers even without Internet access.
I want control of the heating and cooling in three bedrooms and the basement area below these rooms. When my ecobee4 thermostat senses that I am in the basement (Follow Me mode enabled) and it is too cool, the furnace will now heat the basement without overheating the upstairs bedrooms. And when it is hot outside, the AC will cool the bedrooms without freezing the basement. I also adjust the desired room temperatures in the evening to try to cool the bedrooms before bedtime.
To make sure the device worked without Internet or WiFi, I decided that I would run wires to each room for a room temperature sensor. The SparkFun Qwiic components allowed for easy testing and installation with minimal soldering; almost everything is connector-based and just works as-is. The system is installed in a box near the furnace plenum, so I can use a short (0.5 m) Qwiic I2C cable to connect directly to the temperature sensor and environmental combo that are located in the plenum. To use I2C to access the temperature sensors in the rooms, I used I2C extenders that use standard Ethernet cable to extend the distance (e.g. 50 m) between the controller and the room temperature sensors. The only soldering needed was to solder four wires each to connect the temperature sensors and the Qwiic adapter. Otherwise, I used Qwiic I2C cables from a cable kit for the rest of the I2C wiring.
I log all information onto a memory card so that I can compare it with the logs from my ecobee4 thermostat. The logging isn’t totally flawless (some characters get missed), but I added 1 ms delay between each character being stored and the missed characters are typically less than once per day. I store all information more often than needed (every minute) so it isn’t a big issue now; the ecobee4 logs data every 5 minutes.
The dampers use a Belimo actuator to drive the damper either open or closed within 35 s using 24 Vac. If the Photon stops working or loses power, the Relay Shield relays will operate all dampers open; the relay NC contact connects to the damper open terminal. Three of the rooms have two dampers, so these damper pairs are wired in parallel. The Belimo actuators use 1.5 W when running open or closed, and this reduces to 0.2 W once the damper is fully opened or closed.
The above picture shows a closer view of the components mounted inside of the controller box. Below is a closer view of one of the remote temperature sensors with its cover removed. I drilled the holes in the cover to allow air flow.
To minimize the heating effect from operating current on the temperature sensors, I leave them shut down and only wake them up every 10 s to get a temperature reading. I had originally planned on using the BME280 for measuring each room temperature (plus humidity and pressure), but there is a substantial heating effect (a few °C) if there is virtually no air flow across the sensor. So, I replaced the sensors with MCP9808 sensors. I kept the environmental combo (BME280 & CCS811) in with the MCP9808 for the plenum sensor.
All sensor information and various status information gets logged every minute (date, time, room set and actual temperatures, status of dampers (auto/manual & open/closed), in plenum: temperature, pressure, relative humidity, CO2, total volatile organic compounds). I create a new log file for each day so each log file doesn’t get too large.
To make sure that the furnace plenum doesn’t get too much back pressure, I don’t allow more than three room dampers to be closed at a time.
I can access the various sensor and status information from my iPhone. Although I rarely need to, I can also adjust desired room temperatures and manually open or close any of the room dampers. I did need to set the Photon to have a fixed IP from my router so that I didn’t loose Internet access when the IP lease renewed.
So far, everything is working great and the rooms are much more comfortable now.
/*
This is a library written for the Qwiic OpenLog
SparkFun sells these at its website: www.sparkfun.com
Do you like this library? Help support SparkFun. Buy a board!
https://www.sparkfun.com/products/14641
Written by Nathan Seidle @ SparkFun Electronics, February 2nd, 2018
Qwiic OpenLog makes it very easy to record data over I2C to a microSD.
This library handles the initialization of the Qwiic OpenLog and the calculations
to get the temperatures.
https://github.com/sparkfun/SparkFun_Qwiic_OpenLog_Arduino_Library
Development environment specifics:
Arduino IDE 1.8.3
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses></http:>.
*/
#include "SparkFun_Qwiic_OpenLog_Arduino_Library.h"
//Attempt communication with the device
//Return true if we got a 'Polo' back from Marco
boolean OpenLog::begin(uint8_t deviceAddress, TwoWire &wirePort)
{
_deviceAddress = deviceAddress; //If provided, store the I2C address from user
_i2cPort = &wirePort; //Grab which port the user wants us to use
//We require caller to begin their I2C port, with the speed of their choice
//external to the library
//_i2cPort->begin();
//Check communication with device
uint8_t status = getStatus();
if(status & 1<<STATUS_SD_INIT_GOOD)
{
//We are good to go!
return(true);
}
return (false); //SD did not init. Card not present?
}
//Simple begin
boolean OpenLog::begin(int deviceAddress)
{
return(begin(deviceAddress, Wire));
}
//Get the version number from OpenLog
String OpenLog::getVersion()
{
sendCommand(registerMap.firmwareMajor, "");
//Upon completion Qwiic OpenLog will have 2 bytes ready to be read
_i2cPort->requestFrom(_deviceAddress, (uint8_t)1);
uint8_t versionMajor = _i2cPort->read();
sendCommand(registerMap.firmwareMinor, "");
//Upon completion Qwiic OpenLog will have 2 bytes ready to be read
_i2cPort->requestFrom(_deviceAddress, (uint8_t)1);
uint8_t versionMinor = _i2cPort->read();
return(String(versionMajor) + "." + String(versionMinor));
}
//Get the status byte from OpenLog
//This function assumes we are not in the middle of a read, file size, or other function
//where OpenLog has bytes qued up
// Bit 0: SD/Init Good
// Bit 1: Last Command Succeeded
// Bit 2: Last Command Known
// Bit 3: File Currently Open
// Bit 4: In Root Directory
// Bit 5: 0 - Future Use
// Bit 6: 0 - Future Use
// Bit 7: 0 - Future Use
uint8_t OpenLog::getStatus()
{
sendCommand(registerMap.status, "");
//Upon completion OpenLog will have a status byte ready to read
_i2cPort->requestFrom(_deviceAddress, (uint8_t)1);
return(_i2cPort->read());
}
//Change the I2C address of the OpenLog
//This will be recorded to OpenLog's EEPROM and config.txt file.
boolean OpenLog::setI2CAddress(uint8_t addr)
{
String temp;
temp = addr;
boolean result = sendCommand(registerMap.i2cAddress, temp);
//Upon completion any new communication must be with this new I2C address
_deviceAddress = addr; //Change the address internally
return(result);
}
//Append to a given file. If it doesn't exist it will be created
boolean OpenLog::append(String fileName)
{
return (sendCommand(registerMap.openFile, fileName));
//Upon completion any new characters sent to OpenLog will be recorded to this file
}
//Create a given file in the current directory
boolean OpenLog::create(String fileName)
{
return (sendCommand(registerMap.createFile, fileName));
//Upon completion a new file is created but OpenLog is still recording to original file
}
//Given a directory name, create it in whatever directory we are currently in
boolean OpenLog::makeDirectory(String directoryName)
{
return (sendCommand(registerMap.mkDir, directoryName));
//Upon completion Qwiic OpenLog will respond with its status
//Qwiic OpenLog will continue logging whatever it next receives to the current open log
}
//Given a directory name, change to that directory
boolean OpenLog::changeDirectory(String directoryName)
{
return (sendCommand(registerMap.cd, directoryName));
//Upon completion Qwiic OpenLog will respond with its status
//Qwiic OpenLog will continue logging whatever it next receives to the current open log
}
//Return the size of a given file. Returns a 4 byte signed long
int32_t OpenLog::size(String fileName)
{
sendCommand(registerMap.fileSize, fileName);
//Upon completion Qwiic OpenLog will have 4 bytes ready to be read
_i2cPort->requestFrom(_deviceAddress, (uint8_t)4);
int32_t fileSize = 0;
while (_i2cPort->available())
{
uint8_t incoming = _i2cPort->read();
fileSize <<= 8;
fileSize |= incoming;
}
return (fileSize);
}
//Read the contents of a file, up to the size of the buffer, into a given array, from a given spot
void OpenLog::read(uint8_t* userBuffer, uint16_t bufferSize, String fileName)
{
uint16_t spotInBuffer = 0;
uint16_t leftToRead = bufferSize; //Read up to the size of our buffer. We may go past EOF.
sendCommand(registerMap.readFile, fileName);
//Upon completion Qwiic OpenLog will respond with the file contents. Master can request up to 32 bytes at a time.
//Qwiic OpenLog will respond until it reaches the end of file then it will report zeros.
while (leftToRead > 0)
{
uint8_t toGet = I2C_BUFFER_LENGTH; //Request up to a 32 byte block
if (leftToRead < toGet) toGet = leftToRead; //Go smaller if that's all we have left
_i2cPort->requestFrom(_deviceAddress, toGet);
while (_i2cPort->available())
userBuffer[spotInBuffer++] = _i2cPort->read();
leftToRead -= toGet;
}
}
//Read the contents of a directory. Wildcards allowed
//Returns true if OpenLog ack'd. Use getNextDirectoryItem() to get the first item.
boolean OpenLog::searchDirectory(String options)
{
if (sendCommand(registerMap.list, options) == true)
{
_searchStarted = true;
return (true);
//Upon completion Qwiic OpenLog will have a file name or directory name ready to respond with, terminated with a \0
//It will continue to respond with a file name or directory until it responds with all 0xFFs (end of list)
}
return (false);
}
//Returns the name of the next file or directory folder in the current directory
//Returns "" if it is the end of the list
String OpenLog::getNextDirectoryItem()
{
if (_searchStarted == false) return (""); //We haven't done a search yet
String itemName = "";
_i2cPort->requestFrom(_deviceAddress, (uint8_t)I2C_BUFFER_LENGTH);
uint8_t charsReceived = 0;
while (_i2cPort->available())
{
uint8_t incoming = _i2cPort->read();
if (incoming == '\0')
return (itemName); //This is the end of the file name. We don't need to read any more of the 32 bytes
else if (charsReceived == 0 && incoming == 0xFF)
{
_searchStarted = false;
return (""); //End of the directory listing
}
else
itemName += (char)incoming; //Add this byte to the file name
charsReceived++;
}
//We shouldn't get this far but if we do
return(itemName);
}
//Remove a file, wildcards supported
//OpenLog will respond with the number of items removed
uint32_t OpenLog::removeFile(String thingToDelete)
{
return(remove(thingToDelete, false));
}
//Remove a directory, wildcards supported
//OpenLog will respond with 1 when removing a directory
uint32_t OpenLog::removeDirectory(String thingToDelete)
{
return(remove(thingToDelete, true)); //Delete all files in the directory as well
}
//Remove a file or directory (including everything in that directory)
//OpenLog will respond with the number of items removed
//Returns 1 if only a directory is removed (even if directory had files in it)
uint32_t OpenLog::remove(String thingToDelete, boolean removeEverything)
{
if(removeEverything == true)
sendCommand(registerMap.rmrf, thingToDelete); //-rf causes any directory to remove contents as well
else
sendCommand(registerMap.rm, thingToDelete); //Just delete a thing
//Upon completion Qwiic OpenLog will have 4 bytes ready to read, representing the number of files beleted
_i2cPort->requestFrom(_deviceAddress, (uint8_t)4);
int32_t filesDeleted = 0;
while (_i2cPort->available())
{
uint8_t incoming = _i2cPort->read();
filesDeleted <<= 8;
filesDeleted |= incoming;
}
return (filesDeleted); //Return the number of files removed
//Qwiic OpenLog will continue logging whatever it next receives to the current open log
}
//Send a command to the unit with options (such as "append myfile.txt" or "read myfile.txt 10")
boolean OpenLog::sendCommand(uint8_t registerNumber, String option1)
{
_i2cPort->beginTransmission(_deviceAddress);
_i2cPort->write(registerNumber);
if (option1.length() > 0)
{
//_i2cPort->print(" "); //Include space
_i2cPort->print(option1);
}
if (_i2cPort->endTransmission() != 0)
return (false);
return (true);
//Upon completion any new characters sent to OpenLog will be recorded to this file
}
//Write a single character to Qwiic OpenLog
size_t OpenLog::write(uint8_t character) {
_i2cPort->beginTransmission(_deviceAddress);
_i2cPort->write(registerMap.writeFile);//Send the byte that corresponds to writing a file
_i2cPort->write(character);
if (_i2cPort->endTransmission() != 0)
return (0); //Error: Sensor did not ack
return (1);
}
int OpenLog::writeString(String string) {
_i2cPort->beginTransmission(_deviceAddress);
_i2cPort->write(registerMap.writeFile);
//remember, the rx buffer on the i2c openlog is 32 bytes
//and the register address takes up 1 byte so we can only
//send 31 data bytes at a time
if(string.length() > 31)
{
return -1;
}
if (string.length() > 0)
{
//_i2cPort->print(" "); //Include space
_i2cPort->print(string);
}
if (_i2cPort->endTransmission() != 0)
return (0);
return (1);
}
bool OpenLog::syncFile(){
_i2cPort->beginTransmission(_deviceAddress);
_i2cPort->write(registerMap.syncFile);
if (_i2cPort->endTransmission() != 0){
return (0);
}
return (1);
}
bool OpenLog::Println(String sString) {
int iLength = sString.length();
bool bAllOk = 1;
for (int i=0; i<iLength; i++) {
if (!write(sString.charAt(i))) {
bAllOk = 0;
}
delay(1); //to allow file writing to keep up, so no errors
}
write('\n'); //add newline
return (bAllOk);
}
/*
This is a library written for the Qwiic OpenLog
SparkFun sells these at its website: www.sparkfun.com
Do you like this library? Help support SparkFun. Buy a board!
https://www.sparkfun.com/products/14641
Written by Nathan Seidle @ SparkFun Electronics, February 2nd, 2018
Qwiic OpenLog makes it very easy to record data over I2C to a microSD.
This library handles the initialization of the Qwiic OpenLog and the calculations
to get the temperatures.
https://github.com/sparkfun/SparkFun_Qwiic_OpenLog_Arduino_Library
Development environment specifics:
Arduino IDE 1.8.3
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses></http:>.
*/
#pragma once
#if (ARDUINO >= 100)
#include "Arduino.h"
#else
#include "WProgram.h"
#endif
#include <Wire.h>
//The default I2C address for the Qwiic OpenLog is 0x2A (42). 0x29 is also possible.
#define QOL_DEFAULT_ADDRESS (uint8_t)42
//Bits found in the getStatus() byte
#define STATUS_SD_INIT_GOOD 0
#define STATUS_LAST_COMMAND_SUCCESS 1
#define STATUS_LAST_COMMAND_KNOWN 2
#define STATUS_FILE_OPEN 3
#define STATUS_IN_ROOT_DIRECTORY 4
//Platform specific configurations
//Define the size of the I2C buffer based on the platform the user has
//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
#if defined(__AVR_ATmega328P__) || defined(__AVR_ATmega168__)
//I2C_BUFFER_LENGTH is defined in Wire.H
#define I2C_BUFFER_LENGTH BUFFER_LENGTH
#elif defined(__SAMD21G18A__)
//SAMD21 uses RingBuffer.h
#define I2C_BUFFER_LENGTH SERIAL_BUFFER_SIZE
#elif __MK20DX256__
//Teensy
#elif ARDUINO_ARCH_ESP32
//ESP32 based platforms
#else
//The catch-all default is 32
#define I2C_BUFFER_LENGTH 32
#endif
//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
class OpenLog : public Print {
public:
struct memoryMap {
byte id;
byte status;
byte firmwareMajor;
byte firmwareMinor;
byte i2cAddress;
byte logInit;
byte createFile;
byte mkDir;
byte cd;
byte readFile;
byte startPosition;
byte openFile;
byte writeFile;
byte fileSize;
byte list;
byte rm;
byte rmrf;
byte syncFile;
};
const memoryMap registerMap = {
.id = 0x00,
.status = 0x01,
.firmwareMajor = 0x02,
.firmwareMinor = 0x03,
.i2cAddress = 0x1E,
.logInit = 0x05,
.createFile = 0x06,
.mkDir = 0x07,
.cd = 0x08,
.readFile = 0x09,
.startPosition = 0x0A,
.openFile = 0x0B,
.writeFile = 0x0C,
.fileSize = 0x0D,
.list = 0x0E,
.rm = 0x0F,
.rmrf = 0x10,
.syncFile = 0x11,
};
//These functions override the built-in print functions so that when the user does an
//myLogger.println("send this"); it gets chopped up and sent over I2C instead of Serial
virtual size_t write(uint8_t character);
int writeString(String string);
bool Println(String sString);
bool syncFile(void);
//By default use the default I2C addres, and use Wire port
boolean begin(uint8_t deviceAddress = QOL_DEFAULT_ADDRESS, TwoWire &wirePort = Wire);
boolean begin(int deviceAddress);
String getVersion(); //Returns a string that is the current firmware version
uint8_t getStatus(); //Returns various status bits
boolean setI2CAddress(uint8_t addr); //Set the I2C address we read and write to
boolean append(String fileName); //Open and append to a file
boolean create(String fileName); //Create a file but don't open it for writing
boolean makeDirectory(String directoryName); //Create the given directory
boolean changeDirectory(String directoryName); //Change to the given directory
int32_t size(String fileName); //Given a file name, read the size of the file
void read(uint8_t* userBuffer, uint16_t bufferSize, String fileName); //Read the contents of a file into the provided buffer
boolean searchDirectory(String options); //Search the current directory for a given wildcard
String getNextDirectoryItem(); //Return the next file or directory from the search
uint32_t removeFile(String thingToDelete); //Remove file
uint32_t removeDirectory(String thingToDelete); //Remove a directory including the contents of the directory
uint32_t remove(String thingToDelete, boolean removeEverthing); //Remove file or directory including the contents of the directory
//These are the core functions that send a command to OpenLog
boolean sendCommand(uint8_t registerNumber, String option1);
private:
//Variables
TwoWire *_i2cPort; //The generic connection to user's chosen I2C hardware
uint8_t _deviceAddress; //Keeps track of I2C address. setI2CAddress changes this.
uint8_t _escapeCharacter = 26; //The character that needs to be sent to QOL to get it into command mode
uint8_t _escapeCharacterCount = 3; //Number of escape characters to get QOL into command mode
boolean _searchStarted = false; //Goes true when user does a search. Goes false when we reach end of directory.
};
Damper Controller - Revision 1
C/C++//make sure to add these libraries via the Libraries tab in the left sidebar
#include "Particle.h" //automatically included
#include "SparkFunBME280.h"
#include "Adafruit_CCS811.h"
#include "Arduino.h"
//#include <Wire.h>
#include "SparkFun_Qwiic_OpenLog_Arduino_Library.h" //local copy, revised
#include "TCA9548A-RK.h"
#include "RelayShield.h"
#include "Adafruit_MCP9808.h"
BME280 bme; // I2C 0x77, Mux Ch0 (I2C uses D0-1)
Adafruit_CCS811 ccs; // I2C 0x5B, Mux Ch0
Adafruit_MCP9808 mcp; // I2C 0x18, Mux Ch0, 250 ms per temp update
Adafruit_MCP9808 mcp1; // I2C 0x18, Mux Ch1
Adafruit_MCP9808 mcp2; // I2C 0x18, Mux Ch2
Adafruit_MCP9808 mcp3; // I2C 0x18, Mux Ch3
Adafruit_MCP9808 mcp4; // I2C 0x18, Mux Ch4
OpenLog myLog; // I2C 0x2A
TCA9548A mux(Wire, 0); //I2C 0x70
RelayShield myRelays; //RelayShield, D3-6
int boardLed = D7; //blink LED when code is running
//double dTime = 0.0; //used for cloud variable (e.g. time to log one line of data)
//unsigned long lastUpdate = 0; //used to calculate dTime in ms (e.g. to log each line of data)
double T; //Plenum - used for Logging
String sT; //used for cloud variable
double P; //used for Logging
String sP; //used for cloud variable
double H; //used for Logging
String sH; //used for cloud variable
double CO2; //used for Logging
String sCO2; //used for cloud variable
double TVOC; //used for Logging
String sTVOC; //used for cloud variable
double T0; //Plenum - used for Logging
String sT0; //used for cloud variable
double T1; //Room 1 - used for Logging
String sT1; //used for cloud variable
double T2; //Room 2 - used for Logging
String sT2; //used for cloud variable
double T3; //Room 3 - used for Logging
String sT3; //used for cloud variable
double T4; //Room 4 - used for Logging
String sT4; //used for cloud variable
#define TDeadband 0.9 //switch dampers on/off if above/below setpoint +/- Tdeadband (degrees C)
#define THysteresis 0.8 //require this much change in temperature after a damper has switched state, before another switch of state (degrees C)
#define ROOM1 1 //1st bedroom
#define ROOM2 2 //2nd bedroom
#define ROOM3 3 //3rd bedroom
#define ROOM4 4 //basement
float T1RoomSet; //desired Room 1 temperature
float T2RoomSet; //desired Room 2 temperature
float T3RoomSet; //desired Room 3 temperature
float T4RoomSet; //desired Room 4 temperature
#define OPENED 0
#define CLOSED 1
bool Damper1SelectedPosition; //damper SelectedPosition for Room 1
bool Damper2SelectedPosition; //damper SelectedPosition for Room 2
bool Damper3SelectedPosition; //damper SelectedPosition for Room 3
bool Damper4SelectedPosition; //damper SelectedPosition for Room 4
#define AUTO 0
#define MANUAL 1
bool Damper1Mode; //damper mode for Room 1 (Auto/Manual)
bool Damper2Mode; //damper mode for Room 2
bool Damper3Mode; //damper mode for Room 3
bool Damper4Mode; //damper mode for Room 4
unsigned long lYear; // 4 digit year, e.g. 2019
unsigned long lMonth; // 1-12 month
unsigned long lDay; // 1-31 day
unsigned long lHour; // 0-23 hour
unsigned long lMinute; // 0-59
unsigned long lLastMinute; // check every 30 s to see if minute logged already, so no minutes get missed, which does otherwise occur
String sDate = "2019-09-14"; //example
String sTime = "23:45:00"; //example
String sLogFileName = "default.txt"; //example
String sBuf = ""; //use global buffer (and above variables) to reduce stack usage, especially in timers
String sDampersStatus = "A1onM2offA3offA4on"; //example: Damper Mode: A = AUTO, M = MANUAL, Damper Position: on = OPENED, off = CLOSED
String sTSet = "23.0 23.0 23.0 23.0"; //example: Damper temperature trigger setpoints
int iReboots; //use this to count the number of CPU boots (e.g. due to power failures)
void AddHeaderToLogFile() {
//lastUpdate = millis(); //time since the program started, in msec (overflows in ~49 days)
lYear = Time.year(); // 4 digit year
lMonth = Time.month(); //1-12 month
lDay = Time.day(); //1-31 day
sDate = String(lYear) + "-";
if (lMonth < 10) sDate += "0"; //make sure 2 digits so file is aligned
sDate += String(lMonth) + "-";
if (lDay < 10) sDate += "0"; //make sure 2 digits so file is aligned
sDate += String(lDay); // e.g. 2019-09-07
sLogFileName = String(lYear);
if (lMonth < 10) sLogFileName += "0"; //make sure 2 digits for month
sLogFileName += String(lMonth);
if (lDay < 10) sLogFileName += "0"; //make sure 2 digits for day
sLogFileName += String(lDay) + ".txt"; //e.g. 20190907.txt (max 8.3 digits)
myLog.append(sLogFileName); //append or create new Log file and send subsequent text to it (e.g. new day, after midnight)
delay(1); // give it time?
sBuf = "Date,Time,T0(C),T1(C),T2(C),T3(C),T4(C),T1Set(C),T2Set(C),T3Set(C),T4Set(C),DSelStatus,T(C),P(kPa),RH(%),CO2(ppm),TVOC(ppb)";
myLog.Println(sBuf); //custom routine that adds 1 ms delay between each character, otherwise some characters get lost
//dTime = millis() - lastUpdate; //typically about 0.67 ms
}
void logInfo() { //Timers have limited stack space, so use some global variables
//lastUpdate = millis(); //time since the program started, in msec (overflows in ~49 days)
lMinute = Time.minute();
if (lLastMinute != lMinute) { //only log data once a minute
if (lYear != Time.year() || lMonth != Time.month() || lDay != Time.day()) { //if a new day, create a new file + header
AddHeaderToLogFile();
Particle.syncTime(); //keep time correct, updating at the start of every day
}
lHour = Time.hour();
sTime = "";
if (lHour < 10) sTime += "0"; //make sure 2 digits so file is aligned
sTime += String(lHour) + ":";
if (lMinute < 10) sTime += "0"; //make sure 2 digits so file is aligned
sTime += String(lMinute) + ":00"; //e.g. "23:45:00"
sBuf = sDate + "," + sTime + ","; //sDate updated in AddHeaderToLogFile()
sBuf += String(T0,1) + "," + String(T1,1) + "," + String(T2,1) + "," + String(T3,1) + "," + String(T4,1) + ",";
sBuf += String(T1RoomSet,1) + "," + String(T2RoomSet,1) + "," + String(T3RoomSet,1) + "," + String(T4RoomSet,1) + ",";
sBuf += sDampersStatus + ","; //e.g. "A1onM2offA3offA4on" A = AUTO, M = MANUAL, on = OPENED, off = CLOSED
sBuf += String(T,1) + "," + String(P,1) + "," + String(H,0) + "," + String(CO2,0) + "," + String(TVOC,0);
myLog.Println(sBuf);
lLastMinute = lMinute;
}
//dTime = millis() - lastUpdate; //typically about 0.54 s (plus about 0.67 s if new file created for a new day + header added)
}
// create a software timer to log Temp, Press, Humid, CO2, TVOC, Damper Status every minute (takes about 0.67 s)
Timer timer1(30000, logInfo); //log data every 60 s, but check every 30 s, so no minutes get dropped (can otherwise happen, although very infrequent)
void getTempPlus() {
digitalWrite(boardLed,HIGH); //blink LED whenever in this routine
//lastUpdate = millis(); //time since the program started, in msec (overflows in ~49 days)
//dTime = 0.0; //set to zero at start of routine, so if routine hangs, it will not get updated
float fT, fP, fH, fC, fV;
int iT, iP, iH;
//first, wake up all mcp devices (takes 250 ms until a new temperature value is available)
mux.setChannel(0); //enable Mux to Ch0, Plenum
mcp.shutdown_wake(0); //wakeup
mux.setChannel(1); //enable Mux to Ch1 (Room 1)
mcp1.shutdown_wake(0); //wakeup
mux.setChannel(2); //enable Mux to Ch2 (Room 2)
mcp2.shutdown_wake(0); //wakeup
mux.setChannel(3); //enable Mux to Ch3 (Room 3)
mcp3.shutdown_wake(0); //wakeup
mux.setChannel(4); //enable Mux to Ch4 (Room 4)
mcp4.shutdown_wake(0); //wakeup
mux.setNoChannel(); //disable Mux
delay(260); //delay at least 250 ms for sensors to wake up and get temperatures
mux.setChannel(0); //enable Mux to Ch0, Plenum
fT = bme.readTempC(); // degrees C (used only for compensation of ccs)
ccs.setTempOffset(fT - 25.0); // compensation for CCS811
iT = (fT*10) + 0.5; //to use only 1 decimal, rounded
fT = iT;
T = fT/10;
sT = String(T,1);
fP = bme.readFloatPressure(); // Pascals
iP = (fP+0.5)/100; // rounded to 0.1 kPa
fP = iP;
P = fP/10; // kPa with 1 decimal
sP = String(P,1);
fH = bme.readFloatHumidity(); //%
iH = fH + 0.5; //to use 0 decimals, rounded
fH = iH;
H = fH;
sH = String(H,0);
if(ccs.available()){
if(!ccs.readData()) {
fC = ccs.geteCO2(); // ppm
CO2 = fC;
sCO2 = String(CO2,0);
fV = ccs.getTVOC(); //ppb
TVOC = fV;
sTVOC = String(TVOC,0);
}
} else {
//Serial.println("CCS811 not available at time: " + String(millis()));
}
fT = mcp.readTempC(); // degrees C
iT = (fT*10) + 0.5; //to use only 1 decimal, rounded
fT = iT;
T0 = fT/10;
sT0 = String(T0,1);
mcp.shutdown_wake(1); //shutdown, to reduce power (and self heating)
mux.setChannel(1); //enable Mux to Ch1 (Room 1)
fT = mcp1.readTempC(); // degrees C
iT = (fT*10) + 0.5; //to use only 1 decimal, rounded
fT = iT;
T1 = fT/10;
sT1 = String(T1,1);
mcp1.shutdown_wake(1); //shutdown, to reduce power (and self heating)
mux.setChannel(2); //enable Mux to Ch2 (Room 2)
fT = mcp2.readTempC(); // degrees C
iT = (fT*10) + 0.5; //to use only 1 decimal, rounded
fT = iT;
T2 = fT/10;
sT2 = String(T2,1);
mcp2.shutdown_wake(1); //shutdown, to reduce power (and self heating)
mux.setChannel(3); //enable Mux to Ch3 (Room 3)
fT = mcp3.readTempC(); // degrees C
iT = (fT*10) + 0.5; //to use only 1 decimal, rounded
fT = iT;
T3 = fT/10;
sT3 = String(T3,1);
mcp3.shutdown_wake(1); //shutdown, to reduce power (and self heating)
mux.setChannel(4); //enable Mux to Ch4 (Room 4)
fT = mcp4.readTempC(); // degrees C
iT = (fT*10) + 0.5; //to use only 1 decimal, rounded
fT = iT;
T4 = fT/10;
sT4 = String(T4,1);
mcp4.shutdown_wake(1); //shutdown, to reduce power (and self heating)
mux.setNoChannel(); //disable Mux
//dTime = millis() - lastUpdate; //typically 0.28 s, only zero when routine is not running (e.g. errors with temperature sensors?)
digitalWrite(boardLed,LOW);
}
// create a software timer to obtain new readings of Temp+ every 10 seconds
Timer timer2(10000, getTempPlus); //update Temp, Press, Humid, CO2, TVOC every 10 s
// used to display a list of the room setpoint temperatures on an iPhone
void sTSetUpdate () {
sTSet = String(T1RoomSet,1);
sTSet += " ";
sTSet += String(T2RoomSet,1);
sTSet += " ";
sTSet += String(T3RoomSet,1);
sTSet += " ";
sTSet += String(T4RoomSet,1);
}
// use a Particle Function to change the room 1 temperature setpoint
int TSetRoom1Day (String command) {
float temp = command.toFloat(); //convert text String of digits to float (stops on first non-digit char, or end of string)
if ((temp < 15.0) || (temp > 30.0)) {
T1RoomSet = 23.0; //if bad value specified, default to 23 C
return -1; //warn user that a bad value was provided
}
T1RoomSet = temp;
sTSetUpdate();
EEPROM.put(10, temp); //put update here, so only use function when absolutely necessary (NOTE: only use local variables in Particle Function)
return 1;
}
// use a Particle Function to change the room 2 temperature setpoint
int TSetRoom2Day (String command) {
float temp = command.toFloat(); //convert text String of digits to float (stops on first non-digit char, or end of string)
if ((temp < 15.0) || (temp > 30.0)) {
T2RoomSet = 23.0; //if bad value specified, default to 23 C
return -1; //warn user that a bad value was provided
}
T2RoomSet = temp;
sTSetUpdate();
EEPROM.put(20, temp); //put update here, so only use function when absolutely necessary (NOTE: only use local variables in Particle Function)
return 1;
}
// use a Particle Function to change the room 3 temperature setpoint
int TSetRoom3Day (String command) {
float temp = command.toFloat(); //convert text String of digits to float (stops on first non-digit char, or end of string)
if ((temp < 15.0) || (temp > 30.0)) {
T3RoomSet = 23.0; //if bad value specified, default to 23 C
return -1; //warn user that a bad value was provided
}
T3RoomSet = temp;
sTSetUpdate();
EEPROM.put(30, temp); //put update here, so only use function when absolutely necessary (NOTE: only use local variables in Particle Function)
return 1;
}
// use a Particle Function to change the room 4 temperature setpoint
int TSetRoom4 (String command) {
float temp = command.toFloat(); //convert text String of digits to float (stops on first non-digit char, or end of string)
if ((temp < 15.0) || (temp > 30.0)) {
T4RoomSet = 23.0; //if bad value specified, default to 23 C
return -1; //warn user that a bad value was provided
}
T4RoomSet = temp;
sTSetUpdate();
EEPROM.put(40, temp); //put update here, so only use function when absolutely necessary (NOTE: only use local variables in Particle Function)
return 1;
}
// use a Particle Function to reset the iReboots value to 0
int ResetiReboots (String command) { //allow any text to reset the value of iReboots to 0
iReboots = 0;
EEPROM.put(0, 0);
return 1;
}
int sDampersMPdesired (String command) { // command = "4AC" = Damper #1-4, then Mode: A = AUTO, M = MANUAL, then Position: O = OPENED, C = CLOSED (optional if Auto)
int iRoom;
bool bMode, bStatus;
iRoom = command.toInt();
if ((iRoom < 1) || (iRoom > 4)) return -1; // valid Room 1 to 4 not found (or number after 1st digit)
switch (command.charAt(1)) {
case 'A':
case 'a':
bMode = AUTO;
break;
case 'M':
case 'm':
bMode = MANUAL;
break;
default:
return -1;
}
switch (command.charAt(2)) {
case 'C':
case 'c':
bStatus = CLOSED;
break;
default:
bStatus = OPENED; //default to OPENED (even if 3rd char is missing)
}
switch (iRoom) {
case 1:
if (bMode == AUTO) {
Damper1Mode = AUTO;
} else {
Damper1Mode = MANUAL;
if (bStatus == OPENED) {
Damper1SelectedPosition = OPENED;
myRelays.off(ROOM1); //open damper
} else {
Damper1SelectedPosition = CLOSED;
myRelays.on(ROOM1); //close damper
}
}
break;
case 2:
if (bMode == AUTO) {
Damper2Mode = AUTO;
} else {
Damper2Mode = MANUAL;
if (bStatus == OPENED) {
Damper2SelectedPosition = OPENED;
myRelays.off(ROOM2); //open damper
} else {
Damper2SelectedPosition = CLOSED;
myRelays.on(ROOM2); //close damper
}
}
break;
case 3:
if (bMode == AUTO) {
Damper3Mode = AUTO;
} else {
Damper3Mode = MANUAL;
if (bStatus == OPENED) {
Damper3SelectedPosition = OPENED;
myRelays.off(ROOM3); //open damper
} else {
Damper3SelectedPosition = CLOSED;
myRelays.on(ROOM3); //close damper
}
}
break;
case 4:
if (bMode == AUTO) {
Damper4Mode = AUTO;
} else {
Damper4Mode = MANUAL;
if (bStatus == OPENED) {
Damper4SelectedPosition = OPENED;
myRelays.off(ROOM4); //open damper
} else {
Damper4SelectedPosition = CLOSED;
myRelays.on(ROOM4); //close damper
}
}
break;
}
return 1;
}
bool OpenDamper(int Room) { //determine if a room damper should be opened or closed
float TSet, TRoom, TPlenum, TOffset;
int StartHour, FinishHour;
bool DamperSelPos;
switch (lMonth) { //(force cold air upstairs, hot air downstairs)
case 1:
case 2:
case 11:
case 12: // winter with no DST
StartHour = 20; //8 PM
FinishHour = 3; //3 AM
break;
case 6:
case 7:
case 8: //summer, with DST offset
StartHour = 18; //7 PM DST
FinishHour = 4; //5 AM DST
break;
case 3:
case 4:
case 5:
case 9:
case 10: //spring & fall with some DST
StartHour = 19; //7 PM, 8 PM DST
FinishHour = 3; //3 AM, 4 AM DST
break;
}
switch (Room) {
case ROOM1:
TSet = T1RoomSet;
if ((lHour > StartHour) || (lHour < FinishHour)) TSet -= 2.0; //lower temperature at night
TRoom = T1;
DamperSelPos = Damper1SelectedPosition; //present Damper position
break;
case ROOM2:
TSet = T2RoomSet;
if ((lHour > StartHour) || (lHour < FinishHour)) TSet -= 2.0; //lower temperature at night
TRoom = T2;
DamperSelPos = Damper2SelectedPosition; //present Damper position
break;
case ROOM3:
TSet = T3RoomSet;
if ((lHour > StartHour) || (lHour < FinishHour)) TSet -= 2.0; //lower temperature at night
TRoom = T3;
DamperSelPos = Damper3SelectedPosition; //present Damper position
break;
case ROOM4: //no temperature setback (basement)
TSet = T4RoomSet;
TRoom = T4;
DamperSelPos = Damper4SelectedPosition; //present Damper position
}
TPlenum = T0;
if (TPlenum < 5) return TRUE; // keep damper open if there is a temperature issue
if (TPlenum > 80) return TRUE; // keep damper open if there is a temperature issue
if (TRoom < 5) return TRUE; // keep damper open if there is a temperature issue
if (TRoom > 40) return TRUE; // keep damper open if there is a temperature issue
if (TSet < 5) return TRUE; // keep damper open if there is a temperature issue
if (TSet > 40) return TRUE; // keep damper open if there is a temperature issue
TOffset = TDeadband; //e.g. 0.9C above or below temperature setpoint
if (DamperSelPos == CLOSED) { //if already closed, don't open until temperature rises/drops by THysteresis amount
TOffset -= THysteresis; //e.g. stay closed until 0.1C above or below temperature setpoint
}
if ((TPlenum > (TRoom+TOffset)) && (TRoom > (TSet+TOffset))) { //room is already too hot, so don't heat more
return FALSE;
}
if ((TPlenum < (TRoom-TOffset)) && (TRoom < (TSet-TOffset))) { //room is already too cold, so don't cool more
return FALSE;
}
return TRUE; //damper should be open for all other cases
}
// reset the system after 60 seconds if the application is unresponsive
ApplicationWatchdog wd(60000, System.reset);
void setup() {
float tempT;
pinMode(boardLed, OUTPUT); //use this blue LED to blink during code operation (setup + when temperatures are read)
digitalWrite(boardLed, HIGH);
// We are going to declare Particle.variable() here so that we can access the values from the cloud.
//This registration must be completed within 30 s of connecting to the cloud, so do it first thing in setup
//Particle.variable("d_Time_ms", dTime); //text description must NOT have any spaces
//items are listed in alphabetical order on iPhone App
Particle.variable("s_CO2_ppm", sCO2);
Particle.variable("s_Dampers", sDampersStatus);
Particle.variable("s_P_kPa", sP);
Particle.variable("s_RH_Pct", sH);
Particle.variable("s_T0_C", sT0);
Particle.variable("s_T1234Set", sTSet);
Particle.variable("s_T1_C", sT1);
Particle.variable("s_T2_C", sT2);
Particle.variable("s_T3_C", sT3);
Particle.variable("s_T4_C", sT4);
Particle.variable("s_TVOC_ppb", sTVOC);
Particle.variable("s_T_C", sT);
Particle.variable("N_Reboots", iReboots);
Particle.function("T1S",TSetRoom1Day);
Particle.function("T2S",TSetRoom2Day);
Particle.function("T3S",TSetRoom3Day);
Particle.function("T4S",TSetRoom4);
Particle.function("D_Mode_Pos",sDampersMPdesired); //e.g. "4MC" = Damper 4, manual mode, close damper; "3a" = Damper 3, auto mode
Particle.function("ResetReboots", ResetiReboots); //so the number of reboots can be reset back to 0
EEPROM.get(0, iReboots); //The value gets incremented each time the CPU is re-booted.
iReboots += 1;
EEPROM.put(0, iReboots);
mux.begin(); //I2C with 8 output channels (for all T/P/RH/CO2/TVOC), 5 used (0-4)
mux.setNoChannel(); //disables all channels 0-7
while(!myLog.begin(0x2A)){ //prepare OpenLog connection
delay(1000); // 1 s
}
Time.zone(-5); //ignore DST (separately deal with time/temperature variations throughout the year)
AddHeaderToLogFile(); //this will create/append a file name based on the date, and add the header to it (comma separated variables format)
myRelays.begin();
myRelays.allOff(); //wired for output relays off = damper open (dampers open if power failure to relay coil)
Damper1SelectedPosition = OPENED;
Damper2SelectedPosition = OPENED;
Damper3SelectedPosition = OPENED;
Damper4SelectedPosition = OPENED;
Damper1Mode = AUTO;
Damper2Mode = AUTO;
Damper3Mode = AUTO;
Damper4Mode = AUTO;
// Channel 0 - Furnace Plenum (CO2, etc)
mux.setChannel(0); //enable Mux to Ch0 (location of BME280/CCS811 board and MCP9808 board in furnace plenum)
bme.settings.runMode = 0b11; // normal mode
bme.settings.tStandby = 0b101; // 1000 ms
bme.settings.filter = 0b000; // off
bme.settings.tempOverSample = 0b011; // x4
bme.settings.pressOverSample = 0b011; // x4
bme.settings.humidOverSample = 0b011; // x4
while(!bme.begin()){ //prepare bme connection (and I2C), typical is: mode=normal, sampling=x16, filter=off, standby=0.5 ms
delay(1000); // 1 s
}
while(!ccs.begin(0x5B)){ //prepare ccs connection (change default library address to 0x5B)
delay(1000); // 1 s
}
while(!mcp.begin(0x18)){ //it will take at least 250 ms before a temperature value is available
delay(1000); // 1 s
}
wd.checkin(); // resets the AWDT count (must occur every 60 min or less, or system will reset); not needed in setup?
// Channel 1 - Bedroom 1
mux.setChannel(1); //enable Mux to Ch1 (location of 1st remote MCP9808 board, bedroom 1)
while(!mcp1.begin(0x18)){ //it will take at least 250 ms before a temperature value is available
delay(1000); // 1 s
}
EEPROM.get(10, tempT);
// EEPROM.get(10,iOffsetT);
// tempT = (iOffsetT + 100) / 10; //stored in EEPROM as an offset integer
if ((tempT < 15.0) || (tempT > 30.0)) {
tempT = 23.0; //if no value specified, default to 23 C
EEPROM.put(10, tempT); //save value in simulated EEPROM so changed values kept during power failures
}
T1RoomSet = tempT;
wd.checkin(); // resets the AWDT count (must occur every 60 min or less, or system will reset); not needed in setup?
// Channel 2 - Bedroom 2
mux.setChannel(2); //enable Mux to Ch1 (location of 2nd remote MCP9808 board, bedroom 2)
while(!mcp2.begin()){ //it will take at least 250 ms before a temperature value is available
delay(1000); // 1 s
}
EEPROM.get(20, tempT);
if ((tempT < 15.0) || (tempT > 30.0)) {
tempT = 23.0; //if no value specified, default to 23 C
EEPROM.put(20, tempT); //save value in simulated EEPROM so changed values kept during power failures
}
T2RoomSet = tempT;
wd.checkin(); // resets the AWDT count (must occur every 60 min or less, or system will reset); not needed in setup?
// Channel 3 - Bedroom 3
mux.setChannel(3); //enable Mux to Ch1 (location of 3rd remote MCP9808 board, bedroom 3)
while(!mcp3.begin()){ //it will take at least 250 ms before a temperature value is available
delay(1000); // 1 s
}
EEPROM.get(30, tempT);
if ((tempT < 15.0) || (tempT > 30.0)) {
tempT = 23.0; //if no value specified, default to 23 C
EEPROM.put(30, tempT); //save value in simulated EEPROM so changed values kept during power failures
}
T3RoomSet = tempT;
wd.checkin(); // resets the AWDT count (must occur every 60 min or less, or system will reset); not needed in setup?
// Channel 4 - Basement
mux.setChannel(4); //enable Mux to Ch1 (location of 4th remote MCP9808 board, basement)
while(!mcp4.begin()){ //it will take at least 250 ms before a temperature value is available
delay(1000); // 1 s
}
EEPROM.get(40, tempT);
if ((tempT < 15.0) || (tempT > 30.0)) {
tempT = 23.0; //if no value specified, default to 23 C
EEPROM.put(40, tempT); //save value in simulated EEPROM so changed values kept during power failures
}
T4RoomSet = tempT;
sTSetUpdate();
wd.checkin(); // resets the AWDT count (must occur every 60 min or less, or system will reset); not needed in setup?
mux.setNoChannel(); //disable Mux (leave Mux off when not measuring a temperature)
// initialize to some default values (needed until all timers have run)
T0 = 23.0; //these default values will keep all dampers initially open
T1 = 23.0;
T2 = 23.0;
T3 = 23.0;
T4 = 23.0;
lHour = Time.hour();
lMinute = Time.minute();
lLastMinute = lMinute;
timer1.start();
timer2.start();
wd.checkin(); // resets the AWDT count (must occur every 60 seconds or less, or system will reset); not needed in setup?
digitalWrite(boardLed, LOW); //setup completed so turn blue board LED off
}
void loop() { //open and close dampers based mainly on room temperatures (not all dampers can be closed, or too much back pressure on HVAC)
// Allow a max. of 3 dampers closed. If 4 dampers requested closed: In auto mode, force damper 4 open (basement). In manual mode, force damper 1 open (spare bedroom).
sDampersStatus = ""; //e.g. "A1onM2offA3offA4on" A = AUTO, M = MANUAL, on = OPENED, off = CLOSED
if (Damper1Mode == MANUAL) { //don't change damper position if in manual (only change from remote input), unless forced re below
if (Damper1SelectedPosition == OPENED) sDampersStatus += "M1on";
else sDampersStatus += "M1off";
} else {
sDampersStatus += "A1"; //Damper1Mode = AUTO
if (OpenDamper(ROOM1)) { //upstairs bedroom 1
Damper1SelectedPosition = OPENED;
sDampersStatus += "on";
myRelays.off(ROOM1); //open damper
} else {
Damper1SelectedPosition = CLOSED;
sDampersStatus += "off";
myRelays.on(ROOM1); //close damper
}
}
if (Damper2Mode == MANUAL) { //don't change damper position if in manual (only change from remote input)
if (Damper2SelectedPosition == OPENED) sDampersStatus += "M2on";
else sDampersStatus += "M2off";
} else {
sDampersStatus += "A2"; //Damper2Mode = AUTO
if (OpenDamper(ROOM2)) { //upstairs bedroom 2
Damper2SelectedPosition = OPENED;
sDampersStatus += "on";
myRelays.off(ROOM2); //open damper
} else {
Damper2SelectedPosition = CLOSED;
sDampersStatus += "off";
myRelays.on(ROOM2); //close damper
}
}
if (Damper3Mode == MANUAL) { //don't change damper position if in manual (only change from remote input)
if (Damper3SelectedPosition == OPENED) sDampersStatus += "M3on";
else sDampersStatus += "M3off";
} else {
sDampersStatus += "A3"; //Damper3Mode = AUTO
if (OpenDamper(ROOM3)) { //upstairs bedroom 3
Damper3SelectedPosition = OPENED;
sDampersStatus += "on";
myRelays.off(ROOM3); //open damper
} else {
Damper3SelectedPosition = CLOSED;
sDampersStatus += "off";
myRelays.on(ROOM3); //close damper
}
}
if (Damper4Mode == MANUAL) { //don't change damper position if in manual (only change from remote input)
if (Damper4SelectedPosition == OPENED) sDampersStatus += "M4on";
else sDampersStatus += "M4off";
} else {
sDampersStatus += "A4"; //Damper4Mode = AUTO
if (OpenDamper(ROOM4)) { //basement
Damper4SelectedPosition = OPENED;
sDampersStatus += "on";
myRelays.off(ROOM4); //open damper
} else {
Damper4SelectedPosition = CLOSED;
sDampersStatus += "off";
myRelays.on(ROOM4); //close damper
}
}
if ((Damper1SelectedPosition == CLOSED) && (Damper2SelectedPosition == CLOSED) && (Damper3SelectedPosition == CLOSED) && (Damper4SelectedPosition == CLOSED)) { // do something if all Dampers are selected to be closed
if (Damper4Mode == MANUAL) { //if Damper 4 was manually closed, leave it closed and force Damper 1 open
myRelays.off(ROOM1);
} else { //auto open Damper in Room 4 (basement) if upstairs rooms are closed
myRelays.off(ROOM4);
}
}
wd.checkin(); // resets the AWDT count (must occur every 60 seconds or less, or system will reset) [not needed here, see comment at end of loop]
delay(2000); // 2 s (no need to continuously run this loop)
} // AWDT count reset automatically after loop() ends
Damper Controller - Revision 2
C/C++//make sure to add these libraries via the Libraries tab in the left sidebar
#include "Particle.h" //automatically included
#include "SparkFunBME280.h"
#include "Adafruit_CCS811.h"
#include "Arduino.h"
//#include <Wire.h>
#include "SparkFun_Qwiic_OpenLog_Arduino_Library.h" //local copy, revised
#include "TCA9548A-RK.h"
#include "RelayShield.h"
#include "Adafruit_MCP9808.h"
BME280 bme; // I2C 0x77, Mux Ch0 (I2C uses D0-1)
Adafruit_CCS811 ccs; // I2C 0x5B, Mux Ch0
Adafruit_MCP9808 mcp; // I2C 0x18, Mux Ch0, 250 ms per temp update
Adafruit_MCP9808 mcp1; // I2C 0x18, Mux Ch1
Adafruit_MCP9808 mcp2; // I2C 0x18, Mux Ch2
Adafruit_MCP9808 mcp3; // I2C 0x18, Mux Ch3
Adafruit_MCP9808 mcp4; // I2C 0x18, Mux Ch4
OpenLog myLog; // I2C 0x2A
TCA9548A mux(Wire, 0); //I2C 0x70
RelayShield myRelays; //RelayShield, D3-6
int boardLed = D7; //blink LED when code is running
//double dTime = 0.0; //used for cloud variable (e.g. time to log one line of data)
//unsigned long lastUpdate = 0; //used to calculate dTime in ms (e.g. to log each line of data)
double T; //Plenum - used for Logging
String sT; //used for cloud variable
double P; //used for Logging
String sP; //used for cloud variable
double H; //used for Logging
String sH; //used for cloud variable
double CO2; //used for Logging
String sCO2; //used for cloud variable
double TVOC; //used for Logging
String sTVOC; //used for cloud variable
double T0; //Plenum - used for Logging
String sT0; //used for cloud variable
double T1; //Room 1 - used for Logging
String sT1; //used for cloud variable
double T2; //Room 2 - used for Logging
String sT2; //used for cloud variable
double T3; //Room 3 - used for Logging
String sT3; //used for cloud variable
double T4; //Room 4 - used for Logging
String sT4; //used for cloud variable
#define TDeadband 0.9 //switch dampers on/off if above/below setpoint +/- Tdeadband (degrees C)
#define THysteresis 0.8 //require this much change in temperature after a damper has switched state, before another switch of state (degrees C)
#define ROOM1 1 //1st bedroom
#define ROOM2 2 //2nd bedroom
#define ROOM3 3 //3rd bedroom
#define ROOM4 4 //basement
float T1RoomSet; //desired Room 1 temperature
float T2RoomSet; //desired Room 2 temperature
float T3RoomSet; //desired Room 3 temperature
float T4RoomSet; //desired Room 4 temperature
#define OPENED 0
#define CLOSED 1
bool Damper1SelectedPosition; //damper SelectedPosition for Room 1
bool Damper2SelectedPosition; //damper SelectedPosition for Room 2
bool Damper3SelectedPosition; //damper SelectedPosition for Room 3
bool Damper4SelectedPosition; //damper SelectedPosition for Room 4
#define AUTO 0
#define MANUAL 1
bool Damper1Mode; //damper mode for Room 1 (Auto/Manual)
bool Damper2Mode; //damper mode for Room 2
bool Damper3Mode; //damper mode for Room 3
bool Damper4Mode; //damper mode for Room 4
unsigned long lYear; // 4 digit year, e.g. 2019
unsigned long lMonth; // 1-12 month
unsigned long lDay; // 1-31 day
unsigned long lHour; // 0-23 hour
unsigned long lMinute; // 0-59
unsigned long lLastMinute; // check every 30 s to see if minute logged already, so no minutes get missed, which does otherwise occur
String sDate = "2019-09-14"; //example
String sTime = "23:45:00"; //example
String sLogFileName = "default.txt"; //example
String sBuf = ""; //use global buffer (and above variables) to reduce stack usage, especially in timers
String sDampersStatus = "A1onM2offA3offA4on"; //example: Damper Mode: A = AUTO, M = MANUAL, Damper Position: on = OPENED, off = CLOSED
String sTSet = "23.0 23.0 23.0 23.0"; //example: Damper temperature trigger setpoints
int iReboots; //use this to count the number of CPU boots (e.g. due to power failures)
int iTErrors; //incremented every time a T0-4 temperature read is a bad value, good if T0 = 5-80 C or T1-T4 = 5-40 C
void AddHeaderToLogFile() {
//lastUpdate = millis(); //time since the program started, in msec (overflows in ~49 days)
lYear = Time.year(); // 4 digit year
lMonth = Time.month(); //1-12 month
lDay = Time.day(); //1-31 day
sDate = String(lYear) + "-";
if (lMonth < 10) sDate += "0"; //make sure 2 digits so file is aligned
sDate += String(lMonth) + "-";
if (lDay < 10) sDate += "0"; //make sure 2 digits so file is aligned
sDate += String(lDay); // e.g. 2019-09-07
sLogFileName = String(lYear);
if (lMonth < 10) sLogFileName += "0"; //make sure 2 digits for month
sLogFileName += String(lMonth);
if (lDay < 10) sLogFileName += "0"; //make sure 2 digits for day
sLogFileName += String(lDay) + ".txt"; //e.g. 20190907.txt (max 8.3 digits)
myLog.append(sLogFileName); //append or create new Log file and send subsequent text to it (e.g. new day, after midnight)
delay(1); // give it time?
sBuf = "Date,Time,T0(C),T1(C),T2(C),T3(C),T4(C),T1Set(C),T2Set(C),T3Set(C),T4Set(C),DSelStatus,T(C),P(kPa),RH(%),CO2(ppm),TVOC(ppb)";
myLog.Println(sBuf); //custom routine that adds 1 ms delay between each character, otherwise some characters get lost
//dTime = millis() - lastUpdate; //typically about 0.67 ms
}
void logInfo() { //Timers have limited stack space, so use some global variables
//lastUpdate = millis(); //time since the program started, in msec (overflows in ~49 days)
lMinute = Time.minute();
if (lLastMinute != lMinute) { //only log data once a minute
if (lYear != Time.year() || lMonth != Time.month() || lDay != Time.day()) { //if a new day, create a new file + header
AddHeaderToLogFile();
Particle.syncTime(); //keep time correct, updating at the start of every day
}
lHour = Time.hour();
sTime = "";
if (lHour < 10) sTime += "0"; //make sure 2 digits so file is aligned
sTime += String(lHour) + ":";
if (lMinute < 10) sTime += "0"; //make sure 2 digits so file is aligned
sTime += String(lMinute) + ":00"; //e.g. "23:45:00"
sBuf = sDate + "," + sTime + ","; //sDate updated in AddHeaderToLogFile()
sBuf += String(T0,1) + "," + String(T1,1) + "," + String(T2,1) + "," + String(T3,1) + "," + String(T4,1) + ",";
sBuf += String(T1RoomSet,1) + "," + String(T2RoomSet,1) + "," + String(T3RoomSet,1) + "," + String(T4RoomSet,1) + ",";
sBuf += sDampersStatus + ","; //e.g. "A1onM2offA3offA4on" A = AUTO, M = MANUAL, on = OPENED, off = CLOSED
sBuf += String(T,1) + "," + String(P,1) + "," + String(H,0) + "," + String(CO2,0) + "," + String(TVOC,0);
myLog.Println(sBuf);
lLastMinute = lMinute;
}
//dTime = millis() - lastUpdate; //typically about 0.54 s (plus about 0.67 s if new file created for a new day + header added)
}
// create a software timer to log Temp, Press, Humid, CO2, TVOC, Damper Status every minute (takes about 0.67 s)
Timer timer1(30000, logInfo); //log data every 60 s, but check every 30 s, so no minutes get dropped (can otherwise happen, although very infrequent)
void getTempPlus() {
digitalWrite(boardLed,HIGH); //blink LED whenever in this routine
//lastUpdate = millis(); //time since the program started, in msec (overflows in ~49 days)
//dTime = 0.0; //set to zero at start of routine, so if routine hangs, it will not get updated
float fT, fP, fH, fC, fV;
int iT, iP, iH;
//first, wake up all mcp devices (takes 250 ms until a new temperature value is available)
mux.setChannel(0); //enable Mux to Ch0, Plenum
mcp.shutdown_wake(0); //wakeup
mux.setChannel(1); //enable Mux to Ch1 (Room 1)
mcp1.shutdown_wake(0); //wakeup
mux.setChannel(2); //enable Mux to Ch2 (Room 2)
mcp2.shutdown_wake(0); //wakeup
mux.setChannel(3); //enable Mux to Ch3 (Room 3)
mcp3.shutdown_wake(0); //wakeup
mux.setChannel(4); //enable Mux to Ch4 (Room 4)
mcp4.shutdown_wake(0); //wakeup
mux.setNoChannel(); //disable Mux
delay(500); //delay at least 250 ms for sensors to wake up and get temperatures (500 for lots of extra time, sometimes needed?)
mux.setChannel(0); //enable Mux to Ch0, Plenum
fT = bme.readTempC(); // degrees C (used only for compensation of ccs)
if ((fT < 5.0) || (fT > 80)) { //check if temperature read is valid (sometimes a 0 is read?)
iTErrors += 1; //increment variable every time a bad temperature is read
mux.setChannel(0); //try once more
fT = bme.readTempC(); //try once more
}
ccs.setTempOffset(fT - 25.0); // compensation for CCS811
iT = (fT*10) + 0.5; //to use only 1 decimal, rounded
fT = iT;
T = fT/10;
sT = String(T,1);
fP = bme.readFloatPressure(); // Pascals
iP = (fP+0.5)/100; // rounded to 0.1 kPa
fP = iP;
P = fP/10; // kPa with 1 decimal
sP = String(P,1);
fH = bme.readFloatHumidity(); //%
iH = fH + 0.5; //to use 0 decimals, rounded
fH = iH;
H = fH;
sH = String(H,0);
if(ccs.available()){
if(!ccs.readData()) {
fC = ccs.geteCO2(); // ppm
CO2 = fC;
sCO2 = String(CO2,0);
fV = ccs.getTVOC(); //ppb
TVOC = fV;
sTVOC = String(TVOC,0);
}
} else {
//Serial.println("CCS811 not available at time: " + String(millis()));
}
fT = mcp.readTempC(); // degrees C
if ((fT < 5.0) || (fT > 80)) { //check if temperature read is valid (sometimes a 0 is read?)
iTErrors += 1; //increment variable every time a bad temperature is read
mux.setChannel(0); //try once more
fT = mcp.readTempC(); //try once more
if ((fT < 5.0) || (fT >80)) { //if the value read from mcp Plenum sensor is still bad, then
if ((T > 5.0) && (T < 80)) { //check if the other bme Plenum temperature is acceptable
fT = T; //use tha good value read for the other Plenum temperature sensor
}
}
}
iT = (fT*10) + 0.5; //to use only 1 decimal, rounded
fT = iT;
T0 = fT/10;
sT0 = String(T0,1);
mcp.shutdown_wake(1); //shutdown, to reduce power (and self heating)
mux.setChannel(1); //enable Mux to Ch1 (Room 1)
fT = mcp1.readTempC(); // degrees C
if ((fT < 5.0) || (fT > 40)) { //check if temperature read is valid (sometimes a 0 is read?)
iTErrors += 1; //increment variable every time a bad temperature is read
mux.setChannel(1); //try once more
fT = mcp1.readTempC(); //try once more
}
iT = (fT*10) + 0.5; //to use only 1 decimal, rounded
fT = iT;
T1 = fT/10;
sT1 = String(T1,1);
mcp1.shutdown_wake(1); //shutdown, to reduce power (and self heating)
if ((T1 < 5) || (T1 > 40)) {
iTErrors += 1; //incement variable every time a bad temperature is read
}
mux.setChannel(2); //enable Mux to Ch2 (Room 2)
fT = mcp2.readTempC(); // degrees C
if ((fT < 5.0) || (fT > 40)) { //check if temperature read is valid (sometimes a 0 is read?)
iTErrors += 1; //increment variable every time a bad temperature is read
mux.setChannel(2); //try once more
fT = mcp2.readTempC(); //try once more
}
iT = (fT*10) + 0.5; //to use only 1 decimal, rounded
fT = iT;
T2 = fT/10;
sT2 = String(T2,1);
mcp2.shutdown_wake(1); //shutdown, to reduce power (and self heating)
if ((T2 < 5) || (T2 > 40)) {
iTErrors += 1; //incement variable every time a bad temperature is read
}
mux.setChannel(3); //enable Mux to Ch3 (Room 3)
fT = mcp3.readTempC(); // degrees C
if ((fT < 5.0) || (fT > 40)) { //check if temperature read is valid (sometimes a 0 is read?)
iTErrors += 1; //increment variable every time a bad temperature is read
mux.setChannel(3); //try once more
fT = mcp3.readTempC(); //try once more
}
iT = (fT*10) + 0.5; //to use only 1 decimal, rounded
fT = iT;
T3 = fT/10;
sT3 = String(T3,1);
mcp3.shutdown_wake(1); //shutdown, to reduce power (and self heating)
if ((T3 < 5) || (T3 > 40)) {
iTErrors += 1; //increment variable every time a bad temperature is read
}
mux.setChannel(4); //enable Mux to Ch4 (Room 4)
fT = mcp4.readTempC(); // degrees C
if ((fT < 5.0) || (fT > 40)) { //check if temperature read is valid (sometimes a 0 is read?)
iTErrors += 1; //increment variable every time a bad temperature is read
mux.setChannel(4); //try once more
fT = mcp4.readTempC(); //try once more
}
iT = (fT*10) + 0.5; //to use only 1 decimal, rounded
fT = iT;
T4 = fT/10;
sT4 = String(T4,1);
mcp4.shutdown_wake(1); //shutdown, to reduce power (and self heating)
if ((T4 < 5) || (T4 > 40)) {
iTErrors += 1; //increment varibale eery time a bad temperature is read
}
mux.setNoChannel(); //disable Mux
//dTime = millis() - lastUpdate; //typically 0.28 s, only zero when routine is not running (e.g. errors with temperature sensors?)
digitalWrite(boardLed,LOW);
//if there are too many temperature reading errors occuring, add 10 to the reboots EEPROM count, and then reboot the software
if (iTErrors >= 10) { //the tens+ will be used to sum the iTErrors, but more than 10 reboots from power failures (uncommon) will screw this up
EEPROM.get(0, iReboots); //The value gets incremented each time the CPU is re-booted.
iReboots += 9; //will become 10 after the reboot
EEPROM.put(0, iReboots);
System.reset(RESET_NO_WAIT); //reboot the system
}
}
// create a software timer to obtain new readings of Temp+ every 10 seconds
Timer timer2(10000, getTempPlus); //update Temp, Press, Humid, CO2, TVOC every 10 s
// used to display a list of the room setpoint temperatures on an iPhone
void sTSetUpdate () {
sTSet = String(T1RoomSet,1);
sTSet += " ";
sTSet += String(T2RoomSet,1);
sTSet += " ";
sTSet += String(T3RoomSet,1);
sTSet += " ";
sTSet += String(T4RoomSet,1);
}
// use a Particle Function to change the room 1 temperature setpoint
int TSetRoom1Day (String command) {
float temp = command.toFloat(); //convert text String of digits to float (stops on first non-digit char, or end of string)
if ((temp < 15.0) || (temp > 30.0)) {
T1RoomSet = 23.0; //if bad value specified, default to 23 C
return -1; //warn user that a bad value was provided
}
T1RoomSet = temp;
sTSetUpdate();
EEPROM.put(10, temp); //put update here, so only use function when absolutely necessary (NOTE: only use local variables in Particle Function)
return 1;
}
// use a Particle Function to change the room 2 temperature setpoint
int TSetRoom2Day (String command) {
float temp = command.toFloat(); //convert text String of digits to float (stops on first non-digit char, or end of string)
if ((temp < 15.0) || (temp > 30.0)) {
T2RoomSet = 23.0; //if bad value specified, default to 23 C
return -1; //warn user that a bad value was provided
}
T2RoomSet = temp;
sTSetUpdate();
EEPROM.put(20, temp); //put update here, so only use function when absolutely necessary (NOTE: only use local variables in Particle Function)
return 1;
}
// use a Particle Function to change the room 3 temperature setpoint
int TSetRoom3Day (String command) {
float temp = command.toFloat(); //convert text String of digits to float (stops on first non-digit char, or end of string)
if ((temp < 15.0) || (temp > 30.0)) {
T3RoomSet = 23.0; //if bad value specified, default to 23 C
return -1; //warn user that a bad value was provided
}
T3RoomSet = temp;
sTSetUpdate();
EEPROM.put(30, temp); //put update here, so only use function when absolutely necessary (NOTE: only use local variables in Particle Function)
return 1;
}
// use a Particle Function to change the room 4 temperature setpoint
int TSetRoom4 (String command) {
float temp = command.toFloat(); //convert text String of digits to float (stops on first non-digit char, or end of string)
if ((temp < 15.0) || (temp > 30.0)) {
T4RoomSet = 23.0; //if bad value specified, default to 23 C
return -1; //warn user that a bad value was provided
}
T4RoomSet = temp;
sTSetUpdate();
EEPROM.put(40, temp); //put update here, so only use function when absolutely necessary (NOTE: only use local variables in Particle Function)
return 1;
}
// use a Particle Function to reset the iReboots value to 0
int ResetiReboots (String command) { //allow any text to reset the value of iReboots to 0
iReboots = 0;
EEPROM.put(0, 0);
return 1;
}
int sDampersMPdesired (String command) { // command = "4AC" = Damper #1-4, then Mode: A = AUTO, M = MANUAL, then Position: O = OPENED, C = CLOSED (optional if Auto)
int iRoom;
bool bMode, bStatus;
iRoom = command.toInt();
if ((iRoom < 1) || (iRoom > 4)) return -1; // valid Room 1 to 4 not found (or number after 1st digit)
switch (command.charAt(1)) {
case 'A':
case 'a':
bMode = AUTO;
break;
case 'M':
case 'm':
bMode = MANUAL;
break;
default:
return -1;
}
switch (command.charAt(2)) {
case 'C':
case 'c':
bStatus = CLOSED;
break;
default:
bStatus = OPENED; //default to OPENED (even if 3rd char is missing)
}
switch (iRoom) {
case 1:
if (bMode == AUTO) {
Damper1Mode = AUTO; //let main loop open and close damper as needed
} else {
Damper1Mode = MANUAL;
if (bStatus == OPENED) {
Damper1SelectedPosition = OPENED;
myRelays.off(ROOM1); //open damper
} else {
Damper1SelectedPosition = CLOSED;
myRelays.on(ROOM1); //close damper
}
}
break;
case 2:
if (bMode == AUTO) {
Damper2Mode = AUTO; //let main loop open and close damper as needed
} else {
Damper2Mode = MANUAL;
if (bStatus == OPENED) {
Damper2SelectedPosition = OPENED;
myRelays.off(ROOM2); //open damper
} else {
Damper2SelectedPosition = CLOSED;
myRelays.on(ROOM2); //close damper
}
}
break;
case 3:
if (bMode == AUTO) {
Damper3Mode = AUTO; //let main loop open and close damper as needed
} else {
Damper3Mode = MANUAL;
if (bStatus == OPENED) {
Damper3SelectedPosition = OPENED;
myRelays.off(ROOM3); //open damper
} else {
Damper3SelectedPosition = CLOSED;
myRelays.on(ROOM3); //close damper
}
}
break;
case 4:
if (bMode == AUTO) {
Damper4Mode = AUTO; //let main loop open and close damper as needed
} else {
Damper4Mode = MANUAL;
if (bStatus == OPENED) {
Damper4SelectedPosition = OPENED;
myRelays.off(ROOM4); //open damper
} else {
Damper4SelectedPosition = CLOSED;
myRelays.on(ROOM4); //close damper
}
}
break;
}
return 1;
}
bool OpenDamper(int Room) { //determine if a room damper should be opened or closed
float TSet, TRoom, TPlenum, TOffset;
int StartHour, FinishHour;
bool DamperSelPos;
switch (lMonth) { //(force cold air upstairs, hot air downstairs)
case 1:
case 2:
case 11:
case 12: // winter with no DST
StartHour = 20; //8 PM
FinishHour = 3; //3 AM
break;
case 6:
case 7:
case 8: //summer, with DST offset
StartHour = 18; //7 PM DST
FinishHour = 4; //5 AM DST
break;
case 3:
case 4:
case 5:
case 9:
case 10: //spring & fall with some DST
StartHour = 19; //7 PM, 8 PM DST
FinishHour = 3; //3 AM, 4 AM DST
break;
}
TPlenum = T0;
if (TPlenum < 5) return TRUE; // keep damper open if there is a temperature issue
if (TPlenum > 80) return TRUE; // keep damper open if there is a temperature issue
switch (Room) {
case ROOM1:
TSet = T1RoomSet;
if ((lHour > StartHour) || (lHour < FinishHour)) TSet -= 2.0; //lower temperature at night
TRoom = T1;
DamperSelPos = Damper1SelectedPosition; //present Damper position
break;
case ROOM2:
TSet = T2RoomSet;
if ((lHour > StartHour) || (lHour < FinishHour)) TSet -= 2.0; //lower temperature at night
TRoom = T2;
DamperSelPos = Damper2SelectedPosition; //present Damper position
if (TPlenum < 20) return TRUE; //always keep room 2 damper open when air conditioner on
break;
case ROOM3:
TSet = T3RoomSet;
if ((lHour > StartHour) || (lHour < FinishHour)) TSet -= 2.0; //lower temperature at night
TRoom = T3;
DamperSelPos = Damper3SelectedPosition; //present Damper position
if (TPlenum < 20) return TRUE; //always keep room 3 damper open when air conditioner on
break;
case ROOM4: //no temperature setback (basement)
TSet = T4RoomSet;
TRoom = T4;
DamperSelPos = Damper4SelectedPosition; //present Damper position
}
if (TRoom < 5) return TRUE; // keep damper open if there is a temperature issue
if (TRoom > 40) return TRUE; // keep damper open if there is a temperature issue
if (TSet < 5) return TRUE; // keep damper open if there is a temperature issue
if (TSet > 40) return TRUE; // keep damper open if there is a temperature issue
TOffset = TDeadband; //e.g. 0.9C above or below temperature setpoint
if (DamperSelPos == CLOSED) { //if already closed, don't open until temperature rises/drops by THysteresis amount
TOffset -= THysteresis; //e.g. stay closed until 0.1C above or below temperature setpoint
}
if ((TPlenum > (TRoom+TOffset)) && (TRoom > (TSet+TOffset))) { //room is already too hot, so don't heat more
return FALSE;
}
if ((TPlenum < (TRoom-TOffset)) && (TRoom < (TSet-TOffset))) { //room is already too cold, so don't cool more
return FALSE;
}
return TRUE; //damper should be open for all other cases
}
// reset the system after 60 seconds if the application is unresponsive
ApplicationWatchdog wd(60000, System.reset);
void setup() {
float tempT;
pinMode(boardLed, OUTPUT); //use this blue LED to blink during code operation (setup + when temperatures are read)
digitalWrite(boardLed, HIGH);
// We are going to declare Particle.variable() here so that we can access the values from the cloud.
//This registration must be completed within 30 s of connecting to the cloud, so do it first thing in setup
//Particle.variable("d_Time_ms", dTime); //text description must NOT have any spaces
//items are listed in alphabetical order on iPhone App
Particle.variable("s_CO2_ppm", sCO2);
Particle.variable("s_Dampers", sDampersStatus);
Particle.variable("s_P_kPa", sP);
Particle.variable("s_RH_Pct", sH);
Particle.variable("s_T0_C", sT0);
Particle.variable("s_T1234Set", sTSet);
Particle.variable("s_T1_C", sT1);
Particle.variable("s_T2_C", sT2);
Particle.variable("s_T3_C", sT3);
Particle.variable("s_T4_C", sT4);
Particle.variable("s_TVOC_ppb", sTVOC);
Particle.variable("s_T_C", sT);
Particle.variable("N_Reboots", iReboots);
Particle.variable("s_TimeNow",sTime); //present time of last update to the cloud, in case cloud updates stop/crash
Particle.variable("N_TErrors",iTErrors); //incremented every time a T0-4 temperature read is a bad value, good if T0 = 0-80 C or T1-T4 = 10-40 C
Particle.function("T1S",TSetRoom1Day);
Particle.function("T2S",TSetRoom2Day);
Particle.function("T3S",TSetRoom3Day);
Particle.function("T4S",TSetRoom4);
Particle.function("D_Mode_Pos_eg4MC",sDampersMPdesired); //e.g. "4MC" = Damper 4, manual mode, close damper; "3a" = Damper 3, auto mode
Particle.function("ResetReboots", ResetiReboots); //so the number of reboots can be reset back to 0
EEPROM.get(0, iReboots); //The value gets incremented each time the CPU is re-booted.
iReboots += 1;
EEPROM.put(0, iReboots);
iTErrors = 0; //Use to keep track of any T0-4 temperatures read that are not within 10-40 C (starts at 0 after every reboot)
mux.begin(); //I2C with 8 output channels (for all T/P/RH/CO2/TVOC), 5 used (0-4)
mux.setNoChannel(); //disables all channels 0-7
while(!myLog.begin(0x2A)){ //prepare OpenLog connection
delay(1000); // 1 s
}
Time.zone(-5); //ignore DST (separately deal with time/temperature variations throughout the year)
AddHeaderToLogFile(); //this will create/append a file name based on the date, and add the header to it (comma separated variables format)
myRelays.begin();
myRelays.allOff(); //wired for output relays off = damper open (dampers open if power failure to relay coil)
Damper1SelectedPosition = OPENED;
Damper2SelectedPosition = OPENED;
Damper3SelectedPosition = OPENED;
Damper4SelectedPosition = OPENED;
Damper1Mode = AUTO;
Damper2Mode = AUTO;
Damper3Mode = AUTO;
Damper4Mode = AUTO;
// Channel 0 - Furnace Plenum (CO2, etc)
mux.setChannel(0); //enable Mux to Ch0 (location of BME280/CCS811 board and MCP9808 board in furnace plenum)
bme.settings.runMode = 0b11; // normal mode
bme.settings.tStandby = 0b101; // 1000 ms
bme.settings.filter = 0b000; // off
bme.settings.tempOverSample = 0b011; // x4
bme.settings.pressOverSample = 0b011; // x4
bme.settings.humidOverSample = 0b011; // x4
while(!bme.begin()){ //prepare bme connection (and I2C), typical is: mode=normal, sampling=x16, filter=off, standby=0.5 ms
delay(1000); // 1 s
}
while(!ccs.begin(0x5B)){ //prepare ccs connection (change default library address to 0x5B)
delay(1000); // 1 s
}
while(!mcp.begin(0x18)){ //it will take at least 250 ms before a temperature value is available
delay(1000); // 1 s
}
wd.checkin(); // resets the AWDT count (must occur every 60 min or less, or system will reset); not needed in setup?
// Channel 1 - Bedroom 1
mux.setChannel(1); //enable Mux to Ch1 (location of 1st remote MCP9808 board, bedroom 1)
while(!mcp1.begin(0x18)){ //it will take at least 250 ms before a temperature value is available
delay(1000); // 1 s
}
EEPROM.get(10, tempT);
// EEPROM.get(10,iOffsetT);
// tempT = (iOffsetT + 100) / 10; //stored in EEPROM as an offset integer
if ((tempT < 15.0) || (tempT > 30.0)) {
tempT = 23.0; //if no value specified, default to 23 C
EEPROM.put(10, tempT); //save value in simulated EEPROM so changed values kept during power failures
}
T1RoomSet = tempT;
wd.checkin(); // resets the AWDT count (must occur every 60 min or less, or system will reset); not needed in setup?
// Channel 2 - Bedroom 2
mux.setChannel(2); //enable Mux to Ch1 (location of 2nd remote MCP9808 board, bedroom 2)
while(!mcp2.begin()){ //it will take at least 250 ms before a temperature value is available
delay(1000); // 1 s
}
EEPROM.get(20, tempT);
if ((tempT < 15.0) || (tempT > 30.0)) {
tempT = 23.0; //if no value specified, default to 23 C
EEPROM.put(20, tempT); //save value in simulated EEPROM so changed values kept during power failures
}
T2RoomSet = tempT;
wd.checkin(); // resets the AWDT count (must occur every 60 min or less, or system will reset); not needed in setup?
// Channel 3 - Bedroom 3
mux.setChannel(3); //enable Mux to Ch1 (location of 3rd remote MCP9808 board, bedroom 3)
while(!mcp3.begin()){ //it will take at least 250 ms before a temperature value is available
delay(1000); // 1 s
}
EEPROM.get(30, tempT);
if ((tempT < 15.0) || (tempT > 30.0)) {
tempT = 23.0; //if no value specified, default to 23 C
EEPROM.put(30, tempT); //save value in simulated EEPROM so changed values kept during power failures
}
T3RoomSet = tempT;
wd.checkin(); // resets the AWDT count (must occur every 60 min or less, or system will reset); not needed in setup?
// Channel 4 - Basement
mux.setChannel(4); //enable Mux to Ch1 (location of 4th remote MCP9808 board, basement)
while(!mcp4.begin()){ //it will take at least 250 ms before a temperature value is available
delay(1000); // 1 s
}
EEPROM.get(40, tempT);
if ((tempT < 15.0) || (tempT > 30.0)) {
tempT = 23.0; //if no value specified, default to 23 C
EEPROM.put(40, tempT); //save value in simulated EEPROM so changed values kept during power failures
}
T4RoomSet = tempT;
sTSetUpdate();
wd.checkin(); // resets the AWDT count (must occur every 60 min or less, or system will reset); not needed in setup?
mux.setNoChannel(); //disable Mux (leave Mux off when not measuring a temperature)
// initialize to some default values (needed until all timers have run)
T0 = 23.0; //these default values will keep all dampers initially open
T1 = 23.0;
T2 = 23.0;
T3 = 23.0;
T4 = 23.0;
lHour = Time.hour();
lMinute = Time.minute();
lLastMinute = lMinute;
timer1.start();
timer2.start();
wd.checkin(); // resets the AWDT count (must occur every 60 seconds or less, or system will reset); not needed in setup?
digitalWrite(boardLed, LOW); //setup completed so turn blue board LED off
}
void loop() { //open and close dampers based mainly on room temperatures (not all dampers can be closed, or too much back pressure on HVAC)
// Allow a max. of 3 dampers closed. If 4 dampers requested closed: In auto mode, force damper 4 open (basement). In manual mode, force damper 1 open (spare bedroom).
sDampersStatus = ""; //e.g. "A1onM2offA3offA4on" A = AUTO, M = MANUAL, on = OPENED, off = CLOSED
if (Damper1Mode == MANUAL) { //don't change damper position if in manual (only change from remote input), unless forced re below
if (Damper1SelectedPosition == OPENED) sDampersStatus += "M1on";
else sDampersStatus += "M1off";
} else {
sDampersStatus += "A1"; //Damper1Mode = AUTO
if (OpenDamper(ROOM1)) { //upstairs bedroom 1
Damper1SelectedPosition = OPENED;
sDampersStatus += "on";
myRelays.off(ROOM1); //open damper
} else {
Damper1SelectedPosition = CLOSED;
sDampersStatus += "off";
myRelays.on(ROOM1); //close damper
}
}
if (Damper2Mode == MANUAL) { //don't change damper position if in manual (only change from remote input)
if (Damper2SelectedPosition == OPENED) sDampersStatus += "M2on";
else sDampersStatus += "M2off";
} else {
sDampersStatus += "A2"; //Damper2Mode = AUTO
if (OpenDamper(ROOM2)) { //upstairs bedroom 2
Damper2SelectedPosition = OPENED;
sDampersStatus += "on";
myRelays.off(ROOM2); //open damper
} else {
Damper2SelectedPosition = CLOSED;
sDampersStatus += "off";
myRelays.on(ROOM2); //close damper
}
}
if (Damper3Mode == MANUAL) { //don't change damper position if in manual (only change from remote input)
if (Damper3SelectedPosition == OPENED) sDampersStatus += "M3on";
else sDampersStatus += "M3off";
} else {
sDampersStatus += "A3"; //Damper3Mode = AUTO
if (OpenDamper(ROOM3)) { //upstairs bedroom 3
Damper3SelectedPosition = OPENED;
sDampersStatus += "on";
myRelays.off(ROOM3); //open damper
} else {
Damper3SelectedPosition = CLOSED;
sDampersStatus += "off";
myRelays.on(ROOM3); //close damper
}
}
if (Damper4Mode == MANUAL) { //don't change damper position if in manual (only change from remote input)
if (Damper4SelectedPosition == OPENED) sDampersStatus += "M4on";
else sDampersStatus += "M4off";
} else {
sDampersStatus += "A4"; //Damper4Mode = AUTO
if (OpenDamper(ROOM4)) { //basement
Damper4SelectedPosition = OPENED;
sDampersStatus += "on";
myRelays.off(ROOM4); //open damper
} else {
Damper4SelectedPosition = CLOSED;
sDampersStatus += "off";
myRelays.on(ROOM4); //close damper
}
}
if ((Damper1SelectedPosition == CLOSED) && (Damper2SelectedPosition == CLOSED) && (Damper3SelectedPosition == CLOSED) && (Damper4SelectedPosition == CLOSED)) { // do something if all Dampers are selected to be closed
if ((Damper4Mode == MANUAL) || (T0 < T4RoomSet)) { //if Damper 4 was manually closed OR the AC is on, leave it closed and force Damper 1 open
myRelays.off(ROOM1); //open Room 1 damper
} else { //auto open Damper in Room 4 (basement) if upstairs rooms are closed
myRelays.off(ROOM4); //open Room 4 damper
}
}
wd.checkin(); // resets the AWDT count (must occur every 60 seconds or less, or system will reset) [not needed here, see comment at end of loop]
delay(2000); // 2 s (no need to continuously run this loop)
} // AWDT count reset automatically after loop() ends
//make sure to add these libraries via the Libraries tab in the left sidebar
#include "Particle.h" //automatically included
#include "SparkFunBME280.h"
#include "Adafruit_CCS811.h"
#include "Arduino.h"
//#include <Wire.h>
#include "SparkFun_Qwiic_OpenLog_Arduino_Library.h" //local copy, revised
#include "TCA9548A-RK.h"
#include "RelayShield.h"
#include "Adafruit_MCP9808.h"
BME280 bme; // I2C 0x77, Mux Ch0 (I2C uses D0-1)
Adafruit_CCS811 ccs; // I2C 0x5B, Mux Ch0
Adafruit_MCP9808 mcp; // I2C 0x18, Mux Ch0, 250 ms per temp update
Adafruit_MCP9808 mcp1; // I2C 0x18, Mux Ch1
Adafruit_MCP9808 mcp2; // I2C 0x18, Mux Ch2
Adafruit_MCP9808 mcp3; // I2C 0x18, Mux Ch3
Adafruit_MCP9808 mcp4; // I2C 0x18, Mux Ch4
OpenLog myLog; // I2C 0x2A
TCA9548A mux(Wire, 0); //I2C 0x70
RelayShield myRelays; //RelayShield, D3-6
int boardLed = D7; //blink LED when code is running
//double dTime = 0.0; //used for cloud variable (e.g. time to log one line of data)
//unsigned long lastUpdate = 0; //used to calculate dTime in ms (e.g. to log each line of data)
double T; //Plenum - used for cloud variable and Logging
double P; //used for cloud variable and Logging
double H; //used for cloud variable and Logging
double CO2; //used for cloud variable and Logging
double TVOC; //used for cloud variable and Logging
double T0; //Plenum - used for cloud variable and Logging
double T1; //Room 1 - used for cloud variable and Logging
double T2; //Room 2 - used for cloud variable and Logging
double T3; //Room 3 - used for cloud variable and Logging
double T4; //Room 4 - used for cloud variable and Logging
#define TDeadband 0.9 //switch dampers on/off if above/below setpoint +/- Tdeadband (degrees C)
#define THysteresis 0.8 //require this much change in temperature after a damper has switched state, before another switch of state (degrees C)
#define ROOM1 1 //1st bedroom
#define ROOM2 2 //2nd bedroom
#define ROOM3 3 //3rd bedroom
#define ROOM4 4 //basement
float T1RoomSet; //desired Room 1 temperature
float T2RoomSet; //desired Room 2 temperature
float T3RoomSet; //desired Room 3 temperature
float T4RoomSet; //desired Room 4 temperature
#define OPENED 0
#define CLOSED 1
bool Damper1SelectedPosition; //damper SelectedPosition for Room 1
bool Damper2SelectedPosition; //damper SelectedPosition for Room 2
bool Damper3SelectedPosition; //damper SelectedPosition for Room 3
bool Damper4SelectedPosition; //damper SelectedPosition for Room 4
#define AUTO 0
#define MANUAL 1
bool Damper1Mode; //damper mode for Room 1 (Auto/Manual)
bool Damper2Mode; //damper mode for Room 2
bool Damper3Mode; //damper mode for Room 3
bool Damper4Mode; //damper mode for Room 4
unsigned long lYear; // 4 digit year, e.g. 2019
unsigned long lMonth; // 1-12 month
unsigned long lDay; // 1-31 day
unsigned long lHour; // 0-23 hour
unsigned long lMinute; // 0-59
unsigned long lLastMinute; // check every 30 s to see if minute logged already, so no minutes get missed, which does otherwise occur
String sDate = "2019-09-14"; //example
String sTime = "23:45:00"; //example
String sLogFileName = "default.txt"; //example
String sBuf = ""; //use global buffer (and above variables) to reduce stack usage, especially in timers
String sDampersStatus = "A1onM2offA3offA4on"; //example: Damper Mode: A = AUTO, M = MANUAL, Damper Position: on = OPENED, off = CLOSED
void AddHeaderToLogFile() {
//lastUpdate = millis(); //time since the program started, in msec (overflows in ~49 days)
lYear = Time.year(); // 4 digit year
lMonth = Time.month(); //1-12 month
lDay = Time.day(); //1-31 day
sDate = String(lYear) + "-";
if (lMonth < 10) sDate += "0"; //make sure 2 digits so file is aligned
sDate += String(lMonth) + "-";
if (lDay < 10) sDate += "0"; //make sure 2 digits so file is aligned
sDate += String(lDay); // e.g. 2019-09-07
sLogFileName = String(lYear);
if (lMonth < 10) sLogFileName += "0"; //make sure 2 digits for month
sLogFileName += String(lMonth);
if (lDay < 10) sLogFileName += "0"; //make sure 2 digits for day
sLogFileName += String(lDay) + ".txt"; //e.g. 20190907.txt (max 8.3 digits)
myLog.append(sLogFileName); //append or create new Log file and send subsequent text to it (e.g. new day, after midnight)
delay(1); // give it time?
sBuf = "Date,Time,T0(C),T1(C),T2(C),T3(C),T4(C),T1Set(C),T2Set(C),T3Set(C),T4Set(C),DSelStatus,T(C),P(kPa),RH(%),CO2(ppm),TVOC(ppb)";
myLog.Println(sBuf); //custom routine that adds 1 ms delay between each character, otherwise some characters get lost
//dTime = millis() - lastUpdate; //typically about 0.67 ms
}
void logInfo() { //Timers have limited stack space, so use some global variables
//lastUpdate = millis(); //time since the program started, in msec (overflows in ~49 days)
lMinute = Time.minute();
if (lLastMinute != lMinute) { //only log data once a minute
if (lYear != Time.year() || lMonth != Time.month() || lDay != Time.day()) { //if a new day, create a new file + header
AddHeaderToLogFile();
Particle.syncTime(); //keep time correct, updating at the start of every day
}
lHour = Time.hour();
sTime = "";
if (lHour < 10) sTime += "0"; //make sure 2 digits so file is aligned
sTime += String(lHour) + ":";
if (lMinute < 10) sTime += "0"; //make sure 2 digits so file is aligned
sTime += String(lMinute) + ":00"; //e.g. "23:45:00"
sBuf = sDate + "," + sTime + ","; //sDate updated in AddHeaderToLogFile()
sBuf += String(T0,1) + "," + String(T1,1) + "," + String(T2,1) + "," + String(T3,1) + "," + String(T4,1) + ",";
sBuf += String(T1RoomSet,1) + "," + String(T2RoomSet,1) + "," + String(T3RoomSet,1) + "," + String(T4RoomSet,1) + ",";
sBuf += sDampersStatus + ","; //e.g. "A1onM2offA3offA4on" A = AUTO, M = MANUAL, on = OPENED, off = CLOSED
sBuf += String(T,1) + "," + String(P,1) + "," + String(H,0) + "," + String(CO2,0) + "," + String(TVOC,0);
myLog.Println(sBuf);
lLastMinute = lMinute;
}
//dTime = millis() - lastUpdate; //typically about 0.54 s (plus about 0.67 s if new file created for a new day + header added)
}
// create a software timer to log Temp, Press, Humid, CO2, TVOC, Damper Status every minute (takes about 0.67 s)
Timer timer1(30000, logInfo); //log data every 60 s, but check every 30 s, so no minutes get dropped (can otherwise happen, although very infrequent)
void getTempPlus() {
digitalWrite(boardLed,HIGH); //blink LED whenever in this routine
//lastUpdate = millis(); //time since the program started, in msec (overflows in ~49 days)
//dTime = 0.0; //set to zero at start of routine, so if routine hangs, it will not get updated
float fT, fP, fH, fC, fV;
int iT, iP, iH;
//first, wake up all mcp devices (takes 250 ms until a new temperature value is available)
mux.setChannel(0); //enable Mux to Ch0, Plenum
mcp.shutdown_wake(0); //wakeup
mux.setChannel(1); //enable Mux to Ch1 (Room 1)
mcp1.shutdown_wake(0); //wakeup
mux.setChannel(2); //enable Mux to Ch2 (Room 2)
mcp2.shutdown_wake(0); //wakeup
mux.setChannel(3); //enable Mux to Ch3 (Room 3)
mcp3.shutdown_wake(0); //wakeup
mux.setChannel(4); //enable Mux to Ch4 (Room 4)
mcp4.shutdown_wake(0); //wakeup
mux.setNoChannel(); //disable Mux
delay(260); //delay at least 250 ms for sensors to wake up and get temperatures
mux.setChannel(0); //enable Mux to Ch0, Plenum
fT = bme.readTempC(); // degrees C (used only for compensation of ccs)
ccs.setTempOffset(fT - 25.0); // compensation for CCS811
iT = (fT*10) + 0.5; //to use only 1 decimal, rounded
fT = iT;
T = fT/10;
fP = bme.readFloatPressure(); // Pascals
iP = (fP+0.5)/100; // rounded to 0.1 kPa
fP = iP;
P = fP/10; // kPa with 1 decimal
fH = bme.readFloatHumidity(); //%
iH = fH + 0.5; //to use 0 decimals, rounded
fH = iH;
H = fH;
if(ccs.available()){
if(!ccs.readData()) {
fC = ccs.geteCO2(); // ppm
CO2 = fC;
fV = ccs.getTVOC(); //ppb
TVOC = fV;
}
} else {
//Serial.println("CCS811 not available at time: " + String(millis()));
}
fT = mcp.readTempC(); // degrees C
iT = (fT*10) + 0.5; //to use only 1 decimal, rounded
fT = iT;
T0 = fT/10;
mcp.shutdown_wake(1); //shutdown, to reduce power (and self heating)
mux.setChannel(1); //enable Mux to Ch1 (Room 1)
fT = mcp1.readTempC(); // degrees C
iT = (fT*10) + 0.5; //to use only 1 decimal, rounded
fT = iT;
T1 = fT/10;
mcp1.shutdown_wake(1); //shutdown, to reduce power (and self heating)
mux.setChannel(2); //enable Mux to Ch2 (Room 2)
fT = mcp2.readTempC(); // degrees C
iT = (fT*10) + 0.5; //to use only 1 decimal, rounded
fT = iT;
T2 = fT/10;
mcp2.shutdown_wake(1); //shutdown, to reduce power (and self heating)
mux.setChannel(3); //enable Mux to Ch3 (Room 3)
fT = mcp3.readTempC(); // degrees C
iT = (fT*10) + 0.5; //to use only 1 decimal, rounded
fT = iT;
T3 = fT/10;
mcp3.shutdown_wake(1); //shutdown, to reduce power (and self heating)
mux.setChannel(4); //enable Mux to Ch4 (Room 4)
fT = mcp4.readTempC(); // degrees C
iT = (fT*10) + 0.5; //to use only 1 decimal, rounded
fT = iT;
T4 = fT/10;
mcp4.shutdown_wake(1); //shutdown, to reduce power (and self heating)
mux.setNoChannel(); //disable Mux
//dTime = millis() - lastUpdate; //typically 0.28 s, only zero when routine is not running (e.g. errors with temperature sensors?)
digitalWrite(boardLed,LOW);
}
// create a software timer to obtain new readings of Temp+ every 10 seconds
Timer timer2(10000, getTempPlus); //update Temp, Press, Humid, CO2, TVOC every 10 s
int TSetRoom1Day (String command) {
float temp = command.toFloat(); //convert text String of digits to float (stops on first non-digit char, or end of string)
if ((temp < 15.0) || (temp > 30.0)) {
T1RoomSet = 23.0; //if bad value specified, default to 23 C
return -1; //warn user that a bad value was provided
}
T1RoomSet = temp;
return 1;
}
int TSetRoom2Day (String command) {
float temp = command.toFloat(); //convert text String of digits to float (stops on first non-digit char, or end of string)
if ((temp < 15.0) || (temp > 30.0)) {
T2RoomSet = 23.0; //if bad value specified, default to 23 C
return -1; //warn user that a bad value was provided
}
T2RoomSet = temp;
return 1;
}
int TSetRoom3Day (String command) {
float temp = command.toFloat(); //convert text String of digits to float (stops on first non-digit char, or end of string)
if ((temp < 15.0) || (temp > 30.0)) {
T3RoomSet = 23.0; //if bad value specified, default to 23 C
return -1; //warn user that a bad value was provided
}
T3RoomSet = temp;
return 1;
}
int TSetRoom4 (String command) {
float temp = command.toFloat(); //convert text String of digits to float (stops on first non-digit char, or end of string)
if ((temp < 15.0) || (temp > 30.0)) {
T4RoomSet = 23.0; //if bad value specified, default to 23 C
return -1; //warn user that a bad value was provided
}
T4RoomSet = temp;
return 1;
}
int sDampersMPdesired (String command) { // command = "4AC" = Damper #1-4, then Mode: A = AUTO, M = MANUAL, then Position: O = OPENED, C = CLOSED (optional if Auto)
int iRoom;
bool bMode, bStatus;
iRoom = command.toInt();
if ((iRoom < 1) || (iRoom > 4)) return -1; // valid Room 1 to 4 not found (or number after 1st digit)
switch (command.charAt(1)) {
case 'A':
case 'a':
bMode = AUTO;
break;
case 'M':
case 'm':
bMode = MANUAL;
break;
default:
return -1;
}
switch (command.charAt(2)) {
case 'C':
case 'c':
bStatus = CLOSED;
break;
default:
bStatus = OPENED; //default to OPENED (even if 3rd char is missing)
}
switch (iRoom) {
case 1:
if (bMode == AUTO) {
Damper1Mode = AUTO;
} else {
Damper1Mode = MANUAL;
if (bStatus == OPENED) {
Damper1SelectedPosition = OPENED;
myRelays.off(ROOM1); //open damper
} else {
Damper1SelectedPosition = CLOSED;
myRelays.on(ROOM1); //close damper
}
}
break;
case 2:
if (bMode == AUTO) {
Damper2Mode = AUTO;
} else {
Damper2Mode = MANUAL;
if (bStatus == OPENED) {
Damper2SelectedPosition = OPENED;
myRelays.off(ROOM2); //open damper
} else {
Damper2SelectedPosition = CLOSED;
myRelays.on(ROOM2); //close damper
}
}
break;
case 3:
if (bMode == AUTO) {
Damper3Mode = AUTO;
} else {
Damper3Mode = MANUAL;
if (bStatus == OPENED) {
Damper3SelectedPosition = OPENED;
myRelays.off(ROOM3); //open damper
} else {
Damper3SelectedPosition = CLOSED;
myRelays.on(ROOM3); //close damper
}
}
break;
case 4:
if (bMode == AUTO) {
Damper4Mode = AUTO;
} else {
Damper4Mode = MANUAL;
if (bStatus == OPENED) {
Damper4SelectedPosition = OPENED;
myRelays.off(ROOM4); //open damper
} else {
Damper4SelectedPosition = CLOSED;
myRelays.on(ROOM4); //close damper
}
}
break;
}
return 1;
}
bool OpenDamper(int Room) { //determine if a room damper should be opened or closed
float TSet, TRoom, TPlenum, TOffset;
int StartHour, FinishHour;
bool DamperSelPos;
switch (lMonth) { //(force cold air upstairs, hot air downstairs)
case 1:
case 2:
case 11:
case 12: // winter with no DST
StartHour = 20; //8 PM
FinishHour = 3; //3 AM
break;
case 6:
case 7:
case 8: //summer, with DST offset
StartHour = 18; //7 PM DST
FinishHour = 4; //5 AM DST
break;
case 3:
case 4:
case 5:
case 9:
case 10: //spring & fall with some DST
StartHour = 19; //7 PM, 8 PM DST
FinishHour = 3; //3 AM, 4 AM DST
break;
}
switch (Room) {
case ROOM1:
TSet = T1RoomSet;
if ((lHour > StartHour) || (lHour < FinishHour)) TSet -= 2.0; //lower temperature at night
TRoom = T1;
DamperSelPos = Damper1SelectedPosition; //present Damper position
break;
case ROOM2:
TSet = T2RoomSet;
if ((lHour > StartHour) || (lHour < FinishHour)) TSet -= 2.0; //lower temperature at night
TRoom = T2;
DamperSelPos = Damper2SelectedPosition; //present Damper position
break;
case ROOM3:
TSet = T3RoomSet;
if ((lHour > StartHour) || (lHour < FinishHour)) TSet -= 2.0; //lower temperature at night
TRoom = T3;
DamperSelPos = Damper3SelectedPosition; //present Damper position
break;
case ROOM4: //no temperature setback (basement)
TSet = T4RoomSet;
TRoom = T4;
DamperSelPos = Damper4SelectedPosition; //present Damper position
}
TPlenum = T0;
if (TPlenum < 5) return TRUE; // keep damper open if there is a temperature issue
if (TPlenum > 80) return TRUE; // keep damper open if there is a temperature issue
if (TRoom < 5) return TRUE; // keep damper open if there is a temperature issue
if (TRoom > 40) return TRUE; // keep damper open if there is a temperature issue
if (TSet < 5) return TRUE; // keep damper open if there is a temperature issue
if (TSet > 40) return TRUE; // keep damper open if there is a temperature issue
TOffset = TDeadband; //e.g. 0.9C above or below temperature setpoint
if (DamperSelPos == CLOSED) { //if already closed, don't open until temperature rises/drops by THysteresis amount
TOffset -= THysteresis; //e.g. stay closed until 0.1C above or below temperature setpoint
}
if ((TPlenum > (TRoom+TOffset)) && (TRoom > (TSet+TOffset))) { //room is already too hot, so don't heat more
return FALSE;
}
if ((TPlenum < (TRoom-TOffset)) && (TRoom < (TSet-TOffset))) { //room is already too cold, so don't cool more
return FALSE;
}
return TRUE; //damper should be open for all other cases
}
// reset the system after 60 seconds if the application is unresponsive
ApplicationWatchdog wd(60000, System.reset);
void setup() {
pinMode(boardLed, OUTPUT); //use this blue LED to blink during code operation (setup + when temperatures are read)
digitalWrite(boardLed, HIGH);
// We are going to declare Particle.variable() here so that we can access the values from the cloud.
//This registration must be completed within 30 s of connecting to the cloud, so do it first thing in setup
//Particle.variable("d_Time_ms", dTime); //text description must NOT have any spaces
Particle.variable("d_T_C", T);
Particle.variable("d_T0_C", T0);
Particle.variable("d_T1_C", T1);
Particle.variable("d_T2_C", T2);
Particle.variable("d_T3_C", T3);
Particle.variable("d_T4_C", T4);
Particle.variable("d_P_kPa", P);
Particle.variable("d_RH_Pct", H);
Particle.variable("d_CO2_ppm", CO2);
Particle.variable("d_TVOC_ppb", TVOC);
Particle.variable("s_Dampers", sDampersStatus);
Particle.function("T1S",TSetRoom1Day);
Particle.function("T2S",TSetRoom2Day);
Particle.function("T3S",TSetRoom3Day);
Particle.function("T4S",TSetRoom4);
Particle.function("D_Mode_Pos",sDampersMPdesired); //e.g. "4MC" = Damper 4, manual mode, close damper; "3a" = Damper 3, auto mode
mux.begin(); //I2C with 8 output channels (for all T/P/RH/CO2/TVOC), 5 used (0-4)
mux.setNoChannel(); //disables all channels 0-7
while(!myLog.begin(0x2A)){ //prepare OpenLog connection
delay(1000); // 1 s
}
Time.zone(-5); //ignore DST (separately deal with time/temperature variations throughout the year)
AddHeaderToLogFile(); //this will create/append a file name based on the date, and add the header to it (comma separated variables format)
myRelays.begin();
myRelays.allOff(); //wired for output relays off = damper open (dampers open if power failure to relay coil)
Damper1SelectedPosition = OPENED;
Damper2SelectedPosition = OPENED;
Damper3SelectedPosition = OPENED;
Damper4SelectedPosition = OPENED;
Damper1Mode = AUTO;
Damper2Mode = AUTO;
Damper3Mode = AUTO;
Damper4Mode = AUTO;
mux.setChannel(0); //enable Mux to Ch0 (location of BME280/CCS811 board and MCP9808 board in furnace plenum)
bme.settings.runMode = 0b11; // normal mode
bme.settings.tStandby = 0b101; // 1000 ms
bme.settings.filter = 0b000; // off
bme.settings.tempOverSample = 0b011; // x4
bme.settings.pressOverSample = 0b011; // x4
bme.settings.humidOverSample = 0b011; // x4
while(!bme.begin()){ //prepare bme connection (and I2C), typical is: mode=normal, sampling=x16, filter=off, standby=0.5 ms
delay(1000); // 1 s
}
while(!ccs.begin(0x5B)){ //prepare ccs connection (change default library address to 0x5B)
delay(1000); // 1 s
}
while(!mcp.begin(0x18)){ //it will take at least 250 ms before a temperature value is available
delay(1000); // 1 s
}
wd.checkin(); // resets the AWDT count (must occur every 60 min or less, or system will reset); not needed in setup?
mux.setChannel(1); //enable Mux to Ch1 (location of 1st remote MCP9808 board, bedroom 1)
while(!mcp1.begin(0x18)){ //it will take at least 250 ms before a temperature value is available
delay(1000); // 1 s
}
T1RoomSet = 23.0; // 23 degrees C (default room temperature)
wd.checkin(); // resets the AWDT count (must occur every 60 min or less, or system will reset); not needed in setup?
mux.setChannel(2); //enable Mux to Ch1 (location of 2nd remote MCP9808 board, bedroom 2)
while(!mcp2.begin()){ //it will take at least 250 ms before a temperature value is available
delay(1000); // 1 s
}
T2RoomSet = 23.0; // 23 degrees C (default room temperature)
wd.checkin(); // resets the AWDT count (must occur every 60 min or less, or system will reset); not needed in setup?
mux.setChannel(3); //enable Mux to Ch1 (location of 3rd remote MCP9808 board, bedroom 3)
while(!mcp3.begin()){ //it will take at least 250 ms before a temperature value is available
delay(1000); // 1 s
}
T3RoomSet = 23.0; // 23 degrees C (default room temperature)
wd.checkin(); // resets the AWDT count (must occur every 60 min or less, or system will reset); not needed in setup?
mux.setChannel(4); //enable Mux to Ch1 (location of 4th remote MCP9808 board, basement)
while(!mcp4.begin()){ //it will take at least 250 ms before a temperature value is available
delay(1000); // 1 s
}
T4RoomSet = 23.0; // 23 degrees C (default room temperature)
wd.checkin(); // resets the AWDT count (must occur every 60 min or less, or system will reset); not needed in setup?
mux.setNoChannel(); //disable Mux (leave Mux off when not measuring a temperature)
// initialize to some default values (needed until all timers have run)
T0 = 23.0; //these default values will keep all dampers initially open
T1 = 23.0;
T2 = 23.0;
T3 = 23.0;
T4 = 23.0;
lHour = Time.hour();
lMinute = Time.minute();
lLastMinute = lMinute;
timer1.start();
timer2.start();
wd.checkin(); // resets the AWDT count (must occur every 60 seconds or less, or system will reset); not needed in setup?
digitalWrite(boardLed, LOW); //setup completed so turn blue board LED off
}
void loop() { //open and close dampers based mainly on room temperatures (not all dampers can be closed, or too much back pressure on HVAC)
// Allow a max. of 3 dampers closed. If 4 dampers requested closed: In auto mode, force damper 4 open (basement). In manual mode, force damper 1 open (spare bedroom).
sDampersStatus = ""; //e.g. "A1onM2offA3offA4on" A = AUTO, M = MANUAL, on = OPENED, off = CLOSED
if (Damper1Mode == MANUAL) { //don't change damper position if in manual (only change from remote input), unless forced re below
if (Damper1SelectedPosition == OPENED) sDampersStatus += "M1on";
else sDampersStatus += "M1off";
} else {
sDampersStatus += "A1"; //Damper1Mode = AUTO
if (OpenDamper(ROOM1)) { //upstairs bedroom 1
Damper1SelectedPosition = OPENED;
sDampersStatus += "on";
myRelays.off(ROOM1); //open damper
} else {
Damper1SelectedPosition = CLOSED;
sDampersStatus += "off";
myRelays.on(ROOM1); //close damper
}
}
if (Damper2Mode == MANUAL) { //don't change damper position if in manual (only change from remote input)
if (Damper2SelectedPosition == OPENED) sDampersStatus += "M2on";
else sDampersStatus += "M2off";
} else {
sDampersStatus += "A2"; //Damper2Mode = AUTO
if (OpenDamper(ROOM2)) { //upstairs bedroom 2
Damper2SelectedPosition = OPENED;
sDampersStatus += "on";
myRelays.off(ROOM2); //open damper
} else {
Damper2SelectedPosition = CLOSED;
sDampersStatus += "off";
myRelays.on(ROOM2); //close damper
}
}
if (Damper3Mode == MANUAL) { //don't change damper position if in manual (only change from remote input)
if (Damper3SelectedPosition == OPENED) sDampersStatus += "M3on";
else sDampersStatus += "M3off";
} else {
sDampersStatus += "A3"; //Damper3Mode = AUTO
if (OpenDamper(ROOM3)) { //upstairs bedroom 3
Damper3SelectedPosition = OPENED;
sDampersStatus += "on";
myRelays.off(ROOM3); //open damper
} else {
Damper3SelectedPosition = CLOSED;
sDampersStatus += "off";
myRelays.on(ROOM3); //close damper
}
}
if (Damper4Mode == MANUAL) { //don't change damper position if in manual (only change from remote input)
if (Damper4SelectedPosition == OPENED) sDampersStatus += "M4on";
else sDampersStatus += "M4off";
} else {
sDampersStatus += "A4"; //Damper4Mode = AUTO
if (OpenDamper(ROOM4)) { //basement
Damper4SelectedPosition = OPENED;
sDampersStatus += "on";
myRelays.off(ROOM4); //open damper
} else {
Damper4SelectedPosition = CLOSED;
sDampersStatus += "off";
myRelays.on(ROOM4); //close damper
}
}
if ((Damper1SelectedPosition == CLOSED) && (Damper2SelectedPosition == CLOSED) && (Damper3SelectedPosition == CLOSED) && (Damper4SelectedPosition == CLOSED)) { // do something if all Dampers are selected to be closed
if (Damper4Mode == MANUAL) { //if Damper 4 was manually closed, leave it closed and force Damper 1 open
myRelays.off(ROOM1);
} else { //auto open Damper in Room 4 (basement) if upstairs rooms are closed
myRelays.off(ROOM4);
}
}
wd.checkin(); // resets the AWDT count (must occur every 60 seconds or less, or system will reset) [not needed here, see comment at end of loop]
delay(2000); // 2 s (no need to continuously run this loop)
} // AWDT count reset automatically after loop() ends
Comments