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.

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