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.

822 lines
33 KiB

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