Mirror of espurna firmware for wireless switches and more
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.

864 lines
33 KiB

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