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.

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