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.

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