Fork of the espurna firmware for `mhsw` switches
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

826 lines
33 KiB

  1. /*
  2. THERMOSTAT MODULE
  3. Copyright (C) 2017 by Dmitry Blinov <dblinov76 at gmail dot com>
  4. */
  5. #if THERMOSTAT_SUPPORT
  6. #include <ArduinoJson.h>
  7. #include <float.h>
  8. bool _thermostat_enabled = true;
  9. bool _thermostat_mode_cooler = false;
  10. const char* NAME_THERMOSTAT_ENABLED = "thermostatEnabled";
  11. const char* NAME_THERMOSTAT_MODE = "thermostatMode";
  12. const char* NAME_TEMP_RANGE_MIN = "tempRangeMin";
  13. const char* NAME_TEMP_RANGE_MAX = "tempRangeMax";
  14. const char* NAME_REMOTE_SENSOR_NAME = "remoteSensorName";
  15. const char* NAME_REMOTE_TEMP_MAX_WAIT = "remoteTempMaxWait";
  16. const char* NAME_ALONE_ON_TIME = "aloneOnTime";
  17. const char* NAME_ALONE_OFF_TIME = "aloneOffTime";
  18. const char* NAME_MAX_ON_TIME = "maxOnTime";
  19. const char* NAME_MIN_OFF_TIME = "minOffTime";
  20. const char* NAME_BURN_TOTAL = "burnTotal";
  21. const char* NAME_BURN_TODAY = "burnToday";
  22. const char* NAME_BURN_YESTERDAY = "burnYesterday";
  23. const char* NAME_BURN_THIS_MONTH = "burnThisMonth";
  24. const char* NAME_BURN_PREV_MONTH = "burnPrevMonth";
  25. const char* NAME_BURN_DAY = "burnDay";
  26. const char* NAME_BURN_MONTH = "burnMonth";
  27. const char* NAME_OPERATION_MODE = "thermostatOperationMode";
  28. #define ASK_TEMP_RANGE_INTERVAL_INITIAL 15000 // ask initially once per every 15 seconds
  29. #define ASK_TEMP_RANGE_INTERVAL_REGULAR 60000 // ask every minute to be sure
  30. #define MILLIS_IN_SEC 1000
  31. #define MILLIS_IN_MIN 60000
  32. #define THERMOSTAT_STATE_UPDATE_INTERVAL 60000 // 1 min
  33. #define THERMOSTAT_RELAY 0 // use relay 0
  34. #define THERMOSTAT_TEMP_RANGE_MIN 10 // grad. Celsius
  35. #define THERMOSTAT_TEMP_RANGE_MIN_MIN 3 // grad. Celsius
  36. #define THERMOSTAT_TEMP_RANGE_MIN_MAX 30 // grad. Celsius
  37. #define THERMOSTAT_TEMP_RANGE_MAX 20 // grad. Celsius
  38. #define THERMOSTAT_TEMP_RANGE_MAX_MIN 8 // grad. Celsius
  39. #define THERMOSTAT_TEMP_RANGE_MAX_MAX 35 // grad. Celsius
  40. #define THERMOSTAT_ALONE_ON_TIME 5 // 5 min
  41. #define THERMOSTAT_ALONE_OFF_TIME 55 // 55 min
  42. #define THERMOSTAT_MAX_ON_TIME 30 // 30 min
  43. #define THERMOSTAT_MIN_OFF_TIME 10 // 10 min
  44. unsigned long _thermostat_remote_temp_max_wait = THERMOSTAT_REMOTE_TEMP_MAX_WAIT * MILLIS_IN_SEC;
  45. unsigned long _thermostat_alone_on_time = THERMOSTAT_ALONE_ON_TIME * MILLIS_IN_MIN;
  46. unsigned long _thermostat_alone_off_time = THERMOSTAT_ALONE_OFF_TIME * MILLIS_IN_MIN;
  47. unsigned long _thermostat_max_on_time = THERMOSTAT_MAX_ON_TIME * MILLIS_IN_MIN;
  48. unsigned long _thermostat_min_off_time = THERMOSTAT_MIN_OFF_TIME * MILLIS_IN_MIN;
  49. unsigned int _thermostat_on_time_for_day = 0;
  50. unsigned int _thermostat_burn_total = 0;
  51. unsigned int _thermostat_burn_today = 0;
  52. unsigned int _thermostat_burn_yesterday = 0;
  53. unsigned int _thermostat_burn_this_month = 0;
  54. unsigned int _thermostat_burn_prev_month = 0;
  55. unsigned int _thermostat_burn_day = 0;
  56. unsigned int _thermostat_burn_month = 0;
  57. struct temp_t {
  58. float temp;
  59. unsigned long last_update = 0;
  60. bool need_display_update = false;
  61. };
  62. temp_t _remote_temp;
  63. struct temp_range_t {
  64. int min = THERMOSTAT_TEMP_RANGE_MIN;
  65. int max = THERMOSTAT_TEMP_RANGE_MAX;
  66. unsigned long last_update = 0;
  67. unsigned long ask_time = 0;
  68. unsigned int ask_interval = 0;
  69. bool need_display_update = true;
  70. };
  71. temp_range_t _temp_range;
  72. enum temperature_source_t {temp_none, temp_local, temp_remote};
  73. struct thermostat_t {
  74. unsigned long last_update = 0;
  75. unsigned long last_switch = 0;
  76. String remote_sensor_name;
  77. unsigned int temperature_source = temp_none;
  78. };
  79. thermostat_t _thermostat;
  80. enum thermostat_cycle_type {cooling, heating};
  81. unsigned int _thermostat_cycle = heating;
  82. String thermostat_remote_sensor_topic;
  83. //------------------------------------------------------------------------------
  84. void thermostatEnabled(bool enabled) {
  85. _thermostat_enabled = enabled;
  86. }
  87. //------------------------------------------------------------------------------
  88. bool thermostatEnabled() {
  89. return _thermostat_enabled;
  90. }
  91. //------------------------------------------------------------------------------
  92. void thermostatModeCooler(bool cooler) {
  93. _thermostat_mode_cooler = cooler;
  94. }
  95. //------------------------------------------------------------------------------
  96. bool thermostatModeCooler() {
  97. return _thermostat_mode_cooler;
  98. }
  99. //------------------------------------------------------------------------------
  100. std::vector<thermostat_callback_f> _thermostat_callbacks;
  101. void thermostatRegister(thermostat_callback_f callback) {
  102. _thermostat_callbacks.push_back(callback);
  103. }
  104. //------------------------------------------------------------------------------
  105. void updateOperationMode() {
  106. #if WEB_SUPPORT
  107. String message;
  108. if (_thermostat.temperature_source == temp_remote) {
  109. message = "{\"thermostatVisible\": 1, \"thermostatOperationMode\": \"remote temperature\"}";
  110. updateRemoteTemp(true);
  111. } else if (_thermostat.temperature_source == temp_local) {
  112. message = "{\"thermostatVisible\": 1, \"thermostatOperationMode\": \"local temperature\"}";
  113. updateRemoteTemp(false);
  114. } else {
  115. message = "{\"thermostatVisible\": 1, \"thermostatOperationMode\": \"autonomous\"}";
  116. updateRemoteTemp(false);
  117. }
  118. wsSend(message.c_str());
  119. #endif
  120. }
  121. //------------------------------------------------------------------------------
  122. void updateRemoteTemp(bool remote_temp_actual) {
  123. #if WEB_SUPPORT
  124. char tmp_str[6];
  125. if (remote_temp_actual) {
  126. dtostrf(_remote_temp.temp, 1-sizeof(tmp_str), 1, tmp_str);
  127. } else {
  128. strcpy(tmp_str, "\"?\"");
  129. }
  130. char buffer[100];
  131. snprintf_P(buffer, sizeof(buffer), PSTR("{\"thermostatVisible\": 1, \"remoteTmp\": %s}"), tmp_str);
  132. wsSend(buffer);
  133. #endif
  134. }
  135. //------------------------------------------------------------------------------
  136. // MQTT
  137. //------------------------------------------------------------------------------
  138. void thermostatMQTTCallback(unsigned int type, const char * topic, const char * payload) {
  139. if (type == MQTT_CONNECT_EVENT) {
  140. mqttSubscribeRaw(thermostat_remote_sensor_topic.c_str());
  141. mqttSubscribe(MQTT_TOPIC_HOLD_TEMP);
  142. _temp_range.ask_interval = ASK_TEMP_RANGE_INTERVAL_INITIAL;
  143. _temp_range.ask_time = millis();
  144. }
  145. if (type == MQTT_MESSAGE_EVENT) {
  146. // Match topic
  147. String t = mqttMagnitude((char *) topic);
  148. if (strcmp(topic, thermostat_remote_sensor_topic.c_str()) != 0
  149. && !t.equals(MQTT_TOPIC_HOLD_TEMP))
  150. return;
  151. // Parse JSON input
  152. DynamicJsonBuffer jsonBuffer;
  153. JsonObject& root = jsonBuffer.parseObject(payload);
  154. if (!root.success()) {
  155. DEBUG_MSG_P(PSTR("[THERMOSTAT] Error parsing data\n"));
  156. return;
  157. }
  158. // Check rempte sensor temperature
  159. if (strcmp(topic, thermostat_remote_sensor_topic.c_str()) == 0) {
  160. if (root.containsKey(magnitudeTopic(MAGNITUDE_TEMPERATURE))) {
  161. String remote_temp = root[magnitudeTopic(MAGNITUDE_TEMPERATURE)];
  162. _remote_temp.temp = remote_temp.toFloat();
  163. _remote_temp.last_update = millis();
  164. _remote_temp.need_display_update = true;
  165. DEBUG_MSG_P(PSTR("[THERMOSTAT] Remote sensor temperature: %s\n"), remote_temp.c_str());
  166. updateRemoteTemp(true);
  167. }
  168. }
  169. // Check temperature range change
  170. if (t.equals(MQTT_TOPIC_HOLD_TEMP)) {
  171. if (root.containsKey(MQTT_TOPIC_HOLD_TEMP_MIN)) {
  172. int t_min = root[MQTT_TOPIC_HOLD_TEMP_MIN];
  173. int t_max = root[MQTT_TOPIC_HOLD_TEMP_MAX];
  174. if (t_min < THERMOSTAT_TEMP_RANGE_MIN_MIN || t_min > THERMOSTAT_TEMP_RANGE_MIN_MAX ||
  175. t_max < THERMOSTAT_TEMP_RANGE_MAX_MIN || t_max > THERMOSTAT_TEMP_RANGE_MAX_MAX) {
  176. DEBUG_MSG_P(PSTR("[THERMOSTAT] Hold temperature range error\n"));
  177. return;
  178. }
  179. _temp_range.min = root[MQTT_TOPIC_HOLD_TEMP_MIN];
  180. _temp_range.max = root[MQTT_TOPIC_HOLD_TEMP_MAX];
  181. setSetting(NAME_TEMP_RANGE_MIN, _temp_range.min);
  182. setSetting(NAME_TEMP_RANGE_MAX, _temp_range.max);
  183. saveSettings();
  184. _temp_range.ask_interval = ASK_TEMP_RANGE_INTERVAL_REGULAR;
  185. _temp_range.last_update = millis();
  186. _temp_range.need_display_update = true;
  187. DEBUG_MSG_P(PSTR("[THERMOSTAT] Hold temperature range: (%d - %d)\n"), _temp_range.min, _temp_range.max);
  188. // Update websocket clients
  189. #if WEB_SUPPORT
  190. char buffer[100];
  191. snprintf_P(buffer, sizeof(buffer), PSTR("{\"thermostatVisible\": 1, \"tempRangeMin\": %d, \"tempRangeMax\": %d}"), _temp_range.min, _temp_range.max);
  192. wsSend(buffer);
  193. #endif
  194. } else {
  195. DEBUG_MSG_P(PSTR("[THERMOSTAT] Error temperature range data\n"));
  196. }
  197. }
  198. }
  199. }
  200. #if MQTT_SUPPORT
  201. //------------------------------------------------------------------------------
  202. void thermostatSetupMQTT() {
  203. mqttRegister(thermostatMQTTCallback);
  204. }
  205. #endif
  206. //------------------------------------------------------------------------------
  207. void notifyRangeChanged(bool min) {
  208. DEBUG_MSG_P(PSTR("[THERMOSTAT] notifyRangeChanged %s = %d\n"), min ? "MIN" : "MAX", min ? _temp_range.min : _temp_range.max);
  209. char tmp_str[6];
  210. sprintf(tmp_str, "%d", min ? _temp_range.min : _temp_range.max);
  211. mqttSend(min ? MQTT_TOPIC_NOTIFY_TEMP_RANGE_MIN : MQTT_TOPIC_NOTIFY_TEMP_RANGE_MAX, tmp_str, true);
  212. }
  213. //------------------------------------------------------------------------------
  214. // Setup
  215. //------------------------------------------------------------------------------
  216. void commonSetup() {
  217. _thermostat_enabled = getSetting(NAME_THERMOSTAT_ENABLED).toInt() == 1;
  218. DEBUG_MSG_P(PSTR("[THERMOSTAT] _thermostat_enabled = %d\n"), _thermostat_enabled);
  219. _thermostat_mode_cooler = getSetting(NAME_THERMOSTAT_MODE).toInt() == 1;
  220. DEBUG_MSG_P(PSTR("[THERMOSTAT] _thermostat_mode_cooler = %d\n"), _thermostat_mode_cooler);
  221. _temp_range.min = getSetting(NAME_TEMP_RANGE_MIN, THERMOSTAT_TEMP_RANGE_MIN).toInt();
  222. _temp_range.max = getSetting(NAME_TEMP_RANGE_MAX, THERMOSTAT_TEMP_RANGE_MAX).toInt();
  223. DEBUG_MSG_P(PSTR("[THERMOSTAT] _temp_range.min = %d\n"), _temp_range.min);
  224. DEBUG_MSG_P(PSTR("[THERMOSTAT] _temp_range.max = %d\n"), _temp_range.max);
  225. _thermostat.remote_sensor_name = getSetting(NAME_REMOTE_SENSOR_NAME);
  226. thermostat_remote_sensor_topic = _thermostat.remote_sensor_name + String("/") + String(MQTT_TOPIC_JSON);
  227. _thermostat_remote_temp_max_wait = getSetting(NAME_REMOTE_TEMP_MAX_WAIT, THERMOSTAT_REMOTE_TEMP_MAX_WAIT).toInt() * MILLIS_IN_SEC;
  228. _thermostat_alone_on_time = getSetting(NAME_ALONE_ON_TIME, THERMOSTAT_ALONE_ON_TIME).toInt() * MILLIS_IN_MIN;
  229. _thermostat_alone_off_time = getSetting(NAME_ALONE_OFF_TIME, THERMOSTAT_ALONE_OFF_TIME).toInt() * MILLIS_IN_MIN;
  230. _thermostat_max_on_time = getSetting(NAME_MAX_ON_TIME, THERMOSTAT_MAX_ON_TIME).toInt() * MILLIS_IN_MIN;
  231. _thermostat_min_off_time = getSetting(NAME_MIN_OFF_TIME, THERMOSTAT_MIN_OFF_TIME).toInt() * MILLIS_IN_MIN;
  232. }
  233. //------------------------------------------------------------------------------
  234. void thermostatConfigure() {
  235. commonSetup();
  236. _thermostat.temperature_source = temp_none;
  237. _thermostat_burn_total = getSetting(NAME_BURN_TOTAL).toInt();
  238. _thermostat_burn_today = getSetting(NAME_BURN_TODAY).toInt();
  239. _thermostat_burn_yesterday = getSetting(NAME_BURN_YESTERDAY).toInt();
  240. _thermostat_burn_this_month = getSetting(NAME_BURN_THIS_MONTH).toInt();
  241. _thermostat_burn_prev_month = getSetting(NAME_BURN_PREV_MONTH).toInt();
  242. _thermostat_burn_day = getSetting(NAME_BURN_DAY).toInt();
  243. _thermostat_burn_month = getSetting(NAME_BURN_MONTH).toInt();
  244. }
  245. //------------------------------------------------------------------------------
  246. void _thermostatReload() {
  247. int prev_temp_range_min = _temp_range.min;
  248. int prev_temp_range_max = _temp_range.max;
  249. commonSetup();
  250. if (_temp_range.min != prev_temp_range_min)
  251. notifyRangeChanged(true);
  252. if (_temp_range.max != prev_temp_range_max)
  253. notifyRangeChanged(false);
  254. }
  255. #if WEB_SUPPORT
  256. //------------------------------------------------------------------------------
  257. void _thermostatWebSocketOnConnected(JsonObject& root) {
  258. root["thermostatEnabled"] = thermostatEnabled();
  259. root["thermostatMode"] = thermostatModeCooler();
  260. root["thermostatVisible"] = 1;
  261. root[NAME_TEMP_RANGE_MIN] = _temp_range.min;
  262. root[NAME_TEMP_RANGE_MAX] = _temp_range.max;
  263. root[NAME_REMOTE_SENSOR_NAME] = _thermostat.remote_sensor_name;
  264. root[NAME_REMOTE_TEMP_MAX_WAIT] = _thermostat_remote_temp_max_wait / MILLIS_IN_SEC;
  265. root[NAME_MAX_ON_TIME] = _thermostat_max_on_time / MILLIS_IN_MIN;
  266. root[NAME_MIN_OFF_TIME] = _thermostat_min_off_time / MILLIS_IN_MIN;
  267. root[NAME_ALONE_ON_TIME] = _thermostat_alone_on_time / MILLIS_IN_MIN;
  268. root[NAME_ALONE_OFF_TIME] = _thermostat_alone_off_time / MILLIS_IN_MIN;
  269. root[NAME_BURN_TODAY] = _thermostat_burn_today;
  270. root[NAME_BURN_YESTERDAY] = _thermostat_burn_yesterday;
  271. root[NAME_BURN_THIS_MONTH] = _thermostat_burn_this_month;
  272. root[NAME_BURN_PREV_MONTH] = _thermostat_burn_prev_month;
  273. root[NAME_BURN_TOTAL] = _thermostat_burn_total;
  274. if (_thermostat.temperature_source == temp_remote) {
  275. root[NAME_OPERATION_MODE] = "remote temperature";
  276. root["remoteTmp"] = _remote_temp.temp;
  277. } else if (_thermostat.temperature_source == temp_local) {
  278. root[NAME_OPERATION_MODE] = "local temperature";
  279. root["remoteTmp"] = "?";
  280. } else {
  281. root[NAME_OPERATION_MODE] = "autonomous";
  282. root["remoteTmp"] = "?";
  283. }
  284. }
  285. //------------------------------------------------------------------------------
  286. bool _thermostatWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
  287. if (strncmp(key, NAME_THERMOSTAT_ENABLED, strlen(NAME_THERMOSTAT_ENABLED)) == 0) return true;
  288. if (strncmp(key, NAME_THERMOSTAT_MODE, strlen(NAME_THERMOSTAT_MODE)) == 0) return true;
  289. if (strncmp(key, NAME_TEMP_RANGE_MIN, strlen(NAME_TEMP_RANGE_MIN)) == 0) return true;
  290. if (strncmp(key, NAME_TEMP_RANGE_MAX, strlen(NAME_TEMP_RANGE_MAX)) == 0) return true;
  291. if (strncmp(key, NAME_REMOTE_SENSOR_NAME, strlen(NAME_REMOTE_SENSOR_NAME)) == 0) return true;
  292. if (strncmp(key, NAME_REMOTE_TEMP_MAX_WAIT, strlen(NAME_REMOTE_TEMP_MAX_WAIT)) == 0) return true;
  293. if (strncmp(key, NAME_MAX_ON_TIME, strlen(NAME_MAX_ON_TIME)) == 0) return true;
  294. if (strncmp(key, NAME_MIN_OFF_TIME, strlen(NAME_MIN_OFF_TIME)) == 0) return true;
  295. if (strncmp(key, NAME_ALONE_ON_TIME, strlen(NAME_ALONE_ON_TIME)) == 0) return true;
  296. if (strncmp(key, NAME_ALONE_OFF_TIME, strlen(NAME_ALONE_OFF_TIME)) == 0) return true;
  297. return false;
  298. }
  299. //------------------------------------------------------------------------------
  300. void _thermostatWebSocketOnAction(uint32_t client_id, const char * action, JsonObject& data) {
  301. if (strcmp(action, "thermostat_reset_counters") == 0) resetBurnCounters();
  302. }
  303. #endif
  304. //------------------------------------------------------------------------------
  305. void thermostatSetup() {
  306. thermostatConfigure();
  307. #if MQTT_SUPPORT
  308. thermostatSetupMQTT();
  309. #endif
  310. // Websockets
  311. #if WEB_SUPPORT
  312. wsRegister()
  313. .onConnected(_thermostatWebSocketOnConnected)
  314. .onKeyCheck(_thermostatWebSocketOnKeyCheck)
  315. .onAction(_thermostatWebSocketOnAction);
  316. #endif
  317. espurnaRegisterLoop(thermostatLoop);
  318. espurnaRegisterReload(_thermostatReload);
  319. }
  320. //------------------------------------------------------------------------------
  321. void sendTempRangeRequest() {
  322. DEBUG_MSG_P(PSTR("[THERMOSTAT] sendTempRangeRequest\n"));
  323. mqttSend(MQTT_TOPIC_ASK_TEMP_RANGE, "", true);
  324. }
  325. //------------------------------------------------------------------------------
  326. void setThermostatState(bool state) {
  327. DEBUG_MSG_P(PSTR("[THERMOSTAT] setThermostatState: %s\n"), state ? "ON" : "OFF");
  328. relayStatus(THERMOSTAT_RELAY, state, mqttForward(), false);
  329. _thermostat.last_switch = millis();
  330. // Send thermostat change state event to subscribers
  331. for (unsigned char i = 0; i < _thermostat_callbacks.size(); i++) {
  332. (_thermostat_callbacks[i])(state);
  333. }
  334. }
  335. //------------------------------------------------------------------------------
  336. void debugPrintSwitch(bool state, double temp) {
  337. char tmp_str[6];
  338. dtostrf(temp, 1-sizeof(tmp_str), 1, tmp_str);
  339. DEBUG_MSG_P(PSTR("[THERMOSTAT] switch %s, temp: %s, min: %d, max: %d, mode: %s, relay: %s, last switch %d\n"),
  340. state ? "ON" : "OFF", tmp_str, _temp_range.min, _temp_range.max, _thermostat_mode_cooler ? "COOLER" : "HEATER", relayStatus(THERMOSTAT_RELAY) ? "ON" : "OFF", millis() - _thermostat.last_switch);
  341. }
  342. //------------------------------------------------------------------------------
  343. inline bool lastSwitchEarlierThan(unsigned int comparing_time) {
  344. return millis() - _thermostat.last_switch > comparing_time;
  345. }
  346. //------------------------------------------------------------------------------
  347. inline void switchThermostat(bool state, double temp) {
  348. debugPrintSwitch(state, temp);
  349. setThermostatState(state);
  350. }
  351. //------------------------------------------------------------------------------
  352. //----------- Main function that make decision ---------------------------------
  353. //------------------------------------------------------------------------------
  354. void checkTempAndAdjustRelay(double temp) {
  355. if (_thermostat_mode_cooler == false) { // Main operation mode. Thermostat is HEATER.
  356. // if thermostat switched ON and t > max - switch it OFF and start cooling
  357. if (relayStatus(THERMOSTAT_RELAY) && temp > _temp_range.max) {
  358. _thermostat_cycle = cooling;
  359. switchThermostat(false, temp);
  360. // if thermostat switched ON for max time - switch it OFF for rest
  361. } else if (relayStatus(THERMOSTAT_RELAY) && lastSwitchEarlierThan(_thermostat_max_on_time)) {
  362. switchThermostat(false, temp);
  363. // if t < min and thermostat switched OFF for at least minimum time - switch it ON and start
  364. } else if (!relayStatus(THERMOSTAT_RELAY) && temp < _temp_range.min
  365. && (_thermostat.last_switch == 0 || lastSwitchEarlierThan(_thermostat_min_off_time))) {
  366. _thermostat_cycle = heating;
  367. switchThermostat(true, temp);
  368. // if heating cycle and thermostat switchaed OFF for more than min time - switch it ON
  369. // continue heating cycle
  370. } else if (!relayStatus(THERMOSTAT_RELAY) && _thermostat_cycle == heating
  371. && lastSwitchEarlierThan(_thermostat_min_off_time)) {
  372. switchThermostat(true, temp);
  373. }
  374. } else { // Thermostat is COOLER. Inverse logic.
  375. // if thermostat switched ON and t < min - switch it OFF and start heating
  376. if (relayStatus(THERMOSTAT_RELAY) && temp < _temp_range.min) {
  377. _thermostat_cycle = heating;
  378. switchThermostat(false, temp);
  379. // if thermostat switched ON for max time - switch it OFF for rest
  380. } else if (relayStatus(THERMOSTAT_RELAY) && lastSwitchEarlierThan(_thermostat_max_on_time)) {
  381. switchThermostat(false, temp);
  382. // if t > max and thermostat switched OFF for at least minimum time - switch it ON and start
  383. } else if (!relayStatus(THERMOSTAT_RELAY) && temp > _temp_range.max
  384. && (_thermostat.last_switch == 0 || lastSwitchEarlierThan(_thermostat_min_off_time))) {
  385. _thermostat_cycle = cooling;
  386. switchThermostat(true, temp);
  387. // if cooling cycle and thermostat switchaed OFF for more than min time - switch it ON
  388. // continue cooling cycle
  389. } else if (!relayStatus(THERMOSTAT_RELAY) && _thermostat_cycle == cooling
  390. && lastSwitchEarlierThan(_thermostat_min_off_time)) {
  391. switchThermostat(true, temp);
  392. }
  393. }
  394. }
  395. //------------------------------------------------------------------------------
  396. void updateCounters() {
  397. if (relayStatus(THERMOSTAT_RELAY)) {
  398. setSetting(NAME_BURN_TOTAL, ++_thermostat_burn_total);
  399. setSetting(NAME_BURN_TODAY, ++_thermostat_burn_today);
  400. setSetting(NAME_BURN_THIS_MONTH, ++_thermostat_burn_this_month);
  401. }
  402. if (ntpSynced()) {
  403. String value = NTP.getDateStr();
  404. unsigned int day = value.substring(0, 2).toInt();
  405. unsigned int month = value.substring(3, 5).toInt();
  406. if (day != _thermostat_burn_day) {
  407. _thermostat_burn_yesterday = _thermostat_burn_today;
  408. _thermostat_burn_today = 0;
  409. _thermostat_burn_day = day;
  410. setSetting(NAME_BURN_YESTERDAY, _thermostat_burn_yesterday);
  411. setSetting(NAME_BURN_TODAY, _thermostat_burn_today);
  412. setSetting(NAME_BURN_DAY, _thermostat_burn_day);
  413. }
  414. if (month != _thermostat_burn_month) {
  415. _thermostat_burn_prev_month = _thermostat_burn_this_month;
  416. _thermostat_burn_this_month = 0;
  417. _thermostat_burn_month = month;
  418. setSetting(NAME_BURN_PREV_MONTH, _thermostat_burn_prev_month);
  419. setSetting(NAME_BURN_THIS_MONTH, _thermostat_burn_this_month);
  420. setSetting(NAME_BURN_MONTH, _thermostat_burn_month);
  421. }
  422. }
  423. }
  424. //------------------------------------------------------------------------------
  425. double getLocalTemperature() {
  426. #if SENSOR_SUPPORT
  427. for (byte i=0; i<magnitudeCount(); i++) {
  428. if (magnitudeType(i) == MAGNITUDE_TEMPERATURE) {
  429. double temp = magnitudeValue(i);
  430. char tmp_str[6];
  431. dtostrf(temp, 1-sizeof(tmp_str), 1, tmp_str);
  432. DEBUG_MSG_P(PSTR("[THERMOSTAT] getLocalTemperature temp: %s\n"), tmp_str);
  433. return temp > -0.1 && temp < 0.1 ? DBL_MIN : temp;
  434. }
  435. }
  436. #endif
  437. return DBL_MIN;
  438. }
  439. //------------------------------------------------------------------------------
  440. double getLocalHumidity() {
  441. #if SENSOR_SUPPORT
  442. for (byte i=0; i<magnitudeCount(); i++) {
  443. if (magnitudeType(i) == MAGNITUDE_HUMIDITY) {
  444. double hum = magnitudeValue(i);
  445. char tmp_str[4];
  446. dtostrf(hum, 1-sizeof(tmp_str), 0, tmp_str);
  447. DEBUG_MSG_P(PSTR("[THERMOSTAT] getLocalHumidity hum: %s\%\n"), tmp_str);
  448. return hum > -0.1 && hum < 0.1 ? DBL_MIN : hum;
  449. }
  450. }
  451. #endif
  452. return DBL_MIN;
  453. }
  454. //------------------------------------------------------------------------------
  455. // Loop
  456. //------------------------------------------------------------------------------
  457. void thermostatLoop(void) {
  458. if (!thermostatEnabled())
  459. return;
  460. // Update temperature range
  461. if (mqttConnected()) {
  462. if (millis() - _temp_range.ask_time > _temp_range.ask_interval) {
  463. _temp_range.ask_time = millis();
  464. sendTempRangeRequest();
  465. }
  466. }
  467. // Update thermostat state
  468. if (millis() - _thermostat.last_update > THERMOSTAT_STATE_UPDATE_INTERVAL) {
  469. _thermostat.last_update = millis();
  470. updateCounters();
  471. unsigned int last_temp_src = _thermostat.temperature_source;
  472. if (_remote_temp.last_update != 0 && millis() - _remote_temp.last_update < _thermostat_remote_temp_max_wait) {
  473. // we have remote temp
  474. _thermostat.temperature_source = temp_remote;
  475. DEBUG_MSG_P(PSTR("[THERMOSTAT] setup thermostat by remote temperature\n"));
  476. checkTempAndAdjustRelay(_remote_temp.temp);
  477. } else if (getLocalTemperature() != DBL_MIN) {
  478. // we have local temp
  479. _thermostat.temperature_source = temp_local;
  480. DEBUG_MSG_P(PSTR("[THERMOSTAT] setup thermostat by local temperature\n"));
  481. checkTempAndAdjustRelay(getLocalTemperature());
  482. // updateRemoteTemp(false);
  483. } else {
  484. // we don't have any temp - switch thermostat on for N minutes every hour
  485. _thermostat.temperature_source = temp_none;
  486. DEBUG_MSG_P(PSTR("[THERMOSTAT] setup thermostat by timeout\n"));
  487. if (relayStatus(THERMOSTAT_RELAY) && millis() - _thermostat.last_switch > _thermostat_alone_on_time) {
  488. setThermostatState(false);
  489. } else if (!relayStatus(THERMOSTAT_RELAY) && millis() - _thermostat.last_switch > _thermostat_alone_off_time) {
  490. setThermostatState(false);
  491. }
  492. }
  493. if (last_temp_src != _thermostat.temperature_source) {
  494. updateOperationMode();
  495. }
  496. }
  497. }
  498. //------------------------------------------------------------------------------
  499. String getBurnTimeStr(unsigned int burn_time) {
  500. char burnTimeStr[18] = { 0 };
  501. if (burn_time < 60) {
  502. sprintf(burnTimeStr, "%d мин.", burn_time);
  503. } else {
  504. sprintf(burnTimeStr, "%d ч. %d мин.", (int)floor(burn_time / 60), burn_time % 60);
  505. }
  506. return String(burnTimeStr);
  507. }
  508. //------------------------------------------------------------------------------
  509. void resetBurnCounters() {
  510. DEBUG_MSG_P(PSTR("[THERMOSTAT] resetBurnCounters\n"));
  511. setSetting(NAME_BURN_TOTAL, 0);
  512. setSetting(NAME_BURN_TODAY, 0);
  513. setSetting(NAME_BURN_YESTERDAY, 0);
  514. setSetting(NAME_BURN_THIS_MONTH, 0);
  515. setSetting(NAME_BURN_PREV_MONTH, 0);
  516. _thermostat_burn_total = 0;
  517. _thermostat_burn_today = 0;
  518. _thermostat_burn_yesterday = 0;
  519. _thermostat_burn_this_month = 0;
  520. _thermostat_burn_prev_month = 0;
  521. }
  522. #endif // THERMOSTAT_SUPPORT
  523. //#######################################################################
  524. // ___ _ _
  525. // | \ (_) ___ _ __ | | __ _ _ _
  526. // | |) || |(_-<| '_ \| |/ _` || || |
  527. // |___/ |_|/__/| .__/|_|\__,_| \_, |
  528. // |_| |__/
  529. //#######################################################################
  530. #if THERMOSTAT_DISPLAY_SUPPORT
  531. #include "SSD1306.h" // alias for `#include "SSD1306Wire.h"`
  532. #define wifi_on_width 16
  533. #define wifi_on_height 16
  534. const char wifi_on_bits[] PROGMEM = {
  535. 0x00, 0x00, 0x0E, 0x00, 0x7E, 0x00, 0xFE, 0x01, 0xE0, 0x03, 0x80, 0x07,
  536. 0x02, 0x0F, 0x1E, 0x1E, 0x3E, 0x1C, 0x78, 0x38, 0xE0, 0x38, 0xC0, 0x31,
  537. 0xC6, 0x71, 0x8E, 0x71, 0x8E, 0x73, 0x00, 0x00, };
  538. #define mqtt_width 16
  539. #define mqtt_height 16
  540. const char mqtt_bits[] PROGMEM = {
  541. 0x00, 0x00, 0x00, 0x08, 0x00, 0x18, 0x00, 0x38, 0xEA, 0x7F, 0xEA, 0x7F,
  542. 0x00, 0x38, 0x10, 0x18, 0x18, 0x08, 0x1C, 0x00, 0xFE, 0x57, 0xFE, 0x57,
  543. 0x1C, 0x00, 0x18, 0x00, 0x10, 0x00, 0x00, 0x00, };
  544. #define remote_temp_width 16
  545. #define remote_temp_height 16
  546. const char remote_temp_bits[] PROGMEM = {
  547. 0x00, 0x00, 0xE0, 0x18, 0x10, 0x25, 0x10, 0x25, 0x90, 0x19, 0x50, 0x01,
  548. 0x50, 0x01, 0xD0, 0x01, 0x50, 0x01, 0x50, 0x01, 0xD0, 0x01, 0x50, 0x01,
  549. 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0x00, 0x00, };
  550. #define server_width 16
  551. #define server_height 16
  552. const char server_bits[] PROGMEM = {
  553. 0x00, 0x00, 0xF8, 0x1F, 0xFC, 0x3F, 0x0C, 0x30, 0x0C, 0x30, 0x0C, 0x30,
  554. 0x0C, 0x30, 0x0C, 0x30, 0x0C, 0x30, 0xF8, 0x1F, 0xFC, 0x3F, 0xFE, 0x7F,
  555. 0x1E, 0x78, 0xFE, 0x7F, 0xFC, 0x3F, 0x00, 0x00, };
  556. #define LOCAL_TEMP_UPDATE_INTERVAL 60000
  557. #define LOCAL_HUM_UPDATE_INTERVAL 61000
  558. SSD1306 display(0x3c, 1, 3);
  559. unsigned long _local_temp_last_update = 0xFFFF;
  560. unsigned long _local_hum_last_update = 0xFFFF;
  561. bool _display_wifi_status = true;
  562. bool _display_mqtt_status = true;
  563. bool _display_server_status = true;
  564. bool _display_remote_temp_status = true;
  565. bool _display_need_refresh = false;
  566. bool _temp_range_need_update = true;
  567. //------------------------------------------------------------------------------
  568. void drawIco(int16_t x, int16_t y, const char *ico, bool on = true) {
  569. display.drawIco16x16(x, y, ico, !on);
  570. _display_need_refresh = true;
  571. }
  572. //------------------------------------------------------------------------------
  573. void display_wifi_status(bool on) {
  574. _display_wifi_status = on;
  575. drawIco(0, 0, wifi_on_bits, on);
  576. }
  577. //------------------------------------------------------------------------------
  578. void display_mqtt_status(bool on) {
  579. _display_mqtt_status = on;
  580. drawIco(17, 0, mqtt_bits, on);
  581. }
  582. //------------------------------------------------------------------------------
  583. void display_server_status(bool on) {
  584. _display_server_status = on;
  585. drawIco(34, 0, server_bits, on);
  586. }
  587. //------------------------------------------------------------------------------
  588. void display_remote_temp_status(bool on) {
  589. _display_remote_temp_status = on;
  590. drawIco(51, 0, remote_temp_bits, on);
  591. }
  592. //------------------------------------------------------------------------------
  593. void display_temp_range() {
  594. _temp_range.need_display_update = false;
  595. display.setColor(BLACK);
  596. display.fillRect(68, 0, 60, 16);
  597. display.setColor(WHITE);
  598. display.setTextAlignment(TEXT_ALIGN_RIGHT);
  599. display.setFont(ArialMT_Plain_16);
  600. String temp_range = String(_temp_range.min) + "°- " + String(_temp_range.max) + "°";
  601. display.drawString(128, 0, temp_range);
  602. _display_need_refresh = true;
  603. }
  604. //------------------------------------------------------------------------------
  605. void display_remote_temp() {
  606. _remote_temp.need_display_update = false;
  607. display.setColor(BLACK);
  608. display.fillRect(0, 16, 128, 16);
  609. display.setColor(WHITE);
  610. display.setFont(ArialMT_Plain_16);
  611. display.setTextAlignment(TEXT_ALIGN_LEFT);
  612. String temp_range_title = String("Remote t");
  613. display.drawString(0, 16, temp_range_title);
  614. String temp_range_vol = String("= ") + (_display_remote_temp_status ? String(_remote_temp.temp, 1) : String("?")) + "°";
  615. display.drawString(75, 16, temp_range_vol);
  616. _display_need_refresh = true;
  617. }
  618. //------------------------------------------------------------------------------
  619. void display_local_temp() {
  620. display.setColor(BLACK);
  621. display.fillRect(0, 32, 128, 16);
  622. display.setColor(WHITE);
  623. display.setFont(ArialMT_Plain_16);
  624. display.setTextAlignment(TEXT_ALIGN_LEFT);
  625. String local_temp_title = String("Local t");
  626. display.drawString(0, 32, local_temp_title);
  627. String local_temp_vol = String("= ") + (getLocalTemperature() != DBL_MIN ? String(getLocalTemperature(), 1) : String("?")) + "°";
  628. display.drawString(75, 32, local_temp_vol);
  629. _display_need_refresh = true;
  630. }
  631. //------------------------------------------------------------------------------
  632. void display_local_humidity() {
  633. display.setColor(BLACK);
  634. display.fillRect(0, 48, 128, 16);
  635. display.setColor(WHITE);
  636. display.setFont(ArialMT_Plain_16);
  637. display.setTextAlignment(TEXT_ALIGN_LEFT);
  638. String local_hum_title = String("Local h ");
  639. display.drawString(0, 48, local_hum_title);
  640. String local_hum_vol = String("= ") + (getLocalHumidity() != DBL_MIN ? String(getLocalHumidity(), 0) : String("?")) + "%";
  641. display.drawString(75, 48, local_hum_vol);
  642. _display_need_refresh = true;
  643. }
  644. //------------------------------------------------------------------------------
  645. // Setup
  646. //------------------------------------------------------------------------------
  647. void displaySetup() {
  648. display.init();
  649. display.flipScreenVertically();
  650. // display.setFont(ArialMT_Plain_24);
  651. // display.setTextAlignment(TEXT_ALIGN_CENTER);
  652. // display.drawString(64, 17, "Thermostat");
  653. espurnaRegisterLoop(displayLoop);
  654. }
  655. //------------------------------------------------------------------------------
  656. void displayLoop() {
  657. _display_need_refresh = false;
  658. //------------------------------------------------------------------------------
  659. // Indicators
  660. //------------------------------------------------------------------------------
  661. if (!_display_wifi_status) {
  662. if (wifiConnected() && WiFi.getMode() != WIFI_AP)
  663. display_wifi_status(true);
  664. } else if (!wifiConnected() || WiFi.getMode() == WIFI_AP) {
  665. display_wifi_status(false);
  666. }
  667. if (!_display_mqtt_status) {
  668. if (mqttConnected())
  669. display_mqtt_status(true);
  670. } else if (!mqttConnected()) {
  671. display_mqtt_status(false);
  672. }
  673. if (millis() - _temp_range.last_update < THERMOSTAT_SERVER_LOST_INTERVAL) {
  674. if (!_display_server_status)
  675. display_server_status(true);
  676. } else if (_display_server_status) {
  677. display_server_status(false);
  678. }
  679. if (millis() - _remote_temp.last_update < _thermostat_remote_temp_max_wait) {
  680. if (!_display_remote_temp_status)
  681. display_remote_temp_status(true);
  682. } else if (_display_remote_temp_status) {
  683. display_remote_temp_status(false);
  684. display_remote_temp();
  685. }
  686. //------------------------------------------------------------------------------
  687. // Temp range
  688. //------------------------------------------------------------------------------
  689. if (_temp_range.need_display_update) {
  690. display_temp_range();
  691. }
  692. //------------------------------------------------------------------------------
  693. // Remote temp
  694. //------------------------------------------------------------------------------
  695. if (_remote_temp.need_display_update) {
  696. display_remote_temp();
  697. }
  698. //------------------------------------------------------------------------------
  699. // Local temp
  700. //------------------------------------------------------------------------------
  701. if (millis() - _local_temp_last_update > LOCAL_TEMP_UPDATE_INTERVAL) {
  702. _local_temp_last_update = millis();
  703. display_local_temp();
  704. }
  705. //------------------------------------------------------------------------------
  706. // Local temp
  707. //------------------------------------------------------------------------------
  708. if (millis() - _local_hum_last_update > LOCAL_HUM_UPDATE_INTERVAL) {
  709. _local_hum_last_update = millis();
  710. display_local_humidity();
  711. }
  712. //------------------------------------------------------------------------------
  713. // Display update
  714. //------------------------------------------------------------------------------
  715. if (_display_need_refresh) {
  716. yield();
  717. display.display();
  718. }
  719. }
  720. #endif // THERMOSTAT_DISPLAY_SUPPORT