#include "Adafruit_MCP9808.h"
SYSTEM_THREAD(ENABLED); // NOTE: This seems to let the cloud Publish work better.
// STARTUP(System.enableFeature(FEATURE_RETAINED_MEMORY)); // This lets variables be saved during Deep Sleep (not used).
/*
void pnprintf(Print& p, uint16_t bufsize, const char* fmt, ...) {
char buff[bufsize];
va_list args;
va_start(args, fmt);
vsnprintf(buff, bufsize, fmt, args);
va_end(args);
p.println(buff);
}
*/
//#define pprintf(p, fmt, ...) pnprintf(p, 128, fmt, __VA_ARGS__)
//#define Serial_printf(fmt, ...) pprintf(Serial, fmt, __VA_ARGS__)
#define _OFF "0"
#define _ON "1"
Adafruit_MCP9808 tempsensor = Adafruit_MCP9808(); // Create the MCP9808 temperature sensor object
const int T_NUMBERS_SIZE = 3; // number of samples in the running average temperature buffer "numbers".
const int B_NUMBERS_SIZE = 5; // Number of battery voltage samples for running average. Rather jumpy for unknown reasons.
const int RUN_DELAY = 10; // Seconds between updates. 10 is 6 samples per minute.
const float START_TEMP = 999.0;
const int boardLed = D7; // This is the LED that is already on your Photon device.
const int powerPin = 0; // Analog input pin - sensor for Power
const int batteryPin = 1; // Analog input pin = sensor for battery voltage
const int EMAIL_LIMIT = 3; // The device will send this many emails until the temperature rises above the "too_cold".
const int SAMPLES = 8640; //1440 minutes per day * 6 samples per minute.
const unsigned long ONE_DAY_MILLIS = (24 * 60 * 60 * 1000);
const float voltageConversion = 5.616/4000; // converts the analogRead value to battery voltage
const float BATTERY_SLEEP_V = 4.8; // Put the unit to sleep if battery drops below this value.
const int STANDARD_DELAY = 1 * 30; // Delays for alarms, email and awakening from sleeping.
float batteryF; // The real battery voltage after conversion from the analogRead() value.
float temp_numbers[T_NUMBERS_SIZE]; // Running average temperature buffer, FIFO.
int battery_numbers[B_NUMBERS_SIZE]; // running average battery voltage (A to D converter returns 0-4095).
float c; // Centigrade direct from sensor.
float f; // Farenheight, calculated each loop.
float c_sum; // Summed each loop.
int battery_sum; // Set in loop().
int battery_average; // Calculated in loop().
float c_average_temp; // Calculated in loop().
float previous_c; // See setup().
int i = 0; // The basic loop counter. Initialized here once.
int tempAlarmCounter = 0; // Count warning emails.
int too_cold = 50; // This is the default alarm temperature. // This value is overridden by the EPROM value.
int powerVoltage; // This is the voltage on the power Pin. Always read.
int batteryAnalog = 0; // This is the battery voltage ( A number 0 to 4095 from the analogRead() )
float previousBatteryF; // The actual previous battery voltage. // Reset at setup() to trigger an update after startup.
int powerOffCounter = 0; // Counts power off emails. Used in loop().
String ds = "Startup"; // While running ds is the formatted version of now().
String powerFail_ds; // Save the power fail time for the sleeping notice.
String alarm_email_status = "No alarms sent."; // Reset as needed when too cold.
String low_temp_message = "No low temperature data yet."; // Reset in loop() as needed.
String string_too_cold; // Reset by setup() and HTML button actions.
char powerMessage[100] = "Power Off when starting up."; // Unlikely, real power message made in loop().
char powerState[100] = "0"; //Using this string as a binary 0 or 1. To be Published as the power alarm trigger.
char f_buffer[100]= "Default temperature"; // Recalculated at, and after startup.
char v_buffer[100]= "Default Battery Voltage"; // current battery voltage, or last voltage while sleeping.
time_t now; // current unix time in seconds.
time_t next = 0; // Used to set elapsed time between emails and alarms.
time_t powerOffTime; // Time of day the power failed.
bool daylightSavings; // Daylight savings time is checked occasionally.
bool sensorActive; // Determine at setup() if the temperature sensor is available.
bool powerIsOn = false; // Current AC power state. Assumed Off at startup.
bool tempIsCold = false; // Current temperature state. Assumed OK at startup.
bool tempWasCold = false; // Temperature dropped below threshold in the recent past.
bool powerWasOn = false; // Indicates if the power had been on in the past.
bool firmFlag = true; // Prevents multiple firmware notices from being sent during updates.
float lowest_24hour_temp = START_TEMP; // Initialized once
unsigned long lastSync; // Used to trigger the daily time sincronization.
void setup() {
// register the reset handler
System.on(reset, reset_handler);
// listen for Firmware Update events
System.on(firmware_update, handle_all_the_events);
string_too_cold = String(too_cold);
pinMode(boardLed,OUTPUT); // Our on-board LED is output as well
digitalWrite(boardLed, HIGH);
pinMode(powerPin, INPUT);
pinMode(batteryPin, INPUT);
Particle.syncTime(); //set the internal clock
bool daylightSavings = IsDST(Time.day(), Time.month(), Time.weekday());
Time.zone(daylightSavings? -6 : -7); // -7 for Mountain Standard Time, -6 for DST
lastSync = millis();
// Give variables an 12 charaacter - or less - "Eventname" and expose the variable and poll the photon with a
// "request" call like this in Python:
// r = requests.get('https://api.particle.io/v1/devices/LonesomeRidge/MY_I_value?access_token=6f524226dad934d50080ca0602da44d451777e8a')
// Using more than 12 characters for the variable name results in "build couldn't produce binary..."
Particle.variable( "This_Is_J" , f_buffer ); // or publish if desired like so: Particle.publish("This_Is_J",String(j)j,60,PRIVATE);
Particle.variable( "This_Is_LowT" , low_temp_message ); // low_temp_message is pulled by json code, not published.
Particle.variable( "This_Is_Bad" , alarm_email_status );
Particle.variable( "MY_trigger" , string_too_cold );
Particle.variable( "Power_State" , powerState );
Particle.variable( "Power_Mess" , powerMessage ); // powerMessage is pulled by jason code, not published.
Particle.variable( "Battery_Volt" , v_buffer );
Particle.function( "Manual_Test" , manualTest ); // Functions are polled, not published
Particle.function( "triggerUp" , raiseTooCold );
Particle.function( "triggerDn" , lowerTooCold );
delay(1000);
digitalWrite(boardLed, LOW);
while ( !Particle.publish("Power_State", "Unknown" ) )
{ // repeat Publish until the cloud is connected
delay(300);
digitalWrite(boardLed, HIGH);
delay(300);
digitalWrite(boardLed, LOW);
}
while (analogRead(powerPin) < 1000) {
rapidBlink(3);
strcpy(powerMessage, "Power not available at startup.");
strcpy(powerState, _OFF);
//Particle.publish("Power_State", powerState );
delay(2000);
} // dilly until power comes on.
powerWasOn = true;
powerIsOn = true;
powerOffCounter = 0;
strcpy(powerMessage, "Power on at startup.");
strcpy(powerState, _ON);
//Particle.publish("Power_State", powerState );
//delay(2000);
// setup for temperature sensor
// Make sure the sensor is found, you can also pass in a different i2c
// address with tempsensor.begin(0x19) for example
if (!tempsensor.begin()) {
Serial.println("Couldn't find MCP9808 tempsensor!");
Particle.publish("This_Is_J", "Couldn't find MCP9808 tempsensor!");
sensorActive = false;
} else {
Serial.println("Found our sensor.");
Particle.publish("This_Is_J", "Found our sensor.");
delay(2000);
sensorActive = true;
previous_c = START_TEMP; // After sleeping or start, use wild number to compare to real number.
c = tempsensor.readTempC();
for (int n = 0; n < T_NUMBERS_SIZE; n++) { // preload the running average temperature buffer
temp_numbers[n] = c;
}
}
batteryAnalog = analogRead(batteryPin); // read the battery voltage at the pin
for (int l = 0; l < B_NUMBERS_SIZE; l++){ // preload the voltage average numbers so the first loop makes sense
battery_numbers[l] = batteryAnalog;
}
batteryF = voltageConversion * batteryAnalog;
previousBatteryF = -999.0; // Set to wild value so the loop will do an initial update.
sprintf(v_buffer, "Battery volts = %.2f at startup.", batteryF); // Format to 2 decimal places
Particle.publish("Battery_Volt", v_buffer); // to the cloud...
delay(5000);
Serial.println("Running...");
}
void loop() {
firmFlag = true; // lets a single firmware publish notice be sent to the cloud.
// sync once per day
if (millis() - lastSync > ONE_DAY_MILLIS) {
// Request time synchronization from the Particle Cloud
Particle.syncTime();
lastSync = millis();
}
daylightSavings = IsDST(Time.day(), Time.month(), Time.weekday());
Time.zone(daylightSavings? -6 : -7); // -7 for Mountain Standard Time, -6 for DST
now = Time.now();
ds = Time.format(now, "%h. %d, %Y %I:%M %p"); // Standard C++ time formatting.
// Get and average the backup battery voltage.
batteryAnalog = analogRead(batteryPin);
battery_numbers[0] = batteryAnalog;
// average the readings
battery_sum = 0;
for (int l = 0; l < B_NUMBERS_SIZE; l++) {
battery_sum += battery_numbers[l];
}
battery_average = battery_sum/B_NUMBERS_SIZE;
batteryF = voltageConversion * battery_average;// Convert the analog pin number to a real voltage.
if ( abs(previousBatteryF - batteryF) > 0.02 ) { // have a change worth publishing
sprintf(v_buffer, "%.2f V at loop = %d", batteryF, i); // Format to 2 decimal places
previousBatteryF = batteryF; // save it for the next loop.
}
for (int m = B_NUMBERS_SIZE - 1; m > 0; m--){ // Drop the oldest reading
battery_numbers[m] = battery_numbers[m-1]; // "shift right"
}
// Check the AC power is on.
powerIsOn = analogRead(powerPin) > 1000;
if ( !powerIsOn ) { // power is off currently
// if ( powerOffCounter == 0 && powerWasOn) { // This is a new report of powewr fail
if (powerWasOn) { // This is a new report of powewr fail
powerWasOn = false; // Remember that we have experienced a power failure so this block only executed once.
powerOffTime = Time.now();
alarm_email_status = "Power alarm will be sent shortly.";
powerFail_ds = ds; // Save the human-readable power fail time & date.
strcpy( powerMessage, "Lost power at " +powerFail_ds ); // date and time in human readable form.
strcpy(powerState, _OFF); // Available immediately to the browser HTML but not published yet (no email, phonecalls yet).
}
if ( (powerOffCounter < EMAIL_LIMIT) && ( (Time.now() - powerOffTime) >= STANDARD_DELAY) ) { // Power has been off for X minutes
powerOffCounter++; // This will limit the emails-phonecalls to 3
powerOffTime = Time.now(); // Reset the time so another X minutes has to pass for a new alarm.
alarm_email_status = "Power Fail alarm sent! (" +String(powerOffCounter) +")"; // Mark highlights the text with colors.
Particle.publish("Power_State", powerState); // This will trigger email and/or phonecalls from IFTTT
delay(1000); // pause to publish
// Serial.printf("Power off counter=%d ", powerOffCounter);
// Serial.printf( powerMessage );
}
} else if ( !powerWasOn ){ // power is on now but was off previously. Reset counter, powerOff flag, offTime and messages
if ( !tempWasCold ) { alarm_email_status = "Power Restored."; }
if ( tempWasCold ) { alarm_email_status = "Low Temperature Danger Notice."; }
strcpy(powerMessage, "Power restored at " +ds);
strcpy(powerState, _ON);
powerWasOn = true;
powerOffCounter = 0;
}
if ( (!powerIsOn) && (powerOffCounter >= EMAIL_LIMIT) ) { // power is off and alarms have been sent.
if (batteryF < BATTERY_SLEEP_V) { // If battery is too low, reduce the load: Shut down the wifi radio and this application.
sprintf(v_buffer, "%.2f V at loop = %d", batteryF, i ); // not necessary to publish, will be
// pulled by the browser HTML. User already knows about the power being off.
sprintf(powerMessage, "Power failed at " +powerFail_ds +". Battery low. Sleeping to reduce battery drain.");
digitalWrite(boardLed, LOW); // Just in case, don't want any extra battery drain.
delay(10000); // pause a long time to let the HTML do a pull update before going to sleep.
System.sleep(WKP, RISING, STANDARD_DELAY); // Awake on power rising OR after a few minutes to check the temperature in the main loop
c = tempsensor.readTempC(); // Get a new temperature reading
for (int n = 0; n < T_NUMBERS_SIZE; n++) { // Reload the running average temperature buffer because it is out of date.
temp_numbers[n] = c;
}
previous_c = 999.0; // Will force a publish of temperature.
next = 0; // also needed to publish a new temperature warning if necessary.
batteryAnalog = analogRead(batteryPin); // Get a mew battery voltage.
for (int l = 0; l < B_NUMBERS_SIZE; l++){ // Reload the buffer because it is out of date.
battery_numbers[l] = batteryAnalog;
}
batteryF = voltageConversion * batteryAnalog;
sprintf(v_buffer, "%.2f V at loop = %d", batteryF, i); // Format to 2 decimal places
while ( !Particle.publish("Battery_Volt", v_buffer ) )
{ // repeat Publish until the cloud is connected
delay(500);
digitalWrite(boardLed, LOW);
delay(100);
digitalWrite(boardLed, HIGH);
}
} else { // battery is still OK
sprintf(powerMessage, "Power failed at " +powerFail_ds +". Battery OK");
if ( tempAlarmCounter == 0 ) { alarm_email_status = "3 Power Fail emails were sent!"; }
if ( tempAlarmCounter >= EMAIL_LIMIT ) { alarm_email_status = "3 Power Fail emails were sent!
3 Low Temperature Danger emails were sent!"; }
}
}
// Get and publish the current temperature, if the sensor is active.
if (sensorActive)
{
c = tempsensor.readTempC();
if ( fabs(c - previous_c) > 0.125 ) // don't do anything until the temperature changes by double the precision
{
previous_c = c;
temp_numbers[0] = c;
// average the readings
c_sum = 0.0;
for (int l = 0; l < T_NUMBERS_SIZE; l++) {
c_sum += temp_numbers[l];
}
c_average_temp = c_sum/T_NUMBERS_SIZE;
f = c_average_temp * 9.0 / 5.0 + 32.0;
sprintf(f_buffer, "%.1f°F", f ); // round internally to one decimal place.
Particle.publish("This_Is_J", f_buffer, 60,PRIVATE); // Push it to update browser
delay(1000);
// Serial.printf("I loop=%d", i); Serial.printf(" Too Cold=%d°F C_temp=%.2f°C F_avg=%s ", too_cold, c, f_buffer );
// Serial.printf(" powerState=%s \n", powerState);
for (int m = T_NUMBERS_SIZE - 1; m > 0; m--){ // Drop the oldest reading
temp_numbers[m] = temp_numbers[m-1]; // "shift right"
}
} // End of significant change in temp noted.
} // End of temp sensor Active == true
tempIsCold = f < too_cold;
if ( tempIsCold && ( tempAlarmCounter < EMAIL_LIMIT && (now > next) ) )
{
tempWasCold = true;
next = now + ( STANDARD_DELAY ); // Next alarm and email will be sent after X minutes.
tempAlarmCounter++;
alarm_email_status = "Low Temperature Danger Notice.
Temperature is " +String(f_buffer) +" at " +ds +" emails sent = " +String(tempAlarmCounter) +"";
Particle.publish("This_Is_Bad", alarm_email_status , 60,PRIVATE);
delay(2000); // pause to publish
// Serial.printf("BAD, mails=%d Now=", tempAlarmCounter );
// Serial.println(now);
rapidBlink(5);
}
// Next, if the alarm was triggered sending email, and the temp has since risen a reasonable amount, reset everything.
if ( tempWasCold && (f >= (too_cold+5)) ) {
tempAlarmCounter = 0;
next = 0;
tempWasCold = false;
alarm_email_status = "Low Temp alarm reset at " + ds;
}
// Make a note of the lowest temperature.
if ( f < lowest_24hour_temp ) {
lowest_24hour_temp = f;
low_temp_message = String(f_buffer) +" at " +ds;
}
// 24 hour reset of lowest temperature, reset the loop counter,
if ( ++i >= SAMPLES || (i < 0) ) { i = 0; lowest_24hour_temp = START_TEMP; low_temp_message = String("Low temp reset"); next = 0; }
waste_time(RUN_DELAY); // Has very short blinks and one long blink.
} // End of LOOP
int manualTest(String command){ // Parameter String command is required by the compiler for Spark.functions
Particle.publish("This_Is_Bad", "
Danger Notice.
Manual test only!" , 60,PRIVATE);
rapidBlink(5);
delay(2000); // pause to publish
}
int raiseTooCold(String command) { rapidBlink(12); too_cold += 5; string_too_cold = String(too_cold); return(too_cold); } // respond to the trigger_up, _down buttons
int lowerTooCold(String command) { rapidBlink(12); too_cold -= 5; string_too_cold = String(too_cold); return(too_cold); }
/* Decided against using "permanent" flash storage for too_cold. Leaving it here as FYI...
int raiseTooCold(String command) { int ret = eeprom_write(too_cold+5); return(ret); }
int lowerTooCold(String command) { int ret = eeprom_write(too_cold-5); return(ret); }
int eeprom_write(int tc) { rapidBlink(12); EEPROM.put(0, tc); delay(100); int new_alarm_temp = alarm_temp_update(); return(new_alarm_temp); }
int alarm_temp_update() { // Modifies the global variable too_cold and string_too_cold
EEPROM.get( 0, too_cold ); // Global variable reset here.
delay(50);
Serial.print("Too Cold reset to: "); Serial.println(too_cold);
string_too_cold = String(too_cold); // Global value reset and pulled by HTML "get".
return(too_cold); // return value ignored.
}
*/
void waste_time(int seconds_delay) {
digitalWrite(boardLed, LOW);
for ( int i = 1; i < seconds_delay; i++ ) {
digitalWrite(boardLed, HIGH);
delay(5);
digitalWrite(boardLed, LOW);
delay(995); // 1000 milliseconds total
}
// Longer blink every pass through the loop.
digitalWrite(boardLed, HIGH);
delay(1000);
return;
}
void rapidBlink(int b){
for ( int i = 0; i < b; i++) {
digitalWrite(boardLed, HIGH);
delay(40);
digitalWrite(boardLed, LOW);
delay(60);
}
digitalWrite(boardLed, LOW);
}
bool IsDST(int dayOfMonth, int month, int dayOfWeek) // North American values
{
if (month < 3 || month > 11)
{
return false;
}
if (month > 3 && month < 11)
{
return true;
}
int previousSunday = dayOfMonth - dayOfWeek;
if (month == 3)
{
return previousSunday >= 8;
}
return previousSunday <= 0;
}
void reset_handler()
{
// turn off the crankenspitzen
// digitalWrite(D6, LOW);
// tell the world what we are doing
Particle.publish("reset", "going down for reboot NOW!");
}
void handle_all_the_events(system_event_t event, int param)
{
//Serial.printlnf("Got event %d with value %d", event, param);
if ( firmFlag ) {
Particle.publish("firmware", "Firmware Update");
firmFlag = false; // Flag prevents dozens of notices being published.
}
}