├── 3D ├── BackDeepM2.stl ├── BackM2.stl ├── Encoder Wheel.stl ├── Faceplate.stl ├── FaceplateM2.stl ├── Remote back.stl ├── RmtBackM2.stl ├── Thermostat back.stl ├── ThermostatBack_AM2302.stl ├── ThermostatBack_AM2320.stl ├── ThermostatBack_AM2322.stl └── ThermostatBack_SHT21.stl ├── Arduino ├── Encoder.cpp ├── Encoder.h ├── HVAC.cpp ├── HVAC.h ├── Nextion.cpp ├── Nextion.h ├── RunningMedian.h ├── Thermostat.ino ├── WebHandler.cpp ├── WebHandler.h ├── data │ ├── chart.html │ ├── index.html │ ├── settings.html │ └── styles.css ├── display.cpp ├── display.h ├── eeMem.cpp ├── eeMem.h ├── forecast.cpp ├── forecast.h ├── jsonString.h ├── music.cpp ├── music.h └── pages.h ├── Eagle ├── Thermostat.brd ├── Thermostat.sch ├── bom.txt └── eagle.epf ├── EagleESP32 ├── Thermostat.brd ├── Thermostat.sch ├── bom.txt └── eagle.epf ├── EagleNew ├── Thermostat.brd ├── Thermostat.sch ├── bom.txt └── eagle.epf ├── HvacRemote.js ├── LICENSE ├── Libraries ├── AM2320 │ ├── AM2320.cpp │ └── AM2320.h ├── JsonClient │ ├── JsonClient.cpp │ ├── JsonClient.h │ └── keywords.txt ├── JsonParse │ ├── JsonParse.cpp │ ├── JsonParse.h │ └── keywords.txt ├── SHT21 │ ├── SHT21.cpp │ ├── SHT21.h │ └── keywords.txt └── XMLReader │ ├── XMLReader.cpp │ ├── XMLReader.h │ └── keywords.txt ├── README.md ├── RMT.js ├── RemoteSensor ├── BasicSensor.cpp ├── BasicSensor.h ├── RunningMedian.h ├── Sensor.ino ├── defs.h ├── eeMem.cpp ├── eeMem.h ├── jsonString.h ├── pages.h ├── tempArray.cpp ├── tempArray.h ├── tuya.cpp └── tuya.h └── Thermostat.HMI /3D/BackDeepM2.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CuriousTech/ESP-HVAC/4e871605f25c728640b037e73a122562fc7b20c2/3D/BackDeepM2.stl -------------------------------------------------------------------------------- /3D/BackM2.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CuriousTech/ESP-HVAC/4e871605f25c728640b037e73a122562fc7b20c2/3D/BackM2.stl -------------------------------------------------------------------------------- /3D/Encoder Wheel.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CuriousTech/ESP-HVAC/4e871605f25c728640b037e73a122562fc7b20c2/3D/Encoder Wheel.stl -------------------------------------------------------------------------------- /3D/Faceplate.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CuriousTech/ESP-HVAC/4e871605f25c728640b037e73a122562fc7b20c2/3D/Faceplate.stl -------------------------------------------------------------------------------- /3D/FaceplateM2.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CuriousTech/ESP-HVAC/4e871605f25c728640b037e73a122562fc7b20c2/3D/FaceplateM2.stl -------------------------------------------------------------------------------- /3D/Remote back.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CuriousTech/ESP-HVAC/4e871605f25c728640b037e73a122562fc7b20c2/3D/Remote back.stl -------------------------------------------------------------------------------- /3D/RmtBackM2.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CuriousTech/ESP-HVAC/4e871605f25c728640b037e73a122562fc7b20c2/3D/RmtBackM2.stl -------------------------------------------------------------------------------- /3D/Thermostat back.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CuriousTech/ESP-HVAC/4e871605f25c728640b037e73a122562fc7b20c2/3D/Thermostat back.stl -------------------------------------------------------------------------------- /3D/ThermostatBack_AM2302.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CuriousTech/ESP-HVAC/4e871605f25c728640b037e73a122562fc7b20c2/3D/ThermostatBack_AM2302.stl -------------------------------------------------------------------------------- /3D/ThermostatBack_AM2320.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CuriousTech/ESP-HVAC/4e871605f25c728640b037e73a122562fc7b20c2/3D/ThermostatBack_AM2320.stl -------------------------------------------------------------------------------- /3D/ThermostatBack_AM2322.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CuriousTech/ESP-HVAC/4e871605f25c728640b037e73a122562fc7b20c2/3D/ThermostatBack_AM2322.stl -------------------------------------------------------------------------------- /3D/ThermostatBack_SHT21.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CuriousTech/ESP-HVAC/4e871605f25c728640b037e73a122562fc7b20c2/3D/ThermostatBack_SHT21.stl -------------------------------------------------------------------------------- /Arduino/Encoder.cpp: -------------------------------------------------------------------------------- 1 | #include "Encoder.h" 2 | 3 | Encoder::Encoder(int8_t aPin, int8_t bPin, void (*callback)()) 4 | { 5 | pinMode(aPin, INPUT_PULLUP); 6 | pinMode(bPin, INPUT_PULLUP); 7 | 8 | m_aPin = aPin; 9 | m_bPin = bPin; 10 | 11 | m_lastA = digitalRead(aPin); 12 | attachInterrupt(aPin, callback, FALLING); 13 | } 14 | 15 | Encoder::Encoder(int8_t aPin, int8_t bPin) 16 | { 17 | pinMode(aPin, INPUT_PULLUP); 18 | pinMode(bPin, INPUT_PULLUP); 19 | 20 | m_aPin = aPin; 21 | m_bPin = bPin; 22 | 23 | m_lastA = digitalRead(aPin); 24 | } 25 | 26 | int Encoder::poll() 27 | { 28 | int8_t A = digitalRead(m_aPin); 29 | 30 | m_dir = 0; 31 | 32 | if(!A && A != m_lastA) // simulate falling edge of A 33 | { 34 | m_ticks ++; 35 | m_dir = digitalRead(m_bPin) ? -1 : 1; 36 | } 37 | 38 | m_lastA = A; 39 | return m_dir; 40 | } 41 | 42 | // get changes 43 | int Encoder::read() 44 | { 45 | if(m_ticks == 0) 46 | return 0; 47 | int n = m_dir * m_ticks; 48 | m_ticks = 0; 49 | return n; 50 | } 51 | 52 | // isr (falling edge of A) 53 | void Encoder::isr() 54 | { 55 | m_ticks++; 56 | m_dir = digitalRead(m_bPin) ? -1 : 1; 57 | } 58 | -------------------------------------------------------------------------------- /Arduino/Encoder.h: -------------------------------------------------------------------------------- 1 | #ifndef ENCODER_H 2 | #define ENCODER_H 3 | #include 4 | 5 | class Encoder 6 | { 7 | public: 8 | Encoder(int8_t aPin, int8_t bPin, void (*callbackr)()); 9 | Encoder(int8_t aPin, int8_t bPin); 10 | int poll(void); 11 | int read(void); 12 | void isr(void); 13 | private: 14 | void (*isrCallback)(void); 15 | volatile int8_t m_ticks; 16 | volatile int8_t m_dir; 17 | volatile int8_t m_aPin, m_bPin; 18 | int8_t m_lastA; 19 | }; 20 | 21 | #endif // ENCODER_H 22 | -------------------------------------------------------------------------------- /Arduino/HVAC.h: -------------------------------------------------------------------------------- 1 | #ifndef HVAC_H 2 | #define HVAC_H 3 | 4 | // Uncomment to build remote 5 | //#define REMOTE 6 | 7 | #define HOSTNAME "HVAC" 8 | #define RMTNAME 0x31544d52 // 1TMR reversed sensor ID 9 | #define RMTNAMEFULL "HVACRemote1" 10 | 11 | #ifdef ESP8266 12 | #include 13 | #endif 14 | 15 | // HVAC Control 16 | //---------------- 17 | #ifdef ESP32 18 | #define P_FAN 33 // G GPIO for SSRs 19 | #define P_COOL 25 // Y 20 | #define P_REV 26 // O 21 | #define P_HEAT 14 // W 22 | #define P_HUMID 27 // H 23 | #define USE_AUDIO 24 | #define SPEAKER 17 25 | #define FAN_ON LOW 26 | #define FAN_OFF HIGH 27 | #define COOL_ON LOW 28 | #define COOL_OFF HIGH 29 | #define HEAT_ON LOW 30 | #define HEAT_OFF HIGH 31 | #define REV_ON LOW 32 | #define REV_OFF HIGH 33 | #define HUMID_ON LOW 34 | #define HUMID_OFF HIGH 35 | #else 36 | #define P_FAN 16 // G GPIO for SSRs 37 | #define P_COOL 14 // Y 38 | #define P_REV 12 // O 39 | #define P_HEAT 15 // W 40 | #define P_HUMID 0 // H 41 | #define FAN_ON HIGH 42 | #define FAN_OFF LOW 43 | #define COOL_ON HIGH 44 | #define COOL_OFF LOW 45 | #define HEAT_ON HIGH 46 | #define HEAT_OFF LOW 47 | #define REV_ON HIGH 48 | #define REV_OFF LOW 49 | #define HUMID_ON LOW 50 | #define HUMID_OFF HIGH 51 | #endif 52 | //----------------- 53 | #include 54 | 55 | enum Mode 56 | { 57 | Mode_Off, 58 | Mode_Cool, 59 | Mode_Heat, 60 | Mode_Auto, 61 | Mode_Fan, 62 | Mode_Humid 63 | }; 64 | 65 | enum FanMode 66 | { 67 | FM_Auto, 68 | FM_On 69 | }; 70 | 71 | enum Notif 72 | { 73 | Note_None, 74 | Note_Connecting, 75 | Note_Connected, 76 | Note_RemoteOff, 77 | Note_RemoteOn, 78 | Note_CycleLimit, 79 | Note_Network, // Sound errors below this point 80 | Note_Forecast, 81 | Note_Filter, 82 | Note_EspTouch, 83 | Note_Found, 84 | }; 85 | 86 | enum HeatMode 87 | { 88 | Heat_HP, 89 | Heat_NG, 90 | Heat_Auto 91 | }; 92 | 93 | enum State 94 | { 95 | State_Off, 96 | State_Cool, 97 | State_HP, 98 | State_NG 99 | }; 100 | 101 | enum HumidifierMode 102 | { 103 | HM_Off, 104 | HM_Fan, 105 | HM_Run, 106 | HM_Auto1, 107 | HM_Auto2, 108 | }; 109 | 110 | enum ScheduleMode 111 | { 112 | SM_Forecast, 113 | SM_Sine, 114 | SM_Flat, 115 | SM_Reserved 116 | }; 117 | 118 | struct sensorFlags 119 | { 120 | uint32_t Priority:1; 121 | uint32_t Enabled:1; 122 | uint32_t Weight:6; 123 | uint32_t currWeight:6; 124 | uint32_t Reserved:17; 125 | uint32_t Warn:1; 126 | }; 127 | 128 | union usensorFlags 129 | { 130 | uint32_t val; 131 | sensorFlags f; 132 | }; 133 | 134 | struct Sensor 135 | { 136 | uint32_t tm; 137 | uint32_t timer; // seconds, priority timer 138 | uint32_t timerStart; 139 | uint32_t IP; // 140 | usensorFlags f; 141 | int16_t temp; 142 | uint16_t rh; 143 | uint32_t ID; // hex text? 144 | uint8_t pad; // NULL for ID 145 | }; 146 | 147 | #define SNS_PRI (1 << 0) // Give extra weight to this sensor 148 | #define SNS_EN (1 << 1) // Enabled = averaged between all enabled 149 | #define SNS_NEG (1 << 8) // From remote or page, set this bit to disable a flag above 150 | 151 | class HVAC 152 | { 153 | public: 154 | HVAC(void); 155 | void init(void); // after EEPROM read 156 | void disable(void); // Shut it off 157 | void service(void); // call once per second 158 | uint8_t getState(void); // return current run state simplified (0=off, 1=cool, 2=hp, 4=NG) 159 | bool getFanRunning(void); // return fan running 160 | uint8_t getMode(void); // actual mode 161 | uint8_t getHeatMode(void); // heat mode 162 | int8_t getAutoMode(void); // get current auto heat/cool mode 163 | int8_t getSetMode(void); // get last requested mode 164 | void setMode(int mode); // request new mode; see enum Mode 165 | void setHeatMode(int mode); // heat mode 166 | int8_t getFan(void); // fan mode 167 | bool getHumidifierRunning(void); 168 | void setFan(int8_t m); // auto/on/s mode 169 | void filterInc(void); 170 | bool stateChange(void); // change since last call = true 171 | int16_t getSetTemp(int mode, int hl); // get temp set for a mode (cool/heat, hi/lo) 172 | void setTemp(int mode, int16_t Temp, int hl); // set temp for a mode 173 | void enableRemote(void); 174 | void updateIndoorTemp(int16_t Temp, int16_t rh); 175 | void updateOutdoorTemp(int16_t outTemp); 176 | void resetFilter(void); // reset the filter hour count 177 | bool checkFilter(void); 178 | void resetTotal(void); 179 | bool tempChange(void); 180 | void setVar(String sCmd, int val, char *psValue, IPAddress ip); // remote settings 181 | void updateVar(int iName, int iValue); // host values 182 | void setSettings(int iName, int iValue);// remote settings 183 | String settingsJson(void); // get all settings in json format 184 | String settingsJsonMod(void); 185 | String getPushData(void); // get states/temps/data in json 186 | void dayTotals(int d); 187 | void monthTotal(int m, int dys); 188 | 189 | int16_t m_outTemp; // adjusted current temp *10 190 | int16_t m_outRh; 191 | int16_t m_inTemp; // current indoor temperature *10 192 | int16_t m_rh; 193 | int16_t m_localTemp; // this device's temperature *10 194 | int16_t m_localRh; 195 | uint16_t m_targetTemp; // end temp for cycle 196 | uint16_t m_filterMinutes; 197 | uint8_t m_notif; 198 | bool m_bRemoteStream; // remote is streaming temp/rh 199 | bool m_bRemoteDisconnect; 200 | int16_t m_outMin, m_outMax; 201 | uint16_t m_iSecs[3]; 202 | bool m_bLink; // link adjust mode 203 | uint8_t m_DST; 204 | int8_t m_modeShadow = Mode_Cool; // shadow last valid mode 205 | #define SNS_CNT 8 206 | Sensor m_Sensor[SNS_CNT]; // remote and sensors 207 | 208 | private: 209 | void fanSwitch(bool bOn); 210 | void humidSwitch(bool bOn); 211 | void tempCheck(void); 212 | bool preCalcCycle(int16_t tempL, int16_t tempH); 213 | void calcTargetTemp(int mode); 214 | void costAdd(int secs, int mode, int hm); 215 | int CmdIdx(String s); 216 | void sendCmd(const char *szName, int value); 217 | int getSensorID(uint32_t val); 218 | void swapSensors(int n1, int n2); 219 | void shiftSensors(void); 220 | void activateSensor(int idx); 221 | void deactivateSensor(int idx); 222 | 223 | int8_t m_FanMode; // Auto=0, On=1, s=2 224 | bool m_bFanRunning; // when fan is running 225 | bool m_bHumidRunning; 226 | bool m_bRevOn; // shadow for reverse valve (ESP-32 digitalRead may not read latch bit) 227 | bool m_bHeatOn; // shadow for furnace 228 | bool m_bCoolOn; // shadow for compressor 229 | int8_t m_AutoMode; // cool, heat 230 | int8_t m_setMode; // preemted mode request 231 | int8_t m_setHeat; // preemt heat mode request 232 | int8_t m_AutoHeat; // auto heat mode choice 233 | bool m_bRunning; // is operating 234 | bool m_bStart; // signal to start 235 | bool m_bStop; // signal to stop 236 | bool m_bRecheck; // recalculate target now 237 | bool m_bEnabled; // enables system 238 | bool m_bAway; 239 | uint16_t m_fanPreElap = 60*10; 240 | uint16_t m_runTotal; // time HVAC has been running total since reset 241 | uint32_t m_fanOnTimer; // time fan is running 242 | uint16_t m_cycleTimer; // time HVAC has been running 243 | uint16_t m_fanPostTimer; // timer for delay 244 | uint16_t m_fanPreTimer; // timer for fan pre-run 245 | uint16_t m_idleTimer = 3*60; // time not running 246 | uint32_t m_fanIdleTimer; // time fan not running 247 | uint16_t m_fanAutoRunTimer; // auto fan time 248 | int m_overrideTimer; // countdown for override in seconds 249 | int8_t m_ovrTemp; // override delta of target 250 | uint16_t m_remoteTimeout; // timeout for remote sensor 251 | uint16_t m_remoteTimer; // in seconds 252 | uint16_t m_humidTimer; // timer for humidifier cost 253 | int8_t m_furnaceFan; // fake fan timer (actually half watts) 254 | }; 255 | 256 | #endif 257 | -------------------------------------------------------------------------------- /Arduino/Nextion.cpp: -------------------------------------------------------------------------------- 1 | #include "Nextion.h" 2 | 3 | // get changes 4 | int Nextion::service(char *pBuf) 5 | { 6 | dimmer(); 7 | if(!Serial.available()) 8 | return 0; 9 | int len = Serial.readBytesUntil(0xFF, pBuf, 62); 10 | 11 | if(len < 3) // could be the other 2 FFs 12 | return 0; 13 | pBuf[len] = 0; // change FF to NULL 14 | return len; 15 | } 16 | 17 | void Nextion::itemText(uint8_t id, String t) 18 | { 19 | Serial.print(String("t") + id + ".txt=\"" + t + "\""); 20 | FFF(); 21 | } 22 | 23 | void Nextion::btnText(uint8_t id, String t) 24 | { 25 | Serial.print(String("b") + id + ".txt=\"" + t + "\""); 26 | FFF(); 27 | } 28 | 29 | void Nextion::itemFp(uint8_t id, uint16_t val) // 123 to 12.3 30 | { 31 | Serial.print(String("f") + id + ".txt=\"" + (val / 10) + "." + (val % 10) + "\"" ); 32 | FFF(); 33 | } 34 | 35 | void Nextion::itemNum(uint8_t item, int16_t num) 36 | { 37 | Serial.print(String("n") + item + ".val=" + num); 38 | FFF(); 39 | } 40 | 41 | void Nextion::refreshItem(String id) 42 | { 43 | Serial.print(String("ref ") + id); 44 | FFF(); 45 | } 46 | 47 | void Nextion::text(uint16_t x, uint16_t y, uint16_t xCenter, uint16_t color, String sText) 48 | { 49 | const uint16_t bkColor = m_page; // transparent source 50 | const uint8_t h = 16; // 8x16 for small font + space 51 | uint16_t w = sText.length() * 9; 52 | 53 | Serial.print(String("xstr ") + x + "," + y + "," + w + ",16,1," + color + "," + bkColor + 54 | "," + xCenter + ",1,0,\"" + sText + "\""); 55 | FFF(); 56 | } 57 | 58 | void Nextion::fill(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color) 59 | { 60 | Serial.print(String("fill ") + x + "," + y + "," + w + "," + h + "," + color); 61 | FFF(); 62 | } 63 | 64 | void Nextion::line(uint16_t x, uint16_t y, uint16_t x2, uint16_t y2, uint16_t color) 65 | { 66 | Serial.print(String("line ") + x + "," + y + "," + x2 + "," + y2 + "," + color); 67 | FFF(); 68 | } 69 | 70 | void Nextion::visible(String id, uint8_t on) 71 | { 72 | Serial.print(String("vis ") + id + "," + on); 73 | FFF(); 74 | } 75 | 76 | void Nextion::itemPic(uint8_t id, uint8_t idx) 77 | { 78 | Serial.print(String("p") + id + ".pic=" + idx); 79 | FFF(); 80 | } 81 | 82 | void Nextion::brightness(uint8_t level) 83 | { 84 | m_newBrightness = level; 85 | } 86 | 87 | void Nextion::setPage(String sPage) 88 | { 89 | Serial.print(String("page ") + sPage); 90 | FFF(); 91 | switch(sPage.charAt(0)) 92 | { 93 | case 'T': m_page = Page_Thermostat; break; // Theromosat 94 | case 'c': m_page = Page_Clock; break; // clock (analog) 95 | case 'S': m_page = Page_SSID; break; // SSID list 96 | case 'k': m_page = Page_Keyboard; break; // keyboard 97 | case 'g': m_page = Page_Graph; break; // graph 98 | case 'b': m_page = Page_Blank; break; // blank (just color) 99 | } 100 | } 101 | 102 | uint8_t Nextion::getPage() 103 | { 104 | return m_page; 105 | } 106 | 107 | void Nextion::gauge(uint8_t id, uint16_t angle) 108 | { 109 | Serial.print(String("z") + id + ".val=" + angle); 110 | FFF(); 111 | } 112 | 113 | void Nextion::backColor(String sPageName, uint16_t color) 114 | { 115 | Serial.print(sPageName + ".bco=" + color); 116 | FFF(); 117 | } 118 | 119 | void Nextion::itemColor(String s, uint16_t color) 120 | { 121 | Serial.print(s + ".pco=" + color); 122 | FFF(); 123 | } 124 | 125 | void Nextion::cls(uint16_t color) 126 | { 127 | Serial.print(String("cls ") + color); 128 | FFF(); 129 | } 130 | 131 | void Nextion::add(uint8_t comp, uint8_t ch, uint16_t val) 132 | { 133 | Serial.print(String("add ") + comp + "," + ch + "," + val); 134 | FFF(); 135 | } 136 | 137 | void Nextion::refresh(bool bOn) 138 | { 139 | Serial.print(String( bOn ? "ref_star":"ref_stop")); 140 | FFF(); 141 | } 142 | 143 | void Nextion::reset() 144 | { 145 | Serial.print("rest"); 146 | FFF(); 147 | } 148 | 149 | void Nextion::FFF() 150 | { 151 | Serial.write(0xFF); 152 | Serial.write(0xFF); 153 | Serial.write(0xFF); 154 | } 155 | 156 | void Nextion::dimmer() 157 | { 158 | if(m_newBrightness == m_brightness) 159 | return; 160 | if(m_newBrightness > m_brightness + 1) 161 | m_brightness += 2; 162 | else if(m_newBrightness < m_brightness - 1) 163 | m_brightness -= 2; 164 | else 165 | m_brightness = m_newBrightness; 166 | 167 | Serial.print(String("dim=") + m_brightness); 168 | FFF(); 169 | } 170 | -------------------------------------------------------------------------------- /Arduino/Nextion.h: -------------------------------------------------------------------------------- 1 | #ifndef NEXTION_H 2 | #define NEXTION_H 3 | #include 4 | 5 | // from 8 bit components to 5-6-5 bits 6 | #define rgb(r,g,b) ( (((uint16_t)r << 8) & 0xF800) | (((uint16_t)g << 3) & 0x07E0) | ((uint16_t)b >> 3) ) 7 | // from 5-6-5 to 16 bit value (max 31, 63, 31) 8 | #define rgb16(r,g,b) ( ((uint16_t)r << 11) | ((uint16_t)g << 5) | (uint16_t)b ) 9 | 10 | enum Page 11 | { 12 | Page_Thermostat, 13 | Page_Clock, 14 | Page_SSID, 15 | Page_Keyboard, 16 | Page_Graph, 17 | Page_Blank, 18 | }; 19 | 20 | class Nextion 21 | { 22 | public: 23 | Nextion(){}; 24 | int service(char *pBuff); 25 | void itemText(uint8_t id, String t); 26 | void btnText(uint8_t id, String t); 27 | void itemFp(uint8_t id, uint16_t val); 28 | void refreshItem(String id); 29 | void fill(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color); 30 | void line(uint16_t x, uint16_t y, uint16_t x2, uint16_t y2, uint16_t color); 31 | void text(uint16_t x, uint16_t y, uint16_t xCenter, uint16_t color, String sText); 32 | void visible(String id, uint8_t on); 33 | void itemPic(uint8_t id, uint8_t idx); 34 | void itemNum(uint8_t item, int16_t num); 35 | void brightness(uint8_t level); 36 | void setPage(String sPage); 37 | uint8_t getPage(void); 38 | void gauge(uint8_t id, uint16_t angle); 39 | void backColor(String sPageName, uint16_t color); 40 | void itemColor(String s, uint16_t color); 41 | void cls(uint16_t color); 42 | void add(uint8_t comp, uint8_t ch, uint16_t val); 43 | void refresh(bool bOn); 44 | void reset(void); 45 | void FFF(void); 46 | private: 47 | void dimmer(void); 48 | 49 | uint8_t m_brightness = 99; 50 | uint8_t m_newBrightness = 99; 51 | uint8_t m_page; 52 | }; 53 | 54 | extern Nextion nex; 55 | 56 | #endif // NEXTION_H 57 | -------------------------------------------------------------------------------- /Arduino/RunningMedian.h: -------------------------------------------------------------------------------- 1 | #ifndef RunningMedian_h 2 | #define RunningMedian_h 3 | // 4 | // FILE: RunningMedian.h 5 | // AUTHOR: Rob dot Tillaart at gmail dot com 6 | // PURPOSE: RunningMedian library for Arduino 7 | // VERSION: 0.2.00 - template edition 8 | // URL: http://arduino.cc/playground/Main/RunningMedian 9 | // HISTORY: 0.2.00 first template version by Ronny 10 | // 0.2.01 added getAverage(uint8_t nMedians, float val) 11 | // 12 | // Released to the public domain 13 | // 14 | 15 | #include 16 | 17 | template class RunningMedian { 18 | 19 | public: 20 | 21 | enum STATUS {OK = 0, NOK = 1}; 22 | 23 | RunningMedian() { 24 | _size = N; 25 | clear(); 26 | }; 27 | 28 | void clear() { 29 | _cnt = 0; 30 | _idx = 0; 31 | }; 32 | 33 | void add(T value) { 34 | _ar[_idx++] = value; 35 | if (_idx >= _size) _idx = 0; // wrap around 36 | if (_cnt < _size) _cnt++; 37 | }; 38 | 39 | STATUS getMedian(T& value) { 40 | if (_cnt > 0) { 41 | sort(); 42 | value = _as[_cnt/2]; 43 | return OK; 44 | } 45 | return NOK; 46 | }; 47 | 48 | STATUS getAverage(float &value) { 49 | if (_cnt > 0) { 50 | float sum = 0; 51 | for (uint8_t i=0; i< _cnt; i++) sum += _ar[i]; 52 | value = sum / _cnt; 53 | return OK; 54 | } 55 | return NOK; 56 | }; 57 | 58 | STATUS getAverage(uint8_t nMedians, float &value) { 59 | if ((_cnt > 0) && (nMedians > 0)) 60 | { 61 | if (_cnt < nMedians) nMedians = _cnt; // when filling the array for first time 62 | uint8_t start = ((_cnt - nMedians)/2); 63 | uint8_t stop = start + nMedians; 64 | sort(); 65 | float sum = 0; 66 | for (uint8_t i = start; i < stop; i++) sum += _as[i]; 67 | value = sum / nMedians; 68 | return OK; 69 | } 70 | return NOK; 71 | } 72 | 73 | STATUS getHighest(T& value) { 74 | if (_cnt > 0) { 75 | sort(); 76 | value = _as[_cnt-1]; 77 | return OK; 78 | } 79 | return NOK; 80 | }; 81 | 82 | STATUS getLowest(T& value) { 83 | if (_cnt > 0) { 84 | sort(); 85 | value = _as[0]; 86 | return OK; 87 | } 88 | return NOK; 89 | }; 90 | 91 | unsigned getSize() { 92 | return _size; 93 | }; 94 | 95 | unsigned getCount() { 96 | return _cnt; 97 | } 98 | 99 | STATUS getStatus() { 100 | return (_cnt > 0 ? OK : NOK); 101 | }; 102 | 103 | private: 104 | uint8_t _size; 105 | uint8_t _cnt; 106 | uint8_t _idx; 107 | T _ar[N]; 108 | T _as[N]; 109 | void sort() { 110 | // copy 111 | for (uint8_t i=0; i< _cnt; i++) _as[i] = _ar[i]; 112 | 113 | // sort all 114 | for (uint8_t i=0; i< _cnt-1; i++) { 115 | uint8_t m = i; 116 | for (uint8_t j=i+1; j< _cnt; j++) { 117 | if (_as[j] < _as[m]) m = j; 118 | } 119 | if (m != i) { 120 | T t = _as[m]; 121 | _as[m] = _as[i]; 122 | _as[i] = t; 123 | } 124 | } 125 | }; 126 | }; 127 | 128 | #endif 129 | -------------------------------------------------------------------------------- /Arduino/Thermostat.ino: -------------------------------------------------------------------------------- 1 | /**The MIT License (MIT) 2 | 3 | Copyright (c) 2016 by Greg Cunningham, CuriousTech 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | */ 23 | 24 | // Build with Arduino IDE 1.8.19 25 | // ESP8266 (3.1.1) 1MB (FS:64Kb) 26 | // ESP32: (2.0.6) ESP Dev Module, 115200 baud, 80MHz (WiFi/BT) or any speed, QIO, Default 4MB with spiffs 27 | // For remote unit, uncomment #define REMOTE in HVAC.h 28 | 29 | #include // https://github.com/me-no-dev/ESPAsyncWebServer 30 | #include // http://www.pjrc.com/teensy/td_libs_Time.html 31 | #include // https://github.com/CuriousTech/ESP07_WiFiGarageDoor/tree/master/libraries/UdpTime 32 | #include "HVAC.h" 33 | #include "Encoder.h" 34 | #include "WebHandler.h" 35 | #include "display.h" 36 | #include 37 | #include "eeMem.h" 38 | #include "RunningMedian.h" 39 | 40 | // ESP8266 uncomment to swap Serial's pins to 15(TX) and 13(RX) that don't interfere with booting 41 | //#define SER_SWAP https://github.com/esp8266/Arduino/blob/master/doc/reference.md 42 | 43 | // Uncomment only one of these 44 | #include // //Libraries/SHT21 45 | //#include // http://www.github.com/markruys/arduino-DHT 46 | //#include //DallasTemperature from library mamanger 47 | //#include // //Libraries/AM2320 48 | 49 | //----- Pin Configuration - See HVAC.h for the rest - 50 | #ifdef ESP32 51 | #define SDA 21 52 | #define SCL 22 53 | #define ENC_A 16 54 | #define ENC_B 4 55 | #else // ESP8266 pins 56 | #define SDA 2 57 | #define SCL 13 58 | #define ENC_A 5 // Encoder is on GPIO4 and 5 59 | #define ENC_B 4 60 | #endif 61 | //------------------------ 62 | 63 | Display display; 64 | eeMem ee; 65 | 66 | HVAC hvac; 67 | 68 | RunningMedian tempMedian; //median of 25 samples 69 | 70 | #ifdef SHT21_H 71 | SHT21 sht(SDA, SCL, 4); 72 | #endif 73 | #ifdef dht_h 74 | DHT dht; 75 | #endif 76 | #ifdef DallasTemperature_h 77 | const int ds18Resolution = 12; 78 | DeviceAddress ds18addr = { 0x28, 0xC1, 0x02, 0x64, 0x04, 0x00, 0x00, 0x35 }; 79 | unsigned int ds18delay; 80 | unsigned long ds18lastreq = 1; //zero is special 81 | const unsigned int ds18reqdelay = 5000; //request every 5 seconds 82 | unsigned long ds18reqlastreq; 83 | OneWire oneWire(2); //pin 2 84 | DallasTemperature ds18(&oneWire); 85 | #endif 86 | #ifdef AM2303_H 87 | AM2320 am; 88 | #endif 89 | 90 | UdpTime uTime; 91 | 92 | Encoder rot(ENC_B, ENC_A); 93 | 94 | extern void WsSend(String s); 95 | 96 | bool EncoderCheck() 97 | { 98 | if(ee.b.bLock) return false; 99 | 100 | int r = rot.poll(); 101 | 102 | if(r == 0) // no change 103 | return false; 104 | 105 | display.screen(true); // ensure page is thermostat 106 | 107 | int8_t m = (display.m_adjustMode < 2) ? Mode_Cool : Mode_Heat; // lower 2 are cool 108 | int8_t hilo = (display.m_adjustMode ^ 1) & 1; // hi or low of set 109 | int16_t t = hvac.getSetTemp(m, hilo ); // 110 | 111 | t += r; // inc/dec by any amount 112 | 113 | hvac.setTemp(m, t, hilo); 114 | 115 | if(hvac.m_bLink) // adjust both high and low 116 | { 117 | t = hvac.getSetTemp(m, hilo^1 ) + r; // adjust opposite hi/lo the same 118 | hvac.setTemp(m, t, hilo^1); 119 | } 120 | 121 | display.updateTemps(); 122 | return true; 123 | } 124 | 125 | void setup() 126 | { 127 | Serial.begin(115200); // Nextion must be set with bauds=115200 128 | #ifdef SER_SWAP 129 | Serial.swap(); //swap to gpio 15/13 130 | #endif 131 | ee.init(); 132 | hvac.init(); 133 | display.init(); 134 | startServer(); 135 | 136 | #ifdef SHT21_H 137 | sht.init(); 138 | #endif 139 | #ifdef dht_h 140 | dht.setup(SCL, DHT::DHT22); 141 | #endif 142 | #ifdef DallasTemperature_h 143 | ds18.setResolution(ds18addr, ds18Resolution); 144 | ds18.setWaitForConversion(false); //this enables asyncronous calls 145 | ds18.requestTemperatures(); //fire off the first request 146 | ds18lastreq = millis(); 147 | ds18delay = 750 / (1 << (12 - ds18Resolution)); //delay based on resolution 148 | #endif 149 | #ifdef AM2303_H 150 | am.begin(SDA, SCL); 151 | #endif 152 | } 153 | 154 | void loop() 155 | { 156 | static uint8_t hour_save, min_save = 255, sec_save; 157 | static int8_t lastSec; 158 | static int8_t lastHour; 159 | static int8_t lastDay = -1; 160 | 161 | while( EncoderCheck() ); 162 | display.checkNextion(); // check for touch, etc. 163 | if(uTime.check(ee.tz)) 164 | { 165 | hvac.m_DST = uTime.getDST(); 166 | if(lastDay == -1) 167 | lastDay = day() - 1; 168 | } 169 | handleServer(); // handles mDNS, web 170 | #ifdef SHT21_H 171 | if(sht.service()) 172 | { 173 | tempMedian.add((ee.b.bCelcius ? sht.getTemperatureC():sht.getTemperatureF()) * 10); 174 | float temp; 175 | if (tempMedian.getAverage(2, temp) == tempMedian.OK) { 176 | hvac.updateIndoorTemp( temp, sht.getRh() * 10 ); 177 | } 178 | } 179 | #endif 180 | #ifdef DallasTemperature_h 181 | if(ds18lastreq > 0 && millis() - ds18lastreq >= ds18delay) { //new temp is ready 182 | tempMedian.add((ee.b.bCelcius ? ds18.getTempC(ds18addr):ds18.getTempF(ds18addr)) ); 183 | ds18lastreq = 0; //prevents this block from firing repeatedly 184 | float temp; 185 | if (tempMedian.getAverage(temp) == tempMedian.OK) { 186 | hvac.updateIndoorTemp( temp * 10, 500); //fake 50% 187 | } 188 | } 189 | 190 | if(millis() - ds18reqlastreq >= ds18reqdelay) { 191 | ds18.requestTemperatures(); 192 | ds18lastreq = millis(); 193 | ds18reqlastreq = ds18lastreq; 194 | } 195 | #endif 196 | if(sec_save != second()) // only do stuff once per second 197 | { 198 | sec_save = second(); 199 | if(secondsServer()) // once per second stuff, returns true once on connect 200 | uTime.start(); 201 | display.oneSec(); 202 | hvac.service(); // all HVAC code 203 | 204 | #ifdef dht_h 205 | static uint8_t read_delay = 2; 206 | if(--read_delay == 0) 207 | { 208 | float temp; 209 | if(ee.bCelcius) 210 | temp = dht.getTemperature() * 10; 211 | else 212 | temp = dht.toFahrenheit(dht.getTemperature()) * 10; 213 | 214 | if(dht.getStatus() == DHT::ERROR_NONE) 215 | { 216 | tempMedian.add(temp); 217 | if (tempMedian.getAverage(2, temp) == tempMedian.OK) { 218 | hvac.updateIndoorTemp( temp, dht.getHumidity() * 10); 219 | } 220 | } 221 | read_delay = 5; // update every 5 seconds 222 | } 223 | #endif 224 | #ifdef AM2303_H 225 | static uint8_t read_delay = 2; 226 | if(--read_delay == 0) 227 | { 228 | float temp; 229 | float rh; 230 | if(am.measure(temp, rh)) 231 | { 232 | if(!ee.b.bCelcius) 233 | temp = temp * 9 / 5 + 32; 234 | tempMedian.add(temp * 10); 235 | if (tempMedian.getAverage(2, temp) == tempMedian.OK) { 236 | hvac.updateIndoorTemp( temp, rh * 10 ); 237 | } 238 | } 239 | read_delay = 5; // update every 5 seconds 240 | } 241 | #endif 242 | 243 | if(min_save != minute()) // only do stuff once per minute 244 | { 245 | min_save = minute(); 246 | 247 | if(hour_save != hour()) // update our IP and time daily (at 2AM for DST) 248 | { 249 | hour_save = hour(); 250 | if(hour_save == 2) 251 | uTime.start(); // update time daily at DST change 252 | if(hour_save == 0 && year() > 2020) 253 | { 254 | if(lastDay != -1) 255 | { 256 | hvac.dayTotals(lastDay); 257 | hvac.monthTotal(month() - 1, day()); 258 | } 259 | lastDay = day() - 1; 260 | ee.iSecsDay[lastDay][0] = 0; // reset 261 | ee.iSecsDay[lastDay][1] = 0; 262 | ee.iSecsDay[lastDay][2] = 0; 263 | if(lastDay == 0) // new month 264 | { 265 | int m = (month() + 10) % 12; // last month: Dec = 10, Jan = 11, Feb = 0 266 | hvac.monthTotal(m, -1); 267 | } 268 | } 269 | if(ee.check()) 270 | { 271 | if((hour_save & 1) == 0) // every other hour 272 | { 273 | ee.filterMinutes = hvac.m_filterMinutes; 274 | ee.update(); 275 | } 276 | } 277 | } 278 | } 279 | 280 | } 281 | delay(9); // rotary encoder and lines() need 8ms minimum 282 | } 283 | -------------------------------------------------------------------------------- /Arduino/WebHandler.h: -------------------------------------------------------------------------------- 1 | #ifndef WEBHANDLER_H 2 | #define WEBHANDLER_H 3 | 4 | #include 5 | #include // https://github.com/me-no-dev/ESPAsyncWebServer 6 | 7 | void startServer(void); 8 | void handleServer(void); 9 | bool secondsServer(void); 10 | void parseParams(AsyncWebServerRequest *request); 11 | String dataJson(void); 12 | void WsSend(String s); 13 | void historyDump(bool bStart); 14 | void appendDump(uint32_t startTime); 15 | 16 | #endif // WEBHANDLER_H 17 | -------------------------------------------------------------------------------- /Arduino/data/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ESP-HVAC 6 | 7 | 10 | 11 | 213 | 214 | 220 | CuriousTech HVAC Remote
221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 |
IN
in
>
trg
          OUT
out
     
230 |
231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 276 | 277 | 278 | 279 | 281 | 282 | 283 | 284 | 285 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 295 | 296 | 297 | 298 | 299 | 300 |
FAN OFF
 
Cooling
    243 |
 
COOL HI
COOL LO
HEAT HI
HEAT LO
  274 | 275 |
OVR TIME 280 |   HI
OVRRD Δ 286 |        
FRESHEN   LO 294 |
AWAY Δ
301 | 302 | 303 | 304 | 305 | 306 | 307 |
CYCLE
0
TOTAL  FILTER
308 | © 2016 CuriousTech.net 309 | 310 | 311 | -------------------------------------------------------------------------------- /Arduino/data/settings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ESP-HVAC 6 | 7 | 10 | 124 | 125 | 132 | CuriousTech HVAC Settings

133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 |
Threshold
Heat Thresh
AC ∂ Limit
Fan PrePost
cycle MinMax
Idle MinPKW
Away LimitCFM
FC ShiftCCF
LookaheadDisp
Fan Auto RunRun
Temp WeightCal

Effective
158 | 159 | 165 | 166 |
Password 164 |
167 | © 2016 CuriousTech.net 168 | 169 | 170 | -------------------------------------------------------------------------------- /Arduino/data/styles.css: -------------------------------------------------------------------------------- 1 | table{ 2 | border-radius: 2px; 3 | margin-bottom: 2px; 4 | box-shadow: 4px 4px 10px #000000; 5 | background: rgb(160,160,160); 6 | background: linear-gradient(0deg, rgba(94,94,94,1) 0%, rgba(160,160,160,1) 90%); 7 | background-clip: padding-box; 8 | } 9 | input{ 10 | border-radius: 2px; 11 | margin-bottom: 2px; 12 | box-shadow: 4px 4px 10px #000000; 13 | background: rgb(160,160,160); 14 | background: linear-gradient(0deg, rgba(160,160,160,1) 0%, rgba(239,255,255,1) 100%); 15 | background-clip: padding-box; 16 | } 17 | .style1{border-width: 0;} 18 | .style2{text-align: left;} 19 | .style3{ 20 | border-radius: 5px; 21 | margin-bottom: 5px; 22 | box-shadow: 2px 2px 10px #000000; 23 | background: rgb(95,194,230); 24 | background: linear-gradient(0deg, rgba(95,194,230,1) 0%, rgba(79,79,79,1) 100%); 25 | background-clip: padding-box; 26 | } 27 | .style4{ 28 | border-radius: 5px; 29 | margin-bottom: 5px; 30 | box-shadow: 2px 2px 10px #000000; 31 | background: rgb(80,160,255); 32 | background: linear-gradient(0deg, rgba(80,160,255,1) 0%, rgba(79,79,79,1) 100%); 33 | background-clip: padding-box; 34 | } 35 | .style5 { 36 | border-radius: 1px; 37 | box-shadow: 2px 2px 10px #000000; 38 | background: rgb(0,160,224); 39 | background: linear-gradient(0deg, rgba(0,160,224,1) 0%, rgba(0,224,224,1) 100%); 40 | } 41 | -------------------------------------------------------------------------------- /Arduino/display.h: -------------------------------------------------------------------------------- 1 | #ifndef DISPLAY_H 2 | #define DISPLAY_H 3 | 4 | #include 5 | #include "Forecast.h" 6 | 7 | #define NEX_TIMEOUT 90 // 90 seconds 8 | #define NEX_BRIGHT 95 // 100% = full brightness 9 | #define NEX_MEDIUM 25 // For the clock 10 | #define NEX_DIM 3 // for the lines, 1 = very dim, 0 = off 11 | 12 | struct Line{ 13 | int16_t x1; 14 | int16_t y1; 15 | int16_t x2; 16 | int16_t y2; 17 | }; 18 | 19 | union gflags 20 | { 21 | uint32_t u; 22 | struct 23 | { 24 | uint32_t fan:1; 25 | uint32_t state:3; 26 | uint32_t rh:10; 27 | uint32_t tmdiff:9; 28 | int32_t sens4:9; 29 | }; 30 | }; 31 | 32 | union temps 33 | { 34 | uint32_t u; 35 | struct 36 | { 37 | uint32_t inTemp:11; 38 | uint32_t target:10; 39 | int32_t outTemp:11; 40 | }; 41 | }; 42 | 43 | struct gPoint{ 44 | temps t; 45 | int8_t sens0; 46 | int8_t sens1; 47 | int8_t sens2; 48 | int8_t sens3; 49 | gflags bits; 50 | }; 51 | 52 | class Display 53 | { 54 | public: 55 | Display() 56 | { 57 | } 58 | void init(void); 59 | void oneSec(void); 60 | bool screen(bool bOn); 61 | void reset(void); 62 | void checkNextion(void); // all the Nextion recieved commands 63 | void updateTemps(void); 64 | bool drawForecast(bool bRef); 65 | void Note(char *cNote); 66 | bool getGrapthPoints(gPoint *pt, int n); 67 | int minPointVal(int n); 68 | private: 69 | void buttonRepeat(void); 70 | void refreshAll(void); 71 | void updateClock(void); 72 | void cspoint(float &x2, float &y2, float x, float y, float angle, float size); 73 | void displayTime(void); 74 | void displayOutTemp(void); 75 | void updateModes(void); // update any displayed settings 76 | void updateAdjMode(bool bRef); // current adjust indicator of the 4 temp settings 77 | void updateRSSI(void); 78 | void updateNotification(bool bRef); 79 | void updateRunIndicator(bool bForce); // run and fan running 80 | void addGraphPoints(void); 81 | void fillGraph(void); 82 | void drawPoints(int w, uint16_t color); 83 | void drawPointsRh(uint16_t color); 84 | void drawPointsTemp(void); 85 | uint16_t stateColor(gflags v); 86 | void Lines(void); 87 | 88 | uint16_t m_backlightTimer = NEX_TIMEOUT; 89 | #define GPTS 640 // 320 px width - (10+10) padding 90 | gPoint m_points[GPTS]; 91 | uint16_t m_pointsIdx = 0; 92 | uint16_t m_temp_counter = 2*60; 93 | uint8_t m_btnMode = 0; 94 | uint8_t m_btnDelay = 0; 95 | int m_tempLow; // 66.0 base 96 | int m_tempHigh; // 90.0 top 97 | int m_tempMax; 98 | public: 99 | uint32_t m_lastPDate = 0; 100 | uint8_t m_adjustMode = 0; // which of 4 temps to adjust with rotary encoder/buttons 101 | }; 102 | 103 | #endif // DISPLAY_H 104 | -------------------------------------------------------------------------------- /Arduino/eeMem.cpp: -------------------------------------------------------------------------------- 1 | #include "eeMem.h" 2 | #include 3 | 4 | eeMem::eeMem() 5 | { 6 | } 7 | 8 | bool eeMem::init() 9 | { 10 | EEPROM.begin(EESIZE); 11 | 12 | uint8_t data[EESIZE]; 13 | uint16_t *pwTemp = (uint16_t *)data; 14 | 15 | #ifdef ESP32 16 | EEPROM.readBytes(0, &data, EESIZE); 17 | #else 18 | int addr = 0; 19 | for(int i = 0; i < EESIZE; i++, addr++) 20 | data[i] = EEPROM.read( addr ); 21 | #endif 22 | if(pwTemp[0] != EESIZE) 23 | return true; // revert to defaults if struct size changes 24 | 25 | uint16_t sum = pwTemp[1]; 26 | pwTemp[1] = 0; 27 | pwTemp[1] = Fletcher16(data, EESIZE ); 28 | if(pwTemp[1] != sum) 29 | return true; // revert to defaults if sum fails 30 | memcpy(this + offsetof(eeMem, size), data, EESIZE ); 31 | return true; 32 | } 33 | 34 | bool eeMem::update() // write the settings if changed 35 | { 36 | check(); // make sure sum is correct 37 | #ifdef ESP32 38 | EEPROM.writeBytes(0, this + offsetof(eeMem, size), EESIZE); 39 | #else 40 | uint16_t addr = 0; 41 | uint8_t *pData = (uint8_t *)this + offsetof(eeMem, size); 42 | for(int i = 0; i < EESIZE; i++, addr++) 43 | EEPROM.write(addr, pData[i] ); 44 | #endif 45 | return EEPROM.commit(); 46 | } 47 | 48 | bool eeMem::check() 49 | { 50 | uint16_t old_sum = ee.sum; 51 | ee.sum = 0; 52 | ee.sum = Fletcher16((uint8_t*)this + offsetof(eeMem, size), EESIZE); 53 | return (old_sum == ee.sum) ? false:true; 54 | } 55 | 56 | uint16_t eeMem::getSum() 57 | { 58 | return Fletcher16((uint8_t*)this + offsetof(eeMem, size), EESIZE); 59 | } 60 | 61 | uint16_t eeMem::Fletcher16( uint8_t* data, int count) 62 | { 63 | uint16_t sum1 = 0; 64 | uint16_t sum2 = 0; 65 | 66 | for( int index = 0; index < count; ++index ) 67 | { 68 | sum1 = (sum1 + data[index]) % 255; 69 | sum2 = (sum2 + sum1) % 255; 70 | } 71 | 72 | return (sum2 << 8) | sum1; 73 | } 74 | -------------------------------------------------------------------------------- /Arduino/eeMem.h: -------------------------------------------------------------------------------- 1 | #ifndef EEMEM_H 2 | #define EEMEM_H 3 | 4 | #include 5 | 6 | struct Sched 7 | { 8 | uint16_t setTemp; 9 | uint16_t timeSch; 10 | uint8_t thresh; 11 | uint8_t wday; 12 | char name[16]; // names for small display 13 | }; // 22 14 | 15 | struct flags_t 16 | { 17 | uint16_t Mode:3; 18 | uint16_t heatMode:2; 19 | uint16_t humidMode:3; 20 | uint16_t nSchedMode:3; // 0=forecast, 1=sine, 2=flat 21 | uint16_t nFcstSource:2; // 0=local, 1=OpenWeatherMap 22 | uint16_t bCelcius:1; 23 | uint16_t bLock:1; 24 | uint16_t res:1; 25 | }; 26 | 27 | #define EESIZE (offsetof(eeMem, end) - offsetof(eeMem, size) ) 28 | 29 | class eeMem 30 | { 31 | public: 32 | eeMem(); 33 | bool init(void); 34 | bool check(void); 35 | bool update(void); 36 | uint16_t getSum(void); 37 | 38 | private: 39 | uint16_t Fletcher16( uint8_t* data, int count); 40 | 41 | public: 42 | uint16_t size = EESIZE; // if size changes, use defaults 43 | uint16_t sum = 0xAAAA; // if sum is different from memory struct, write 44 | char szSSID[24] = ""; // Enter you WiFi router SSID 45 | char szSSIDPassword[24] = ""; // and password 46 | uint16_t coolTemp[2] = {850, 860}; // cool to temp *10 low/high F/C issue 47 | uint16_t heatTemp[2] = {740, 750}; // heat to temp *10 low/high F/C issue 48 | flags_t b = {0,0,0,0,0,0,0,0}; // see flags_t 49 | int8_t cycleThresh[2] = {28, 8}; // temp range for cycle *10 [cool|heat] F/C issue 50 | uint8_t eHeatThresh = 33; // degree threshold to switch to gas F/C issue 51 | uint16_t cycleMin = 60*4; // min time to run a cycle in minutes 52 | uint16_t cycleMax = 60*30; // max time to run a cycle in minutes 53 | uint16_t idleMin = 60*8; // min time to not run in minutes 54 | uint16_t filterMinutes; // resettable minutes run timer (200 hours is standard change interval) 55 | uint16_t fanPostDelay[2] = {60*2, 60*2}; // delay to run auto fan after [hp/cool] stops 56 | uint16_t fanPreTime[2] = {60*1, 60*1}; // fan pre-run before [cool/heat] 57 | uint16_t overrideTime = 60*10; // time used for an override in minutes 58 | int8_t tz = -5; // current timezone 59 | int8_t adj; // temp sensor offset adjust by 0.1 60 | uint16_t rhLevel[2] = {450, 750}; // rh low/high 45%, 75% 61 | int8_t awayDelta[2] = {40, -40}; // temp offset in away mode[cool][heat] by 0.1 62 | uint16_t awayTime = 60*8; // time limit for away offset (in minutes) 63 | uint8_t hostIp[4] = {192,168,31,100}; // Device to read local forecast info 64 | uint16_t hostPort = 80; 65 | char cityID[8] = "4291945"; // For OpenWeatherMap 4311646 66 | char password[24] = "password"; // Web interface password 67 | uint8_t fcRange = 23; // number in forecasts (3 hours) 68 | uint8_t fcDisplay = 46; // number in forecasts (3 hours) 69 | uint16_t iSecsDay[32][3] = { // Saved from latest 70 | {0,0,1794},{0,0,1794},{0,0,1196},{5460,0,9072},{14343,0,21945},{14635,0,21993},{16979,0,26391},{2783,0,5246},{2675,0,4414},{0,0,1794},{3183,0,5402}, 71 | {6896,0,9241},{5280,0,7381},{6663,0,8945},{8941,0,11168},{10682,0,16647},{7228,0,12776},{3022,0,4879},{4659,1156,8608},{6037,0,8319},{4487,0,6289}, 72 | {2327,0,4184},{3012,0,5050},{5853,0,7954},{14164,0,16753},{16901,0,19671},{10425,0,12589},{11859,0,14747},{10630,0,13156},{11409,0,13998},{0,0,1794}}; 73 | uint32_t iSecsMon[12][3] = { // Save from latest (compressor,gas,fan) 74 | {199124,36845,336081},{16565,357781,769917},{0,170664,419895},{523,57837,146926},{27756,26256,113103},{153956,0,235082}, 75 | {212466,0,304331},{0,0,4784},{0,0,0},{0,0,0},{113429,52189,295135},{0,251368,492489}}; 76 | uint16_t ppkwh = 147; // price per KWH in cents * 10000 (0.147) 77 | uint16_t ccf = 1190; // nat gas cost per 1000 cubic feet in 10th of cents * 1000 ($1.190) 78 | uint16_t cfm = 820; // cubic feet per minute * 1000 of furnace (0.82) // cubic feet per minute 79 | uint16_t compressorWatts = 2600; // compressorWatts 80 | uint8_t fanWatts = 250; // Blower motor watts 81 | uint8_t furnaceWatts = 220; // 1.84A inducer motor mostly 82 | uint8_t humidWatts = 150; 83 | uint16_t furnacePost = 114; // furnace internal fan timer 84 | uint16_t diffLimit = 300; // in/out thermal differential limit. Set to 30 deg limit F/C issue 85 | int16_t fcOffset[2] = {-180,0}; // forecast offset adjust in minutes (cool/heat) 86 | uint16_t fanIdleMax = 60*4; // fan idle max in minutes 87 | uint8_t fanAutoRun = 5; // 5 minutes on 88 | int16_t sineOffset[2] = {0, 0}; // sine offset adjust (cool/heat) 89 | uint32_t sensorActive[3]; // sensor IDs ifor restart 90 | uint8_t end; 91 | }; // 512 bytes 92 | 93 | static_assert(EESIZE <= 512, "EEPROM struct too big"); 94 | 95 | extern eeMem ee; 96 | 97 | #endif // EEMEM_H 98 | -------------------------------------------------------------------------------- /Arduino/forecast.cpp: -------------------------------------------------------------------------------- 1 | #include "Forecast.h" 2 | #include "Display.h" 3 | #include "TimeLib.h" 4 | #include "eeMem.h" 5 | 6 | // OWM Format: https://openweathermap.org/forecast5 7 | 8 | extern void WsSend(String s); // for debug, open chrome console 9 | 10 | // forecast retrieval 11 | 12 | Forecast::Forecast() 13 | { 14 | m_ac.onConnect([](void* obj, AsyncClient* c) { (static_cast(obj))->_onConnect(c); }, this); 15 | m_ac.onDisconnect([](void* obj, AsyncClient* c) { (static_cast(obj))->_onDisconnect(c); }, this); 16 | m_ac.onData([](void* obj, AsyncClient* c, void* data, size_t len) { (static_cast(obj))->_onData(c, static_cast(data), len); }, this); 17 | 18 | for(int i = 0; i < FC_CNT; i++) 19 | m_fc.Data[i].temp = -1000; 20 | m_fc.Date = 0; 21 | } 22 | 23 | void Forecast::init(int16_t tzOff) 24 | { 25 | m_tzOffset = tzOff; 26 | } 27 | 28 | // File retrieval start 29 | void Forecast::start(IPAddress serverIP, uint16_t port, bool bCelcius, int8_t type) 30 | { 31 | if(m_ac.connected() || m_ac.connecting()) 32 | return; 33 | m_status = FCS_Busy; 34 | m_bCelcius = bCelcius; 35 | m_serverIP = serverIP; 36 | if(!m_ac.connect(serverIP, port)) 37 | m_status = FCS_ConnectError; 38 | m_bLocal = true; 39 | m_type = type; 40 | } 41 | 42 | // OpenWeaterMap start 43 | void Forecast::start(char *pCityID, bool bCelcius) 44 | { 45 | if(m_ac.connected() || m_ac.connecting()) 46 | return; 47 | m_bCelcius = bCelcius; 48 | strcpy(m_cityID, pCityID); 49 | m_status = FCS_Busy; 50 | if(!m_ac.connect("api.openweathermap.org", 80)) 51 | m_status = FCS_ConnectError; 52 | m_bLocal = false; 53 | } 54 | 55 | int Forecast::checkStatus() 56 | { 57 | if(m_status == FCS_Done) 58 | { 59 | m_status = FCS_Idle; 60 | m_fc.loadDate = now(); 61 | m_bUpdateFcstIdle = true; 62 | m_bFcstUpdated = true; 63 | return FCS_Done; 64 | } 65 | return m_status; 66 | } 67 | 68 | void Forecast::_onConnect(AsyncClient* client) 69 | { 70 | String path = "GET /"; 71 | 72 | if(m_bLocal) // file 73 | { 74 | switch(m_type) 75 | { 76 | case 0: 77 | path += "Forecast.log"; 78 | break; 79 | case 1: 80 | path += "Forecast.json"; 81 | break; 82 | } 83 | path += " HTTP/1.1\n" 84 | "Host: "; 85 | path += client->remoteIP().toString(); 86 | path += "\n" 87 | "Connection: close\n" 88 | "Accept: */*\n\n"; 89 | } 90 | else // OWM server 91 | { 92 | path = "GET /data/2.5/forecast?id="; 93 | path += m_cityID; 94 | path += "&appid="; 95 | path += APPID; // Account 96 | path += "&units="; 97 | if(m_bCelcius) 98 | path += "celcius"; 99 | else 100 | path += "imperial"; 101 | path += " HTTP/1.1\n" 102 | "Host: "; 103 | path += client->remoteIP().toString(); 104 | path += "\n" 105 | "Connection: close\n" 106 | "Accept: */*\n\n"; 107 | } 108 | m_ac.add(path.c_str(), path.length()); 109 | m_pBuffer = new char[OWBUF_SIZE]; 110 | if(m_pBuffer) m_pBuffer[0] = 0; 111 | else m_status = FCS_MemoryError; 112 | m_bufIdx = 0; 113 | } 114 | 115 | // build file in chunks 116 | void Forecast::_onData(AsyncClient* client, char* data, size_t len) 117 | { 118 | if(m_pBuffer == NULL || m_bufIdx + len >= OWBUF_SIZE) 119 | return; 120 | memcpy(m_pBuffer + m_bufIdx, data, len); 121 | m_bufIdx += len; 122 | m_pBuffer[m_bufIdx] = 0; 123 | } 124 | 125 | // Remove most outdated entries to fill in new 126 | int Forecast::makeroom(uint32_t newTm) 127 | { 128 | if(m_fc.Date == 0) // not filled in yet 129 | return 0; 130 | uint32_t tm2 = m_fc.Date; 131 | int fcIdx; 132 | for(fcIdx = 0; fcIdx < FC_CNT-4 && m_fc.Data[fcIdx].temp != -1000; fcIdx++) 133 | { 134 | if(tm2 >= newTm) 135 | break; 136 | tm2 += m_fc.Freq; 137 | } 138 | if(fcIdx > (FC_CNT - m_fcCnt - 1)) // not enough room left 139 | { 140 | int n = fcIdx - (FC_CNT - 56); 141 | uint8_t *p = (uint8_t*)m_fc.Data; 142 | memcpy(p, p + (n*sizeof(forecastItem)), FC_CNT - (n*sizeof(forecastItem)) ); // make room 143 | m_fc.Date += m_fc.Freq * n; 144 | fcIdx -= n; 145 | } 146 | return fcIdx; 147 | } 148 | 149 | void Forecast::_onDisconnect(AsyncClient* client) 150 | { 151 | (void)client; 152 | 153 | char *p = m_pBuffer; 154 | m_status = FCS_Done; 155 | if(p == NULL) 156 | return; 157 | if(m_bufIdx == 0) 158 | { 159 | delete m_pBuffer; 160 | return; 161 | } 162 | 163 | m_fcIdx = 0; 164 | m_bFirst = false; 165 | m_lastTm = 0; 166 | 167 | switch(m_type) 168 | { 169 | case 0: 170 | processCDT(); // text file type 171 | break; 172 | case 1: 173 | processOWM(); // json type 174 | break; 175 | } 176 | 177 | m_fc.Data[m_fcIdx].temp = -1000; // mark past last as invalid 178 | delete m_pBuffer; 179 | } 180 | 181 | void Forecast::processOWM() 182 | { 183 | const char *jsonListOw[] = { // root keys 184 | "cod", // 0 (200=good) 185 | "message", // 1 (0) 186 | "cnt", // 2 list count (40) 187 | "list", // 3 the list 188 | "city", // 4 "id", "name", "coord", "country", "population", "timezone", "sunrise", "sunset" 189 | NULL 190 | }; 191 | 192 | char *p = m_pBuffer; 193 | 194 | if(p[0] != '{') // local copy has no headers 195 | while(p[4]) // skip all the header lines 196 | { 197 | if(p[0] == '\r' && p[1] == '\n' && p[2] == '\r' && p[3] == '\n') 198 | { 199 | p += 4; 200 | break; 201 | } 202 | p++; 203 | } 204 | 205 | processJson(p, 0, jsonListOw); 206 | } 207 | 208 | // read data as comma delimited 'time,temp,rh,code' per line 209 | void Forecast::processCDT() 210 | { 211 | const char *p = m_pBuffer; 212 | m_status = FCS_Done; 213 | 214 | while(m_fcIdx < FC_CNT-1 && *p) 215 | { 216 | uint32_t tm = atoi(p); // this should be local time 217 | if(tm > 1700516696) // skip the headers 218 | { 219 | if(!m_bFirst) 220 | { 221 | m_bFirst = true; 222 | m_fcIdx = makeroom(tm); 223 | if(m_fc.Date == 0) 224 | m_fc.Date = tm; 225 | } 226 | else 227 | { 228 | m_fc.Freq = tm - m_lastTm; 229 | } 230 | m_lastTm = tm; 231 | while(*p && *p != ',') p ++; 232 | if(*p == ',') p ++; 233 | else break; 234 | m_fc.Data[m_fcIdx].temp = (atof(p)*10); 235 | while(*p && *p != ',') p ++; 236 | if(*p == ',') p ++; 237 | else break; 238 | m_fc.Data[m_fcIdx].humidity = (atof(p)*10); 239 | while(*p && *p != ',') p ++; 240 | if(*p == ',') p ++; 241 | { 242 | m_fc.Data[m_fcIdx].id = atoi(p); 243 | } 244 | m_fcIdx++; 245 | } 246 | while(*p && *p != '\r' && *p != '\n') p ++; 247 | while(*p == '\r' || *p == '\n') p ++; 248 | } 249 | m_fc.Data[m_fcIdx].temp = -1000; 250 | delete m_pBuffer; 251 | } 252 | 253 | bool Forecast::getCurrentIndex(int8_t& fcOff, int8_t& fcCnt, uint32_t& tm) 254 | { 255 | fcOff = 0; 256 | fcCnt = 0; 257 | tm = m_fc.Date; 258 | 259 | if(m_fc.Date == 0) // not read yet or time not set 260 | { 261 | if(m_bUpdateFcstIdle) 262 | m_bUpdateFcst = true; 263 | return false; 264 | } 265 | 266 | for(fcCnt = 0; fcCnt < FC_CNT && m_fc.Data[fcCnt].temp != -1000; fcCnt++) // get current time in forecast and valid count 267 | { 268 | if( tm + m_fc.Freq < now() - m_tzOffset) 269 | { 270 | fcOff++; 271 | tm += m_fc.Freq; 272 | } 273 | } 274 | 275 | if(fcCnt >= FC_CNT || m_fc.Data[fcOff].temp == -1000 ) // entire list outdated 276 | { 277 | if(m_bUpdateFcstIdle) 278 | m_bUpdateFcst = true; 279 | return false; 280 | } 281 | 282 | return true; 283 | } 284 | 285 | void Forecast::callback(int8_t iEvent, uint8_t iName, int32_t iValue, char *psValue) 286 | { 287 | switch(iEvent) 288 | { 289 | case 0: // root 290 | switch(iName) 291 | { 292 | case 0: // cod 293 | if(iValue != 200) 294 | m_status = FCS_Fail; 295 | break; 296 | case 1: // message 297 | break; 298 | case 2: // cnt 299 | m_fcCnt = iValue; 300 | break; 301 | case 3: // list 302 | { 303 | static const char *jsonList[] = { 304 | "dt", // 0 305 | "main", // 1 306 | "weather", // 2 307 | // "clouds", // 3 308 | // "wind", 309 | // "visibility", 310 | // "pop", 311 | // "sys", 312 | // "dt_txt", 313 | NULL 314 | }; 315 | 316 | processJson(psValue, 1, jsonList); 317 | } 318 | break; 319 | case 4: // city 320 | { 321 | static const char *jsonCity[] = { 322 | "id", // 0 3163858, 323 | "timezone", // 1 7200, 324 | "sunrise", // 2 1661834187, 325 | "sunset", // 3 1661882248 326 | // "name", // "Zocca", 327 | // "coord", // {"lat": 44.34, "lon": 10.99} 328 | // "country", // "IT", 329 | // "population", // 4593, 330 | NULL 331 | }; 332 | processJson(psValue, 4, jsonCity); 333 | } 334 | break; 335 | } 336 | break; 337 | case 1: // list 338 | switch(iName) 339 | { 340 | case 0: // dt 341 | if(!m_bFirst) 342 | { 343 | m_bFirst = true; 344 | m_fcIdx = makeroom(iValue); 345 | if(m_fc.Date == 0) // first time uses external date, subsequent will shift 346 | m_fc.Date = iValue; 347 | } 348 | else 349 | { 350 | m_fc.Freq = iValue - m_lastTm; // figure out frequency of periods 351 | } 352 | m_lastTm = iValue; 353 | break; 354 | case 1: // main 355 | { 356 | const char *jsonList[] = { 357 | "temp", // 0 358 | "feels_like", // 1 359 | "temp_min", // 2 360 | "temp_max", // 3 361 | "pressure", 362 | "humidity", // 5 363 | "temp_kf", 364 | NULL 365 | }; 366 | processJson(psValue, 2, jsonList); 367 | } 368 | break; 369 | case 2: // weather 370 | { 371 | static const char *jsonList[] = { 372 | "id", // 802 373 | "main", // Clouds 374 | "description", // scattered clouds 375 | "icon", // 03d 376 | NULL 377 | }; 378 | processJson(psValue, 3, jsonList); 379 | } 380 | if(m_fcIdx < FC_CNT - 1) 381 | m_fcIdx++; 382 | break; 383 | } 384 | break; 385 | case 2: // main 386 | switch(iName) 387 | { 388 | case 0: // temp 389 | m_fc.Data[m_fcIdx].temp = (atof(psValue)*10); 390 | break; 391 | case 5: // humidity 392 | m_fc.Data[m_fcIdx].humidity = (atoi(psValue)*10); 393 | break; 394 | } 395 | break; 396 | case 3: // weather 397 | switch(iName) 398 | { 399 | case 0: // id 804 400 | m_fc.Data[m_fcIdx].id = atoi(psValue); 401 | break; 402 | case 1: // main "Clouds" 403 | break; 404 | case 2: // description "Overcast clouds" 405 | break; 406 | case 3: // icon 04d (id has more values) 407 | break; 408 | } 409 | break; 410 | case 4: // city 411 | switch(iName) 412 | { 413 | case 0: // id 414 | break; 415 | case 1: // timezone 416 | break; 417 | case 2: // sunrise 418 | break; 419 | case 3: // sunset 420 | break; 421 | } 422 | break; 423 | } 424 | } 425 | 426 | void Forecast::processJson(char *p, int8_t event, const char **jsonList) 427 | { 428 | char *pPair[2]; // param:data pair 429 | int8_t brace = 0; 430 | int8_t bracket = 0; 431 | int8_t inBracket = 0; 432 | int8_t inBrace = 0; 433 | 434 | while(*p) 435 | { 436 | p = skipwhite(p); 437 | if(*p == '{'){p++; brace++;} 438 | if(*p == '['){p++; bracket++;} 439 | if(*p == ',') p++; 440 | p = skipwhite(p); 441 | 442 | bool bInQ = false; 443 | if(*p == '"'){p++; bInQ = true;} 444 | pPair[0] = p; 445 | if(bInQ) 446 | { 447 | while(*p && *p!= '"') p++; 448 | if(*p == '"') *p++ = 0; 449 | }else 450 | { 451 | while(*p && *p != ':') p++; 452 | } 453 | if(*p != ':') 454 | return; 455 | 456 | *p++ = 0; 457 | p = skipwhite(p); 458 | bInQ = false; 459 | if(*p == '{') inBrace = brace+1; // data: { 460 | else if(*p == '['){p++; inBracket = bracket+1;} // data: [ 461 | else if(*p == '"'){p++; bInQ = true;} 462 | pPair[1] = p; 463 | if(bInQ) 464 | { 465 | while(*p && *p!= '"') p++; 466 | if(*p == '"') *p++ = 0; 467 | }else if(inBrace) 468 | { 469 | while(*p && inBrace != brace){ 470 | p++; 471 | if(*p == '{') inBrace++; 472 | if(*p == '}') inBrace--; 473 | } 474 | if(*p=='}') p++; 475 | }else if(inBracket) 476 | { 477 | while(*p && inBracket != bracket){ 478 | p++; 479 | if(*p == '[') inBracket++; 480 | if(*p == ']') inBracket--; 481 | } 482 | if(*p == ']') *p++ = 0; 483 | }else while(*p && *p != ',' && *p != '\r' && *p != '\n') p++; 484 | if(*p) *p++ = 0; 485 | p = skipwhite(p); 486 | if(*p == ',') *p++ = 0; 487 | 488 | inBracket = 0; 489 | inBrace = 0; 490 | p = skipwhite(p); 491 | 492 | if(pPair[0][0]) 493 | { 494 | for(int i = 0; jsonList[i]; i++) 495 | { 496 | if(!strcmp(pPair[0], jsonList[i])) 497 | { 498 | int32_t n = atol(pPair[1]); 499 | if(!strcmp(pPair[1], "true")) n = 1; // bool case 500 | callback(event, i, n, pPair[1]); 501 | break; 502 | } 503 | } 504 | } 505 | 506 | } 507 | } 508 | 509 | char *Forecast::skipwhite(char *p) 510 | { 511 | while(*p == ' ' || *p == '\t' || *p =='\r' || *p == '\n') 512 | p++; 513 | return p; 514 | } 515 | 516 | void Forecast::getMinMax(int16_t& tmin, int16_t& tmax, int8_t offset, int8_t range) 517 | { 518 | // Update min/max 519 | tmax = tmin = m_fc.Data[offset].temp; 520 | 521 | // Get min/max of current forecast 522 | for(int8_t i = offset + 1; i < offset + range && m_fc.Data[i].temp != -1000 && i < FC_CNT; i++) 523 | { 524 | int16_t t = m_fc.Data[i].temp; 525 | if(tmin > t) tmin = t; 526 | if(tmax < t) tmax = t; 527 | } 528 | 529 | if(tmin == tmax) tmax++; // div by 0 check 530 | } 531 | 532 | int16_t Forecast::getCurrentTemp(int& shiftedTemp, uint8_t shiftMins) 533 | { 534 | int8_t fcOff; 535 | int8_t fcCnt; 536 | uint32_t tm; 537 | if(!getCurrentIndex(fcOff, fcCnt, tm)) 538 | return 0; 539 | 540 | int16_t m = minute(); 541 | uint32_t tmNow = now() - m_tzOffset; 542 | int16_t r = m_fc.Freq / 60; // usually 3 hour range (180 m) 543 | 544 | if( tmNow >= tm ) 545 | m = (tmNow - tm) / 60; // offset = minutes past forecast up to 179 546 | 547 | int16_t temp = tween(m_fc.Data[fcOff].temp, m_fc.Data[fcOff+1].temp, m, r); 548 | 549 | m += shiftMins; // get the adjust shift 550 | while(m >= r && fcOff < fcCnt - 2 && m_fc.Data[fcOff + 1].temp != -1000) // skip a window if 3h+ over range 551 | { 552 | fcOff++; 553 | m -= r; 554 | } 555 | 556 | while(m < 0 && fcOff) // skip a window if 3h+ prior to range 557 | { 558 | fcOff--; 559 | m += r; 560 | } 561 | if(m < 0) m = 0; // if just started up 562 | 563 | shiftedTemp = tween(m_fc.Data[fcOff].temp, m_fc.Data[fcOff+1].temp, m, r); 564 | 565 | return temp; 566 | } 567 | 568 | // get value at current minute between hours 569 | int Forecast::tween(int16_t t1, int16_t t2, int m, int r) 570 | { 571 | if(r == 0) r = 1; // div by zero check 572 | float t = (float)(t2 - t1) * (m * 100 / r) / 100; 573 | return (int)(t + (float)t1); 574 | } 575 | -------------------------------------------------------------------------------- /Arduino/forecast.h: -------------------------------------------------------------------------------- 1 | #ifndef FORECAST_H 2 | #define FORECAST_H 3 | 4 | #include 5 | #ifdef ESP32 6 | #include 7 | #else 8 | #include 9 | #endif 10 | 11 | #define APPID "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" // OpenWeathermap APP ID 12 | 13 | #define FC_CNT 74 // Max forecast items 14 | 15 | struct forecastItem 16 | { 17 | int16_t temp; 18 | int16_t humidity; 19 | int16_t id; 20 | }; 21 | 22 | struct forecastData 23 | { 24 | uint32_t Date; 25 | uint32_t loadDate; 26 | uint16_t Freq; 27 | forecastItem Data[FC_CNT]; 28 | }; 29 | 30 | enum FCS_Status 31 | { 32 | FCS_Idle, 33 | FCS_Busy, 34 | FCS_Done, 35 | FCS_ConnectError, 36 | FCS_Fail, 37 | FCS_MemoryError, 38 | }; 39 | 40 | class Forecast 41 | { 42 | public: 43 | Forecast(void); 44 | void init(int16_t tzOff); 45 | 46 | void start(IPAddress serverIP, uint16_t port, bool bCelcius, int8_t type); 47 | void start(char *pCityID, bool bCelcius); // Openweathermmap start 48 | int checkStatus(); 49 | bool getCurrentIndex(int8_t& fcOff, int8_t& fcCnt, uint32_t& tm); // index into stored data at current timeframme 50 | void getMinMax(int16_t& tmin, int16_t& tmax, int8_t offset, int8_t range); 51 | int16_t getCurrentTemp(int& shiftedTemp, uint8_t shiftMins); 52 | int tween(int16_t t1, int16_t t2, int m, int r); 53 | 54 | private: 55 | void _onConnect(AsyncClient* client); 56 | void _onDisconnect(AsyncClient* client); 57 | void _onData(AsyncClient* client, char* data, size_t len); 58 | void processCDT(void); 59 | void processOWM(void); 60 | void processJson(char *p, int8_t event, const char **jsonList); 61 | int makeroom(uint32_t newTm); 62 | char *skipwhite(char *p); 63 | void callback(int8_t iEvent, uint8_t iName, int32_t iValue, char *psValue); 64 | 65 | IPAddress m_serverIP; 66 | AsyncClient m_ac; 67 | char m_cityID[8]; 68 | char *m_pBuffer = NULL; 69 | int m_bufIdx; 70 | int m_fcIdx; 71 | int m_fcCnt; 72 | uint32_t m_lastTm; 73 | bool m_bFirst; 74 | bool m_bDone = false; 75 | bool m_bCelcius; 76 | int m_status; 77 | bool m_bLocal; 78 | int8_t m_type; 79 | int16_t m_tzOffset; 80 | public: 81 | forecastData m_fc; 82 | bool m_bUpdateFcst = true; 83 | bool m_bUpdateFcstIdle = true; 84 | bool m_bFcstUpdated = false; 85 | }; 86 | 87 | #define OWBUF_SIZE 17500 88 | 89 | #endif // FORECAST_H 90 | -------------------------------------------------------------------------------- /Arduino/jsonString.h: -------------------------------------------------------------------------------- 1 | #include "eeMem.h" 2 | 3 | class jsonString 4 | { 5 | public: 6 | jsonString(const char *pLabel = NULL, int rsv = 0) 7 | { 8 | m_cnt = 0; 9 | if(rsv) s.reserve(rsv); 10 | s = String("{"); 11 | if(pLabel) 12 | { 13 | s += "\"cmd\":\""; 14 | s += pLabel, s += "\","; 15 | } 16 | } 17 | 18 | String Close(void) 19 | { 20 | s += "}"; 21 | return s; 22 | } 23 | 24 | void Var(const char *key, int iVal) 25 | { 26 | if(m_cnt) s += ","; 27 | s += "\""; 28 | s += key; 29 | s += "\":"; 30 | s += iVal; 31 | m_cnt++; 32 | } 33 | 34 | void Var(const char *key, uint32_t iVal) 35 | { 36 | if(m_cnt) s += ","; 37 | s += "\""; 38 | s += key; 39 | s += "\":"; 40 | s += iVal; 41 | m_cnt++; 42 | } 43 | 44 | void Var(const char *key, long int iVal) 45 | { 46 | if(m_cnt) s += ","; 47 | s += "\""; 48 | s += key; 49 | s += "\":"; 50 | s += iVal; 51 | m_cnt++; 52 | } 53 | 54 | void Var(const char *key, float fVal) 55 | { 56 | if(m_cnt) s += ","; 57 | s += "\""; 58 | s += key; 59 | s += "\":"; 60 | s += fVal; 61 | m_cnt++; 62 | } 63 | 64 | void Var(const char *key, bool bVal) 65 | { 66 | if(m_cnt) s += ","; 67 | s += "\""; 68 | s += key; 69 | s += "\":"; 70 | s += bVal ? 1:0; 71 | m_cnt++; 72 | } 73 | 74 | void Var(const char *key, const char *sVal) 75 | { 76 | if(m_cnt) s += ","; 77 | s += "\""; 78 | s += key; 79 | s += "\":\""; 80 | s += sVal; 81 | s += "\""; 82 | m_cnt++; 83 | } 84 | 85 | void Var(const char *key, String sVal) 86 | { 87 | if(m_cnt) s += ","; 88 | s += "\""; 89 | s += key; 90 | s += "\":\""; 91 | s += sVal; 92 | s += "\""; 93 | m_cnt++; 94 | } 95 | 96 | void Array(const char *key, uint16_t iVal[], int n) 97 | { 98 | if(m_cnt) s += ","; 99 | s += "\""; 100 | s += key; 101 | s += "\":["; 102 | for(int i = 0; i < n; i++) 103 | { 104 | if(i) s += ","; 105 | s += iVal[i]; 106 | } 107 | s += "]"; 108 | m_cnt++; 109 | } 110 | 111 | // custom arrays for waterbed 112 | void Array(const char *key, Sched sVal[], int n) 113 | { 114 | if(m_cnt) s += ","; 115 | s += "\""; 116 | s += key; 117 | s += "\":["; 118 | 119 | for(int i = 0; i < n; i++) 120 | { 121 | if(i) s += ","; 122 | s += "[\""; s += sVal[i].name; s += "\","; 123 | s += sVal[i].timeSch; 124 | s += ","; s += String( (float)sVal[i].setTemp/10, 1 ); 125 | s += ","; s += String( (float)sVal[i].thresh/10,1 ); 126 | s += "]"; 127 | } 128 | s += "]"; 129 | m_cnt++; 130 | } 131 | 132 | void ArrayCost(const char *key, uint16_t iVal[], int n) 133 | { 134 | if(m_cnt) s += ","; 135 | s += "\""; 136 | s += key; 137 | s += "\":["; 138 | for(int i = 0; i < n; i++) 139 | { 140 | if(i) s += ","; 141 | s += (float)iVal[i]/100; 142 | } 143 | s += "]"; 144 | m_cnt++; 145 | } 146 | 147 | void Array(const char *key, Sensor sns[]) 148 | { 149 | if(m_cnt) s += ","; 150 | s += "\""; 151 | s += key; 152 | s += "\":["; 153 | bool bSend = false; 154 | for(int i = 0; i < SNS_CNT; i++) 155 | { 156 | if(sns[i].IP) 157 | { 158 | if(bSend) s += ","; 159 | bSend = true; 160 | s += "["; 161 | IPAddress ip(sns[i].IP); 162 | s += "\""; 163 | s += ip.toString(); 164 | s += "\""; 165 | s += ","; 166 | s += sns[i].temp; 167 | s += ","; 168 | s += sns[i].rh; 169 | s += ","; 170 | s += sns[i].f.val; 171 | s += ",\""; 172 | s += (char*)&sns[i].ID; 173 | s += "\"]"; 174 | } 175 | } 176 | s += "]"; 177 | m_cnt++; 178 | } 179 | 180 | protected: 181 | String s; 182 | int m_cnt; 183 | }; 184 | -------------------------------------------------------------------------------- /Arduino/music.cpp: -------------------------------------------------------------------------------- 1 | #include "music.h" 2 | 3 | extern void WsPrint(String s); 4 | 5 | // notes of the moledy followed by the duration. 6 | // a 4 means a quarter note, 8 an eighteenth , 16 sixteenth, so on 7 | // !!negative numbers are used to represent dotted notes, 8 | // so -4 means a dotted quarter note, that is, a quarter plus an eighteenth!! 9 | 10 | const int melody_dingdong[] = { 11 | NOTE_B4, 8, NOTE_G4, 4 12 | }; 13 | 14 | const int melody_pacman[] = { 15 | // Pacman 16 | // Score available at https://musescore.com/user/85429/scores/107109 17 | NOTE_B4, 16, NOTE_B5, 16, NOTE_FS5, 16, NOTE_DS5, 16, //1 18 | NOTE_B5, 32, NOTE_FS5, -16, NOTE_DS5, 8, NOTE_C5, 16, 19 | NOTE_C6, 16, NOTE_G6, 16, NOTE_E6, 16, NOTE_C6, 32, NOTE_G6, -16, NOTE_E6, 8, 20 | 21 | NOTE_B4, 16, NOTE_B5, 16, NOTE_FS5, 16, NOTE_DS5, 16, NOTE_B5, 32, //2 22 | NOTE_FS5, -16, NOTE_DS5, 8, NOTE_DS5, 32, NOTE_E5, 32, NOTE_F5, 32, 23 | NOTE_F5, 32, NOTE_FS5, 32, NOTE_G5, 32, NOTE_G5, 32, NOTE_GS5, 32, NOTE_A5, 16, NOTE_B5, 8 24 | }; 25 | 26 | int melody_tetris[] = { 27 | //Based on the arrangement at https://www.flutetunes.com/tunes.php?id=192 28 | 29 | NOTE_E5, 4, NOTE_B4,8, NOTE_C5,8, NOTE_D5,4, NOTE_C5,8, NOTE_B4,8, 30 | NOTE_A4, 4, NOTE_A4,8, NOTE_C5,8, NOTE_E5,4, NOTE_D5,8, NOTE_C5,8, 31 | NOTE_B4, -4, NOTE_C5,8, NOTE_D5,4, NOTE_E5,4, 32 | NOTE_C5, 4, NOTE_A4,4, NOTE_A4,8, NOTE_A4,4, NOTE_B4,8, NOTE_C5,8, 33 | 34 | NOTE_D5, -4, NOTE_F5,8, NOTE_A5,4, NOTE_G5,8, NOTE_F5,8, 35 | NOTE_E5, -4, NOTE_C5,8, NOTE_E5,4, NOTE_D5,8, NOTE_C5,8, 36 | NOTE_B4, 4, NOTE_B4,8, NOTE_C5,8, NOTE_D5,4, NOTE_E5,4, 37 | NOTE_C5, 4, NOTE_A4,4, NOTE_A4,4, REST, 4, 38 | 39 | NOTE_E5, 4, NOTE_B4,8, NOTE_C5,8, NOTE_D5,4, NOTE_C5,8, NOTE_B4,8, 40 | NOTE_A4, 4, NOTE_A4,8, NOTE_C5,8, NOTE_E5,4, NOTE_D5,8, NOTE_C5,8, 41 | NOTE_B4, -4, NOTE_C5,8, NOTE_D5,4, NOTE_E5,4, 42 | NOTE_C5, 4, NOTE_A4,4, NOTE_A4,8, NOTE_A4,4, NOTE_B4,8, NOTE_C5,8, 43 | 44 | NOTE_D5, -4, NOTE_F5,8, NOTE_A5,4, NOTE_G5,8, NOTE_F5,8, 45 | NOTE_E5, -4, NOTE_C5,8, NOTE_E5,4, NOTE_D5,8, NOTE_C5,8, 46 | NOTE_B4, 4, NOTE_B4,8, NOTE_C5,8, NOTE_D5,4, NOTE_E5,4, 47 | NOTE_C5, 4, NOTE_A4,4, NOTE_A4,4, REST, 4, 48 | 49 | 50 | NOTE_E5,2, NOTE_C5,2, 51 | NOTE_D5,2, NOTE_B4,2, 52 | NOTE_C5,2, NOTE_A4,2, 53 | NOTE_GS4,2, NOTE_B4,4, REST,8, 54 | NOTE_E5,2, NOTE_C5,2, 55 | NOTE_D5,2, NOTE_B4,2, 56 | NOTE_C5,4, NOTE_E5,4, NOTE_A5,2, 57 | NOTE_GS5,2, 58 | }; 59 | 60 | const int melody_DarthVader[] = { 61 | 62 | // Dart Vader theme (Imperial March) - Star wars 63 | // Score available at https://musescore.com/user/202909/scores/1141521 64 | // The tenor saxophone part was used 65 | 66 | NOTE_AS4,8, NOTE_AS4,8, NOTE_AS4,8,//1 67 | NOTE_F5,2, NOTE_C6,2, 68 | NOTE_AS5,8, NOTE_A5,8, NOTE_G5,8, NOTE_F6,2, NOTE_C6,4, 69 | NOTE_AS5,8, NOTE_A5,8, NOTE_G5,8, NOTE_F6,2, NOTE_C6,4, 70 | NOTE_AS5,8, NOTE_A5,8, NOTE_AS5,8, NOTE_G5,2, NOTE_C5,8, NOTE_C5,8, NOTE_C5,8, 71 | NOTE_F5,2, NOTE_C6,2, 72 | NOTE_AS5,8, NOTE_A5,8, NOTE_G5,8, NOTE_F6,2, NOTE_C6,4, 73 | 74 | NOTE_AS5,8, NOTE_A5,8, NOTE_G5,8, NOTE_F6,2, NOTE_C6,4, //8 75 | NOTE_AS5,8, NOTE_A5,8, NOTE_AS5,8, NOTE_G5,2, NOTE_C5,-8, NOTE_C5,16, 76 | NOTE_D5,-4, NOTE_D5,8, NOTE_AS5,8, NOTE_A5,8, NOTE_G5,8, NOTE_F5,8, 77 | NOTE_F5,8, NOTE_G5,8, NOTE_A5,8, NOTE_G5,4, NOTE_D5,8, NOTE_E5,4,NOTE_C5,-8, NOTE_C5,16, 78 | NOTE_D5,-4, NOTE_D5,8, NOTE_AS5,8, NOTE_A5,8, NOTE_G5,8, NOTE_F5,8, 79 | 80 | NOTE_C6,-8, NOTE_G5,16, NOTE_G5,2, REST,8, NOTE_C5,8,//13 81 | NOTE_D5,-4, NOTE_D5,8, NOTE_AS5,8, NOTE_A5,8, NOTE_G5,8, NOTE_F5,8, 82 | NOTE_F5,8, NOTE_G5,8, NOTE_A5,8, NOTE_G5,4, NOTE_D5,8, NOTE_E5,4,NOTE_C6,-8, NOTE_C6,16, 83 | NOTE_F6,4, NOTE_DS6,8, NOTE_CS6,4, NOTE_C6,8, NOTE_AS5,4, NOTE_GS5,8, NOTE_G5,4, NOTE_F5,8, 84 | NOTE_C6,1 85 | }; 86 | 87 | const int melody_silentnight[] = { 88 | 89 | // Silent Night, Original Version 90 | // Score available at https://musescore.com/marcsabatella/scores/3123436 91 | 92 | NOTE_G4,-4, NOTE_A4,8, NOTE_G4,4, 93 | NOTE_E4,-2, 94 | NOTE_G4,-4, NOTE_A4,8, NOTE_G4,4, 95 | NOTE_E4,-2, 96 | NOTE_D5,2, NOTE_D5,4, 97 | NOTE_B4,-2, 98 | NOTE_C5,2, NOTE_C5,4, 99 | NOTE_G4,-2, 100 | 101 | NOTE_A4,2, NOTE_A4,4, 102 | NOTE_C5,-4, NOTE_B4,8, NOTE_A4,4, 103 | NOTE_G4,-4, NOTE_A4,8, NOTE_G4,4, 104 | NOTE_E4,-2, 105 | NOTE_A4,2, NOTE_A4,4, 106 | NOTE_C5,-4, NOTE_B4,8, NOTE_A4,4, 107 | NOTE_G4,-4, NOTE_A4,8, NOTE_G4,4, 108 | NOTE_E4,-2, 109 | 110 | NOTE_D5,2, NOTE_D5,4, 111 | NOTE_F5,-4, NOTE_D5,8, NOTE_B4,4, 112 | NOTE_C5,-2, 113 | NOTE_E5,-2, 114 | NOTE_C5,4, NOTE_G4,4, NOTE_E4,4, 115 | NOTE_G4,-4, NOTE_F4,8, NOTE_D4,4, 116 | NOTE_C4,-2, 117 | NOTE_C4,-1, 118 | }; 119 | 120 | const int melody_Nokia[] = { 121 | 122 | // Nokia Ringtone 123 | // Score available at https://musescore.com/user/29944637/scores/5266155 124 | 125 | NOTE_E5, 8, NOTE_D5, 8, NOTE_FS4, 4, NOTE_GS4, 4, 126 | NOTE_CS5, 8, NOTE_B4, 8, NOTE_D4, 4, NOTE_E4, 4, 127 | NOTE_B4, 8, NOTE_A4, 8, NOTE_CS4, 4, NOTE_E4, 4, 128 | NOTE_A4, 2, 129 | }; 130 | 131 | const int melody_imperial[] = { 132 | 133 | // Dart Vader theme (Imperial March) - Star wars 134 | // Score available at https://musescore.com/user/202909/scores/1141521 135 | // The tenor saxophone part was used 136 | 137 | NOTE_A4,-4, NOTE_A4,-4, NOTE_A4,16, NOTE_A4,16, NOTE_A4,16, NOTE_A4,16, NOTE_F4,8, REST,8, 138 | NOTE_A4,-4, NOTE_A4,-4, NOTE_A4,16, NOTE_A4,16, NOTE_A4,16, NOTE_A4,16, NOTE_F4,8, REST,8, 139 | NOTE_A4,4, NOTE_A4,4, NOTE_A4,4, NOTE_F4,-8, NOTE_C5,16, 140 | 141 | NOTE_A4,4, NOTE_F4,-8, NOTE_C5,16, NOTE_A4,2,//4 142 | NOTE_E5,4, NOTE_E5,4, NOTE_E5,4, NOTE_F5,-8, NOTE_C5,16, 143 | NOTE_A4,4, NOTE_F4,-8, NOTE_C5,16, NOTE_A4,2, 144 | 145 | NOTE_A5,4, NOTE_A4,-8, NOTE_A4,16, NOTE_A5,4, NOTE_GS5,-8, NOTE_G5,16, //7 146 | NOTE_DS5,16, NOTE_D5,16, NOTE_DS5,8, REST,8, NOTE_A4,8, NOTE_DS5,4, NOTE_D5,-8, NOTE_CS5,16, 147 | 148 | NOTE_C5,16, NOTE_B4,16, NOTE_C5,16, REST,8, NOTE_F4,8, NOTE_GS4,4, NOTE_F4,-8, NOTE_A4,-16,//9 149 | NOTE_C5,4, NOTE_A4,-8, NOTE_C5,16, NOTE_E5,2, 150 | 151 | NOTE_A5,4, NOTE_A4,-8, NOTE_A4,16, NOTE_A5,4, NOTE_GS5,-8, NOTE_G5,16, //7 152 | NOTE_DS5,16, NOTE_D5,16, NOTE_DS5,8, REST,8, NOTE_A4,8, NOTE_DS5,4, NOTE_D5,-8, NOTE_CS5,16, 153 | 154 | NOTE_C5,16, NOTE_B4,16, NOTE_C5,16, REST,8, NOTE_F4,8, NOTE_GS4,4, NOTE_F4,-8, NOTE_A4,-16,//9 155 | NOTE_A4,4, NOTE_F4,-8, NOTE_C5,16, NOTE_A4,2, 156 | 157 | }; 158 | 159 | int melody_startrek[] = { 160 | // Star Trek Intro 161 | // Score available at https://musescore.com/user/10768291/scores/4594271 162 | 163 | NOTE_D4, -8, NOTE_G4, 16, NOTE_C5, -4, 164 | NOTE_B4, 8, NOTE_G4, -16, NOTE_E4, -16, NOTE_A4, -16, 165 | NOTE_D5, 2, 166 | 167 | }; 168 | 169 | void Music::init() 170 | { 171 | #ifdef ESP32 172 | pinMode(SPEAKER, OUTPUT); 173 | digitalWrite(SPEAKER, LOW); 174 | ledcSetup(0, 2000, 10); 175 | ledcAttachPin(SPEAKER, 0); 176 | #endif 177 | 178 | } 179 | 180 | bool Music::add(uint16_t freq, uint16_t delay) 181 | { 182 | if(m_bPlaying) 183 | { 184 | if(m_idx >= MUS_LEN) 185 | return false; 186 | m_arr[m_idx].note = freq; 187 | m_arr[m_idx].ms = delay; 188 | m_idx++; 189 | } 190 | else 191 | { 192 | playNote(freq, delay); 193 | m_bPlaying = true; 194 | } 195 | return true; 196 | } 197 | 198 | void Music::playNote(int freq, int duration) 199 | { 200 | if(freq) 201 | { 202 | #ifdef ESP32 203 | ledcWriteTone(0, freq); 204 | int duty = 512 + freq * 500; 205 | ledcWrite(SPEAKER, duty); 206 | #else 207 | analogWriteFreq(freq); 208 | analogWrite(SPEAKER, 40); 209 | #endif 210 | } 211 | else 212 | { 213 | #ifdef ESP32 214 | ledcWrite(SPEAKER, 0); 215 | ledcWriteTone(0, 0); 216 | #else 217 | analogWrite(SPEAKER, 0); 218 | #endif 219 | } 220 | m_toneEnd = millis() + duration; 221 | } 222 | 223 | bool Music::play(int song) 224 | { 225 | int tempo = 100; 226 | int notes; 227 | const int *pSong; 228 | 229 | switch(song) 230 | { 231 | case 0: 232 | tempo = 60; 233 | notes = sizeof(melody_dingdong) / sizeof(melody_dingdong[0]) / 2; 234 | pSong = melody_dingdong; 235 | break; 236 | case 1: 237 | tempo = 80; 238 | notes = sizeof(melody_startrek) / sizeof(melody_startrek[0]) / 2; 239 | pSong = melody_startrek; 240 | break; 241 | case 2: 242 | tempo = 140; 243 | notes = sizeof(melody_silentnight) / sizeof(melody_silentnight[0]) / 2; 244 | pSong = melody_silentnight; 245 | break; 246 | case 3: 247 | tempo = 108; 248 | notes = sizeof(melody_DarthVader) / sizeof(melody_DarthVader[0]) / 2; 249 | pSong = melody_DarthVader; 250 | break; 251 | case 4: 252 | tempo = 180; 253 | notes = sizeof(melody_Nokia) / sizeof(melody_Nokia[0]) / 2; 254 | pSong = melody_Nokia; 255 | break; 256 | case 5: 257 | tempo = 120; 258 | notes = sizeof(melody_imperial) / sizeof(melody_imperial[0]) / 2; 259 | pSong = melody_imperial; 260 | break; 261 | default: 262 | return false; 263 | } 264 | if(notes >= MUS_LEN - m_idx) 265 | notes = MUS_LEN - m_idx; 266 | 267 | // this calculates the duration of a whole note in ms 268 | int wholenote = (60000 * 4) / tempo; 269 | int divider = 0, noteDuration = 0; 270 | 271 | for (int thisNote = 0; thisNote < notes * 2; thisNote = thisNote + 2) 272 | { 273 | // calculates the duration of each note 274 | divider = pSong[thisNote + 1]; 275 | if (divider > 0) { 276 | // regular note, just proceed 277 | noteDuration = wholenote / divider; 278 | } else if (divider < 0) { 279 | // dotted notes are represented with negative durations!! 280 | noteDuration = wholenote / abs(divider); 281 | noteDuration *= 1.5; // increases the duration in half for dotted notes 282 | } 283 | 284 | // we only play the note for 90% of the duration, leaving 10% as a pause 285 | add(pSong[thisNote], noteDuration * 0.9); 286 | } 287 | return true; 288 | } 289 | 290 | void Music::service() 291 | { 292 | if(m_bPlaying == false) 293 | return; 294 | else if(millis() >= m_toneEnd) 295 | { 296 | #ifdef ESP32 297 | ledcWrite(SPEAKER, 0); 298 | ledcWriteTone(0, 0); 299 | #else 300 | analogWrite(SPEAKER, 0); 301 | #endif 302 | m_toneEnd = 0; 303 | m_bPlaying = false; 304 | } 305 | else 306 | { 307 | return; 308 | } 309 | if(m_idx <= 0) 310 | return; 311 | playNote(m_arr[0].note, m_arr[0].ms); 312 | memcpy(m_arr, m_arr + 1, sizeof(musicArr) * MUS_LEN); 313 | m_idx--; 314 | m_bPlaying = true; 315 | } 316 | -------------------------------------------------------------------------------- /Arduino/music.h: -------------------------------------------------------------------------------- 1 | #ifndef MUSIC_H 2 | #define MUSIC_H 3 | 4 | #define SPEAKER 17 // Speaker IO pin 5 | 6 | #include 7 | 8 | // More songs and code available at https://github.com/robsoncouto/arduino-songs 9 | // Robson Couto, 2019 10 | 11 | #define NOTE_B0 31 12 | #define NOTE_C1 33 13 | #define NOTE_CS1 35 14 | #define NOTE_D1 37 15 | #define NOTE_DS1 39 16 | #define NOTE_E1 41 17 | #define NOTE_F1 44 18 | #define NOTE_FS1 46 19 | #define NOTE_G1 49 20 | #define NOTE_GS1 52 21 | #define NOTE_A1 55 22 | #define NOTE_AS1 58 23 | #define NOTE_B1 62 24 | #define NOTE_C2 65 25 | #define NOTE_CS2 69 26 | #define NOTE_D2 73 27 | #define NOTE_DS2 78 28 | #define NOTE_E2 82 29 | #define NOTE_F2 87 30 | #define NOTE_FS2 93 31 | #define NOTE_G2 98 32 | #define NOTE_GS2 104 33 | #define NOTE_A2 110 34 | #define NOTE_AS2 117 35 | #define NOTE_B2 123 36 | #define NOTE_C3 131 37 | #define NOTE_CS3 139 38 | #define NOTE_D3 147 39 | #define NOTE_DS3 156 40 | #define NOTE_E3 165 41 | #define NOTE_F3 175 42 | #define NOTE_FS3 185 43 | #define NOTE_G3 196 44 | #define NOTE_GS3 208 45 | #define NOTE_A3 220 46 | #define NOTE_AS3 233 47 | #define NOTE_B3 247 48 | #define NOTE_C4 262 49 | #define NOTE_CS4 277 50 | #define NOTE_D4 294 51 | #define NOTE_DS4 311 52 | #define NOTE_E4 330 53 | #define NOTE_F4 349 54 | #define NOTE_FS4 370 55 | #define NOTE_G4 392 56 | #define NOTE_GS4 415 57 | #define NOTE_A4 440 58 | #define NOTE_AS4 466 59 | #define NOTE_B4 494 60 | #define NOTE_C5 523 61 | #define NOTE_CS5 554 62 | #define NOTE_D5 587 63 | #define NOTE_DS5 622 64 | #define NOTE_E5 659 65 | #define NOTE_F5 698 66 | #define NOTE_FS5 740 67 | #define NOTE_G5 784 68 | #define NOTE_GS5 831 69 | #define NOTE_A5 880 70 | #define NOTE_AS5 932 71 | #define NOTE_B5 988 72 | #define NOTE_C6 1047 73 | #define NOTE_CS6 1109 74 | #define NOTE_D6 1175 75 | #define NOTE_DS6 1245 76 | #define NOTE_E6 1319 77 | #define NOTE_F6 1397 78 | #define NOTE_FS6 1480 79 | #define NOTE_G6 1568 80 | #define NOTE_GS6 1661 81 | #define NOTE_A6 1760 82 | #define NOTE_AS6 1865 83 | #define NOTE_B6 1976 84 | #define NOTE_C7 2093 85 | #define NOTE_CS7 2217 86 | #define NOTE_D7 2349 87 | #define NOTE_DS7 2489 88 | #define NOTE_E7 2637 89 | #define NOTE_F7 2794 90 | #define NOTE_FS7 2960 91 | #define NOTE_G7 3136 92 | #define NOTE_GS7 3322 93 | #define NOTE_A7 3520 94 | #define NOTE_AS7 3729 95 | #define NOTE_B7 3951 96 | #define NOTE_C8 4186 97 | #define NOTE_CS8 4435 98 | #define NOTE_D8 4699 99 | #define NOTE_DS8 4978 100 | #define REST 0 101 | 102 | struct musicArr{ 103 | uint16_t note; 104 | uint16_t ms; 105 | }; 106 | 107 | class Music 108 | { 109 | public: 110 | Music(){}; 111 | void init(void); 112 | bool add(uint16_t freq, uint16_t delay); 113 | void service(void); 114 | bool play(int song); 115 | protected: 116 | void playNote(int freq, int duration); 117 | #define MUS_LEN 99 118 | musicArr m_arr[MUS_LEN+1]; 119 | int16_t m_idx; 120 | bool m_bPlaying; 121 | uint32_t m_toneEnd; 122 | }; 123 | 124 | #endif 125 | -------------------------------------------------------------------------------- /Eagle/bom.txt: -------------------------------------------------------------------------------- 1 | ESP-07 eBay 2 | USB Mini-B Only use for powering remote unit with wall adapter 3 | Nextion HMI 2.8" eBay 4 | AM2302/DHT22 Don't use with SHT21 5 | SHT21 Don't use with DHT22 6 | R1-R5 * 0805 470 7 | R6-R9 * 0805 10K (do not remove R9 - GPIO15 must be low on boot) 8 | R10,R11 0805 10K 9 | R12,R13 0805 4K7 10 | R14,R15 ADC voltage divider (not used) 11 | R16,R17 5K6 12 | R18 0 jumper (Do not use: Allows for I2C device in place of DHT22) 13 | R19 10K 0805 (pullup in case 10K isn't removed from Nextion) 14 | R20 * 4K7 0805 15 | C1 * UWD1V150MCL1 35V 15uF 16 | C2 * UCL1A221MCL1GS 10V 220uF 17 | C3,C4 0805 16V 0.1uF 18 | C5,C6 0805 10V 0.01uF 19 | C7,C8 0805 16V 1uF 20 | C9 0805 16V 0.1uF 21 | F1 * 1812 PPTC Fuse 1.5A PTS181224V150 22 | LCD,NEXPROG 4-pin 0.1" header. Same pinout. Use either for serial programming of display. 23 | J1 ADC expansion. Not used. 24 | PIC10F * Not used. Watchdog connected to SCL for monitor, RESET output. 25 | 5VSWPP * MIC4680-5.0YM 26 | 3V3R MCP1755T-3302E/OT 27 | L1 * SRU1048-680Y 28 | K1-K5 * AQY282SZ or AQY282S (3 main, 1 for HP, 1 for humidifier) 29 | B1 * DF206S-G 30 | D1 * B260A-13-F 31 | X1 * 39357-0008 Screw terimal, 8 position 3.5mm pitch 32 | Encoder EC12E24204A2 or other 12mm short shaft 33 | Q1 * MMBT2907ALT1G (general purpose BJT PNP) GPIO0 must be high or Z on boot. 34 | PRG,TXRX 2-pin headers. Use for serial programming ESP, debug, Nextion simulator. 35 | 36 | * Only for main controller 37 | -------------------------------------------------------------------------------- /Eagle/eagle.epf: -------------------------------------------------------------------------------- 1 | [Eagle] 2 | Version="07 06 00" 3 | Platform="Windows" 4 | Serial="62191E841E-LSR-WLM-1EL" 5 | Globals="Globals" 6 | Desktop="Desktop" 7 | 8 | [Globals] 9 | AutoSaveProject=1 10 | UsedLibrary="C:/Program Files/Eagle/lbr/Special/CuriousTech.lbr" 11 | UsedLibrary="C:/Program Files/Eagle/lbr/SparkFun/SparkFun-Connectors.lbr" 12 | UsedLibrary="C:/Program Files/Eagle/lbr/SparkFun/SparkFun-DiscreteSemi.lbr" 13 | UsedLibrary="C:/Program Files/Eagle/lbr/SparkFun/SparkFun-Retired.lbr" 14 | UsedLibrary="C:/Program Files/Eagle/lbr/Special/SHT21.lbr" 15 | 16 | [Win_1] 17 | Type="Schematic Editor" 18 | Loc="361 84 1708 797" 19 | State=2 20 | Number=2 21 | File="Thermostat.sch" 22 | View="0.282672 -45.4862 508.961 241.33" 23 | WireWidths=" 0 0.3048 0.6096 0.8128 1.016 1.27 1.4224 1.6764 1.778 1.9304 2.1844 2.54 3.81 6.4516 0.4064 0.1524" 24 | PadDiameters=" 0.254 0.3048 0.4064 0.6096 0.8128 1.016 1.27 1.4224 1.6764 1.778 1.9304 2.1844 2.54 3.81 6.4516 0" 25 | PadDrills=" 0.5 0.6 0.7 0.9 1 1.1 1.2 1.3 1.4 1.5 1.6 2 2.2 2.8 3.2 0.8" 26 | ViaDiameters=" 0.254 0.3048 0.4064 0.6096 0.8128 1.016 1.27 1.4224 1.6764 1.778 1.9304 2.1844 2.54 3.81 6.4516 0" 27 | ViaDrills=" 0.5 0.7 0.8 0.9 1 1.1 1.2 1.3 1.4 1.5 1.6 2 2.2 2.8 3.2 0.6" 28 | HoleDrills=" 0.5 0.7 0.8 0.9 1 1.1 1.2 1.3 1.4 1.5 1.6 2 2.2 2.8 3.2 0.6" 29 | TextSizes=" 0.254 0.3048 0.4064 0.6096 0.8128 1.016 1.27 1.4224 1.6764 1.9304 2.1844 2.54 3.81 5.08 6.4516 1.778" 30 | PolygonSpacings=" 0.254 0.3048 0.4064 0.6096 0.8128 1.016 1.4224 1.6764 1.778 1.9304 2.1844 2.54 3.81 5.08 6.4516 1.27" 31 | PolygonIsolates=" 0.254 0.3048 0.4064 0.6096 0.8128 1.016 1.27 1.4224 1.6764 1.778 1.9304 2.1844 2.54 3.81 6.4516 0" 32 | MiterRadiuss=" 0.254 0.3175 0.635 1.27 2.54 1 2 2.5 5 7.5 10 0" 33 | DimensionWidths=" 0 0.127 0.254 0.1 0.26 0.13" 34 | DimensionExtWidths=" 0.127 0.254 0.1 0.13 0.26 0" 35 | DimensionExtLengths=" 1.27 2.54 1 2 3 0" 36 | DimensionExtOffsets=" 1.27 2.54 1 2 3 0" 37 | SmdSizes=" 0.3048 0.1524 0.4064 0.2032 0.6096 0.3048 0.8128 0.4064 1.016 0.508 1.27 0.6604 1.4224 0.7112 1.6764 0.8128 1.778 0.9144 1.9304 0.9652 2.1844 1.0668 2.54 1.27 3.81 1.9304 5.08 2.54 6.4516 3.2512 1.27 0.635" 38 | WireBend=1 39 | WireBendSet=31 40 | WireCap=1 41 | MiterStyle=0 42 | PadShape=0 43 | ViaShape=0 44 | PolygonPour=0 45 | PolygonRank=0 46 | PolygonThermals=1 47 | PolygonOrphans=0 48 | TextRatio=8 49 | DimensionUnit=1 50 | DimensionPrecision=2 51 | DimensionShowUnit=0 52 | PinDirection=3 53 | PinFunction=0 54 | PinLength=2 55 | PinVisible=3 56 | SwapLevel=0 57 | ArcDirection=0 58 | AddLevel=2 59 | PadsSameType=0 60 | Layer=91 61 | Views=" 1: 0.282672 -45.4862 508.961 241.33" 62 | Sheet="1" 63 | 64 | [Win_2] 65 | Type="Board Editor" 66 | Loc="586 169 1773 989" 67 | State=2 68 | Number=1 69 | File="Thermostat.brd" 70 | View="33.726 -14.4389 46.7706 41.6531" 71 | WireWidths=" 1.9304 2.1844 2.54 3.81 6.4516 1.778 1.6764 1.4224 1.016 1.27 0.8128 0 0.6096 0.4064 0.3048 0.254" 72 | PadDiameters=" 0.254 0.3048 0.4064 0.6096 0.8128 1.016 1.27 1.4224 1.6764 1.778 1.9304 2.1844 2.54 3.81 6.4516 0" 73 | PadDrills=" 0.5 0.6 0.7 0.9 1 1.1 1.2 1.3 1.4 1.5 1.6 2 2.2 2.8 3.2 0.8" 74 | ViaDiameters=" 0.254 0.3048 0.4064 0.6096 0.8128 1.016 1.27 1.4224 1.6764 1.778 1.9304 2.1844 2.54 3.81 6.4516 0" 75 | ViaDrills=" 0.7 0.8 1.1 1.2 1.3 1.4 1.5 1.6 2.2 3.2 0.6 0.9 1 2 2.8 0.5" 76 | HoleDrills=" 0.5 0.7 0.8 0.9 1 1.1 1.2 1.3 1.4 1.5 1.6 2 2.2 2.8 3.2 0.6" 77 | TextSizes=" 0.254 0.3048 0.4064 0.6096 1.9304 2.1844 3.81 5.08 6.4516 1.778 2.54 1.6764 1.27 1.4224 1.016 0.8128" 78 | PolygonSpacings=" 0.254 0.3048 0.4064 0.6096 0.8128 1.016 1.4224 1.6764 1.778 1.9304 2.1844 2.54 3.81 5.08 6.4516 1.27" 79 | PolygonIsolates=" 0.3048 0.4064 0.6096 0.8128 1.016 1.27 1.4224 1.6764 1.778 1.9304 2.1844 2.54 3.81 6.4516 0 0.254" 80 | MiterRadiuss=" 0.254 0.3175 0.635 1 5 7.5 10 1.27 2 2.5 2.54 0" 81 | DimensionWidths=" 0 0.127 0.254 0.1 0.26 0.13" 82 | DimensionExtWidths=" 0.127 0.254 0.1 0.13 0.26 0" 83 | DimensionExtLengths=" 1.27 2.54 1 2 3 0" 84 | DimensionExtOffsets=" 1.27 2.54 1 2 3 0" 85 | SmdSizes=" 0.3048 0.1524 0.4064 0.2032 0.6096 0.3048 0.8128 0.4064 1.016 0.508 1.27 0.6604 1.4224 0.7112 1.6764 0.8128 1.778 0.9144 1.9304 0.9652 2.1844 1.0668 2.54 1.27 3.81 1.9304 5.08 2.54 6.4516 3.2512 1.27 0.635" 86 | WireBend=1 87 | WireBendSet=0 88 | WireCap=1 89 | MiterStyle=0 90 | PadShape=0 91 | ViaShape=0 92 | PolygonPour=0 93 | PolygonRank=1 94 | PolygonThermals=1 95 | PolygonOrphans=1 96 | TextRatio=16 97 | DimensionUnit=1 98 | DimensionPrecision=2 99 | DimensionShowUnit=0 100 | PinDirection=3 101 | PinFunction=0 102 | PinLength=2 103 | PinVisible=3 104 | SwapLevel=0 105 | ArcDirection=0 106 | AddLevel=2 107 | PadsSameType=0 108 | Layer=16 109 | 110 | [Win_3] 111 | Type="Control Panel" 112 | Loc="53 189 1098 1084" 113 | State=2 114 | Number=0 115 | 116 | [Desktop] 117 | Screen="1920 1680" 118 | Window="Win_1" 119 | Window="Win_2" 120 | Window="Win_3" 121 | -------------------------------------------------------------------------------- /EagleESP32/bom.txt: -------------------------------------------------------------------------------- 1 | PCB OSHPark $17.55 for 3 2 | 3 | ESP-32 eBay 4 | USB Micro-B ZX62D-B-5PA8(30) or common eBay (remote only) 5 | Nextion HMI 2.8" eBay 6 | 3V3R MCP1755T-3302E/OT or MCP1802T-3302 7 | TXRX,LCD 0.1" headers. Use for serial programming ESP, debug, Nextion. 8 | 9 | rh/temp sensor options: 10 | AM2320 (larger 4 pin, i2c) 11 | AM2322 (lower 4 pin, i2c) 12 | SHT21 DFN-6 13 | Si7021 cheaper DFN-6 14 | Si7034 super cheap 15 | 16 | R1-R5 * 0805 470 (390 lowest) 17 | R7 * 0805 470 18 | R8,R9 0805 10K 19 | R10,R11 0805 4K7 20 | R12 0805 10K (pullup in case 10K isn't removed from Nextion) 21 | R13,R15 0805 1K 22 | R14 0805 5K1 23 | 24 | F1 * 1812 PPTC Fuse 1.5A PTS181224V150 25 | BR * MDB6S 26 | VREG * LMR16006YQ5DDCRQ1 27 | L1 * 0805 6.8uH IFSC0806AZER6R8M01 28 | D1,D2 (*) SOT-223 NSVR0320MW2T1G 29 | C1 or C2 * 10uF+ 25V+ 5mm (25SEPF56M 56uF throughole) 30 | C3-C6 * 0805 16V 0.1uF 31 | C7 * 0805 16V 1uF 32 | C8 * 0805 16V 10uF 33 | C9 0603 16V 0.1uF (if SHTxx used) 34 | C10 * 0805 2.2uF 35 | 36 | Q1 PMV20ENR (for future speaker) 37 | SPK 20mm-36mm 32 ohm speaker 38 | K1-K5 * G3VM-61GR2 39 | X1 * 39357-0008 (or 691322110008 and 691361100008) Screw terimal, 8 position 3.5mm pitch 40 | Button TL1014BF160QG 41 | 42 | * Only for main controller 43 | -------------------------------------------------------------------------------- /EagleESP32/eagle.epf: -------------------------------------------------------------------------------- 1 | [Eagle] 2 | Version="09 06 02" 3 | Platform="Windows" 4 | Globals="Globals" 5 | Desktop="Desktop" 6 | 7 | [Globals] 8 | AutoSaveProject=1 9 | UsedLibrary="C:/Users/Greg/Documents/Eagle/lbr/SparkFun/SparkFun-Connectors.lbr" 10 | UsedLibrary="C:/Users/Greg/Documents/Eagle/lbr/seeed/Seeed-OPL-Connector.lbr" 11 | UsedLibrary="C:/Users/Greg/Documents/Eagle/lbr/SparkFun/SparkFun-Electromechanical.lbr" 12 | UsedLibrary="C:/Users/Greg/Documents/Eagle/lbr/SparkFun/SparkFun-Capacitors.lbr" 13 | UsedLibrary="C:/Users/Greg/Documents/EAGLE/libraries/Misc/CuriousTech.lbr" 14 | UsedLibrary="C:/Users/Greg/Documents/EAGLE/libraries/SparkFun/SparkFun-DiscreteSemi.lbr" 15 | UsedLibrary="C:/Users/Greg/Documents/EAGLE/libraries/Misc/esp32.lbr" 16 | 17 | [Win_1] 18 | Type="Schematic Editor" 19 | Number=2 20 | File="Thermostat.sch" 21 | View="34.3112 118.72 79.887 145.047" 22 | WireWidths=" 0.0762 0.1016 0.127 0.15 0.2 0.2032 0.254 0.3048 0.4064 0.508 0.6096 0.8128 1.016 1.27 2.54 0.1524" 23 | PadDiameters=" 0.254 0.3048 0.4064 0.6096 0.8128 1.016 1.27 1.4224 1.6764 1.778 1.9304 2.1844 2.54 3.81 6.4516 0" 24 | PadDrills=" 0.2 0.25 0.3 0.35 0.4 0.45 0.5 0.55 0.65 0.7 0.75 0.8 0.85 0.9 1 0.6" 25 | ViaDiameters=" 0.55 0.6 0.65 0.7 0.75 0.8 0.85 0.9 0.95 1 1.05 1.1 1.15 1.2 1.3 0" 26 | ViaDrills=" 0.2 0.25 0.3 0.4 0.45 0.5 0.55 0.6 0.65 0.7 0.75 0.8 0.85 0.9 1 0.35" 27 | HoleDrills=" 0.2 0.25 0.3 0.4 0.45 0.5 0.55 0.6 0.65 0.7 0.75 0.8 0.85 0.9 1 0.35" 28 | TextSizes=" 0.254 0.3048 0.4064 0.6096 0.8128 1.016 1.27 1.4224 1.6764 1.9304 2.1844 2.54 3.81 5.08 6.4516 1.778" 29 | PolygonSpacings=" 0.254 0.3048 0.4064 0.6096 0.8128 1.016 1.4224 1.6764 1.778 1.9304 2.1844 2.54 3.81 5.08 6.4516 1.27" 30 | PolygonIsolates=" 0.254 0.3048 0.4064 0.6096 0.8128 1.016 1.27 1.4224 1.6764 1.778 1.9304 2.1844 2.54 3.81 6.4516 0" 31 | MiterRadiuss=" 0.254 0.3175 0.635 1.27 2.54 1 2 2.5 5 7.5 10 0" 32 | DimensionWidths=" 0 0.127 0.254 0.1 0.26 0.13" 33 | DimensionExtWidths=" 0.127 0.254 0.1 0.13 0.26 0" 34 | DimensionExtLengths=" 1.27 2.54 1 2 3 0" 35 | DimensionExtOffsets=" 1.27 2.54 1 2 3 0" 36 | SmdSizes=" 0.3048 0.1524 0.4064 0.2032 0.6096 0.3048 0.8128 0.4064 1.016 0.508 1.27 0.6604 1.4224 0.7112 1.6764 0.8128 1.778 0.9144 1.9304 0.9652 2.1844 1.0668 2.54 1.27 3.81 1.9304 5.08 2.54 6.4516 3.2512 1.27 0.635" 37 | WireBend=0 38 | WireBendSet=31 39 | WireCap=1 40 | MiterStyle=1 41 | PadShape=0 42 | ViaShape=1 43 | PolygonPour=0 44 | PolygonRank=0 45 | PolygonThermals=1 46 | PolygonOrphans=0 47 | TextRatio=8 48 | DimensionUnit=1 49 | DimensionPrecision=2 50 | DimensionShowUnit=0 51 | PinDirection=3 52 | PinFunction=0 53 | PinLength=2 54 | PinVisible=3 55 | SwapLevel=0 56 | ArcDirection=0 57 | AddLevel=2 58 | PadsSameType=0 59 | Layer=91 60 | Views=" 1: 34.3112 118.72 79.887 145.047" 61 | Sheet="1" 62 | 63 | [Win_2] 64 | Type="Board Editor" 65 | Number=1 66 | File="Thermostat.brd" 67 | View="6.35548 -2.3886 13.4373 5.14675" 68 | WireWidths=" 0.0762 0.1016 0.127 0.15 0.2 0.2032 0.254 0.508 0.6096 0.8128 1.016 1.27 2.54 0.1524 0.3048 0.4064" 69 | PadDiameters=" 0.254 0.3048 0.4064 0.6096 0.8128 1.016 1.27 1.4224 1.6764 1.778 1.9304 2.1844 2.54 3.81 6.4516 0" 70 | PadDrills=" 0.2 0.25 0.3 0.35 0.4 0.45 0.5 0.55 0.65 0.7 0.75 0.8 0.85 0.9 1 0.6" 71 | ViaDiameters=" 0.55 0.6 0.65 0.7 0.75 0.8 0.85 0.9 0.95 1 1.05 1.1 1.15 1.2 1.3 0" 72 | ViaDrills=" 0.2 0.25 0.3 0.4 0.45 0.5 0.55 0.6 0.65 0.7 0.75 0.8 0.85 0.9 1 0.35" 73 | HoleDrills=" 0.2 0.25 0.3 0.4 0.45 0.5 0.55 0.6 0.65 0.7 0.75 0.8 0.85 0.9 1 0.35" 74 | TextSizes=" 0.254 0.3048 0.4064 0.6096 0.8128 1.016 1.27 1.4224 1.6764 1.9304 2.1844 2.54 3.81 5.08 6.4516 1.778" 75 | PolygonSpacings=" 0.254 0.3048 0.4064 0.6096 0.8128 1.016 1.4224 1.6764 1.778 1.9304 2.1844 2.54 3.81 5.08 6.4516 1.27" 76 | PolygonIsolates=" 0.254 0.3048 0.4064 0.6096 0.8128 1.016 1.27 1.4224 1.6764 1.778 1.9304 2.1844 2.54 3.81 6.4516 0" 77 | MiterRadiuss=" 0.254 0.3175 0.635 1.27 2.54 1 2 2.5 5 7.5 10 0" 78 | DimensionWidths=" 0 0.127 0.254 0.1 0.26 0.13" 79 | DimensionExtWidths=" 0.127 0.254 0.1 0.13 0.26 0" 80 | DimensionExtLengths=" 1.27 2.54 1 2 3 0" 81 | DimensionExtOffsets=" 1.27 2.54 1 2 3 0" 82 | SmdSizes=" 0.3048 0.1524 0.4064 0.2032 0.6096 0.3048 0.8128 0.4064 1.016 0.508 1.27 0.6604 1.4224 0.7112 1.6764 0.8128 1.778 0.9144 1.9304 0.9652 2.1844 1.0668 2.54 1.27 3.81 1.9304 5.08 2.54 6.4516 3.2512 1.27 0.635" 83 | WireBend=1 84 | WireBendSet=0 85 | WireCap=1 86 | MiterStyle=1 87 | PadShape=0 88 | ViaShape=1 89 | PolygonPour=0 90 | PolygonRank=1 91 | PolygonThermals=1 92 | PolygonOrphans=0 93 | TextRatio=8 94 | DimensionUnit=1 95 | DimensionPrecision=2 96 | DimensionShowUnit=0 97 | PinDirection=3 98 | PinFunction=0 99 | PinLength=2 100 | PinVisible=3 101 | SwapLevel=0 102 | ArcDirection=0 103 | AddLevel=2 104 | PadsSameType=0 105 | Layer=1 106 | 107 | [Win_3] 108 | Type="Control Panel" 109 | Number=0 110 | 111 | [Desktop] 112 | Screen="2560 1560" 113 | Window="Win_1" 114 | Window="Win_2" 115 | Window="Win_3" 116 | -------------------------------------------------------------------------------- /EagleNew/bom.txt: -------------------------------------------------------------------------------- 1 | PCB OSHPark $16.50 for 3 2 | 3 | ESP-07 eBay 4 | USB Micro-B ZX62D-B-5PA8(30) or common eBay (remote only) 5 | Nextion HMI 2.8" eBay 6 | 3V3R MCP1755T-3302E/OT 7 | TXRX,LCD,DHT 0.1" headers. Use for serial programming ESP, debug, Nextion. 8 | 9 | rh/temp sensor options: 10 | AM2302/DHT22 (upper 4 pin) 11 | AM2320 (lower 4 pin, i2c) 12 | SHT21 DFN-6 13 | Si7021 cheaper DFN-6 14 | Si7034 super cheap 15 | 16 | R1-R5 * 0805 470 17 | R6-R8 * 0805 10K 18 | R9 0805 10K (use R9 on remote as well) 19 | R10,R11 0805 10K 20 | R12,R13 0805 4K7 (ignore for AM2302) 21 | R14,R15 ADC voltage divider (not used) 22 | R16,R17 0805 5K6 23 | R19 0805 10K (pullup in case 10K isn't removed from Nextion) 24 | R20 * 0805 4K7 25 | 26 | F1 * 1812 PPTC Fuse 1.5A PTS181224V150 27 | BR * MDB10SV 28 | VREG * LMR16006YQ5DDCRQ1 29 | L1 * 0805 6.8uH IFSC0806AZER6R8M01 30 | D1 * SOT-223 NSVR0320MW2T1G 31 | C1 or C2 * 10uF+ 25V+ 5mm (PCV1E470MCL2GS 6mm) or UZR1V100MCL1GB 32 | C3-C6 * 0805 16V 0.1uF 33 | C7 * 0805 16V 1uF 34 | C8 * 0805 16V 10uF 35 | C9 0603 16V 0.1uF (if SHTxx used) 36 | C10 * 0805 2.2uF 37 | 38 | K1-K5 * G3VM-61GR2 (stays cooler), or AQY282S, or any SMD-DIP4 1FormA 30VAC+ 500mA~5A 39 | X1 * 39357-0008 (or 691322110008 and 691361100008) Screw terimal, 8 position 3.5mm pitch 40 | J1 ADC expansion (Not used) 41 | 42 | * Only for main controller 43 | -------------------------------------------------------------------------------- /EagleNew/eagle.epf: -------------------------------------------------------------------------------- 1 | [Eagle] 2 | Version="09 01 01" 3 | Platform="Windows" 4 | Globals="Globals" 5 | Desktop="Desktop" 6 | 7 | [Globals] 8 | AutoSaveProject=1 9 | UsedLibrary="C:/Users/Greg/Documents/Eagle/lbr/SparkFun/SparkFun-Capacitors.lbr" 10 | UsedLibrary="C:/Users/Greg/Documents/Eagle/lbr/SparkFun/SparkFun-Connectors.lbr" 11 | UsedLibrary="C:/Users/Greg/Documents/Eagle/lbr/SparkFun/SparkFun-Electromechanical.lbr" 12 | UsedLibrary="C:/Users/Greg/Documents/Eagle/lbr/seeed/Seeed-OPL-Connector.lbr" 13 | 14 | [Win_1] 15 | Type="Schematic Editor" 16 | Number=3 17 | File="Thermostat.sch" 18 | View="40.9911 102.654 139.577 158.24" 19 | WireWidths=" 0 0.3048 0.6096 0.8128 1.016 1.27 1.4224 1.6764 1.778 1.9304 2.1844 2.54 3.81 6.4516 0.4064 0.1524" 20 | PadDiameters=" 0.254 0.3048 0.4064 0.6096 0.8128 1.016 1.27 1.4224 1.6764 1.778 1.9304 2.1844 2.54 3.81 6.4516 0" 21 | PadDrills=" 0.5 0.6 0.7 0.9 1 1.1 1.2 1.3 1.4 1.5 1.6 2 2.2 2.8 3.2 0.8" 22 | ViaDiameters=" 0.254 0.3048 0.4064 0.6096 0.8128 1.016 1.27 1.4224 1.6764 1.778 1.9304 2.1844 2.54 3.81 6.4516 0" 23 | ViaDrills=" 0.5 0.7 0.8 0.9 1 1.1 1.2 1.3 1.4 1.5 1.6 2 2.2 2.8 3.2 0.6" 24 | HoleDrills=" 0.5 0.7 0.8 0.9 1 1.1 1.2 1.3 1.4 1.5 1.6 2 2.2 2.8 3.2 0.6" 25 | TextSizes=" 0.254 0.3048 0.4064 0.6096 0.8128 1.016 1.27 1.4224 1.6764 1.9304 2.1844 2.54 3.81 5.08 6.4516 1.778" 26 | PolygonSpacings=" 0.254 0.3048 0.4064 0.6096 0.8128 1.016 1.4224 1.6764 1.778 1.9304 2.1844 2.54 3.81 5.08 6.4516 1.27" 27 | PolygonIsolates=" 0.254 0.3048 0.4064 0.6096 0.8128 1.016 1.27 1.4224 1.6764 1.778 1.9304 2.1844 2.54 3.81 6.4516 0" 28 | MiterRadiuss=" 0.254 0.3175 0.635 1.27 2.54 1 2 2.5 5 7.5 10 0" 29 | DimensionWidths=" 0 0.127 0.254 0.1 0.26 0.13" 30 | DimensionExtWidths=" 0.127 0.254 0.1 0.13 0.26 0" 31 | DimensionExtLengths=" 1.27 2.54 1 2 3 0" 32 | DimensionExtOffsets=" 1.27 2.54 1 2 3 0" 33 | SmdSizes=" 0.3048 0.1524 0.4064 0.2032 0.6096 0.3048 0.8128 0.4064 1.016 0.508 1.27 0.6604 1.4224 0.7112 1.6764 0.8128 1.778 0.9144 1.9304 0.9652 2.1844 1.0668 2.54 1.27 3.81 1.9304 5.08 2.54 6.4516 3.2512 1.27 0.635" 34 | WireBend=1 35 | WireBendSet=31 36 | WireCap=1 37 | MiterStyle=0 38 | PadShape=0 39 | ViaShape=0 40 | PolygonPour=0 41 | PolygonRank=0 42 | PolygonThermals=1 43 | PolygonOrphans=0 44 | TextRatio=8 45 | DimensionUnit=1 46 | DimensionPrecision=2 47 | DimensionShowUnit=0 48 | PinDirection=3 49 | PinFunction=0 50 | PinLength=2 51 | PinVisible=3 52 | SwapLevel=0 53 | ArcDirection=0 54 | AddLevel=2 55 | PadsSameType=0 56 | Layer=91 57 | Views=" 1: 40.9911 102.654 139.577 158.24" 58 | Sheet="1" 59 | 60 | [Win_2] 61 | Type="Board Editor" 62 | Number=1 63 | File="Thermostat.brd" 64 | View="5.74354 -4.00064 11.179 19.3711" 65 | WireWidths=" 1.9304 2.1844 3.81 6.4516 1.6764 0 0.8128 1.016 2.54 1.778 1.4224 0.254 1.27 0.4064 0.6096 0.3048" 66 | PadDiameters=" 0.254 0.3048 0.4064 0.6096 0.8128 1.016 1.27 1.4224 1.6764 1.778 1.9304 2.1844 2.54 3.81 6.4516 0" 67 | PadDrills=" 0.5 0.6 0.7 0.9 1 1.1 1.2 1.3 1.4 1.5 1.6 2 2.2 2.8 3.2 0.8" 68 | ViaDiameters=" 0.254 0.3048 0.4064 0.6096 0.8128 1.016 1.27 1.4224 1.6764 1.778 1.9304 2.1844 2.54 3.81 6.4516 0" 69 | ViaDrills=" 0.7 1.1 1.2 1.3 1.4 1.5 1.6 2.2 3.2 0.6 1 2 2.8 0.9 0.8 0.5" 70 | HoleDrills=" 0.5 0.7 0.8 0.9 1 1.1 1.2 1.3 1.4 1.5 1.6 2 2.2 2.8 3.2 0.6" 71 | TextSizes=" 0.254 0.3048 0.6096 1.9304 2.1844 3.81 5.08 6.4516 1.778 2.54 1.6764 1.27 1.4224 1.016 0.4064 0.8128" 72 | PolygonSpacings=" 0.254 0.3048 0.4064 0.6096 0.8128 1.016 1.4224 1.6764 1.778 1.9304 2.1844 2.54 3.81 5.08 6.4516 1.27" 73 | PolygonIsolates=" 0.3048 0.4064 0.6096 0.8128 1.016 1.27 1.4224 1.6764 1.778 1.9304 2.1844 2.54 3.81 6.4516 0 0.254" 74 | MiterRadiuss=" 0.254 0.3175 0.635 1 5 7.5 10 1.27 2 2.5 2.54 0" 75 | DimensionWidths=" 0 0.127 0.254 0.1 0.26 0.13" 76 | DimensionExtWidths=" 0.127 0.254 0.1 0.13 0.26 0" 77 | DimensionExtLengths=" 1.27 2.54 1 2 3 0" 78 | DimensionExtOffsets=" 1.27 2.54 1 2 3 0" 79 | SmdSizes=" 0.3048 0.1524 0.4064 0.2032 0.6096 0.3048 0.8128 0.4064 1.016 0.508 1.27 0.6604 1.4224 0.7112 1.6764 0.8128 1.778 0.9144 1.9304 0.9652 2.1844 1.0668 2.54 1.27 3.81 1.9304 5.08 2.54 6.4516 3.2512 1.27 0.635" 80 | WireBend=3 81 | WireBendSet=0 82 | WireCap=1 83 | MiterStyle=0 84 | PadShape=0 85 | ViaShape=1 86 | PolygonPour=0 87 | PolygonRank=1 88 | PolygonThermals=1 89 | PolygonOrphans=1 90 | TextRatio=16 91 | DimensionUnit=1 92 | DimensionPrecision=2 93 | DimensionShowUnit=0 94 | PinDirection=3 95 | PinFunction=0 96 | PinLength=2 97 | PinVisible=3 98 | SwapLevel=0 99 | ArcDirection=0 100 | AddLevel=2 101 | PadsSameType=0 102 | Layer=1 103 | 104 | [Win_3] 105 | Type="Control Panel" 106 | Number=0 107 | 108 | [Desktop] 109 | Screen="2560 1560" 110 | Window="Win_1" 111 | Window="Win_2" 112 | Window="Win_3" 113 | -------------------------------------------------------------------------------- /HvacRemote.js: -------------------------------------------------------------------------------- 1 | // HvacRemote script running on PngMagic http://www.curioustech.net/pngmagic.html 2 | // Device is set to a fixed IP in the router 3 | 4 | hvacUrl = 'ws://192.168.31.46/ws' 5 | password = 'password' 6 | 7 | kwh = 3600 // killowatt hours (compressor+fan) 8 | ppkwh = 0.153 // electric price per KWH (price / KWH) 9 | ccfs = 0.70 / (60*60) // NatGas cost per hour divided into seconds 10 | 11 | modes = new Array('Off', 'Cool', 'Heat', 'Auto','Cycle') 12 | 13 | fontSize = 20 14 | 15 | if(Reg.overrideTemp == 0) 16 | Reg.overrideTemp = -1.2 17 | 18 | var coolTempH 19 | var cycleThresh 20 | var hvacJson 21 | var mode 22 | var last 23 | var last1, last2 24 | var cycleState = 0 25 | var lastCycleTimer = 0 26 | var cycleTotal = 0 27 | 28 | Pm.Window('HvacRemote') 29 | 30 | Gdi.Width = 300 // resize drawing area 31 | Gdi.Height = 340 32 | 33 | Http.Close() 34 | if(!Http.Connected) 35 | Http.Connect('HVAC', hvacUrl) 36 | 37 | Pm.SetTimer(1000) 38 | 39 | // Handle published events 40 | function OnCall(msg, event, data, d2) 41 | { 42 | switch(msg) 43 | { 44 | case 'HTTPCONNECTED': 45 | Pm.Echo('HVAC Connected') 46 | break 47 | case 'HTTPSTATUS': 48 | switch(+event) 49 | { 50 | case 400: s = 'Bad request'; break 51 | case 408: s = 'Request timeout'; break 52 | case 12002: s = 'Timeout'; break 53 | case 12152: s = 'INVALID_SERVER_RESPONSE'; break 54 | default: s = ' ' 55 | } 56 | Pm.Echo( 'HvacRemote error: ' + event + ' ' + s) 57 | break 58 | case 'HTTPDATA': 59 | timeout = new Date() 60 | if(data.length) procLine(data) 61 | break 62 | case 'HTTPCLOSE': 63 | switch(+data) 64 | { 65 | case 400: data += ' Bad request'; break 66 | case 408: data += ' Request timeout'; break 67 | case 12002: data += ' Timeout'; break 68 | case 12017: data += ' ERROR_INTERNET_OPERATION_CANCELLED'; break 69 | } 70 | Pm.Echo( 'HVAC WS closed ' + data) 71 | break 72 | 73 | case 'BUTTON': 74 | switch(event) 75 | { 76 | case 0: // Override 77 | ovrActive = !ovrActive 78 | SetVar('override', ovrActive ? (Reg.overrideTemp * 10) : 0) 79 | break 80 | case 1: // reset filter 81 | SetVar('resetfilter', 0) 82 | filterMins = 0 83 | break 84 | case 2: // fan 85 | fanMode = (fanMode+1) % 2; SetVar('fanmode', fanMode) 86 | break 87 | case 3: // Unused 88 | SetVar('fw', 300) 89 | break 90 | case 4: // mode 91 | mode = (mode + 1) % 5; SetVar('mode', mode) 92 | break 93 | case 5: // heat mode 94 | heatMode = (heatMode+1) % 3; SetVar('heatMode', heatMode) 95 | break 96 | case 6: // cool H up 97 | setTemp(1, coolTempH + 0.1, 1); SetVar('cooltemph', (coolTempH * 10).toFixed()) 98 | break 99 | case 7: // cool H dn 100 | setTemp(1, coolTempH - 0.1, 1); SetVar('cooltemph', (coolTempH * 10).toFixed()) 101 | break 102 | case 8: // cool L up 103 | setTemp(1, coolTempL + 0.1, 0); SetVar('cooltempl', (coolTempL * 10).toFixed()) 104 | break 105 | case 9: // cool L dn 106 | setTemp(1, coolTempL - 0.1, 0); SetVar('cooltempl', (coolTempL * 10).toFixed()) 107 | break 108 | case 10: // heat H up 109 | setTemp(2, heatTempH + 0.2, 1); SetVar('heattemph', (heatTempH * 10).toFixed()) 110 | break 111 | case 11: // heat H dn 112 | setTemp(2, heatTempH - 0.2, 1); SetVar('heattemph', (heatTempH * 10).toFixed()) 113 | break 114 | case 12: // heat L up 115 | setTemp(2, heatTempL + 0.2, 0); SetVar('heattempl', (heatTempL * 10).toFixed()) 116 | break 117 | case 13: // heat L dn 118 | setTemp(2, heatTempL - 0.2, 0); SetVar('heattempl', (heatTempL * 10).toFixed()) 119 | break 120 | case 14: // thresh up 121 | if(cycleThresh < 6.3){ cycleThresh += 0.1; SetVar('cyclethresh', (cycleThresh * 10).toFixed()); } 122 | break 123 | case 15: // thresh dn 124 | if(cycleThresh > 0.1){ cycleThresh -= 0.1; SetVar('cyclethresh', (cycleThresh * 10).toFixed()); } 125 | break 126 | case 16: // override time up 127 | overrideTime += 60 128 | SetVar('overridetime', overrideTime) 129 | break 130 | case 17: // override time dn 131 | overrideTime -= 10 132 | SetVar('overridetime', overrideTime) 133 | break 134 | case 18: // override temp up 135 | Reg.overrideTemp = +Reg.overrideTemp + 0.1 136 | break 137 | case 19: // override temp dn 138 | Reg.overrideTemp = +Reg.overrideTemp - 0.1 139 | break 140 | } 141 | Draw() 142 | break 143 | 144 | default: 145 | Pm.Echo('HVAC Unrecognised ' + msg) 146 | break 147 | } 148 | } 149 | 150 | function OnTimer() 151 | { 152 | if(Http.Connected) 153 | return 154 | Http.Connect('HVAC', hvacUrl) 155 | } 156 | 157 | function SetVar(v, val) 158 | { 159 | Http.Send( '{key:' + password + ',' + v + ':' + val + '}' ) 160 | } 161 | 162 | function procLine(data) 163 | { 164 | if(data.length < 2) return 165 | //Pm.Echo(data) 166 | json = !(/[^,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]/.test( 167 | data.replace(/"(\\.|[^"\\])*"/g, ''))) && eval('(' + data + ')') 168 | 169 | switch(json.cmd) 170 | { 171 | case 'settings': 172 | mode = +json.m 173 | autoMode = +json.am 174 | heatMode = +json.hm 175 | fanMode = +json.fm 176 | ovrActive = +json.ot 177 | eHeatThresh = +json.ht 178 | 179 | coolTempL = +json.c0 / 10 180 | coolTempH = +json.c1 / 10 181 | heatTempL = +json.h0 / 10 182 | heatTempH = +json.h1 / 10 183 | cycleThresh = +json.ct / 10 184 | Reg.cycleThresh = cycleThresh 185 | overrideTime = +json.ov 186 | remoteTimer = json.rm 187 | remoteTimeout = json.ro 188 | ppkwh = json.ppk / 10000 189 | kwh = json.cw + json.fw 190 | 191 | ccfs = json.ccf / 100000 // Nat gas = 1.243 CCF on bill / into CF 192 | cfm = json.cfm / 1000 // Cubic feet per minute into seconds 193 | 194 | Draw() 195 | break 196 | 197 | case 'state': 198 | hvacJson = json 199 | running = +json.r 200 | state = +json.s 201 | fan = +json.fr 202 | inTemp = +json.it / 10 203 | rh = (+json.rh / 10).toFixed(1) 204 | targetTemp = +json.tt / 10 205 | filterMins = +json.fm 206 | outTemp = +json.ot / 10 207 | outFL = +json.ol 208 | outFH = +json.oh 209 | cycleTimer = +json.ct 210 | fanTimer = +json.ft 211 | runTotal = +json.rt 212 | 213 | //Pm.Echo('HV ' + inTemp + ' ' + rh) 214 | 215 | if(Pm.FindWindow( 'HvacHistory' )) 216 | Pm.History( 'REFRESH' ) 217 | Draw() 218 | LogTemps(json.snd) 219 | Pm.Server('STATTEMP', inTemp + '° ' + rh + '% > ' + targetTemp + '° ') 220 | break 221 | case 'update': 222 | break 223 | case 'alert': 224 | date = new Date() 225 | Pm.Echo('HVAC Alert: ' + date.toLocaleTimeString() + ' ' + json.text) 226 | break 227 | case 'print': 228 | date = new Date() 229 | Pm.Echo( 'HVAC ' + date.toLocaleTimeString() + ' ' + json.text) 230 | break 231 | default: 232 | Pm.Echo('HVAC Unknown event: ' + data) 233 | break 234 | } 235 | } 236 | 237 | // mimic thermostat 238 | function setTemp( mode, Temp, hl) 239 | { 240 | if(mode == 3) // auto 241 | { 242 | mode = autoMode 243 | } 244 | 245 | switch(mode) 246 | { 247 | case 1: 248 | if(Temp < 65.0 || Temp > 92.0) // ensure sane values 249 | break 250 | if(hl) 251 | { 252 | coolTempH = Temp 253 | coolTempL = Math.min(coolTempH, coolTempL) // don't allow h/l to invert 254 | } 255 | else 256 | { 257 | coolTempL = Temp 258 | coolTempH = Math.max(coolTempL, coolTempH) 259 | } 260 | save = heatTempH - heatTempL 261 | heatTempH = Math.min(coolTempL - 2, heatTempH) // Keep 2.0 degree differencial for Auto mode 262 | heatTempL = heatTempH - save // shift heat low by original diff 263 | break 264 | case 2: 265 | if(Temp < 63.0 || Temp > 86.0) // ensure sane values 266 | break 267 | if(hl) 268 | { 269 | heatTempH = Temp 270 | heatTempL = Math.min(heatTempH, heatTempL) 271 | } 272 | else 273 | { 274 | heatTempL = Temp; 275 | heatTempH = Math.max(heatTempL, heatTempH); 276 | } 277 | save = coolTempH - coolTempL; 278 | coolTempL = Math.max(heatTempH - 2, coolTempL); 279 | coolTempH = coolTempL + save; 280 | break 281 | } 282 | } 283 | 284 | function Draw() 285 | { 286 | Gdi.Clear(0) // transaprent 287 | 288 | btnW = fontSize * 3 289 | btnX = Gdi.Width - btnW * 2 - 4 290 | btnY = fontSize * 2 + 14 291 | 292 | // rounded window 293 | Gdi.Brush( Gdi.Argb( 160, 0, 0, 0) ) 294 | Gdi.FillRectangle(0, 0, Gdi.Width-1, Gdi.Height-1) 295 | Gdi.Pen( Gdi.Argb(255, 0, 0, 255), 1 ) 296 | Gdi.Rectangle(0, 0, Gdi.Width-1, Gdi.Height-1) 297 | 298 | // Title 299 | Gdi.Font( 'Courier New', fontSize, 'BoldItalic') 300 | Gdi.Brush( Gdi.Argb(255, 255, 230, 25) ) 301 | Gdi.Text( 'HVAC Remote', 5, 1 ) 302 | 303 | color = Gdi.Argb(255, 255, 0, 0) 304 | Gdi.Brush( color ) 305 | Gdi.Text( 'X', Gdi.Width-17, 1 ) 306 | 307 | Gdi.Font( 'Arial' , fontSize -1, 'Regular') 308 | Gdi.Brush( Gdi.Argb(255, 255, 255, 255) ) 309 | 310 | date = new Date() 311 | Gdi.Text( date.toLocaleTimeString(), Gdi.Width - fontSize * 6.4, 2 ) 312 | 313 | Gdi.Font( 'Arial' , fontSize, 'Regular') 314 | 315 | x = 5 316 | y = fontSize + 4 317 | if(hvacJson == undefined || coolTempH == undefined) 318 | return 319 | 320 | Gdi.Text('In: ' + inTemp + '°', x, y) 321 | Gdi.Text( '>' + targetTemp + '° ' + rh + '%', x + fontSize * 4, y) 322 | 323 | Gdi.Text('O:' + outTemp + '°', x + fontSize * 11, y) 324 | 325 | y = btnY 326 | Gdi.Text('Fan:', x, y); Gdi.Text(fan ? "On" : "Off", x + fontSize * 5, y, 'Right') 327 | y += fontSize + 4 328 | 329 | s = 'huh' 330 | switch(mode) 331 | { 332 | case 0: s = 'Off'; break 333 | case 1: s = 'Cooling'; break 334 | case 2: s = 'Heating'; break 335 | case 3: s = 'eHeating'; break 336 | case 4: s = 'Cycling'; break 337 | } 338 | 339 | bh = fontSize + 3 340 | 341 | Gdi.Text('Run:', x, y) 342 | Gdi.Text(running ? s : "Off", x + fontSize * 7, y, 'Right') 343 | y += bh 344 | 345 | Gdi.Text('Cool Hi:', x, y); Gdi.Text(coolTempH.toFixed(1) + '°', x + fontSize * 8, y, 'Right') 346 | y += bh 347 | Gdi.Text('Cool Lo:', x, y); Gdi.Text(coolTempL.toFixed(1) + '°', x + fontSize * 8, y, 'Right') 348 | y += bh 349 | Gdi.Text('Heat Hi:', x, y); Gdi.Text(heatTempH.toFixed(1) + '°', x + fontSize * 8, y, 'Right') 350 | y += bh 351 | Gdi.Text('Heat Lo:', x, y); Gdi.Text(heatTempL.toFixed(1) + '°', x + fontSize * 8, y, 'Right') 352 | y += bh 353 | Gdi.Text('Threshold:', x, y); Gdi.Text(cycleThresh.toFixed(1) + '°', x + fontSize * 8, y, 'Right') 354 | y += bh 355 | Gdi.Text('ovr Time:', x, y); Gdi.Text(overrideTime , x + fontSize * 8, y, 'Time') 356 | y += bh 357 | a = +Reg.overrideTemp 358 | Gdi.Text('Override:', x, y); Gdi.Text(a.toFixed(1) + '°' , x + fontSize * 8, y, 'Right') 359 | 360 | if(ovrActive) 361 | Gdi.Pen(Gdi.Argb(255,255,20,20), 2 ) // Button square 362 | else 363 | Gdi.Pen(Gdi.Argb(255,20,20,255), 2 ) // Button square 364 | Gdi.Rectangle(x, y, fontSize * 4, fontSize, 2) 365 | Pm.Button(x, y, fontSize * 4, fontSize) 366 | 367 | y = Gdi.Height - fontSize * 2.4 368 | 369 | if(mode == 1 || (mode==2 && heatMode == 0)) // cool or HP 370 | cost = ppkwh * runTotal / (1000*60*60) * kwh 371 | else 372 | cost = ccfs * runTotal * cfm 373 | 374 | Gdi.Text('Filter:', x, y); Gdi.Text(filterMins*60, x + fontSize * 7.5, y, 'Time') 375 | Gdi.Pen(Gdi.Argb(255,20,20,255), 2 ) // Button square 376 | Pm.Button(x, y, fontSize * 8, fontSize) 377 | Gdi.Rectangle(x, y, fontSize * 7.8, fontSize, 2) 378 | Gdi.Text('Cost:', x + fontSize * 7.8, y); Gdi.Text( '$' +cost.toFixed(2) , x + fontSize * 14, y, 'Right') 379 | 380 | y += bh 381 | Gdi.Text('Cycle:', x, y); Gdi.Text( cycleTimer, x + fontSize * 6, y, 'Time') 382 | Gdi.Text('Total:', x+fontSize * 7, y); Gdi.Text(runTotal, x + fontSize * 14, y, 'Time') 383 | 384 | heatModes = Array('HP', 'NG', 'Auto') 385 | fanModes = Array('Auto', 'On') 386 | buttons = Array(fanModes[fanMode], ' ', 387 | modes[mode], heatModes[heatMode], 388 | '+', '-', '+', '-', '+', '-', '+', '-', '+', '-', '+', '-', '+', '-' ) 389 | 390 | for (n = 0, row = 0; row < buttons.length / 2; row++) 391 | { 392 | for (col = 0; col < 2; col++) 393 | { 394 | x = btnX + (col * btnW) 395 | y = btnY + (row * bh) 396 | drawButton(buttons[n++], x, y, btnW, bh-2) 397 | } 398 | } 399 | } 400 | 401 | function drawButton(text, x, y, w, h) 402 | { 403 | Gdi.GradientBrush( 0,y, 22, 24, Gdi.Argb(200, 200, 200, 255), Gdi.Argb(200, 60, 60, 255 ), 90) 404 | Gdi.FillRectangle( x, y, w-2, h, 3) 405 | ShadowText( text, x+(w/2), y, Gdi.Argb(255, 255, 255, 255) ) 406 | Pm.Button(x, y, w, h) 407 | } 408 | 409 | function ShadowText(str, x, y, clr) 410 | { 411 | Gdi.Brush( Gdi.Argb(255, 0, 0, 0) ) 412 | Gdi.Text( str, x+1, y+1, 'Center') 413 | Gdi.Brush( clr ) 414 | Gdi.Text( str, x, y, 'Center') 415 | } 416 | 417 | function LogTemps(snd ) 418 | { 419 | if(cycleThresh == undefined) 420 | return 421 | if(targetTemp == 0) 422 | return 423 | 424 | if( (inTemp == last1 || inTemp == last2) && last == state+fan) // reduce logging some 425 | { 426 | last1 = last2 427 | last2 = inTemp 428 | return 429 | } 430 | if(state != cycleState) 431 | { 432 | cycleState = state 433 | cycleTotal += cycleTimer 434 | if(state == 0 && cycleTimer != lastCycleTimer && cycleTimer) 435 | Pm.Echo('Cycle = ' + cycleTimer + ' total = ' + cycleTotal) 436 | } 437 | last = state+fan 438 | last1 = last2 439 | last2 = inTemp 440 | 441 | ttL = targetTemp 442 | ttH = targetTemp 443 | 444 | if(Reg.hvacMode == 2) 445 | ttH += cycleThresh // heat 446 | else ttH -= cycleThresh // cool 447 | 448 | if(mode != Reg.hvacMode) 449 | { 450 | Reg.hvacMode = mode 451 | Pm.Echo('HVAC mode change') 452 | } 453 | s = hvacJson.t + ',' + state + ',' + fan + ',' + inTemp + ',' + ttL + ',' + ttH.toFixed(1)+ ',' + rh 454 | for(i=0; i < snd.length; i++) 455 | { 456 | s += ',' + snd[i][1]/10 457 | s += ',' + snd[i][2]/10 458 | } 459 | Pm.Log( 'statTemp.log', s) 460 | } 461 | -------------------------------------------------------------------------------- /Libraries/AM2320/AM2320.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | Modified by CuriousTech for stability Orgininal: https://github.com/hibikiledo/AM2320 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | 17 | Copyright 2016 Ratthanan Nalintasnai 18 | **/ 19 | 20 | #include "AM2320.h" 21 | 22 | #include 23 | 24 | AM2320::AM2320() { 25 | // do nothing 26 | } 27 | 28 | void AM2320::begin() { 29 | Wire.begin(); 30 | } 31 | 32 | void AM2320::begin(int sda, int scl) { 33 | Wire.begin(sda, scl); 34 | _scl = scl; 35 | _sda = sda; 36 | } 37 | 38 | void AM2320::getbuf(uint8_t *p) 39 | { 40 | memcpy(p, _buf, 8); 41 | } 42 | 43 | bool AM2320::measure(float& temp, float& rh) 44 | { 45 | Wire.setClock(100000); 46 | code = 0; 47 | if ( ! _read_registers(0x00, 4)) 48 | { 49 | return false; 50 | } 51 | 52 | uint16_t receivedCrc = *(uint16_t*)(_buf+6); // little endien 53 | crc = receivedCrc; 54 | if (receivedCrc != crc16(_buf, 6)) 55 | { 56 | code = 2; 57 | return false; 58 | } 59 | int16_t r = (int16_t)((_buf[2] << 8) | _buf[3]); // big endien 60 | rh = (float)r / 10; 61 | int16_t t = (int16_t)((_buf[4] << 8) | _buf[5]); 62 | temp = (float)t / 10; 63 | return true; 64 | } 65 | 66 | bool AM2320::_read_registers(int startAddress, int numByte) { 67 | // Wire.setClock(100000); 68 | Wire.beginTransmission(AM2320_ADDR); 69 | Wire.endTransmission(); 70 | delay(10); // heat time >800us 71 | Wire.beginTransmission(AM2320_ADDR); 72 | Wire.write(0x03); // function code: 0x03 - read register data 73 | Wire.write(startAddress); // begin address 74 | Wire.write(numByte); // number of bytes to read 75 | 76 | // send and check result if not success 77 | if (Wire.endTransmission(true) != 0) { 78 | code = 1; 79 | Wire.begin(_sda, _scl); 80 | return false; // sensor not ready 81 | } 82 | delay(2); // as specified in datasheet 83 | Wire.requestFrom(AM2320_ADDR, numByte + 4); // request bytes from sensor 84 | 85 | for ( int i = 0; i < numByte + 4; i++) // read 86 | _buf[i] = Wire.read(); 87 | 88 | return true; 89 | } 90 | 91 | uint16_t AM2320::crc16(byte *byte, int numByte) { 92 | uint16_t crc = 0xFFFF; // 16-bit crc register 93 | 94 | while (numByte > 0) { // loop until process all bytes 95 | crc ^= *byte; // exclusive-or crc with first byte 96 | 97 | for (int i = 0; i < 8; i++) { // perform 8 shifts 98 | uint16_t lsb = crc & 0x01; // extract LSB from crc 99 | crc >>= 1; // shift be one position to the right 100 | 101 | if (lsb == 0) { // LSB is 0 102 | continue; // repete the process 103 | } 104 | else { // LSB is 1 105 | crc ^= 0xA001; // exclusive-or with 1010 0000 0000 0001 106 | } 107 | } 108 | 109 | numByte--; // decrement number of byte left to be processed 110 | byte++; // move to next byte 111 | } 112 | 113 | return crc; 114 | } 115 | -------------------------------------------------------------------------------- /Libraries/AM2320/AM2320.h: -------------------------------------------------------------------------------- 1 | /** 2 | Modified by CuriousTech for stability Orgininal: https://github.com/hibikiledo/AM2320 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | 17 | Copyright 2016 Ratthanan Nalintasnai 18 | **/ 19 | 20 | #ifndef AM2303_H 21 | #define AM2303_H 22 | 23 | #include 24 | 25 | #define AM2320_ADDR 0x5C // address of AM2320 26 | 27 | class AM2320 { 28 | public: 29 | AM2320(); 30 | void begin(); 31 | void begin(int sda, int scl); 32 | bool measure(float& temp, float& rh); 33 | void getbuf(uint8_t *p); 34 | int code; 35 | uint16_t crc; 36 | uint8_t _sda; 37 | uint8_t _scl; 38 | private: 39 | uint8_t _buf[8]; 40 | bool _read_registers(int startAddress, int numByte); 41 | uint16_t crc16(byte *byte, int numByte); 42 | }; 43 | 44 | #endif 45 | -------------------------------------------------------------------------------- /Libraries/JsonClient/JsonClient.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | JsonClient.h - Arduino library for reading JSON data streamed or single request. 3 | Copyright 2014 Greg Cunningham, CuriousTech.net 4 | 5 | This library is free software; you can redistribute it and/or modify it under the terms of the GNU GPL 2.1 or later. 6 | 7 | 1024 byte limit for data received 8 | */ 9 | #include "JsonClient.h" 10 | 11 | // Initialize instance with a callback (event list index, name index from 0, integer value, string value) 12 | JsonClient::JsonClient(void (*callback)(int16_t iName, int iValue, char *psValue), uint16_t nSize ) 13 | { 14 | m_callback = callback; 15 | m_Status = JC_IDLE; 16 | m_bufcnt = 0; 17 | m_bKeepAlive = false; 18 | m_szPath[0] = 0; 19 | m_nBufSize = nSize; 20 | m_ac.onConnect([](void* obj, AsyncClient* c) { (static_cast(obj))->_onConnect(c); }, this); 21 | m_ac.onDisconnect([](void* obj, AsyncClient* c) { (static_cast(obj))->_onDisconnect(c); }, this); 22 | m_ac.onTimeout([](void* obj, AsyncClient* c, uint32_t time) { (static_cast(obj))->_onTimeout(c, time); }, this); 23 | m_ac.onData([](void* obj, AsyncClient* c, void* data, size_t len) { (static_cast(obj))->_onData(c, static_cast(data), len); }, this); 24 | } 25 | 26 | // add a json list {"event name", "valname1", "valname2", "valname3", NULL} 27 | // If first string is "" or NULL, the data is expected as JSON without an event name 28 | // If second string is "" or NULL, the event name is expected, but the "data:" string is assumed non-JSON 29 | bool JsonClient::setList(const char **pList) 30 | { 31 | m_jsonList = pList; 32 | return true; 33 | } 34 | 35 | // begin with host, /path?param=x¶m=x, port, streaming 36 | bool JsonClient::begin(const char *pHost, const char *pPath, uint16_t port, bool bKeepAlive, bool bPost, const char **pHeaders, char *pData, uint16_t to) 37 | { 38 | m_pszHost = (char *)pHost; 39 | return begin(pPath, port, bKeepAlive, bPost, pHeaders, pData, to); 40 | } 41 | 42 | // begin with host, /path?param=x¶m=x, port, streaming 43 | bool JsonClient::begin(IPAddress ip, const char *pPath, uint16_t port, bool bKeepAlive, bool bPost, const char **pHeaders, char *pData, uint16_t to) 44 | { 45 | m_pszHost = (char *)""; 46 | m_ip = ip; 47 | return begin(pPath, port, bKeepAlive, bPost, pHeaders, pData, to); 48 | } 49 | 50 | bool JsonClient::begin(const char *pPath, uint16_t port, bool bKeepAlive, bool bPost, const char **pHeaders, char *pData, uint16_t to) 51 | { 52 | if(m_pBuffer == NULL) 53 | m_pBuffer = new char[m_nBufSize]; 54 | 55 | if(m_ac.state()) 56 | return false; 57 | if(m_Status != JC_IDLE) 58 | return false; 59 | if(m_timer && millis() > m_timer + m_to * 1000) 60 | { 61 | m_ac.close(true); 62 | m_timer = 0; 63 | return false; 64 | } 65 | m_bufcnt = 0; 66 | strncpy(m_szPath, pPath, sizeof(m_szPath) ); 67 | m_szData[0] = 0; 68 | if(pData) 69 | strncpy(m_szData, pData, sizeof(m_szData) ); 70 | 71 | m_nPort = port; 72 | m_bKeepAlive = bKeepAlive; 73 | m_timer = millis(); 74 | m_pHeaders = pHeaders; 75 | m_bPost = bPost; 76 | m_retryCnt = 0; 77 | m_to = to; 78 | m_ac.setRxTimeout(to); 79 | m_ac.setAckTimeout(to*1000); 80 | return connect(); 81 | } 82 | 83 | void JsonClient::process(char *data) 84 | { 85 | int nSize = strlen(data) + 1; 86 | 87 | if(m_pBuffer && m_nBufSize < nSize) 88 | { 89 | delete m_pBuffer; 90 | m_pBuffer = NULL; 91 | } 92 | if(m_nBufSize < nSize) 93 | m_nBufSize = nSize; 94 | if(m_pBuffer == NULL) 95 | m_pBuffer = new char[m_nBufSize]; 96 | 97 | m_bufcnt = nSize - 1; 98 | strcpy(m_pBuffer, data); 99 | processLine(); 100 | m_callback( -1, JC_DONE, (char *)""); 101 | } 102 | 103 | // not used normally 104 | void JsonClient::end() 105 | { 106 | m_ac.stop(); 107 | m_Status = JC_IDLE; 108 | } 109 | 110 | int JsonClient::status() 111 | { 112 | if(m_Status != JC_IDLE && m_bKeepAlive == false) 113 | { 114 | if(millis() - m_timer > m_to * 1000) 115 | end(); 116 | } 117 | return m_Status; 118 | } 119 | 120 | void JsonClient::sendHeader(const char *pHeaderName, const char *pHeaderValue, AsyncClient* client) // string 121 | { 122 | client->add(pHeaderName, strlen(pHeaderName)); 123 | client->add(": ", 2); 124 | client->add(pHeaderValue, strlen(pHeaderValue) ); 125 | client->add("\n", 1); 126 | } 127 | 128 | void JsonClient::sendHeader(const char *pHeaderName, int nHeaderValue, AsyncClient* client) // integer 129 | { 130 | client->add(pHeaderName, strlen(pHeaderName) ); 131 | client->add(": ", 2); 132 | String s = String(nHeaderValue); 133 | client->add(s.c_str(), s.length()); 134 | client->add("\n", 1); 135 | } 136 | 137 | bool JsonClient::connect() 138 | { 139 | if(m_szPath[0] == 0) 140 | { 141 | m_Status = JC_IDLE; 142 | return false; 143 | } 144 | if( m_retryCnt > RETRIES) 145 | { 146 | m_Status = JC_RETRY_FAIL; 147 | m_callback(-1, m_Status, m_pszHost); 148 | m_Status = JC_IDLE; 149 | return false; 150 | } 151 | 152 | // if(m_Status == JC_CONNECTING || m_Status == JC_CONNECTED) 153 | // return false; 154 | 155 | m_Status = JC_CONNECTING; 156 | if(m_pszHost && m_pszHost[0]) 157 | { 158 | if( m_ac.connect(m_pszHost, m_nPort) ) 159 | return true; 160 | } 161 | else 162 | { 163 | if( m_ac.connect(m_ip, m_nPort) ) 164 | return true; 165 | } 166 | 167 | m_Status = JC_NO_CONNECT; 168 | m_callback(-1, m_Status, m_pszHost); 169 | m_Status = JC_IDLE; 170 | m_retryCnt++; 171 | return false; 172 | } 173 | 174 | void JsonClient::_onDisconnect(AsyncClient* client) 175 | { 176 | (void)client; 177 | 178 | if(m_bKeepAlive == false) 179 | { 180 | m_Status = JC_DONE; 181 | if(m_bufcnt) // no LF at end? 182 | { 183 | m_pBuffer[m_bufcnt] = '\0'; 184 | processLine(); 185 | } 186 | m_callback(-1, m_Status, m_pszHost); 187 | m_Status = JC_IDLE; 188 | return; 189 | } 190 | connect(); 191 | } 192 | 193 | void JsonClient::_onTimeout(AsyncClient* client, uint32_t time) 194 | { 195 | (void)client; 196 | 197 | m_Status = JC_TIMEOUT; 198 | m_callback(-1, m_Status, m_pszHost); 199 | m_Status = JC_IDLE; 200 | } 201 | 202 | void JsonClient::_onConnect(AsyncClient* client) 203 | { 204 | (void)client; 205 | 206 | if(m_bPost) 207 | client->add("POST ", 5); 208 | else 209 | client->add("GET ", 4); 210 | 211 | client->add(m_szPath, strlen(m_szPath)); 212 | client->add(" HTTP/1.1\n", 10); 213 | 214 | if(m_pszHost && m_pszHost[0]) 215 | sendHeader("Host", m_pszHost, client); 216 | else 217 | sendHeader("Host", m_ip.toString().c_str(), client); 218 | 219 | sendHeader("User-Agent", "Arduino", client); 220 | sendHeader("Connection", m_bKeepAlive ? "keep-alive" : "close", client); 221 | sendHeader("Accept", "*/*", client); // use application/json for strict 222 | if(m_pHeaders) 223 | { 224 | for(int i = 0; m_pHeaders[i] && m_pHeaders[i+1]; i += 2) 225 | { 226 | sendHeader(m_pHeaders[i], m_pHeaders[i+1], client); 227 | } 228 | } 229 | if(m_szData[0]) 230 | sendHeader("Content-Length", strlen(m_szData), client); 231 | 232 | client->add("\n", 1); 233 | if(m_szData[0]) 234 | { 235 | client->add(m_szData, strlen(m_szData)); 236 | client->add("\n", 1); 237 | } 238 | 239 | m_brace = 0; 240 | m_Status = JC_CONNECTED; 241 | m_callback(-1, m_Status, m_pszHost); 242 | } 243 | 244 | void JsonClient::_onData(AsyncClient* client, char* data, size_t len) 245 | { 246 | (void)client; 247 | if(m_pBuffer == NULL) 248 | return; 249 | 250 | for(int i = 0; i < len; i++) 251 | { 252 | char c = data[i]; 253 | if(c != '\r' && c != '\n' && m_bufcnt < m_nBufSize) 254 | m_pBuffer[m_bufcnt++] = c; 255 | if(c == '\r' || c == '\n') 256 | { 257 | if(m_bufcnt > 1) // ignore keepalive 258 | { 259 | m_pBuffer[m_bufcnt-1] = '\0'; 260 | processLine(); 261 | } 262 | m_bufcnt = 0; 263 | } 264 | } 265 | m_timer = millis(); 266 | } 267 | 268 | void JsonClient::processLine() 269 | { 270 | if(m_jsonList == NULL) 271 | return; 272 | 273 | char *pPair[2]; // param:data pair 274 | 275 | char *p = m_pBuffer; 276 | 277 | while(*p) 278 | { 279 | p = skipwhite(p); 280 | if(*p == '{'){p++; m_brace++;} 281 | if(*p == '['){p++; m_bracket++;} 282 | if(*p == ',') p++; 283 | p = skipwhite(p); 284 | 285 | bool bInQ = false; 286 | if(*p == '"'){p++; bInQ = true;} 287 | pPair[0] = p; 288 | if(bInQ) 289 | { 290 | while(*p && *p!= '"') p++; 291 | if(*p == '"') *p++ = 0; 292 | }else 293 | { 294 | while(*p && *p != ':') p++; 295 | } 296 | if(*p != ':') 297 | return; 298 | 299 | *p++ = 0; 300 | p = skipwhite(p); 301 | bInQ = false; 302 | if(*p == '{') m_inBrace = m_brace+1; // data: { 303 | else if(*p == '['){p++; m_inBracket = m_bracket+1;} // data: [ 304 | else if(*p == '"'){p++; bInQ = true;} 305 | pPair[1] = p; 306 | if(bInQ) 307 | { 308 | while(*p && *p!= '"') p++; 309 | if(*p == '"') *p++ = 0; 310 | }else if(m_inBrace) 311 | { 312 | while(*p && m_inBrace != m_brace){ 313 | p++; 314 | if(*p == '{') m_inBrace++; 315 | if(*p == '}') m_inBrace--; 316 | } 317 | if(*p=='}') p++; 318 | }else if(m_inBracket) 319 | { 320 | while(*p && m_inBracket != m_bracket){ 321 | p++; 322 | if(*p == '[') m_inBracket++; 323 | if(*p == ']') m_inBracket--; 324 | } 325 | if(*p == ']') *p++ = 0; 326 | }else while(*p && *p != ',' && *p != '\r' && *p != '\n' && *p != '}') p++; 327 | if(*p) *p++ = 0; 328 | p = skipwhite(p); 329 | if(*p == ',') *p++ = 0; 330 | 331 | m_inBracket = 0; 332 | m_inBrace = 0; 333 | p = skipwhite(p); 334 | 335 | if(pPair[0][0]) 336 | { 337 | for(int i = 0; m_jsonList[i]; i++) 338 | { 339 | if( !strcmp(pPair[0], m_jsonList[i]) ) 340 | { 341 | int32_t n = atoi(pPair[1]); 342 | if(!strcmp(pPair[1], "true")) n = 1; // bool case 343 | m_callback( i, n, pPair[1]); 344 | break; 345 | } 346 | } 347 | } 348 | } 349 | } 350 | 351 | char * JsonClient::skipwhite(char *p) 352 | { 353 | while(*p == ' ' || *p == '\t' || *p =='\r' || *p == '\n') 354 | p++; 355 | return p; 356 | } 357 | -------------------------------------------------------------------------------- /Libraries/JsonClient/JsonClient.h: -------------------------------------------------------------------------------- 1 | /* 2 | JsonClient.h - Arduino library for reading JSON data streamed or single request. 3 | Copyright 2014 Greg Cunningham, CuriousTech.net 4 | 5 | This library is free software; you can redistribute it and/or modify it under the terms of the GNU GPL 2.1 or later. 6 | */ 7 | #ifndef JSONCLIENT_H 8 | #define JSONCLIENT_H 9 | 10 | #include 11 | #ifdef ESP32 12 | #include 13 | #else 14 | #include 15 | #endif 16 | 17 | enum JC_Status 18 | { 19 | JC_IDLE, 20 | JC_CONNECTING, 21 | JC_CONNECTED, 22 | JC_DONE, 23 | JC_TIMEOUT, 24 | JC_NO_CONNECT, 25 | JC_RETRY_FAIL, 26 | JC_ERROR, 27 | }; 28 | 29 | #define RETRIES 6 30 | #define TIMEOUT 30 // Allow maximum 30s between data packets. 31 | 32 | class JsonClient 33 | { 34 | public: 35 | JsonClient(void (*callback)(int16_t iName, int iValue, char *psValue), uint16_t nSize = 1024); 36 | bool setList(const char **pList); 37 | bool begin(const char *pHost, const char *pPath, uint16_t port, bool bKeepAlive = false, bool bPost = false, const char **pHeaders = NULL, char *pData = NULL, uint16_t to = TIMEOUT); 38 | bool begin(IPAddress ip, const char *pPath, uint16_t port, bool bKeepAlive = false, bool bPost = false, const char **pHeaders = NULL, char *pData = NULL, uint16_t to = TIMEOUT); 39 | void end(void); 40 | void process(char *data); 41 | int status(void); 42 | 43 | private: 44 | bool begin(const char *pPath, uint16_t port, bool bKeepAlive = false, bool bPost = false, const char **pHeaders = NULL, char *pData = NULL, uint16_t to = TIMEOUT); 45 | bool connect(void); 46 | void processLine(void); 47 | void sendHeader(const char *pHeaderName, const char *pHeaderValue, AsyncClient* client); 48 | void sendHeader(const char *pHeaderName, int nHeaderValue, AsyncClient* client); 49 | void (*m_callback)(int16_t iName, int iValue, char *psValue); 50 | char *skipwhite(char *p); 51 | 52 | AsyncClient m_ac; 53 | void _onConnect(AsyncClient* client); 54 | void _onDisconnect(AsyncClient* client); 55 | static void _onError(AsyncClient* client, int8_t error); 56 | void _onTimeout(AsyncClient* client, uint32_t time); 57 | void _onData(AsyncClient* client, char* data, size_t len); 58 | uint32_t m_timer; 59 | uint32_t m_to; 60 | IPAddress m_ip; 61 | char *m_pszHost; 62 | char m_szPath[128]; 63 | char m_szData[256]; 64 | #define LIST_CNT 8 65 | const char **m_jsonList; 66 | const char **m_pHeaders; 67 | uint16_t m_bufcnt; 68 | uint16_t m_nPort; 69 | uint16_t m_nBufSize; 70 | char *m_pBuffer; 71 | uint8_t m_acIdx; 72 | int8_t m_brace; 73 | int8_t m_bracket; 74 | int8_t m_inBrace; 75 | int8_t m_inBracket; 76 | int8_t m_retryCnt; 77 | int8_t m_Status; 78 | bool m_bKeepAlive; 79 | bool m_bPost; 80 | }; 81 | 82 | #endif // JSONCLIENT_H 83 | 84 | -------------------------------------------------------------------------------- /Libraries/JsonClient/keywords.txt: -------------------------------------------------------------------------------- 1 | ################################### 2 | # Syntax Coloring Map For JsonClient 3 | ################################### 4 | 5 | ################################### 6 | # Datatypes (KEYWORD1) 7 | ################################### 8 | 9 | JsonClient KEYWORD1 10 | 11 | ################################### 12 | # Methods and Functions (KEYWORD2) 13 | ################################### 14 | 15 | setList KEYWORD2 16 | begin KEYWORD2 17 | service KEYWORD2 18 | end KEYWORD2 19 | 20 | ################################### 21 | # Constants (LITERAL1) 22 | ################################### 23 | -------------------------------------------------------------------------------- /Libraries/JsonParse/JsonParse.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | JsonParse.h - Arduino library for parsing JSON data. Note: Data string is written to for NULL termination. 3 | Copyright 2016 Greg Cunningham, CuriousTech.net 4 | This library is free software; you can redistribute it and/or modify it under the terms of the GNU GPL 2.1 or later. 5 | 8 lists max per instance 6 | */ 7 | #include "JsonParse.h" 8 | 9 | // Initialize instance with a callback (event list index, name index from 0, integer value, string value) 10 | JsonParse::JsonParse( void (*callback)(int16_t iName, int iValue, char *psValue) ) 11 | { 12 | m_callback = callback; 13 | } 14 | 15 | // add a json list { "valname1", "valname2", "valname3", NULL} 16 | void JsonParse::setList( const char **pList ) 17 | { 18 | m_jsonList = pList; 19 | } 20 | 21 | void JsonParse::process( char *data ) 22 | { 23 | if(m_jsonList == NULL) 24 | return; 25 | 26 | char *pPair[2]; // param:data pair 27 | int8_t brace = 0; 28 | int8_t bracket = 0; 29 | int8_t inBracket = 0; 30 | int8_t inBrace = 0; 31 | 32 | char *p = data; 33 | while(*p && *p != '{') // skip old label 34 | p++; 35 | 36 | while(*p) 37 | { 38 | p = skipwhite(p); 39 | if(*p == '{'){p++; brace++;} 40 | if(*p == '['){p++; bracket++;} 41 | if(*p == ',') p++; 42 | p = skipwhite(p); 43 | 44 | bool bInQ = false; 45 | if(*p == '"'){p++; bInQ = true;} 46 | pPair[0] = p; 47 | if(bInQ) 48 | { 49 | while(*p && *p!= '"') p++; 50 | if(*p == '"') *p++ = 0; 51 | }else 52 | { 53 | while(*p && *p != ':') p++; 54 | } 55 | if(*p != ':') 56 | return; 57 | 58 | *p++ = 0; 59 | p = skipwhite(p); 60 | bInQ = false; 61 | if(*p == '{') inBrace = brace+1; // data: { 62 | else if(*p == '['){p++; inBracket = bracket+1;} // data: [ 63 | else if(*p == '"'){p++; bInQ = true;} 64 | pPair[1] = p; 65 | if(bInQ) 66 | { 67 | while(*p && *p!= '"') p++; 68 | if(*p == '"') *p++ = 0; 69 | }else if(inBrace) 70 | { 71 | while(*p && inBrace != brace){ 72 | p++; 73 | if(*p == '{') inBrace++; 74 | if(*p == '}') inBrace--; 75 | } 76 | if(*p=='}') p++; 77 | }else if(inBracket) 78 | { 79 | while(*p && inBracket != bracket){ 80 | p++; 81 | if(*p == '[') inBracket++; 82 | if(*p == ']') inBracket--; 83 | } 84 | if(*p == ']') *p++ = 0; 85 | }else while(*p && *p != ',' && *p != '\r' && *p != '\n' && *p != '}') p++; 86 | if(*p) *p++ = 0; 87 | p = skipwhite(p); 88 | if(*p == ',') *p++ = 0; 89 | 90 | inBracket = 0; 91 | inBrace = 0; 92 | p = skipwhite(p); 93 | 94 | if(pPair[0][0]) 95 | { 96 | for(int i = 0; m_jsonList[i]; i++) 97 | { 98 | if( !strcmp(pPair[0], m_jsonList[i]) ) 99 | { 100 | int32_t n = atoi(pPair[1]); 101 | if(!strcmp(pPair[1], "true")) n = 1; // bool case 102 | m_callback( i, n, pPair[1]); 103 | break; 104 | } 105 | } 106 | } 107 | } 108 | m_callback( -1, 0, (char*)""); // end 109 | } 110 | 111 | char * JsonParse::skipwhite(char *p) 112 | { 113 | while(*p == ' ' || *p == '\t' || *p =='\r' || *p == '\n') 114 | p++; 115 | return p; 116 | } 117 | -------------------------------------------------------------------------------- /Libraries/JsonParse/JsonParse.h: -------------------------------------------------------------------------------- 1 | /* 2 | JsonParse.h - Arduino library for reading JSON data. 3 | Copyright 2016 Greg Cunningham, CuriousTech.net 4 | This library is free software; you can redistribute it and/or modify it under the terms of the GNU GPL 2.1 or later. 5 | */ 6 | #ifndef JSONPARSE_H 7 | #define JSONPARSE_H 8 | 9 | #include 10 | 11 | class JsonParse 12 | { 13 | public: 14 | JsonParse(void (*callback)(int16_t iName, int iValue, char *psValue)); 15 | void setList(const char **pList); 16 | void process(char *data); 17 | 18 | private: 19 | char *skipwhite(char *p); 20 | void (*m_callback)(int16_t iName, int iValue, char *psValue); 21 | 22 | const char **m_jsonList; 23 | }; 24 | 25 | #endif // JSONPARSE_H 26 | -------------------------------------------------------------------------------- /Libraries/JsonParse/keywords.txt: -------------------------------------------------------------------------------- 1 | ################################### 2 | # Syntax Coloring Map For JsonParse 3 | ################################### 4 | 5 | ################################### 6 | # Datatypes (KEYWORD1) 7 | ################################### 8 | 9 | JsonParse KEYWORD1 10 | 11 | ################################### 12 | # Methods and Functions (KEYWORD2) 13 | ################################### 14 | 15 | setList KEYWORD2 16 | process KEYWORD2 17 | 18 | ################################### 19 | # Constants (LITERAL1) 20 | ################################### 21 | -------------------------------------------------------------------------------- /Libraries/SHT21/SHT21.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | LibHumidity - A Humidity Library for Arduino. 3 | 4 | Supported Sensor modules: 5 | SHT21-Breakout Module - https://moderndevice.com/products/sht21-humidity-sensor 6 | 7 | Created by Christopher Ladden at Modern Device on December 2009. 8 | modified by Paul Badger March 2010 9 | 10 | This library is free software; you can redistribute it and/or 11 | modify it under the terms of the GNU Lesser General Public 12 | License as published by the Free Software Foundation; either 13 | version 2.1 of the License, or (at your option) any later version. 14 | 15 | This library is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 18 | Lesser General Public License for more details. 19 | 20 | You should have received a copy of the GNU Lesser General Public 21 | License along with this library; if not, write to the Free Software 22 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 23 | */ 24 | 25 | #include 26 | #include 27 | #include "SHT21.h" 28 | #include "Arduino.h" 29 | 30 | /********************************************************** 31 | * Initialize the sensor based on the specified type. 32 | **********************************************************/ 33 | SHT21::SHT21(uint8_t sda, uint8_t sdc, uint8_t seconds) { 34 | m_sda = sda; 35 | m_sdc = sdc; 36 | m_interval = seconds * 10; 37 | } 38 | 39 | void SHT21::init() { 40 | Wire.begin(m_sda, m_sdc); 41 | Wire.setClock(400000); 42 | m_mil = millis(); 43 | } 44 | 45 | /********************************************************** 46 | * The SHT21 humidity sensor datasheet says: 47 | * Parameter Resolution typ max Units 48 | * 14 bit 66 85 ms 49 | * 13 bit 33 43 ms 50 | * 12 Bit 17 22 ms 51 | * 11 bit 8 11 ms 52 | * 10 bit 4 6 ms 53 | * 54 | * Measurement time 55 | * (max values for -40°C 56 | * 125°C.) 57 | * 8 bit 1 3 ms 58 | * 59 | **********************************************************/ 60 | 61 | bool SHT21::service() 62 | { 63 | static uint8_t state = 0; 64 | bool bRc = false; 65 | 66 | if(millis() - m_mil < 100) 67 | return false; 68 | 69 | switch(state) 70 | { 71 | case 0: 72 | Wire.beginTransmission(eSHT21Address); //begin 73 | Wire.write(eTempNoHoldCmd); //send the pointer location 74 | Wire.endTransmission(); //end 75 | break; 76 | case 1: // 100ms later... 77 | Wire.requestFrom(eSHT21Address, 3); 78 | break; 79 | case 2: // 100ms later... 80 | if(Wire.available() < 3) return false; // not ready 81 | m_temp = ( Wire.read() << 8 ) | ( Wire.read() & 0xFFFC ); 82 | break; 83 | case 3: 84 | break; 85 | case 4: 86 | Wire.beginTransmission(eSHT21Address); //begin 87 | Wire.write(eRHumidityNoHoldCmd); //send the pointer location 88 | Wire.endTransmission(); //end 89 | break; 90 | case 5: 91 | Wire.requestFrom(eSHT21Address, 3); 92 | break; 93 | case 6: 94 | if(Wire.available() < 3) return false; // not ready 95 | m_rh = ( Wire.read() << 8 ) | ( Wire.read() & 0xFFFC ); 96 | bRc = true; 97 | break; 98 | } 99 | 100 | if(++state > m_interval) 101 | state = 0; 102 | m_mil = millis(); 103 | return bRc; 104 | } 105 | 106 | float SHT21::getTemperatureC() 107 | { 108 | return calculateTemperatureC(m_temp); 109 | } 110 | 111 | float SHT21::getTemperatureF() 112 | { 113 | return calculateTemperatureF(m_temp); 114 | } 115 | 116 | float SHT21::getRh() 117 | { 118 | return calculateHumidity(m_rh, m_temp); 119 | } 120 | 121 | /****************************************************************************** 122 | * Private Functions 123 | ******************************************************************************/ 124 | 125 | float SHT21::calculateTemperatureC(uint16_t analogTempValue) { 126 | 127 | return (((175.72/65536.0) * (float)analogTempValue) - 46.85); //T= -46.85 + 175.72 * ST/2^16 128 | } 129 | 130 | float SHT21::calculateTemperatureF(uint16_t analogTempValue) { 131 | 132 | return (((175.72/65536.0) * (float)analogTempValue) - 46.85) * 9/5 + 32; //T= -46.85 + 175.72 * ST/2^16 133 | } 134 | 135 | float SHT21::calculateHumidity(uint16_t analogHumValue, uint16_t analogTempValue) 136 | { 137 | float srh = analogHumValue; 138 | float humidityRH; // variable for result 139 | 140 | //-- calculate relative humidity [%RH] -- 141 | humidityRH = -6.0 + 125.0/65536.0 * srh; // RH= -6 + 125 * SRH/2^16 142 | return humidityRH; 143 | } 144 | -------------------------------------------------------------------------------- /Libraries/SHT21/SHT21.h: -------------------------------------------------------------------------------- 1 | /* 2 | LibHumidity - A Humidity Library for Arduino. 3 | 4 | Supported Sensor modules: 5 | SHT21-Breakout Module - https://moderndevice.com/products/sht21-humidity-sensor 6 | 7 | Created by Christopher Ladden at Modern Device on December 2009. 8 | 9 | This library is free software; you can redistribute it and/or 10 | modify it under the terms of the GNU Lesser General Public 11 | License as published by the Free Software Foundation; either 12 | version 2.1 of the License, or (at your option) any later version. 13 | 14 | This library is distributed in the hope that it will be useful, 15 | but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 17 | Lesser General Public License for more details. 18 | 19 | You should have received a copy of the GNU Lesser General Public 20 | License along with this library; if not, write to the Free Software 21 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 22 | */ 23 | 24 | 25 | #ifndef SHT21_H 26 | #define SHT21_H 27 | 28 | #include 29 | 30 | class SHT21 31 | { 32 | typedef enum { 33 | eSHT21Address = 0x40, 34 | } HUM_SENSOR_T; 35 | 36 | typedef enum { 37 | eTempHoldCmd = 0xE3, 38 | eRHumidityHoldCmd = 0xE5, 39 | eTempNoHoldCmd = 0xF3, 40 | eRHumidityNoHoldCmd = 0xF5, 41 | } HUM_MEASUREMENT_CMD_T; 42 | public: 43 | SHT21(uint8_t sda, uint8_t sdc, uint8_t seconds); 44 | void init(void); 45 | bool service(void); // returns true when both have been acquired 46 | float getTemperatureC(void); 47 | float getTemperatureF(void); 48 | float getRh(void); 49 | private: 50 | float calculateHumidity(uint16_t analogHumValue, uint16_t analogTempValue); 51 | float calculateTemperatureC(uint16_t analogTempValue); 52 | float calculateTemperatureF(uint16_t analogTempValue); 53 | uint8_t m_sda; 54 | uint8_t m_sdc; 55 | uint16_t m_temp; 56 | uint16_t m_rh; 57 | uint8_t m_interval; 58 | unsigned long m_mil; 59 | }; 60 | 61 | #endif 62 | -------------------------------------------------------------------------------- /Libraries/SHT21/keywords.txt: -------------------------------------------------------------------------------- 1 | ####################################### 2 | # Syntax Coloring Map For SHT21 3 | ####################################### 4 | 5 | ####################################### 6 | # Datatypes (KEYWORD1) 7 | ####################################### 8 | SHT21 KEYWORD1 9 | ####################################### 10 | # Methods and Functions (KEYWORD2) 11 | ####################################### 12 | init KEYWORD2 13 | service KEYWORD2 14 | getTemperatureC KEYWORD2 15 | getTemperatureF KEYWORD2 16 | getRh KEYWORD2 17 | ####################################### 18 | # Instances (KEYWORD2) 19 | ####################################### 20 | 21 | ####################################### 22 | # Constants (LITERAL1) 23 | ####################################### 24 | -------------------------------------------------------------------------------- /Libraries/XMLReader/XMLReader.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | XMLReader.cpp - Arduino library for simple serialized multi-chunk reading of XML. 3 | Copyright 2014 Greg Cunningham, CuriousTech.net 4 | 5 | This library is free software; you can redistribute it and/or modify it under the terms of the GNU GPL 2.1 or later. 6 | */ 7 | #include "XMLReader.h" 8 | 9 | #define TIMEOUT 30000 // Allow maximum 30s between data packets. 10 | 11 | // Initialize with a buffer, it's length, and a callback to iterate values in a list tag (item = tag#, idx = index in list, p = next value string) 12 | XMLReader::XMLReader(void (*xml_callback)(int item, int idx, char *p, char *pTag), const XML_tag_t *pTags ) 13 | { 14 | m_xml_callback = xml_callback; 15 | m_pTags = pTags; 16 | 17 | m_client.onConnect([](void* obj, AsyncClient* c) { (static_cast(obj))->_onConnect(c); }, this); 18 | m_client.onDisconnect([](void* obj, AsyncClient* c) { (static_cast(obj))->_onDisconnect(c); }, this); 19 | m_client.onTimeout([](void* obj, AsyncClient* c, uint32_t time) { (static_cast(obj))->_onTimeout(c, time); }, this); 20 | m_client.onData([](void* obj, AsyncClient* c, void* data, size_t len) { (static_cast(obj))->_onData(c, static_cast(data), len); }, this); 21 | 22 | m_client.setRxTimeout(TIMEOUT); 23 | } 24 | 25 | // begin with host and /path 26 | bool XMLReader::begin(const char *pHost, int port, String path) 27 | { 28 | if(m_client.connected()) 29 | { 30 | m_client.stop(); 31 | } 32 | 33 | if( !m_client.connect(pHost, port) ) 34 | { 35 | return false; 36 | } 37 | 38 | m_pHost = pHost; 39 | m_path = path; 40 | m_tagIdx = 0; 41 | m_tagState = 0; 42 | m_binValues = false; 43 | m_pPtr = m_buffer; 44 | m_pIn = m_pPtr; 45 | m_pEnd = m_buffer + sizeof(m_buffer) - 2; 46 | return true; 47 | } 48 | 49 | void XMLReader::_onConnect(AsyncClient* client) 50 | { 51 | (void)client; 52 | 53 | m_client.add("GET ", 4); 54 | m_client.add(m_path.c_str(), m_path.length()); 55 | m_client.add(" HTTP/1.1\n", 10); 56 | 57 | sendHeader("Host", m_pHost); 58 | sendHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36"); 59 | sendHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"); 60 | sendHeader("Accept-Encoding", "gzip, deflate, sdch"); 61 | sendHeader("Accept-Language", "en-US,en;q=0.8"); 62 | sendHeader("Cache-Control", "max-age=0"); 63 | sendHeader("Connection", "keep-alive"); 64 | 65 | m_client.add("\n", 1); 66 | } 67 | 68 | void XMLReader::sendHeader(const char *pHeaderName, const char *pHeaderValue) // string 69 | { 70 | m_client.add(pHeaderName, strlen(pHeaderName)); 71 | m_client.add(": ", 2); 72 | m_client.add(pHeaderValue, strlen(pHeaderValue) ); 73 | m_client.add("\n", 1); 74 | } 75 | 76 | void XMLReader::sendHeader(const char *pHeaderName, int nHeaderValue) // integer 77 | { 78 | m_client.add(pHeaderName, strlen(pHeaderName) ); 79 | m_client.add(": ", 2); 80 | String s = String(nHeaderValue); 81 | m_client.add(s.c_str(), s.length()); 82 | m_client.add("\n", 1); 83 | } 84 | 85 | // Note: Chunks are up to about 1460 bytes 86 | void XMLReader::_onData(AsyncClient* client, char* data, size_t len) 87 | { 88 | (void)client; 89 | char *dataEnd = data + len; 90 | 91 | bool bDone = false; 92 | 93 | if(!m_pTags[m_tagIdx].pszTag) // already done 94 | return; 95 | 96 | do{ 97 | while(m_pIn < m_pEnd && data < dataEnd) 98 | { 99 | *m_pIn++ = *data++; 100 | } 101 | 102 | *m_pIn = 0; // null terminate to make things easy 103 | 104 | if(!m_pTags[m_tagIdx].pszTag) // completed 105 | { 106 | m_xml_callback(-1, XML_COMPLETED, NULL, NULL); 107 | return; 108 | } 109 | 110 | if(m_binValues) // if not in values, increment to next tag 111 | { 112 | nextValue(); 113 | } 114 | else if( combTag(m_pTags[m_tagIdx].pszTag, m_pTags[m_tagIdx].pszAttr, m_pTags[m_tagIdx].pszValue)) // scan for next tag 115 | { 116 | m_binValues = true; 117 | m_valIdx = 0; 118 | } 119 | emptyBuffer(); 120 | 121 | if(data >= dataEnd && tagCnt() < 4 ) // this is just bad 122 | { 123 | bDone = true; 124 | } 125 | }while(!bDone); 126 | } 127 | 128 | void XMLReader::emptyBuffer() 129 | { 130 | if(m_pPtr >= m_pEnd) // all bytes are used. Just reset 131 | { 132 | m_pPtr = m_buffer; 133 | m_pIn = m_buffer; 134 | } 135 | else if(m_pPtr > m_buffer) // remove all used bytes 136 | { 137 | memcpy(m_buffer, m_pPtr, m_pEnd - m_pPtr); // shift remaining 138 | 139 | m_pIn -= (m_pPtr - m_buffer); // shift in-ptr back same as remaining data 140 | m_pPtr = m_buffer; 141 | } 142 | } 143 | 144 | // Find a tag 145 | bool XMLReader::combTag(const char *pTagName, const char *pAttr, const char *pValue) 146 | { 147 | switch(m_tagState) 148 | { 149 | case 0: // not in tag 150 | if(!tagStart() ) 151 | return false; // find start of a tag 152 | if(*m_pPtr != '<') 153 | return false; 154 | m_tagState = 1; 155 | m_pPtr++; 156 | return false; 157 | case 1: // found a tag 158 | if(tagCompare(m_pPtr, pTagName)) 159 | { 160 | m_pTagName = pTagName; 161 | m_pPtr += strlen(pTagName); 162 | bool bFound = false; 163 | if(!pAttr) // no attribute required 164 | { 165 | bFound = true; 166 | tagEnd(); 167 | IncPtr(); 168 | char *p = m_pPtr; // start of data in tag 169 | tagStart(); // end of data 170 | *m_pPtr++ = 0; 171 | m_xml_callback(m_tagIdx, m_valIdx, p, m_pTag); 172 | tagEnd(); // skip end tag 173 | } 174 | else while(*m_pPtr != '>' && m_pPtr < m_pEnd && !bFound) // find the correct attribute 175 | { 176 | if(tagCompare(m_pPtr++, pAttr)) 177 | { 178 | m_pPtr += strlen(pAttr) + 1; 179 | if(pValue) 180 | { 181 | bFound = tagCompare(m_pPtr, pValue); 182 | } 183 | else // no value required 184 | { 185 | bFound = true; 186 | } 187 | } 188 | } 189 | if(tagEnd()) // skip past this tag 190 | m_pPtr++; 191 | m_tagState = 0; 192 | return bFound; 193 | } 194 | else m_tagState = 2; 195 | break; 196 | case 2: // find possibly long end of tag 197 | if(!tagEnd()) // retry on next pass with more data 198 | return false; 199 | m_tagState = 0; 200 | break; 201 | } 202 | return false; 203 | } 204 | 205 | // Get next tag data 206 | bool XMLReader::nextValue() 207 | { 208 | if(!tagStart()) 209 | return true; // Find start of tag 210 | IncPtr(); 211 | m_pTag = m_pPtr; 212 | if(m_pPtr >= m_pEnd) 213 | return false; 214 | 215 | char *p = m_pPtr; 216 | 217 | while(*p++ != '<') // lookahead 218 | { 219 | if(p >= m_pEnd) 220 | return true; // not enough data to continue 221 | } 222 | 223 | if(*m_pPtr == '/') // an end tag 224 | { 225 | IncPtr(); 226 | if(tagCompare(m_pPtr, m_pTags[m_tagIdx].pszTag)) // end of value list 227 | { 228 | m_tagIdx++; 229 | m_binValues = false; 230 | return true; 231 | } 232 | } 233 | 234 | if(!tagEnd()) 235 | return true; // end of start tag 236 | *m_pPtr = 0; 237 | IncPtr(); 238 | if(m_pPtr >= m_pEnd) 239 | return false; 240 | 241 | char *ptr = m_pPtr; // data 242 | 243 | if(!tagStart()) 244 | return true; 245 | 246 | if(m_pPtr >= m_pEnd) 247 | return false; 248 | 249 | *m_pPtr++ = 0; // null term data (unsafe increment) 250 | if(m_pPtr >= m_pEnd) 251 | return false; 252 | 253 | m_xml_callback(m_tagIdx, m_valIdx, ptr, m_pTag); 254 | 255 | if(++m_valIdx >= m_pTags[m_tagIdx].valueCount) 256 | { 257 | m_binValues = false; 258 | m_tagIdx++; 259 | } 260 | tagEnd(); // skip past end of end tag 261 | IncPtr(); 262 | 263 | return true; 264 | } 265 | 266 | bool XMLReader::tagCompare(char *p1, const char *p2) // compare at lenngth of p2 with special chars 267 | { 268 | while(*p2) 269 | { 270 | if(*p1 == 0) return false; 271 | if(*p1++ != *p2++) return false; 272 | } 273 | return (*p2 == 0 && (*p1 == ' ' || *p1 == '>' || *p1 == '=' || *p1 == '"') ); 274 | } 275 | 276 | void XMLReader::IncPtr() 277 | { 278 | if(++m_pPtr >= m_pEnd) // not entirely safe 279 | m_pPtr--; 280 | } 281 | 282 | bool XMLReader::tagStart() 283 | { 284 | while(*m_pPtr != '<') // find start of tag 285 | { 286 | if(++m_pPtr >= m_pEnd) 287 | return false; 288 | } 289 | return true; 290 | } 291 | 292 | int XMLReader::tagCnt() 293 | { 294 | char *p = m_pPtr; 295 | int cnt = 0; 296 | 297 | while(*p) // find start of tag 298 | { 299 | if(*p++ == '<') cnt++; 300 | if(p >= m_pEnd) 301 | return cnt; 302 | } 303 | return cnt; 304 | } 305 | 306 | bool XMLReader::tagEnd() 307 | { 308 | while(*m_pPtr != '>') // find end of tag 309 | { 310 | if(++m_pPtr >= m_pEnd) 311 | return false; 312 | } 313 | return true; 314 | } 315 | 316 | void XMLReader::_onDisconnect(AsyncClient* client) 317 | { 318 | (void)client; 319 | m_xml_callback(-1, XML_DONE, NULL, NULL); 320 | } 321 | 322 | void XMLReader::_onTimeout(AsyncClient* client, uint32_t time) 323 | { 324 | (void)client; 325 | m_xml_callback(-1, XML_TIMEOUT, NULL, NULL); 326 | } 327 | -------------------------------------------------------------------------------- /Libraries/XMLReader/XMLReader.h: -------------------------------------------------------------------------------- 1 | #ifndef XMLREADER_H 2 | #define XMLREADER_H 3 | 4 | #include 5 | #ifdef ESP32 6 | #include 7 | #else 8 | #include 9 | #endif 10 | 11 | enum XML_Status 12 | { 13 | XML_IDLE, 14 | XML_DONE, 15 | XML_COMPLETED, 16 | XML_TIMEOUT 17 | }; 18 | 19 | struct XML_tag_t 20 | { 21 | const char *pszTag; 22 | const char *pszAttr; 23 | const char *pszValue; 24 | int16_t valueCount; 25 | }; 26 | 27 | class XMLReader 28 | { 29 | public: 30 | XMLReader(void (*xml_callback)(int item, int idx, char *p, char *pTag), const XML_tag_t *pTags); 31 | bool begin(const char *pHost, int port, String path); 32 | 33 | private: 34 | bool combTag(const char *pTagName, const char *pAttr, const char *pValue); 35 | bool nextValue(void); 36 | bool fillBuffer(char* data, size_t len); 37 | void emptyBuffer(void); 38 | void sendHeader(const char *pHeaderName, const char *pHeaderValue); 39 | void sendHeader(const char *pHeaderName, int nHeaderValue); 40 | bool tagCompare(char *p1, const char *p2); 41 | void IncPtr(void); 42 | bool tagStart(void); 43 | int tagCnt(void); 44 | bool tagEnd(void); 45 | 46 | void (*m_xml_callback)(int item, int idx, char *p, char *pTag); 47 | 48 | AsyncClient m_client; 49 | 50 | void _onConnect(AsyncClient* client); 51 | void _onDisconnect(AsyncClient* client); 52 | static void _onError(AsyncClient* client, int8_t error); 53 | void _onTimeout(AsyncClient* client, uint32_t time); 54 | void _onData(AsyncClient* client, char* data, size_t len); 55 | 56 | const char *m_pHost; 57 | char m_buffer[512]; 58 | String m_path; 59 | const XML_tag_t *m_pTags; 60 | char *m_pPtr; 61 | char *m_pEnd; 62 | char *m_pIn; 63 | char *m_pTag; 64 | const char *m_pTagName; 65 | bool m_binValues; 66 | int m_tagIdx; 67 | int m_valIdx; 68 | int8_t m_tagState; 69 | }; 70 | 71 | #endif // XMLREADER_H 72 | -------------------------------------------------------------------------------- /Libraries/XMLReader/keywords.txt: -------------------------------------------------------------------------------- 1 | ################################### 2 | # Syntax Coloring Map For XMLReader 3 | ################################### 4 | 5 | ################################### 6 | # Datatypes (KEYWORD1) 7 | ################################### 8 | 9 | XMLReader KEYWORD1 10 | 11 | ################################### 12 | # Methods and Functions (KEYWORD2) 13 | ################################### 14 | 15 | begin KEYWORD2 16 | service KEYWORD2 17 | end KEYWORD2 18 | getStatus KEYWORD2 19 | 20 | ################################### 21 | # Constants (LITERAL1) 22 | ################################### 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ESP-HVAC (ESP8266 & ESP-32) 2 | WiFi Smart Omniscient HVAC Touchscreen Theromstat 3 | 4 | This project is discontinued (except for bugfixes and support code changes). New work will be on 5 | [WT32-SC01-HVAC](https://github.com/CuriousTech/WT32-SC01-HVAC) 6 | 7 | 2018 Model: The new model was redesigned with a cooler switching regulator (~20mA idle, with a dim screen), smaller ($16.50 from OSHPark) more options for the temp/RH sensor, and the rotory encoder was removed, but has a header for an external encoder or buttons. The only difference in connections is the SCL/SDA were accidentaly reversed on the onboard SHTxx chip. Pics should be at the bottom. 8 | 9 | Notes about the Nextion: The baudrate is bauds=115200 in the main initialize varaibles. This will allow it to communicate at the highest speed. 10 | 11 | The "Basic" (Discovery model works without modification) has a 10K pullduown resistor for 5V tolerance (R13: next to the connector). Removing this allows the ESP8266 to boot normally and operate without any resistors added, or add a 10K pullup between TX and 3V3 when serial debug is not connected. I've opted for the removal of the resistor, which fixes all problems. The 5K6 on the thermostat (R17) allowed it to operate with serial connected plus the pulldown, but not without. The blue wire (data sent to the ESP) doesn't really need any resistors. The ESP can't be programmed without a 1K while both are connected, and with the 1K and debug the touchscreen won't work. So just disconnect the wire while programming and reconnect it after. 12 | 13 | This has replaced the old Spark-O-Stat with a newer system using the ESP-07 (now ESP32), with a better screen (Nextion HMI 2.8" touchscreen), 5 outputs (1 extra output for humidifier), option for SHT21 (I2C) or DHT22/11, AM2302/22, and anolog input for expansion. 14 | 15 | Main and remote units are designed into the single PCB with wired 5V or USB Mini-B for the remote units, and 24VAC for main. Headers on top are for onboard serial programming of the ESP and Nextion. Only power it with 1 source at a time. (3V3 for just the ESP, encoder, and temp sensor, 5V to include the display, or 24VAC for all of it including the SSRs). Add the switching regulator to the remote for battery power (6-40V). 16 | 17 | Remote: By tapping the target temperature on the display, it will toggle transmitting the remote temperature and humidity to the main unit to use in all operations. Tapping the same item on the main unit will also end the remote temperature connection. There are notifications to indicate use as well as a flashing run indicator instead of solid. 18 | 19 | Some screens including a keyboard and SSID chooser, but this is using the auto connect with SoftAP server (which attempts to find the stored SSID while waiting on input) so it should never be needed. The dimmed screensaver will change to a clock, and a few other odd displays over time. 20 | 21 | ![Some display screenshots](http://www.curioustech.net/images/hvacscreens.png) 22 | 23 | Cool/heat low/high: 24 | These are the desired temperature ranges for cooling and heating, which are calculated based on outside temperature over the next set number of days. 25 | 26 | Cycle: 27 | Disable heat and cool, but still cycles the fan by the Auto Fan: Run: settings. 4:00 and 5 default = 5 minutes every 4 hours. 28 | 29 | Threshold: 30 | The temperature offset to complete a cycle. 31 | 32 | Pre-cycle fan time: 33 | 0 disables, 1 second to 5 minutes. This is the first phase of the cycle. When the temperature reaches the target, the fan is turned on to cycle air for the set time, which causes the thermostat to read a more overall temperature. If the threshold is reached before this ends, which it can, it will shut off. Otherwise it continues to the run phase, which runs for at least the cycleMin period to save wear on the compressor because this does shorten the run time considerably. A good value to use is the time it takes to lower the temperature after turning the fan on before it plateaus. Usually around 2 minutes. 34 | 35 | Post-cycle fan time: 36 | Runs fan after cycle completes. This is the third phase of the cycle, which continues to push air through the cold radiator and ductwork. 2 minutes is good, but you can use an IR thermometer on a vent to see how long the cold air continues. 37 | 38 | Humidifier settings: 39 | Off: Always off 40 | Fan: Run when fan is on 41 | Run: Run when thermostat is running a cycle 42 | Auto1: Operate by humidistat during run cycles 43 | Auto2: Humidistat runs indepentantly of thermostat (shares fan control) 44 | 45 | Override: 46 | Use to heat or cool by a selected offset temperature for a specified time. 47 | 48 | Freshen: 49 | Starts the fan with a timeout. This uses the post fan timer, so a normal cycle will cancel it. 50 | 51 | Idle min, cycle min/max: 52 | These are timers in seconds to control the thermstat operating limits. Idle min is the shortest time between cycles. Cycle min is the shortest time for a cycle, and max is the longest. Be careful with these settings. Running the compressor too short or too long can cause damage. 53 | 54 | Away time and temperature delta offset: 55 | The away time is in hours:minutes unlike the rest of the timers, but uses the override timer. It can be turned on and off with the button, but will also turn off with the timer. There are 2 temp values that are displayed depending on heat or cool mode. One for heat and one for cool, like the pre/post fan. 56 | Like any setting, it can be automated with something like Tasker. To turn on would be http://ip:85?key=password&away=1 57 | 58 | Other settings: 59 | AC differential limit: Increases thermostat if inside/outside difference is more than 30. This assumes Fahrenheit. Change for Celcius. 60 | Heat Threshold: Heatpump will switch off and use gas when outside temp goes below set value (default 33). Change for Celcius. 61 | PKW (price per killawatt hour), CFM (gas cubic feet per minute), CCF (cost per 100 cubic foot): Set the PPKW and CCF based on billing. CFM is dependant on the furnace. 62 | FC Shift (in minutes): Shifts the forecast for the temperature adjust. Use the setting on the chart page (they don't affect the actual settings) to determine what's best. There are 2 values for heat and cool here as well. 63 | 64 | ![remotepage](http://www.curioustech.net/images/hvacweb2.png) 65 | 66 | The web chart. Dark gray is off, cyan is fan running, blue is compressor+fan, NG is red, HP is yellow, rh is green, the target/threshold is the wave. The thermostat is in the hottest room in the house, so it does look warmer than it really is, but that's why there's a remote unit, and now multiple sensors. 67 | ![chartpage](http://www.curioustech.net/images/hvacchart2.png) 68 | 69 | ![dualstats](http://www.curioustech.net/images/hvac2.jpg) 70 | 71 | New Model 72 | ![model2](http://www.curioustech.net/images/esphvac2.jpg) 73 | Remote (this uses only a few components: the 3.3V regulator, 3 capacitors, USB connector, temp sensor, and 7 required resistors) 74 | ![model2rem](http://www.curioustech.net/images/hvacremote.jpg) 75 | ![model2side](http://www.curioustech.net/images/esphvac21.jpg) 76 | 77 | Sensor web page 78 | ![envmon](http://www.curioustech.net/images/sensor32.png) 79 | -------------------------------------------------------------------------------- /RMT.js: -------------------------------------------------------------------------------- 1 | rmtIP = '192.168.0.105:86' 2 | Url = 'ws://' + rmtIP + '/ws' 3 | 4 | if(!Http.Connected) 5 | Http.Connect( 'event', Url ) // Start the event stream 6 | 7 | var last 8 | mute = false 9 | Pm.SetTimer(10*1000) 10 | heartbeat = 0 11 | // Handle published events 12 | function OnCall(msg, event, data) 13 | { 14 | switch(msg) 15 | { 16 | case 'HTTPDATA': 17 | heartbeat = new Date() 18 | mute = false 19 | //Pm.Echo(data) 20 | procLine(data) 21 | break 22 | case 'HTTPSTATUS': 23 | Pm.Echo('RMT Status ' + event + ' ' + data) 24 | break 25 | case 'HTTPCLOSE': 26 | Pm.Echo('RMT stream retry') 27 | break 28 | } 29 | } 30 | 31 | function procLine(data) 32 | { 33 | if(data.length < 2) return 34 | data = data.replace(/\n|\r/g, "") 35 | parts = data.split(';') 36 | 37 | switch(parts[0]) 38 | { 39 | case 'state': 40 | LogRemote(parts[1]) 41 | break 42 | case 'print': 43 | Pm.Echo( 'RMT Print: ' + parts[1]) 44 | break 45 | case 'alert': 46 | Pm.Echo( 'RMT Alert: ' + parts[1]) 47 | Pm.Beep(0) 48 | break 49 | case 'OTA': 50 | Pm.Echo( 'RMT Update: ' + parts[1]) 51 | break 52 | } 53 | } 54 | 55 | function OnTimer() 56 | { 57 | time = (new Date()).valueOf() 58 | if(time - heartbeat > 120*1000) 59 | { 60 | if(!Http.Connected) 61 | { 62 | if(!mute) 63 | { 64 | mute = true 65 | Pm.Echo('RMT timeout') 66 | } 67 | Http.Connect( 'event', Url ) // Start the event stream 68 | } 69 | } 70 | } 71 | 72 | function LogRemote(str) 73 | { 74 | rmtJson = !(/[^,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]/.test( 75 | str.replace(/"(\\.|[^"\\])*"/g, ''))) && eval('(' + str + ')') 76 | 77 | line = rmtJson.tempi + ',' + rmtJson.rhi 78 | 79 | if(line == last || +rmtJson.tempi == -1) 80 | return 81 | last = line 82 | 83 | Pm.Log( 'Remote.log', rmtJson.t + ',' + line ) 84 | } 85 | -------------------------------------------------------------------------------- /RemoteSensor/BasicSensor.cpp: -------------------------------------------------------------------------------- 1 | // Class for AM2320 temp and humidity 2 | 3 | #include "BasicSensor.h" 4 | #include // http://www.pjrc.com/teensy/td_libs_Time.html 5 | 6 | #define ESP_LED 2 // low turns on ESP blue LED 7 | #define CF_BTN 13 // C/F button 8 | 9 | extern void WsSend(String s); 10 | 11 | void BasicInterface::init(bool bCF) 12 | { 13 | pinMode(ESP_LED, OUTPUT); 14 | digitalWrite(ESP_LED, LOW); 15 | m_am.begin(5, 4); 16 | m_bCF = bCF; 17 | } 18 | 19 | int BasicInterface::service(int8_t tcal, int8_t rhcal) 20 | { 21 | static uint8_t lastSec; 22 | 23 | if(second() == lastSec) 24 | return 0; 25 | 26 | lastSec = second(); 27 | 28 | if((lastSec % 5) == 0) 29 | { 30 | float ftemp, frh; 31 | if(m_am.measure(ftemp, frh)) 32 | { 33 | m_status = 0; 34 | 35 | if(m_bCF) 36 | ftemp = ( 1.8 * ftemp + 32.0) * 10; 37 | else 38 | ftemp *= 10; 39 | ftemp += tcal; 40 | m_tempMedian[0].add( ftemp); 41 | m_tempMedian[0].getAverage(2, ftemp); 42 | m_tempMedian[1].add(frh * 10); 43 | m_tempMedian[1].getAverage(2, frh); 44 | if(m_values[DE_TEMP] != (uint16_t)ftemp || m_values[DE_RH] != (uint16_t)frh) 45 | m_bUpdated = true; 46 | m_values[DE_TEMP] = ftemp; 47 | m_values[DE_RH] = frh + rhcal; 48 | } 49 | else 50 | { 51 | m_status = 1; 52 | } 53 | } 54 | 55 | static bool bBtn = true; 56 | if( digitalRead(CF_BTN) != bBtn ) 57 | { 58 | bBtn = digitalRead(CF_BTN); 59 | if( bBtn ) // release 60 | setCF( !m_bCF ); 61 | } 62 | return m_status; 63 | } 64 | 65 | void BasicInterface::setCF(bool f) 66 | { 67 | m_bCF = f; 68 | } 69 | 70 | int BasicInterface::status() 71 | { 72 | return m_status; 73 | } 74 | 75 | void BasicInterface::setLED(uint8_t no, bool bOn) 76 | { 77 | m_bLED[0] = bOn; 78 | digitalWrite(ESP_LED, !bOn); // No external LED 79 | } 80 | -------------------------------------------------------------------------------- /RemoteSensor/BasicSensor.h: -------------------------------------------------------------------------------- 1 | #ifndef BASICSENSOR_H 2 | #define BASICSENSOR_H 3 | 4 | #include 5 | #include "RunningMedian.h" 6 | #include 7 | #include "defs.h" 8 | 9 | class BasicInterface 10 | { 11 | public: 12 | BasicInterface(){}; 13 | void init(bool bCF); 14 | int service(int8_t tcal, int8_t rhcal); 15 | void setLED(uint8_t no, bool bOn); 16 | void setCF(bool f); 17 | int status(void); 18 | void setSignal(int db){}; 19 | bool m_bLED[2]; 20 | bool m_bUpdated; 21 | bool m_bCF; 22 | uint16_t m_dataFlags = 3; 23 | uint16_t m_values[6]; 24 | uint8_t m_signal; 25 | private: 26 | RunningMedian m_tempMedian[2]; 27 | AM2320 m_am; 28 | int m_status; 29 | }; 30 | 31 | #endif // BASICSENSOR_H 32 | -------------------------------------------------------------------------------- /RemoteSensor/RunningMedian.h: -------------------------------------------------------------------------------- 1 | #ifndef RunningMedian_h 2 | #define RunningMedian_h 3 | // 4 | // FILE: RunningMedian.h 5 | // AUTHOR: Rob dot Tillaart at gmail dot com 6 | // PURPOSE: RunningMedian library for Arduino 7 | // VERSION: 0.2.00 - template edition 8 | // URL: http://arduino.cc/playground/Main/RunningMedian 9 | // HISTORY: 0.2.00 first template version by Ronny 10 | // 0.2.01 added getAverage(uint8_t nMedians, float val) 11 | // 12 | // Released to the public domain 13 | // 14 | 15 | #include 16 | 17 | template class RunningMedian { 18 | 19 | public: 20 | 21 | enum STATUS {OK = 0, NOK = 1}; 22 | 23 | RunningMedian() { 24 | _size = N; 25 | clear(); 26 | }; 27 | 28 | void clear() { 29 | _cnt = 0; 30 | _idx = 0; 31 | }; 32 | 33 | void add(T value) { 34 | _ar[_idx++] = value; 35 | if (_idx >= _size) _idx = 0; // wrap around 36 | if (_cnt < _size) _cnt++; 37 | }; 38 | 39 | STATUS getMedian(T& value) { 40 | if (_cnt > 0) { 41 | sort(); 42 | value = _as[_cnt/2]; 43 | return OK; 44 | } 45 | return NOK; 46 | }; 47 | 48 | STATUS getAverage(float &value) { 49 | if (_cnt > 0) { 50 | float sum = 0; 51 | for (uint8_t i=0; i< _cnt; i++) sum += _ar[i]; 52 | value = sum / _cnt; 53 | return OK; 54 | } 55 | return NOK; 56 | }; 57 | 58 | STATUS getAverage(uint8_t nMedians, float &value) { 59 | if ((_cnt > 0) && (nMedians > 0)) 60 | { 61 | if (_cnt < nMedians) nMedians = _cnt; // when filling the array for first time 62 | uint8_t start = ((_cnt - nMedians)/2); 63 | uint8_t stop = start + nMedians; 64 | sort(); 65 | float sum = 0; 66 | for (uint8_t i = start; i < stop; i++) sum += _as[i]; 67 | value = sum / nMedians; 68 | return OK; 69 | } 70 | return NOK; 71 | } 72 | 73 | STATUS getHighest(T& value) { 74 | if (_cnt > 0) { 75 | sort(); 76 | value = _as[_cnt-1]; 77 | return OK; 78 | } 79 | return NOK; 80 | }; 81 | 82 | STATUS getLowest(T& value) { 83 | if (_cnt > 0) { 84 | sort(); 85 | value = _as[0]; 86 | return OK; 87 | } 88 | return NOK; 89 | }; 90 | 91 | unsigned getSize() { 92 | return _size; 93 | }; 94 | 95 | unsigned getCount() { 96 | return _cnt; 97 | } 98 | 99 | STATUS getStatus() { 100 | return (_cnt > 0 ? OK : NOK); 101 | }; 102 | 103 | private: 104 | uint8_t _size; 105 | uint8_t _cnt; 106 | uint8_t _idx; 107 | T _ar[N]; 108 | T _as[N]; 109 | void sort() { 110 | // copy 111 | for (uint8_t i=0; i< _cnt; i++) _as[i] = _ar[i]; 112 | 113 | // sort all 114 | for (uint8_t i=0; i< _cnt-1; i++) { 115 | uint8_t m = i; 116 | for (uint8_t j=i+1; j< _cnt; j++) { 117 | if (_as[j] < _as[m]) m = j; 118 | } 119 | if (m != i) { 120 | T t = _as[m]; 121 | _as[m] = _as[i]; 122 | _as[i] = t; 123 | } 124 | } 125 | }; 126 | }; 127 | 128 | #endif 129 | -------------------------------------------------------------------------------- /RemoteSensor/defs.h: -------------------------------------------------------------------------------- 1 | #ifndef DEFS_H 2 | #define DEFS_H 3 | 4 | enum DataEnum{ 5 | DE_TEMP, 6 | DE_RH, 7 | DE_CO2, 8 | DE_CH2O, 9 | DE_VOC, 10 | DE_COUNT 11 | }; 12 | 13 | #define DF_TEMP (1< 3 | 4 | void eeMem::init() 5 | { 6 | EEPROM.begin(EESIZE); 7 | 8 | uint8_t data[EESIZE]; 9 | uint16_t *pwTemp = (uint16_t *)data; 10 | 11 | int addr = 0; 12 | for(int i = 0; i < EESIZE; i++, addr++) 13 | { 14 | data[i] = EEPROM.read( addr ); 15 | } 16 | 17 | if(pwTemp[0] != EESIZE) return; // revert to defaults if struct size changes 18 | uint16_t sum = pwTemp[1]; 19 | pwTemp[1] = 0; 20 | pwTemp[1] = Fletcher16(data, EESIZE ); 21 | if(pwTemp[1] != sum) return; // revert to defaults if sum fails 22 | memcpy(this + offsetof(eeMem, size), data, EESIZE ); 23 | } 24 | 25 | void eeMem::update() // write the settings if changed 26 | { 27 | uint16_t old_sum = ee.sum; 28 | ee.sum = 0; 29 | ee.sum = Fletcher16((uint8_t*)this + offsetof(eeMem, size), EESIZE); 30 | 31 | if(old_sum == ee.sum) 32 | return; // Nothing has changed? 33 | 34 | uint16_t addr = 0; 35 | uint8_t *pData = (uint8_t *)this + offsetof(eeMem, size); 36 | for(int i = 0; i < EESIZE; i++, addr++) 37 | { 38 | EEPROM.write(addr, pData[i] ); 39 | } 40 | EEPROM.commit(); 41 | } 42 | 43 | uint16_t eeMem::Fletcher16( uint8_t* data, int count) 44 | { 45 | uint16_t sum1 = 0; 46 | uint16_t sum2 = 0; 47 | 48 | for( int index = 0; index < count; ++index ) 49 | { 50 | sum1 = (sum1 + data[index]) % 255; 51 | sum2 = (sum2 + sum1) % 255; 52 | } 53 | 54 | return (sum2 << 8) | sum1; 55 | } 56 | -------------------------------------------------------------------------------- /RemoteSensor/eeMem.h: -------------------------------------------------------------------------------- 1 | #ifndef EEMEM_H 2 | #define EEMEM_H 3 | 4 | #include 5 | 6 | struct eflags 7 | { 8 | uint8_t PriEn:2; 9 | uint8_t bPIR:1; 10 | uint8_t bCall:1; 11 | uint8_t bCF:1; 12 | uint8_t bUseTime:1; 13 | uint8_t bEnableOLED:1; 14 | uint8_t res:1; 15 | }; 16 | 17 | #define EESIZE (offsetof(eeMem, end) - offsetof(eeMem, size) ) 18 | 19 | class eeMem 20 | { 21 | public: 22 | eeMem(){}; 23 | void init(void); 24 | void update(void); 25 | private: 26 | uint16_t Fletcher16( uint8_t* data, int count); 27 | public: 28 | uint16_t size = EESIZE; // if size changes, use defauls 29 | uint16_t sum = 0xAAAA; // if sum is diiferent from memory struct, write 30 | char szSSID[32] = ""; 31 | char szSSIDPassword[64] = ""; 32 | int8_t tz = -5; // Timezone offset 33 | eflags e = {0,1,1,1,0,0,0}; // PirEn, bPIR, bCall, bCF, bUseTime, bEnableOLED, res 34 | char szName[32] = "Sensor1"; 35 | uint32_t sensorID = 0x31534e53; // '1SNS'; 36 | int8_t tempCal = 0; 37 | uint16_t sendRate = 15; 38 | uint16_t logRate = 60; 39 | char szControlPassword[32] = "password"; 40 | uint8_t hostIP[4] = {192,168,31,100}; 41 | uint16_t hostPort = 80; 42 | uint8_t hvacIP[4] = {192,168,31,46}; 43 | uint32_t time_off = 0; 44 | uint32_t sleep = 30; 45 | uint32_t priSecs = 60*5; 46 | uint8_t pirPin = 12; 47 | uint16_t wAlertLevel[16] = {320, 1000, 0, 900, 0, 1000, 0, 10, 0, 20, 0, 1000, 0, 1000, 0, 1000}; // alert levels L/H 48 | int8_t rhCal = 0; 49 | uint8_t weight = 1; 50 | uint8_t res[30]; 51 | uint8_t end; 52 | }; 53 | 54 | extern eeMem ee; 55 | 56 | #endif // EEMEM_H 57 | -------------------------------------------------------------------------------- /RemoteSensor/jsonString.h: -------------------------------------------------------------------------------- 1 | class jsonString 2 | { 3 | public: 4 | jsonString(const char *pLabel = NULL) 5 | { 6 | m_cnt = 0; 7 | s = String("{"); 8 | if(pLabel) 9 | { 10 | s += "\"cmd\":\""; 11 | s += pLabel, s += "\","; 12 | } 13 | } 14 | 15 | String Close(void) 16 | { 17 | s += "}"; 18 | return s; 19 | } 20 | 21 | void Var(const char *key, int iVal) 22 | { 23 | if(m_cnt) s += ","; 24 | s += "\""; 25 | s += key; 26 | s += "\":"; 27 | s += iVal; 28 | m_cnt++; 29 | } 30 | 31 | void Var(const char *key, uint32_t iVal) 32 | { 33 | if(m_cnt) s += ","; 34 | s += "\""; 35 | s += key; 36 | s += "\":"; 37 | s += iVal; 38 | m_cnt++; 39 | } 40 | 41 | void Var(const char *key, long int iVal) 42 | { 43 | if(m_cnt) s += ","; 44 | s += "\""; 45 | s += key; 46 | s += "\":"; 47 | s += iVal; 48 | m_cnt++; 49 | } 50 | 51 | void Var(const char *key, float fVal) 52 | { 53 | if(m_cnt) s += ","; 54 | s += "\""; 55 | s += key; 56 | s += "\":"; 57 | s += fVal; 58 | m_cnt++; 59 | } 60 | 61 | void Var(const char *key, bool bVal) 62 | { 63 | if(m_cnt) s += ","; 64 | s += "\""; 65 | s += key; 66 | s += "\":"; 67 | s += bVal ? 1:0; 68 | m_cnt++; 69 | } 70 | 71 | void Var(const char *key, const char *sVal) 72 | { 73 | if(m_cnt) s += ","; 74 | s += "\""; 75 | s += key; 76 | s += "\":\""; 77 | s += sVal; 78 | s += "\""; 79 | m_cnt++; 80 | } 81 | 82 | void Var(const char *key, String sVal) 83 | { 84 | if(m_cnt) s += ","; 85 | s += "\""; 86 | s += key; 87 | s += "\":\""; 88 | s += sVal; 89 | s += "\""; 90 | m_cnt++; 91 | } 92 | 93 | void Array(const char *key, const char *sVal[]) 94 | { 95 | if(m_cnt) s += ","; 96 | s += "\""; 97 | s += key; 98 | s += "\":["; 99 | for(int i = 0; sVal[i]; i++) 100 | { 101 | if(i) s += ","; 102 | s += "\""; 103 | s += sVal[i]; 104 | s += "\""; 105 | } 106 | s += "]"; 107 | m_cnt++; 108 | } 109 | 110 | void Array(const char *key, uint16_t iVal[], int n) 111 | { 112 | if(m_cnt) s += ","; 113 | s += "\""; 114 | s += key; 115 | s += "\":["; 116 | for(int i = 0; i < n; i++) 117 | { 118 | if(i) s += ","; 119 | s += iVal[i]; 120 | } 121 | s += "]"; 122 | m_cnt++; 123 | } 124 | 125 | void Array(const char *key, uint32_t iVal[], int n) 126 | { 127 | if(m_cnt) s += ","; 128 | s += "\""; 129 | s += key; 130 | s += "\":["; 131 | for(int i = 0; i < n; i++) 132 | { 133 | if(i) s += ","; 134 | s += iVal[i]; 135 | } 136 | s += "]"; 137 | m_cnt++; 138 | } 139 | 140 | protected: 141 | String s; 142 | int m_cnt; 143 | }; 144 | -------------------------------------------------------------------------------- /RemoteSensor/tempArray.cpp: -------------------------------------------------------------------------------- 1 | #include "tempArray.h" 2 | #include "eeMem.h" 3 | #include "jsonstring.h" 4 | #include 5 | #include 6 | 7 | extern void WsSend(String s); 8 | 9 | void TempArray::init(uint16_t flags) 10 | { 11 | File F; 12 | 13 | F = SPIFFS.open("/weekly", "r"); 14 | F.read((byte*) &m_weekly, sizeof(m_weekly)); 15 | F.close(); 16 | F = SPIFFS.open("/daily", "r"); 17 | F.read((byte*) &m_daily, sizeof(m_daily)); 18 | F.close(); 19 | m_dataFlags = flags; 20 | } 21 | 22 | void TempArray::saveData() 23 | { 24 | File F; 25 | 26 | F = SPIFFS.open("/weekly", "w"); 27 | F.write((byte*) &m_weekly, sizeof(m_weekly)); 28 | F.close(); 29 | F = SPIFFS.open("/daily", "w"); 30 | F.write((byte*) &m_daily, sizeof(m_daily)); 31 | F.close(); 32 | } 33 | 34 | void TempArray::update(uint16_t Values[]) 35 | { 36 | if(Values[DE_TEMP] == 0 && Values[DE_RH] == 0) // check for invalid data 37 | return; 38 | 39 | for(int i = 0; i < DE_COUNT; i++) 40 | if(Values[i] > m_peakVal[i]) 41 | m_peakVal[i] = Values[i]; 42 | if(m_nWeek >= 0) // check for valid date 43 | logLH(Values, m_weekly, m_nWeek); 44 | if(m_nWeekDay >= 0) 45 | logLH(Values, m_daily, m_nWeekDay); 46 | 47 | m_sampleCount++; 48 | } 49 | 50 | void TempArray::logLH(uint16_t Values[], LHLog log[], int idx) 51 | { 52 | if(Values[DE_TEMP] < log[idx].temp[0]) log[idx].temp[0] = Values[DE_TEMP]; 53 | checkAlert("Temp", false, Values[DE_TEMP], ee.wAlertLevel[0]); 54 | if(Values[DE_TEMP] > log[idx].temp[1]) log[idx].temp[1] = Values[DE_TEMP]; 55 | checkAlert("Temp", true, Values[DE_TEMP], ee.wAlertLevel[1]); 56 | 57 | if(Values[DE_RH ] < log[idx].rh[0]) log[idx].rh[0] = Values[DE_RH]; 58 | checkAlert("Rh", false, Values[DE_RH], ee.wAlertLevel[2]); 59 | if(Values[DE_RH ] > log[idx].rh[1]) log[idx].rh[1] = Values[DE_RH]; 60 | checkAlert("Rh", true, Values[DE_RH], ee.wAlertLevel[3]); 61 | 62 | if(Values[DE_CO2] < log[idx].co2[0]) log[idx].co2[0] = Values[DE_CO2]; 63 | checkAlert("CO2", false, Values[DE_CO2], ee.wAlertLevel[4]); 64 | if(Values[DE_CO2] > log[idx].co2[1]) log[idx].co2[1] = Values[DE_CO2]; 65 | checkAlert("CO2", true, Values[DE_CO2], ee.wAlertLevel[5]); 66 | 67 | if(Values[DE_CH2O] < log[idx].ch2o[0]) log[idx].ch2o[0] = Values[DE_CH2O]; 68 | checkAlert("CH2O", false, Values[DE_CH2O], ee.wAlertLevel[6]); 69 | if(Values[DE_CH2O] > log[idx].ch2o[1]) log[idx].ch2o[1] = Values[DE_CH2O]; 70 | checkAlert("CH2O", true, Values[DE_CH2O], ee.wAlertLevel[7]); 71 | 72 | if(Values[DE_VOC] < log[idx].voc[0]) log[idx].voc[0] = Values[DE_VOC]; 73 | checkAlert("VOC", false, Values[DE_VOC], ee.wAlertLevel[8]); 74 | if(Values[DE_VOC] > log[idx].voc[1]) log[idx].voc[1] = Values[DE_VOC]; 75 | checkAlert("VOC", true, Values[DE_VOC], ee.wAlertLevel[9]); 76 | } 77 | 78 | void TempArray::checkAlert(String sName, bool bUD, uint16_t nNow, uint16_t nAlert) 79 | { 80 | if(m_bSilence || nAlert == 0) 81 | return; 82 | 83 | if( (bUD && nNow > nAlert) || (!bUD && nNow < nAlert) ) 84 | { 85 | String s = "{\"cmd\":\"alert\",\"text\":\""; 86 | s += sName; 87 | s += bUD ? " above " : " below "; 88 | s += nAlert; 89 | s += " at "; 90 | s += nNow; 91 | s += "\"}"; 92 | WsSend(s); 93 | // Todo: send report 94 | } 95 | } 96 | 97 | void TempArray::rangeAlert(const char *name, int16_t val) 98 | { 99 | String s = "{\"cmd\":\"alert\",\"text\":\""; 100 | s += name; 101 | s += " diff out of range "; 102 | s += val; 103 | s += "\"}"; 104 | WsSend(s); 105 | } 106 | 107 | void TempArray::resetLogEntry(LHLog log[], int idx) 108 | { 109 | log[idx].temp[0] = 0xFFFF; 110 | log[idx].temp[1] = 0; 111 | log[idx].rh[0] = 0xFFFF; 112 | log[idx].rh[1] = 0; 113 | log[idx].co2[0] = 0xFFFF; 114 | log[idx].co2[1] = 0; 115 | log[idx].ch2o[0] = 0xFF; 116 | log[idx].ch2o[1] = 0; 117 | log[idx].voc[0] = 0xFF; 118 | log[idx].voc[1] = 0; 119 | } 120 | 121 | void TempArray::add(uint32_t date, AsyncWebSocket &ws, int WsClientID) 122 | { 123 | if( (m_peakVal[DE_TEMP] == 0 && m_peakVal[DE_RH] == 0) || m_bValidDate == false) // nothing to do 124 | return; 125 | 126 | tempArr *p = &m_log[m_idx]; 127 | 128 | if(m_lastDate == 0) 129 | m_lastDate = date; 130 | p->m.tmdiff = constrain(date - m_lastDate, 0, 0xFFF); 131 | m_lastDate = date; 132 | 133 | int16_t val = m_lastVal[DE_TEMP] - m_peakVal[DE_TEMP]; 134 | p->m.temp = val; 135 | if(p->m.temp != val ) 136 | { 137 | p->m.temp /= 2; 138 | rangeAlert("Temp", val); 139 | } 140 | val = m_lastVal[DE_RH ] - m_peakVal[DE_RH ]; 141 | p->m.rh = val; 142 | if(p->m.rh != val ) 143 | { 144 | p->m.rh /= 2; 145 | rangeAlert("Rh", val); 146 | } 147 | 148 | if(m_dataFlags & DF_CO2) 149 | { 150 | val = m_lastVal[DE_CO2] - m_peakVal[DE_CO2]; 151 | p->m.co2 = val; 152 | if(p->m.co2 != val ) 153 | { 154 | p->m.co2 /= 2; 155 | rangeAlert("CO2", val ); 156 | } 157 | } 158 | if(m_dataFlags & DF_CH2O) 159 | { 160 | val = m_lastVal[DE_CH2O] - m_peakVal[DE_CH2O]; 161 | p->m.ch2o = val; 162 | if(p->m.ch2o != val ) 163 | { 164 | p->m.ch2o /= 2; 165 | rangeAlert("CH2O", val ); 166 | } 167 | } 168 | if(m_dataFlags & DF_VOC) 169 | { 170 | val = m_lastVal[DE_VOC ] - m_peakVal[DE_VOC ]; 171 | p->m.voc = val; 172 | if(p->m.voc != val ) 173 | { 174 | p->m.voc /= 2; 175 | rangeAlert("VOC", val ); 176 | } 177 | } 178 | 179 | tmElements_t tm; 180 | breakTime(date, tm); 181 | tm.Hour = tm.Minute = tm.Second = 0; 182 | tm.Wday = 1; 183 | tm.Month = 1; 184 | tm.Day = 1; 185 | 186 | uint32_t tmDiff = date - makeTime(tm); 187 | int8_t week = tmDiff / (60*60*24*7); 188 | bool bReset = (m_nWeek != -1 && week != m_nWeek) || (m_weekly[week].temp[0] == 0 && m_weekly[week].temp[1] == 0); // initial range fix 189 | 190 | m_nWeek = week; 191 | 192 | if(bReset) 193 | { 194 | resetLogEntry(m_weekly, m_nWeek); 195 | } 196 | 197 | if(weekday()-1 != m_nWeekDay) 198 | { 199 | bReset = (m_nWeekDay != -1) || (m_daily[weekday() - 1].temp[0] == 0 && m_daily[weekday() - 1].temp[1] == 0); 200 | m_nWeekDay = weekday() - 1; 201 | if(bReset) 202 | { 203 | saveData(); 204 | resetLogEntry(m_daily, m_nWeekDay); 205 | } 206 | } 207 | 208 | m_sampleCount = 0; 209 | sendNew(m_peakVal, date, ws, WsClientID); 210 | memcpy(&m_lastVal, m_peakVal, sizeof(m_lastVal)); 211 | memset(&m_peakVal, 0, sizeof(m_peakVal)); // reset the peaks 212 | 213 | if(++m_idx >= LOG_CNT) 214 | m_idx = 0; 215 | m_log[m_idx].m.u[0] = 0; // mark as invalid data/end 216 | } 217 | 218 | bool TempArray::get(int &pidx, int n) 219 | { 220 | if(n < 0 || n > LOG_CNT-1) // convert 0-(LOG_CNT-1) to reverse index circular buffer 221 | return false; 222 | int idx = m_idx - 1 - n; // 0 = last entry 223 | if(idx < 0) idx += LOG_CNT; 224 | if(m_log[idx].m.u[0] == 0) // invalid data 225 | return false; 226 | pidx = idx; 227 | return true; 228 | } 229 | 230 | #define CHUNK_SIZE 800 231 | 232 | // send the log in chucks of CHUNK_SIZE 233 | void TempArray::historyDump(bool bStart, AsyncWebSocket &ws, int WsClientID) 234 | { 235 | static bool bSending; 236 | static int entryIdx; 237 | 238 | if(ws.availableForWrite(WsClientID) == false) 239 | return; 240 | 241 | int aidx; 242 | if(bStart) 243 | { 244 | m_nSending = 1; 245 | entryIdx = 0; 246 | 247 | jsonString js("ref"); 248 | js.Var("tb" , m_lastDate); // date of first entry 249 | const char *labels[] = {"Temp", "Rh", "CO2", "CH2O", "VOC", NULL}; 250 | js.Array("label", labels); 251 | uint16_t decimals[] = {1, 1, 0, 0, 0}; 252 | js.Array("dec", decimals, sizeof(decimals)/sizeof(uint16_t)); 253 | js.Array("base", m_lastVal, sizeof(m_lastVal)/sizeof(uint16_t)); 254 | js.Array("alert", ee.wAlertLevel, sizeof(ee.wAlertLevel)/sizeof(uint16_t)); 255 | ws.text(WsClientID, js.Close()); 256 | 257 | if( get(aidx, 0) == false) 258 | { 259 | bSending = false; 260 | ws.text(WsClientID, "{\"cmd\":\"data\",{\"d\":[]}"); 261 | return; 262 | } 263 | } 264 | 265 | switch(m_nSending) 266 | { 267 | case 0: return; 268 | case 1: // daily 269 | logDump(true, ws, WsClientID, 1); 270 | m_nSending = 2; 271 | return; 272 | case 2: // weekly 273 | if(logDump(true, ws, WsClientID, 0) == false) 274 | m_nSending = 4; // completed 275 | else 276 | m_nSending = 3; 277 | return; 278 | case 3: // weekly cont 279 | if(logDump(false, ws, WsClientID, 0) == false) 280 | m_nSending = 4; 281 | return; 282 | } 283 | 284 | // minute 285 | String out; 286 | out.reserve(CHUNK_SIZE + 100); 287 | 288 | out = "{\"cmd\":\"data\",\"d\":["; 289 | 290 | bool bC = false; 291 | 292 | for(; entryIdx < LOG_CNT - 1 && out.length() < CHUNK_SIZE && get(aidx, entryIdx); entryIdx++) 293 | { 294 | int len = out.length(); 295 | if(bC) out += ","; 296 | bC = true; 297 | out += "["; // [seconds, temp, rh], 298 | out += m_log[aidx].m.tmdiff; // seconds differential from next entry 299 | out += ","; 300 | out += m_log[aidx].m.temp; 301 | out += ","; 302 | out += m_log[aidx].m.rh; 303 | if(m_dataFlags & DF_CO2 ) 304 | { 305 | out += ","; 306 | out += m_log[aidx].m.co2; 307 | } 308 | if(m_dataFlags & DF_CH2O ) 309 | { 310 | out += ","; 311 | out += m_log[aidx].m.ch2o; 312 | } 313 | if(m_dataFlags & DF_VOC ) 314 | { 315 | out += ","; 316 | out += m_log[aidx].m.voc; 317 | } 318 | out += "]"; 319 | if( out.length() == len) // memory full 320 | break; 321 | } 322 | out += "]}"; 323 | if(bC == false) // done 324 | m_nSending = 0; 325 | else 326 | ws.text(WsClientID, out); 327 | } 328 | 329 | bool TempArray::logDump(bool bStart, AsyncWebSocket &ws, int WsClientID, int logType) 330 | { 331 | static bool bSending[3]; 332 | static int entryIdx; 333 | 334 | if(bStart) 335 | bSending[logType] = true; 336 | if(bSending[logType] == false) 337 | return false; 338 | 339 | if(bStart) 340 | entryIdx = 0; 341 | 342 | String out; 343 | out.reserve(CHUNK_SIZE + 100); 344 | 345 | LHLog *pLog = m_daily; 346 | int nCount = 7; 347 | 348 | switch(logType) 349 | { 350 | case 0:// Weekly 351 | pLog = m_weekly; 352 | nCount = 52; 353 | out = "{\"cmd\":\"weekly\",\"d\":["; 354 | break; 355 | case 1:// daily 356 | pLog = m_daily; 357 | nCount = 7; 358 | out = "{\"cmd\":\"daily\",\"d\":["; 359 | break; 360 | } 361 | 362 | bool bC = false; 363 | 364 | for(; entryIdx < nCount && out.length() < CHUNK_SIZE; entryIdx++) 365 | { 366 | int len = out.length(); 367 | if(bC) out += ","; 368 | bC = true; 369 | out += "["; 370 | out += pLog[entryIdx].temp[0]; 371 | out += ","; 372 | out += pLog[entryIdx].temp[1]; 373 | out += ","; 374 | out += pLog[entryIdx].rh[0]; 375 | out += ","; 376 | out += pLog[entryIdx].rh[1]; 377 | if(m_dataFlags & DF_CO2 ) 378 | { 379 | out += ","; 380 | out += pLog[entryIdx].co2[0]; 381 | out += ","; 382 | out += pLog[entryIdx].co2[1]; 383 | } 384 | if(m_dataFlags & DF_CH2O ) 385 | { 386 | out += ","; 387 | out += pLog[entryIdx].ch2o[0]; 388 | out += ","; 389 | out += pLog[entryIdx].ch2o[1]; 390 | } 391 | if(m_dataFlags & DF_VOC ) 392 | { 393 | out += ","; 394 | out += pLog[entryIdx].voc[0]; 395 | out += ","; 396 | out += pLog[entryIdx].voc[1]; 397 | } 398 | out += "]"; 399 | if( out.length() == len) // memory full 400 | break; 401 | } 402 | out += "]}"; 403 | if(bC) 404 | ws.text(WsClientID, out); 405 | else 406 | bSending[logType] = false; 407 | return bC; 408 | } 409 | 410 | void TempArray::sendNew(uint16_t Values[], uint32_t date, AsyncWebSocket &ws, int WsClientID) 411 | { 412 | String out = "{\"cmd\":\"data2\",\"d\":[["; 413 | 414 | out += m_lastDate; 415 | out += ","; 416 | out += Values[DE_TEMP]; 417 | out += ","; 418 | out += Values[DE_RH]; 419 | if(m_dataFlags & DF_CO2) 420 | { 421 | out += ","; 422 | out += Values[DE_CO2]; 423 | } 424 | if(m_dataFlags & DF_CH2O ) 425 | { 426 | out += ","; 427 | out += Values[DE_CH2O]; 428 | } 429 | if(m_dataFlags & DF_VOC ) 430 | { 431 | out += ","; 432 | out += Values[DE_VOC]; 433 | } 434 | 435 | out += "]]}"; 436 | ws.text(WsClientID, out); 437 | } 438 | -------------------------------------------------------------------------------- /RemoteSensor/tempArray.h: -------------------------------------------------------------------------------- 1 | #ifndef TEMPARRAY_H 2 | #define TEMPARRAY_H 3 | 4 | #include 5 | #include 6 | #include "defs.h" 7 | 8 | union mbits 9 | { 10 | uint32_t u[2]; 11 | struct 12 | { 13 | uint32_t tmdiff:12; 14 | int32_t rh:7; 15 | int32_t temp:7; 16 | int32_t co2:6; 17 | //-- 18 | int32_t voc:4; 19 | int32_t ch2o:4; 20 | int32_t error:6; 21 | int32_t res:18; 22 | }; 23 | }; 24 | 25 | struct tempArr 26 | { 27 | mbits m; 28 | }; 29 | 30 | struct LHLog 31 | { 32 | uint16_t temp[2]; 33 | uint16_t rh[2]; 34 | uint16_t co2[2]; 35 | uint8_t voc[2]; 36 | uint8_t ch2o[2]; 37 | }; 38 | 39 | #define LOG_CNT 1440 // 1 day at 1 minute 40 | 41 | class TempArray 42 | { 43 | public: 44 | TempArray(){}; 45 | void init(uint16_t flags); 46 | void saveData(void); 47 | void update(uint16_t Values[]); 48 | void add(uint32_t date, AsyncWebSocket &ws, int WsClientID); 49 | void historyDump(bool bStart, AsyncWebSocket &ws, int WsClientID); 50 | 51 | bool m_bSilence; 52 | bool m_bValidDate; 53 | 54 | protected: 55 | bool get(int &pidx, int n); 56 | void sendNew(uint16_t Values[], uint32_t date, AsyncWebSocket &ws, int WsClientID); 57 | void logLH(uint16_t Values[], LHLog log[], int idx); 58 | void resetLogEntry(LHLog log[], int idx); 59 | void checkAlert(String sName, bool bUD, uint16_t nNow, uint16_t nAlert); 60 | void rangeAlert(char *name, int16_t val); 61 | bool logDump(bool bStart, AsyncWebSocket &ws, int WsClientID, int logType); 62 | 63 | tempArr m_log[LOG_CNT]; 64 | LHLog m_daily[7]; 65 | LHLog m_weekly[53]; 66 | int16_t m_idx; 67 | uint16_t m_dataFlags; 68 | uint32_t m_lastDate; 69 | uint16_t m_peakVal[DE_COUNT]; 70 | uint16_t m_lastVal[DE_COUNT]; 71 | uint16_t m_sampleCount; 72 | int8_t m_nWeek = -1; 73 | int8_t m_nWeekDay = -1; 74 | uint8_t m_nSending; 75 | }; 76 | 77 | #endif 78 | -------------------------------------------------------------------------------- /RemoteSensor/tuya.cpp: -------------------------------------------------------------------------------- 1 | // Class for Tuya temp and humidity 2 | 3 | #include "tuya.h" 4 | #include 5 | #include 6 | #include "defs.h" 7 | 8 | //#define BWAR01 // BlitzWolf BW-AR01 9 | 10 | //#define DEBUG 11 | #define BAUD 9600 12 | 13 | #ifdef BWAR01 // BlitzWolf BW-AR01 14 | #define WIFI_LED 13 // high = on 15 | #define BTN 14 // button (reset button) 16 | #else // Tuya WIFI Temperature Humidity Smart Sensor Clock Digital Display - Type A (must replace with ESP8266 and add resistors to IO15 and REST, cut trace to REST) 17 | #define ESP_LED 2 // low = on 18 | #define WIFI_LED 4 // high = on 19 | #define BTN 13 // C/F button (connected to MCU as well) 20 | #endif 21 | 22 | extern UdpTime utime; 23 | 24 | extern void WsSend(String s); 25 | 26 | void TuyaInterface::init(bool bCF) 27 | { 28 | #ifdef BWAR01 29 | m_dataFlags = 0x1F; // enable all sensors 30 | #else 31 | m_dataFlags = (DF_TEMP | DF_RH); 32 | #endif 33 | pinMode(WIFI_LED, OUTPUT); 34 | digitalWrite(WIFI_LED, LOW); 35 | #ifdef ESP_LED 36 | pinMode(ESP_LED, OUTPUT); 37 | digitalWrite(ESP_LED, HIGH); 38 | #endif 39 | pinMode(BTN, INPUT_PULLUP); 40 | Serial.begin(BAUD); 41 | m_bCF = bCF; 42 | } 43 | 44 | int TuyaInterface::service(int8_t tcal, int8_t rhcal) 45 | { 46 | static uint8_t inBuffer[52]; 47 | static uint8_t idx; 48 | static uint8_t v; 49 | static uint8_t state; 50 | static uint8_t cmd; 51 | static bool bInit = false; 52 | static uint16_t len; 53 | uint8_t n; 54 | uint8_t buf[2]; 55 | uint32_t val; 56 | 57 | while(Serial.available()) 58 | { 59 | uint8_t c = Serial.read(); 60 | switch(state) 61 | { 62 | case 0: // data packet: 55 AA vers cmd 00 len d0 d1 d2.... chk 63 | if(c == 0x55) 64 | state = 1; 65 | else if(c == 0xAA) 66 | state = 2; 67 | break; 68 | case 1: 69 | if(c == 0xAA) 70 | state = 2; 71 | break; 72 | case 2: 73 | // version 3 74 | v = c; 75 | state = 3; 76 | break; 77 | case 3: 78 | cmd = c; 79 | state = 4; 80 | break; 81 | case 4: 82 | len = (uint16_t)c<<8; 83 | state = 5; 84 | break; 85 | case 5: 86 | len |= (uint16_t)c; 87 | state = 6; 88 | idx = 0; 89 | break; 90 | case 6: 91 | inBuffer[idx++] = c; // get length + checksum 92 | if(idx > len || idx >= sizeof(inBuffer) ) 93 | { 94 | uint8_t chk = 0xFF + len + v + cmd; 95 | for(int a = 0; a < len; a++) 96 | chk += inBuffer[a]; 97 | #ifdef DEBUG 98 | String s = "{\"cmd\":\"print\",\"text\":\"RX "; 99 | s += len; 100 | s += " "; 101 | for(int a = 0; a < len; a++) 102 | { 103 | s += " "; 104 | s += String(inBuffer[a], 16); 105 | } 106 | s += "\"}"; 107 | WsSend(s); 108 | #endif 109 | if( inBuffer[len] == chk) // good checksum 110 | { 111 | switch(cmd) 112 | { 113 | case TC_HEARTBEAT: // heartbeat 01 = MCU reset 114 | writeSerial(TC_MCU_CONF); 115 | break; 116 | case TC_QUERY_PRODUCT: // product ID (42 bytes) 117 | break; 118 | case TC_MCU_CONF: // ack for MCU conf 09 06 or 0D 0E 119 | m_mcuConf[0] = inBuffer[0]; 120 | m_mcuConf[1] = inBuffer[1]; 121 | #ifdef BWAR01 // Causes AR-01 to start/reset (first temp/RH values are 0) 122 | if(bInit == false) 123 | { 124 | bInit = true; 125 | writeSerial(TC_QUERY_STATE); 126 | } 127 | #else // Causes Tuya clock to continue running 128 | writeSerial(TC_QUERY_STATE); 129 | #endif 130 | break; 131 | case TC_STATE: // data 132 | val = (inBuffer[4] << 24) | (inBuffer[5] << 16) | (inBuffer[6] << 8) | inBuffer[7]; // big endien long 133 | switch(inBuffer[0]) 134 | { 135 | #ifdef BWAR01 136 | case 2: // 02 02 00 04 00 00 00 01 // CH2O 137 | if(val != m_values[DE_CH2O]) 138 | m_bUpdated = true; 139 | m_values[DE_CH2O] = val; 140 | break; 141 | 142 | case 0x12: // 12 02 00 04 00 00 00 E5 // temp C 143 | if(val == 0) break; // reset 144 | if(m_bCF) 145 | val = val * 90 / 50 + 320; 146 | val += tcal; 147 | if(val != m_values[DE_TEMP] ) 148 | m_bUpdated = true; 149 | m_values[DE_TEMP] = val; 150 | break; 151 | case 0x13: // 13 02 00 04 00 00 02 1B // Rh 152 | if(val == 0) break; // reset 153 | if(val != m_values[DE_RH] ) 154 | m_bUpdated = true; 155 | m_values[DE_RH] = val + rhcal; 156 | break; 157 | case 0x15: // 15 02 00 04 00 00 00 01 // VOC 158 | if(val != m_values[DE_VOC] ) 159 | m_bUpdated = true; 160 | m_values[DE_VOC] = val; 161 | break; 162 | case 0x16: // 16 02 00 04 00 00 01 66 // CO2 163 | if(val != m_values[DE_CO2] ) 164 | m_bUpdated = true; 165 | m_values[DE_CO2] = val; 166 | break; 167 | 168 | #else // Tuya T & H clock Type A (banggood) 169 | case 1: // 01 02 00 04 00 00 00 FC // temp 00FC = 25.2C 77.36F 170 | if(m_bCF) 171 | val = val * 90 / 50 + 320; 172 | val += tcal; 173 | if(val != m_values[DE_TEMP] ) 174 | m_bUpdated = true; 175 | m_values[DE_TEMP] = val; 176 | break; 177 | case 2: // 02 02 00 04 00 00 00 3B // rh 003B = 55% 178 | val *= 10; 179 | val += rhcal; 180 | if(val != m_values[DE_RH]) 181 | m_bUpdated = true; 182 | m_values[DE_RH] = val; 183 | break; 184 | #endif 185 | case 9: // len = 5 186 | // 09 04 00 01 01 // C/F button setting 187 | // m_bCF = inBuffer[4]; 188 | break; 189 | default: 190 | { 191 | String s = "{\"cmd\":\"print\",\"text\":\"unknown register "; 192 | s += inBuffer[0]; 193 | s += "\"}"; 194 | WsSend(s); 195 | } 196 | break; 197 | } 198 | break; 199 | case TC_SET_DATE: // requesting date 200 | sendDate(); 201 | break; 202 | case TC_SIGNAL: 203 | buf[0] = m_signal; 204 | writeSerial(TC_SIGNAL, buf, 1); 205 | break; 206 | case TC_UNK1: // not sure yet 207 | buf[0] = 4; 208 | writeSerial(TC_UNK1, buf, 1); 209 | break; 210 | default: 211 | { 212 | String s = "{\"cmd\":\"print\",\"text\":\"Tuya cmd unknown "; 213 | s += cmd; 214 | s += "\"}"; 215 | WsSend(s); 216 | } 217 | break; 218 | } 219 | } 220 | state = 0; 221 | idx = 0; 222 | len = 0; 223 | } 224 | break; 225 | } 226 | return 0; 227 | } 228 | 229 | static uint8_t sec; 230 | if(sec != second()) 231 | { 232 | sec = second(); 233 | if(--m_cs == 0) // start the sequence 234 | { 235 | writeSerial(TC_HEARTBEAT); 236 | m_cs = 15; 237 | } 238 | } 239 | 240 | static bool bBtn = true; 241 | if( digitalRead(BTN) != bBtn ) 242 | { 243 | bBtn = digitalRead(BTN); 244 | // if( bBtn ) // release 245 | // setCF( !m_bCF ); 246 | } 247 | return 0; 248 | } 249 | 250 | int TuyaInterface::status() 251 | { 252 | return m_status; 253 | } 254 | 255 | void TuyaInterface::setSignal(int db) 256 | { 257 | m_signal = 255 + (db * 2); 258 | } 259 | 260 | void TuyaInterface::setCF(bool f) 261 | { 262 | m_bCF = f; 263 | 264 | /* uint8_t data[5]; 265 | 266 | data[0] = 0x09; 267 | data[1] = 0x04; 268 | data[2] = 0x00; 269 | data[3] = 0x01; // byte value 270 | data[4] = f ? 1:0; // C/F 271 | writeSerial(TC_STATE, data, 5); 272 | */ 273 | } 274 | 275 | // writeSerial(TC_SET_DP); 276 | 277 | void TuyaInterface::sendDate() 278 | { 279 | tmElements_t tm; 280 | breakTime(now(), tm); 281 | 282 | uint8_t data[8]; 283 | data[0] = 1; // must be 1 284 | data[1] = tm.Year - 30; // offset from 1971 285 | data[2] = tm.Month; 286 | data[3] = tm.Day; 287 | data[4] = tm.Hour; 288 | data[5] = tm.Minute; 289 | data[6] = tm.Second; 290 | data[7] = weekday() - 1; 291 | writeSerial(TC_SET_DATE, data, 8); 292 | } 293 | 294 | bool TuyaInterface::writeSerial(uint8_t cmd, uint8_t *p, uint16_t len) 295 | { 296 | uint8_t buf[16]; 297 | 298 | buf[0] = 0x55; 299 | buf[1] = 0xAA; 300 | buf[2] = 0; // version 301 | buf[3] = cmd; 302 | buf[4] = len >> 8; // 16 bit len big endien 303 | buf[5] = len & 0xFF; 304 | 305 | int i; 306 | if(p) for(i = 0; i < len; i++) 307 | buf[6 + i] = p[i]; 308 | 309 | uint16_t chk = 0; 310 | for(i = 0; i < len + 6; i++) 311 | chk += buf[i]; 312 | buf[6 + len] = (uint8_t)chk; 313 | 314 | #ifdef DEBUG 315 | String s = "{\"cmd\":\"print\",\"text\":\"TX "; 316 | s += len; 317 | s += " "; 318 | for(int a = 0; a < len+7; a++) 319 | { 320 | s += " "; 321 | s += String(buf[a], 16); 322 | } 323 | s += "\"}"; 324 | WsSend(s); 325 | #endif 326 | return Serial.write(buf, 7 + len); 327 | } 328 | 329 | void TuyaInterface::setLED(uint8_t no, bool bOn) 330 | { 331 | #ifdef ESP_LED 332 | m_bLED[no] = bOn; 333 | no = constrain(no, 0, 1); 334 | if(no == 0) 335 | digitalWrite(WIFI_LED, bOn); 336 | else 337 | digitalWrite(ESP_LED, !bOn); 338 | #else // no LED on module 339 | m_bLED[0] = bOn; 340 | digitalWrite(WIFI_LED, bOn); 341 | #endif 342 | } 343 | -------------------------------------------------------------------------------- /RemoteSensor/tuya.h: -------------------------------------------------------------------------------- 1 | #ifndef TUYA_H 2 | #define TUYA_H 3 | 4 | #include 5 | #include "defs.h" 6 | 7 | // Tuya commands 8 | enum TuyaCmd{ 9 | TC_HEARTBEAT, // 0 MCU responds with 0 or 1 10 | TC_QUERY_PRODUCT, // 0x01 MCU responds with long product string 11 | TC_MCU_CONF, // 0x02 12 | TC_WIFI_STATE, // 0x03 13 | TC_WIFI_RESET, // 0x04 14 | TC_WIFI_SELECT, // 0x05 15 | TC_SET_DP, // 0x06 16 | TC_STATE, // 0x07 17 | TC_QUERY_STATE, // 0x08 18 | TC_SET_DATE = 0x1C, 19 | TC_SIGNAL = 0x24, // From MCU, respond with 00-D9 for singal strength 20 | TC_UNK1 = 0x2B, // From MCU, respond with 02-D4 21 | }; 22 | 23 | enum TuyaDataType{ 24 | TT_NULL, 25 | TT_BOOL, // 0x01 26 | TT_VALUE, // 0x02 27 | TT_STRING,// 0x03 28 | TT_ENUM, // 0x04 29 | }; 30 | 31 | enum TuyaDataRegister{ 32 | TR_NULL, 33 | TR_TEMP1, // Tuya clock 34 | TR_CHO2 = 2, // Rh on Tuya clock 35 | TR_TEMP = 0x12, // C 36 | TR_RH = 0x13, // % 37 | TR_VOC = 0x15, // mg/m3 38 | TR_CO2 = 0x16, // ppm 39 | }; 40 | 41 | class TuyaInterface 42 | { 43 | public: 44 | TuyaInterface(){}; 45 | void init(bool bCF); 46 | int service(int8_t tcal, int8_t rhcal); 47 | void setLED(uint8_t no, bool bOn); 48 | void setCF(bool f); 49 | void setSignal(int db); 50 | int status(void); 51 | bool m_bLED[2]; 52 | bool m_bUpdated = false; 53 | bool m_bCF = false; 54 | uint16_t m_dataFlags; 55 | uint16_t m_values[6]; 56 | private: 57 | bool writeSerial(uint8_t cmd, uint8_t *p = NULL, uint16_t len = 0); 58 | void checkStatus(void); 59 | void sendDate(void); 60 | uint8_t m_cs = 2; 61 | int m_status = 0; 62 | uint8_t m_mcuConf[2]; 63 | uint8_t m_signal = 0; 64 | }; 65 | 66 | #endif // TUYA_H 67 | -------------------------------------------------------------------------------- /Thermostat.HMI: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CuriousTech/ESP-HVAC/4e871605f25c728640b037e73a122562fc7b20c2/Thermostat.HMI --------------------------------------------------------------------------------