/** * * Thermostat * */ //#define DEBUG #include "EtherCard.h" //#include "Timer.h" #include //#include "RTClib.h" #include #include #include /** **************************************************** * EEPROM map * * 0 ID 0x99 * 1 T1 16 bit * 3 T2 16 bit * 5 T3 16 bit * 7 slot 7 * 16 bit * 14 8 * 8 * 8 bit weekly_program * 78 10 * 7 * 8 bit daily_program * 148 * * */ #define EEPROM_ID = 0x99 #define EEPROM_WEEKLY_BASE #define HTTP_PORT 80 #define BUFFER_SIZE 545 #define STR_BUFFER_SIZE 32 //please modify the following two lines. mac and ip have to be unique //in your local area network. You can not have the same numbers in //two devices: static uint8_t mymac[6] = {0x54,0x55,0x58,0x10,0x00,0x24}; static uint8_t myip[4] = {192,168,99,123}; // Used here and there... static char strbuf[STR_BUFFER_SIZE+1]; // Needed for prog_char PROGMEM //#include /** **************************************************** * * Main constants, all times in millis */ // Number of rooms #define ROOMS 5 #define WEEKLY_PROGRAM_NUMBER 10 #define DAILY_PROGRAM_NUMBER 8 #define SLOT_NUMBER 8 // Pins #define ONE_WIRE_PIN 9 #define PUMP_PIN 1 #define COLD_PIN 3 #define HOT_PIN 2 #define BEAT_PIN 8 #define ROOM_1_PIN 2 #define ROOM_2_PIN 3 #define ROOM_3_PIN 4 #define ROOM_4_PIN 5 #define ROOM_5_PIN 6 // TODO: move to config #ifdef DEBUG #include // Run FAST!!!! #define TEMP_READ_INTERVAL 4000 // millis #define VALVE_OPENING_TIME_S 10UL // 10 sec #define BLOCKED_TIME_S 60UL // 1 minute #define RISE_TEMP_TIME_S 30UL // 30 seconds #else #define TEMP_READ_INTERVAL 4000 // millis #define VALVE_OPENING_TIME_S 120UL // 2 minutes #define BLOCKED_TIME_S 3600UL // 1 hour #define RISE_TEMP_TIME_S 300UL // 5 minutes #endif #define HYSTERESIS 50 #define MAX_ALLOWED_T 2500 // in cents #define RISE_TEMP_DELTA 200 // Minimum difference // Room status #define OPENING 'V' // valves are opening for VALVE_OPENING_TIME #define CLOSED 'C' // Closed #define OPEN 'O' // Open (main pump is also open) #define BLOCKED 'B' // Blocked until BLOCKED_TIME is elapsed // Error codes #define ERR_NO 0 #define ERR_WRONG_COMMAND 1 #define ERR_WRONG_ROOM 2 #define ERR_WRONG_PROGRAM 3 #define ERR_WRONG_PARM 4 // Generic parameter error // Commands #define CMD_ROOM_SET_PGM 1 #define CMD_WRITE_EEPROM 2 #define CMD_TIME_SET 3 #define CMD_TEMPERATURE_SET 4 #define CMD_RESET 5 #define CMD_W_PGM_SET_D_PGM 6 #define CMD_D_PGM_SET_T_PGM 7 #define CMD_SLOT_SET_UPPER_BOUND 8 /****************************** * * EEPROM * */ //#include /** ************************************************ * * DS18B20 part * */ //#include //#include // Data wire is plugged into pin 6 on the Arduino #define ONE_WIRE_BUS ONE_WIRE_PIN // Setup a oneWire instance to communicate with any OneWire devices OneWire oneWire(ONE_WIRE_BUS); // Pass our oneWire reference to Dallas Temperature. DallasTemperature sensors(&oneWire); // Assign the addresses of your 1-Wire temp sensors. // See the tutorial on how to obtain these addresses: // http://www.hacktronics.com/Tutorials/arduino-1-wire-address-finder.html /** ************************************* * * Programs * */ // Global time (minutes from 0) uint32_t pump_blocked_time = 0; uint32_t pump_open_time = 0; byte this_weekday; byte last_error_code = ERR_NO; byte pump_open = 0; // Temperatures // TODO: configurable uint16_t T[] = {500, 1500, 1800, 2800}; uint16_t hot_temp, cold_temp; // Programs // 8 slots 6:30 8:00 12:00 13:00 16:00 20:00 22:00 uint16_t slot[SLOT_NUMBER - 1] = { 390, 480, 720, 780, 960, 1200, 1320 }; // 6 programs, T level for each slot/pgm tuple static byte daily_program[DAILY_PROGRAM_NUMBER][SLOT_NUMBER] = { //0:00 6:30 8:00 12:00 13:00 16:00 20:00 22:00 { 0, 0, 0, 0, 0, 0, 0, 0 }, // all T0 { 1, 1, 1, 1, 1, 1, 1, 1 }, // all T1 { 2, 2, 2, 2, 2, 2, 2, 2 }, // all T2 { 3, 3, 3, 3, 3, 3, 3, 3 }, // all T3 { 1, 3, 1, 1, 1, 3, 2, 1 }, // awakening supper and evening 4 { 1, 3, 1, 3, 1, 3, 2, 1 }, // awakening, meals and evening 5 { 1, 3, 1, 3, 3, 3, 2, 1 }, // awakening, meals, afternoon and evening 6 { 1, 3, 3, 3, 3, 3, 2, 1 }, // all day 7 }; // Weekly programs, 0 is monday static byte weekly_program[WEEKLY_PROGRAM_NUMBER][7] = { // Mo Tu Th We Fr Sa Su {0, 0, 0, 0, 0, 0, 0}, // always off {1, 1, 1, 1, 1, 1, 1}, // Always 1 {2, 2, 2, 2, 2, 2, 2}, // Always 2 {3, 3, 3, 3, 3, 3, 3}, // Always 3 {4, 4, 4, 4, 4, 7, 7}, // 4 (5+2) {4, 4, 4, 4, 4, 4, 7}, // 4 (6+1) {5, 5, 5, 5, 5, 7, 7}, // 5 (5+2) {5, 5, 5, 5, 5, 5, 7}, // 5 (6+1) {6, 6, 6, 6, 6, 7, 7}, // 6 (5+2) {6, 6, 6, 6, 6, 6, 7} // 6 (6+1) }; // Array of rooms static struct room_t { DeviceAddress address; byte pin; byte program; char status; int temperature; uint32_t last_status_change; } rooms[ROOMS] = { {{ 0x28, 0xAD, 0x4C, 0xC4, 0x03, 0x00, 0x00, 0x13}, ROOM_1_PIN, 3, CLOSED}, // 1 - Bagno {{ 0x28, 0x6C, 0x41, 0xC4, 0x03, 0x00, 0x00, 0x57}, ROOM_2_PIN, 8, CLOSED}, // 2 - Camera A {{ 0x28, 0x6C, 0x41, 0xC4, 0x03, 0x00, 0x00, 0x37}, ROOM_3_PIN, 8, CLOSED}, // - Sala {{ 0x28, 0x6C, 0x41, 0xC4, 0x03, 0x00, 0x00, 0x27}, ROOM_4_PIN, 8, CLOSED}, // - Camera O {{ 0x28, 0x6C, 0x41, 0xC4, 0x03, 0x00, 0x00, 0x67}, ROOM_5_PIN, 8, CLOSED} // - Camera P }; float get_desired_temperature(byte room, uint32_t this_time){ // Get slot byte _slot = 0; while(_slot <= 6 && this_time > slot[_slot]){ _slot++; } return T[daily_program[weekly_program[rooms[room].program][this_weekday]][_slot]]; } /** * Check temperatures and perform actions * * Here is the core logic fo the heating system: * * a global var pump_open controls the pin, when it changes * a global pump_open_time is set and used to determine when * to check for a T-delta on the hot and cold pipes sensors. * If the T-delta is lower than the threshold, then the system * is blocked and the timestamp stored in pump_blocked_time. * As the time passes pump_blocked_time + BLOCKED_TIME_S * the system is unblocked and everything is reset to try * another cycle. */ void check_temperatures(){ sensors.requestTemperatures(); // Read pump temp hot_temp = (uint16_t) (48.828125 * analogRead(HOT_PIN)); cold_temp = (uint16_t) (48.828125 * analogRead(COLD_PIN)); // Get Time now = RTC.now(); this_weekday = now.dayOfWeek(); // sunday is 0 this_weekday = this_weekday ? this_weekday - 1 : 6; // Check if can unlock if(pump_blocked_time && (now.unixtime() > pump_blocked_time + BLOCKED_TIME_S)){ pump_blocked_time = 0; } // If the pump is not blocked and has been open more than RISE_TEMP_TIME_S, // check hot_temp and cold_temp if(!pump_blocked_time && pump_open_time && (now.unixtime() - pump_open_time > RISE_TEMP_TIME_S)){ // Block if lower if( hot_temp - cold_temp < RISE_TEMP_DELTA ){ pump_blocked_time = now.unixtime(); pump_open = 0; } } // Local flags to check if any room needs_heating and // is ready to open the pump. byte needs_pump_open = 0; byte needs_heating = 0; for(int i=0; i= 0) ? r * 10 + (*buf_p - 0x30) : (*buf_p - 0x30); buf_p++; } } return r; } /** * Standard header */ void print_200ok(){ bfill.emit_p(PSTR("HTTP/1.0 200 OK\r\nContent-Type: text/html; charset=UTF-8\r\n\r\n")); } /** * Main home page */ void print_homepage(){ print_200ok(); bfill.emit_p(PSTR("")); bfill.emit_p(PSTR("

EtherShieldThermo

")); bfill.emit_p(PSTR("")); bfill.emit_p(PSTR("")); } /** * Int to float to string */ char* decimal_string(int num, char* _buf){ char buf3[3]; itoa(num/100, _buf, 10); strcat(_buf, "."); itoa(num%100, buf3, 10); strcat(_buf, buf3); return _buf; } /** * Json helpers */ void bracket_open(){ return bfill.emit_p( PSTR("[")); } void bracket_close(){ return bfill.emit_p( PSTR("]")); } void json_array_wrap(uint16_t p[], int length){ bracket_open(); for(int i = 0; i < length; i++){ decimal_string(p[i], strbuf); bfill.emit_raw(strbuf, strlen(strbuf)); if(i 0){ // Switch? switch(cmd){ case CMD_ROOM_SET_PGM: parm1 = analyse_cmd(data, "p"); if(in_range(parm1, 0, ROOMS - 1)){ parm2 = analyse_cmd(data, "v"); if(in_range(parm2, 0, WEEKLY_PROGRAM_NUMBER - 1 )){ rooms[parm1].program = parm2; } else { last_error_code = ERR_WRONG_PROGRAM; } } else { last_error_code = ERR_WRONG_ROOM; } break; case CMD_WRITE_EEPROM: // Write to EEPROM break; case CMD_W_PGM_SET_D_PGM: parm1 = analyse_cmd(data, "p"); if(in_range(parm1, 0, WEEKLY_PROGRAM_NUMBER - 1)){ parm2 = analyse_cmd(data, "v"); // dayOfWeek if(in_range(parm2, 0, 6)){ parm3 = analyse_cmd(data, "v"); if(in_range(parm3, 0, DAILY_PROGRAM_NUMBER - 1)){ weekly_program[parm1][parm2] = parm3; } else { last_error_code = ERR_WRONG_PARM; } } else { last_error_code = ERR_WRONG_PARM; } } else { last_error_code = ERR_WRONG_PARM; } break; case CMD_D_PGM_SET_T_PGM: // Write to EEPROM break; case CMD_SLOT_SET_UPPER_BOUND: // Write to EEPROM break; case CMD_TEMPERATURE_SET: // Set T1, T2 and T3 parm1 = analyse_cmd(data, "p"); if(in_range(parm2, 1, 3)){ last_error_code = ERR_WRONG_PARM; } else { parm2 = analyse_cmd(data, "v"); switch(parm1){ case 1: if(in_range(parm2, T[0] + 50, T[2] - 50)){ T[1] = parm2; } else { last_error_code = ERR_WRONG_PARM; } break; case 2: if(in_range(parm2, T[1] + 50, T[3] - 50)){ T[2] = parm2; } else { last_error_code = ERR_WRONG_PARM; } break; case 3: if(in_range(parm2, T[2] + 50, MAX_ALLOWED_T )){ T[3] = parm2; } else { last_error_code = ERR_WRONG_PARM; } break; } } break; case CMD_TIME_SET: parm1 = analyse_cmd(data, "p"); if(parm1 < 0 || parm1 > 5){ // 0 = hh, 1 = mm, 2 = ss, 3 = Y, 4 = m, 5 = d last_error_code = ERR_WRONG_PARM; } else { now = RTC.now(); parm2 = analyse_cmd(data, "v"); switch(parm1){ case 0: RTC.adjust(DateTime(now.year(), now.month(), now.day(), parm2, now.minute(), now.second())); break; case 1: RTC.adjust(DateTime(now.year(), now.month(), now.day(), now.hour(), parm2, now.second())); break; case 2: RTC.adjust(DateTime(now.year(), now.month(), now.day(), now.hour(), now.minute(), parm2)); break; case 3: RTC.adjust(DateTime(parm2, now.month(), now.day(), now.hour(), now.minute(), now.second())); break; case 4: RTC.adjust(DateTime(now.year(), parm2, now.day(), now.hour(), now.minute(), now.second())); break; case 5: RTC.adjust(DateTime(now.year(), now.month(), parm2, now.hour(), now.minute(), now.second())); break; default: last_error_code = ERR_WRONG_PARM; } } break; default: last_error_code = ERR_WRONG_COMMAND; } } print_json_response(0); last_error_code = ERR_NO; SENDTCP:ether.httpServerReply(bfill.position()); } // Thermo thermo_loop(); }