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.

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