/* THERMOSTAT MODULE Copyright (C) 2017 by Dmitry Blinov */ #if THERMOSTAT_SUPPORT #include #include #include "relay.h" #include "ntp.h" #include "ws.h" bool _thermostat_enabled = true; bool _thermostat_mode_cooler = false; const char* NAME_THERMOSTAT_ENABLED = "thermostatEnabled"; const char* NAME_THERMOSTAT_MODE = "thermostatMode"; const char* NAME_TEMP_RANGE_MIN = "tempRangeMin"; const char* NAME_TEMP_RANGE_MAX = "tempRangeMax"; const char* NAME_REMOTE_SENSOR_NAME = "remoteSensorName"; const char* NAME_REMOTE_TEMP_MAX_WAIT = "remoteTempMaxWait"; const char* NAME_ALONE_ON_TIME = "aloneOnTime"; const char* NAME_ALONE_OFF_TIME = "aloneOffTime"; const char* NAME_MAX_ON_TIME = "maxOnTime"; const char* NAME_MIN_OFF_TIME = "minOffTime"; const char* NAME_BURN_TOTAL = "burnTotal"; const char* NAME_BURN_TODAY = "burnToday"; const char* NAME_BURN_YESTERDAY = "burnYesterday"; const char* NAME_BURN_THIS_MONTH = "burnThisMonth"; const char* NAME_BURN_PREV_MONTH = "burnPrevMonth"; const char* NAME_BURN_DAY = "burnDay"; const char* NAME_BURN_MONTH = "burnMonth"; const char* NAME_OPERATION_MODE = "thermostatOperationMode"; #define ASK_TEMP_RANGE_INTERVAL_INITIAL 15000 // ask initially once per every 15 seconds #define ASK_TEMP_RANGE_INTERVAL_REGULAR 60000 // ask every minute to be sure #define MILLIS_IN_SEC 1000 #define MILLIS_IN_MIN 60000 #define THERMOSTAT_STATE_UPDATE_INTERVAL 60000 // 1 min #define THERMOSTAT_RELAY 0 // use relay 0 #define THERMOSTAT_TEMP_RANGE_MIN 10 // grad. Celsius #define THERMOSTAT_TEMP_RANGE_MIN_MIN 3 // grad. Celsius #define THERMOSTAT_TEMP_RANGE_MIN_MAX 30 // grad. Celsius #define THERMOSTAT_TEMP_RANGE_MAX 20 // grad. Celsius #define THERMOSTAT_TEMP_RANGE_MAX_MIN 8 // grad. Celsius #define THERMOSTAT_TEMP_RANGE_MAX_MAX 35 // grad. Celsius #define THERMOSTAT_ALONE_ON_TIME 5 // 5 min #define THERMOSTAT_ALONE_OFF_TIME 55 // 55 min #define THERMOSTAT_MAX_ON_TIME 30 // 30 min #define THERMOSTAT_MIN_OFF_TIME 10 // 10 min unsigned long _thermostat_remote_temp_max_wait = THERMOSTAT_REMOTE_TEMP_MAX_WAIT * MILLIS_IN_SEC; unsigned long _thermostat_alone_on_time = THERMOSTAT_ALONE_ON_TIME * MILLIS_IN_MIN; unsigned long _thermostat_alone_off_time = THERMOSTAT_ALONE_OFF_TIME * MILLIS_IN_MIN; unsigned long _thermostat_max_on_time = THERMOSTAT_MAX_ON_TIME * MILLIS_IN_MIN; unsigned long _thermostat_min_off_time = THERMOSTAT_MIN_OFF_TIME * MILLIS_IN_MIN; unsigned int _thermostat_on_time_for_day = 0; unsigned int _thermostat_burn_total = 0; unsigned int _thermostat_burn_today = 0; unsigned int _thermostat_burn_yesterday = 0; unsigned int _thermostat_burn_this_month = 0; unsigned int _thermostat_burn_prev_month = 0; unsigned int _thermostat_burn_day = 0; unsigned int _thermostat_burn_month = 0; struct temp_t { float temp; unsigned long last_update = 0; bool need_display_update = false; }; temp_t _remote_temp; struct temp_range_t { int min = THERMOSTAT_TEMP_RANGE_MIN; int max = THERMOSTAT_TEMP_RANGE_MAX; unsigned long last_update = 0; unsigned long ask_time = 0; unsigned int ask_interval = 0; bool need_display_update = true; }; temp_range_t _temp_range; enum temperature_source_t {temp_none, temp_local, temp_remote}; struct thermostat_t { unsigned long last_update = 0; unsigned long last_switch = 0; String remote_sensor_name; unsigned int temperature_source = temp_none; }; thermostat_t _thermostat; enum thermostat_cycle_type {cooling, heating}; unsigned int _thermostat_cycle = heating; String thermostat_remote_sensor_topic; //------------------------------------------------------------------------------ void thermostatEnabled(bool enabled) { _thermostat_enabled = enabled; } //------------------------------------------------------------------------------ bool thermostatEnabled() { return _thermostat_enabled; } //------------------------------------------------------------------------------ void thermostatModeCooler(bool cooler) { _thermostat_mode_cooler = cooler; } //------------------------------------------------------------------------------ bool thermostatModeCooler() { return _thermostat_mode_cooler; } //------------------------------------------------------------------------------ std::vector _thermostat_callbacks; void thermostatRegister(thermostat_callback_f callback) { _thermostat_callbacks.push_back(callback); } //------------------------------------------------------------------------------ void updateOperationMode() { #if WEB_SUPPORT String message; if (_thermostat.temperature_source == temp_remote) { message = "{\"thermostatVisible\": 1, \"thermostatOperationMode\": \"remote temperature\"}"; updateRemoteTemp(true); } else if (_thermostat.temperature_source == temp_local) { message = "{\"thermostatVisible\": 1, \"thermostatOperationMode\": \"local temperature\"}"; updateRemoteTemp(false); } else { message = "{\"thermostatVisible\": 1, \"thermostatOperationMode\": \"autonomous\"}"; updateRemoteTemp(false); } wsSend(message.c_str()); #endif } //------------------------------------------------------------------------------ void updateRemoteTemp(bool remote_temp_actual) { #if WEB_SUPPORT char tmp_str[16]; if (remote_temp_actual) { dtostrf(_remote_temp.temp, 1, 1, tmp_str); } else { strcpy(tmp_str, "\"?\""); } char buffer[128]; snprintf_P(buffer, sizeof(buffer), PSTR("{\"thermostatVisible\": 1, \"remoteTmp\": %s}"), tmp_str); wsSend(buffer); #endif } //------------------------------------------------------------------------------ // MQTT //------------------------------------------------------------------------------ void thermostatMQTTCallback(unsigned int type, const char * topic, const char * payload) { if (type == MQTT_CONNECT_EVENT) { mqttSubscribeRaw(thermostat_remote_sensor_topic.c_str()); mqttSubscribe(MQTT_TOPIC_HOLD_TEMP); _temp_range.ask_interval = ASK_TEMP_RANGE_INTERVAL_INITIAL; _temp_range.ask_time = millis(); } if (type == MQTT_MESSAGE_EVENT) { // Match topic String t = mqttMagnitude((char *) topic); if (strcmp(topic, thermostat_remote_sensor_topic.c_str()) != 0 && !t.equals(MQTT_TOPIC_HOLD_TEMP)) return; // Parse JSON input DynamicJsonBuffer jsonBuffer; JsonObject& root = jsonBuffer.parseObject(payload); if (!root.success()) { DEBUG_MSG_P(PSTR("[THERMOSTAT] Error parsing data\n")); return; } // Check rempte sensor temperature if (strcmp(topic, thermostat_remote_sensor_topic.c_str()) == 0) { if (root.containsKey(magnitudeTopic(MAGNITUDE_TEMPERATURE))) { String remote_temp = root[magnitudeTopic(MAGNITUDE_TEMPERATURE)]; _remote_temp.temp = remote_temp.toFloat(); _remote_temp.last_update = millis(); _remote_temp.need_display_update = true; DEBUG_MSG_P(PSTR("[THERMOSTAT] Remote sensor temperature: %s\n"), remote_temp.c_str()); updateRemoteTemp(true); } } // Check temperature range change if (t.equals(MQTT_TOPIC_HOLD_TEMP)) { if (root.containsKey(MQTT_TOPIC_HOLD_TEMP_MIN)) { int t_min = root[MQTT_TOPIC_HOLD_TEMP_MIN]; int t_max = root[MQTT_TOPIC_HOLD_TEMP_MAX]; if (t_min < THERMOSTAT_TEMP_RANGE_MIN_MIN || t_min > THERMOSTAT_TEMP_RANGE_MIN_MAX || t_max < THERMOSTAT_TEMP_RANGE_MAX_MIN || t_max > THERMOSTAT_TEMP_RANGE_MAX_MAX) { DEBUG_MSG_P(PSTR("[THERMOSTAT] Hold temperature range error\n")); return; } _temp_range.min = root[MQTT_TOPIC_HOLD_TEMP_MIN]; _temp_range.max = root[MQTT_TOPIC_HOLD_TEMP_MAX]; setSetting(NAME_TEMP_RANGE_MIN, _temp_range.min); setSetting(NAME_TEMP_RANGE_MAX, _temp_range.max); saveSettings(); _temp_range.ask_interval = ASK_TEMP_RANGE_INTERVAL_REGULAR; _temp_range.last_update = millis(); _temp_range.need_display_update = true; DEBUG_MSG_P(PSTR("[THERMOSTAT] Hold temperature range: (%d - %d)\n"), _temp_range.min, _temp_range.max); // Update websocket clients #if WEB_SUPPORT char buffer[100]; snprintf_P(buffer, sizeof(buffer), PSTR("{\"thermostatVisible\": 1, \"tempRangeMin\": %d, \"tempRangeMax\": %d}"), _temp_range.min, _temp_range.max); wsSend(buffer); #endif } else { DEBUG_MSG_P(PSTR("[THERMOSTAT] Error temperature range data\n")); } } } } #if MQTT_SUPPORT //------------------------------------------------------------------------------ void thermostatSetupMQTT() { mqttRegister(thermostatMQTTCallback); } #endif //------------------------------------------------------------------------------ void notifyRangeChanged(bool min) { DEBUG_MSG_P(PSTR("[THERMOSTAT] notifyRangeChanged %s = %d\n"), min ? "MIN" : "MAX", min ? _temp_range.min : _temp_range.max); char tmp_str[6]; sprintf(tmp_str, "%d", min ? _temp_range.min : _temp_range.max); mqttSend(min ? MQTT_TOPIC_NOTIFY_TEMP_RANGE_MIN : MQTT_TOPIC_NOTIFY_TEMP_RANGE_MAX, tmp_str, true); } //------------------------------------------------------------------------------ // Setup //------------------------------------------------------------------------------ void commonSetup() { _thermostat_enabled = getSetting(NAME_THERMOSTAT_ENABLED, false); DEBUG_MSG_P(PSTR("[THERMOSTAT] _thermostat_enabled = %d\n"), _thermostat_enabled); _thermostat_mode_cooler = getSetting(NAME_THERMOSTAT_MODE, false); DEBUG_MSG_P(PSTR("[THERMOSTAT] _thermostat_mode_cooler = %d\n"), _thermostat_mode_cooler); _temp_range.min = getSetting(NAME_TEMP_RANGE_MIN, THERMOSTAT_TEMP_RANGE_MIN); _temp_range.max = getSetting(NAME_TEMP_RANGE_MAX, THERMOSTAT_TEMP_RANGE_MAX); DEBUG_MSG_P(PSTR("[THERMOSTAT] _temp_range.min = %d\n"), _temp_range.min); DEBUG_MSG_P(PSTR("[THERMOSTAT] _temp_range.max = %d\n"), _temp_range.max); _thermostat.remote_sensor_name = getSetting(NAME_REMOTE_SENSOR_NAME); thermostat_remote_sensor_topic = _thermostat.remote_sensor_name + String("/") + String(MQTT_TOPIC_JSON); _thermostat_remote_temp_max_wait = getSetting(NAME_REMOTE_TEMP_MAX_WAIT, THERMOSTAT_REMOTE_TEMP_MAX_WAIT) * MILLIS_IN_SEC; _thermostat_alone_on_time = getSetting(NAME_ALONE_ON_TIME, THERMOSTAT_ALONE_ON_TIME) * MILLIS_IN_MIN; _thermostat_alone_off_time = getSetting(NAME_ALONE_OFF_TIME, THERMOSTAT_ALONE_OFF_TIME) * MILLIS_IN_MIN; _thermostat_max_on_time = getSetting(NAME_MAX_ON_TIME, THERMOSTAT_MAX_ON_TIME) * MILLIS_IN_MIN; _thermostat_min_off_time = getSetting(NAME_MIN_OFF_TIME, THERMOSTAT_MIN_OFF_TIME) * MILLIS_IN_MIN; } //------------------------------------------------------------------------------ void thermostatConfigure() { commonSetup(); _thermostat.temperature_source = temp_none; _thermostat_burn_total = getSetting(NAME_BURN_TOTAL, 0); _thermostat_burn_today = getSetting(NAME_BURN_TODAY, 0); _thermostat_burn_yesterday = getSetting(NAME_BURN_YESTERDAY, 0); _thermostat_burn_this_month = getSetting(NAME_BURN_THIS_MONTH, 0); _thermostat_burn_prev_month = getSetting(NAME_BURN_PREV_MONTH, 0); _thermostat_burn_day = getSetting(NAME_BURN_DAY, 0); _thermostat_burn_month = getSetting(NAME_BURN_MONTH, 0); } //------------------------------------------------------------------------------ void _thermostatReload() { int prev_temp_range_min = _temp_range.min; int prev_temp_range_max = _temp_range.max; commonSetup(); if (_temp_range.min != prev_temp_range_min) notifyRangeChanged(true); if (_temp_range.max != prev_temp_range_max) notifyRangeChanged(false); } #if WEB_SUPPORT //------------------------------------------------------------------------------ void _thermostatWebSocketOnConnected(JsonObject& root) { root["thermostatEnabled"] = thermostatEnabled(); root["thermostatMode"] = thermostatModeCooler(); root["thermostatVisible"] = 1; root[NAME_TEMP_RANGE_MIN] = _temp_range.min; root[NAME_TEMP_RANGE_MAX] = _temp_range.max; root[NAME_REMOTE_SENSOR_NAME] = _thermostat.remote_sensor_name; root[NAME_REMOTE_TEMP_MAX_WAIT] = _thermostat_remote_temp_max_wait / MILLIS_IN_SEC; root[NAME_MAX_ON_TIME] = _thermostat_max_on_time / MILLIS_IN_MIN; root[NAME_MIN_OFF_TIME] = _thermostat_min_off_time / MILLIS_IN_MIN; root[NAME_ALONE_ON_TIME] = _thermostat_alone_on_time / MILLIS_IN_MIN; root[NAME_ALONE_OFF_TIME] = _thermostat_alone_off_time / MILLIS_IN_MIN; root[NAME_BURN_TODAY] = _thermostat_burn_today; root[NAME_BURN_YESTERDAY] = _thermostat_burn_yesterday; root[NAME_BURN_THIS_MONTH] = _thermostat_burn_this_month; root[NAME_BURN_PREV_MONTH] = _thermostat_burn_prev_month; root[NAME_BURN_TOTAL] = _thermostat_burn_total; if (_thermostat.temperature_source == temp_remote) { root[NAME_OPERATION_MODE] = "remote temperature"; root["remoteTmp"] = _remote_temp.temp; } else if (_thermostat.temperature_source == temp_local) { root[NAME_OPERATION_MODE] = "local temperature"; root["remoteTmp"] = "?"; } else { root[NAME_OPERATION_MODE] = "autonomous"; root["remoteTmp"] = "?"; } } //------------------------------------------------------------------------------ bool _thermostatWebSocketOnKeyCheck(const char * key, JsonVariant& value) { if (strncmp(key, NAME_THERMOSTAT_ENABLED, strlen(NAME_THERMOSTAT_ENABLED)) == 0) return true; if (strncmp(key, NAME_THERMOSTAT_MODE, strlen(NAME_THERMOSTAT_MODE)) == 0) return true; if (strncmp(key, NAME_TEMP_RANGE_MIN, strlen(NAME_TEMP_RANGE_MIN)) == 0) return true; if (strncmp(key, NAME_TEMP_RANGE_MAX, strlen(NAME_TEMP_RANGE_MAX)) == 0) return true; if (strncmp(key, NAME_REMOTE_SENSOR_NAME, strlen(NAME_REMOTE_SENSOR_NAME)) == 0) return true; if (strncmp(key, NAME_REMOTE_TEMP_MAX_WAIT, strlen(NAME_REMOTE_TEMP_MAX_WAIT)) == 0) return true; if (strncmp(key, NAME_MAX_ON_TIME, strlen(NAME_MAX_ON_TIME)) == 0) return true; if (strncmp(key, NAME_MIN_OFF_TIME, strlen(NAME_MIN_OFF_TIME)) == 0) return true; if (strncmp(key, NAME_ALONE_ON_TIME, strlen(NAME_ALONE_ON_TIME)) == 0) return true; if (strncmp(key, NAME_ALONE_OFF_TIME, strlen(NAME_ALONE_OFF_TIME)) == 0) return true; return false; } //------------------------------------------------------------------------------ void _thermostatWebSocketOnAction(uint32_t client_id, const char * action, JsonObject& data) { if (strcmp(action, "thermostat_reset_counters") == 0) resetBurnCounters(); } #endif //------------------------------------------------------------------------------ void thermostatSetup() { thermostatConfigure(); #if MQTT_SUPPORT thermostatSetupMQTT(); #endif // Websockets #if WEB_SUPPORT wsRegister() .onConnected(_thermostatWebSocketOnConnected) .onKeyCheck(_thermostatWebSocketOnKeyCheck) .onAction(_thermostatWebSocketOnAction); #endif espurnaRegisterLoop(thermostatLoop); espurnaRegisterReload(_thermostatReload); } //------------------------------------------------------------------------------ void sendTempRangeRequest() { DEBUG_MSG_P(PSTR("[THERMOSTAT] sendTempRangeRequest\n")); mqttSend(MQTT_TOPIC_ASK_TEMP_RANGE, "", true); } //------------------------------------------------------------------------------ void setThermostatState(bool state) { DEBUG_MSG_P(PSTR("[THERMOSTAT] setThermostatState: %s\n"), state ? "ON" : "OFF"); relayStatus(THERMOSTAT_RELAY, state, mqttForward(), false); _thermostat.last_switch = millis(); // Send thermostat change state event to subscribers for (unsigned char i = 0; i < _thermostat_callbacks.size(); i++) { (_thermostat_callbacks[i])(state); } } //------------------------------------------------------------------------------ void debugPrintSwitch(bool state, double temp) { char tmp_str[16]; dtostrf(temp, 1, 1, tmp_str); DEBUG_MSG_P(PSTR("[THERMOSTAT] switch %s, temp: %s, min: %d, max: %d, mode: %s, relay: %s, last switch %d\n"), 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); } //------------------------------------------------------------------------------ inline bool lastSwitchEarlierThan(unsigned int comparing_time) { return millis() - _thermostat.last_switch > comparing_time; } //------------------------------------------------------------------------------ inline void switchThermostat(bool state, double temp) { debugPrintSwitch(state, temp); setThermostatState(state); } //------------------------------------------------------------------------------ //----------- Main function that make decision --------------------------------- //------------------------------------------------------------------------------ void checkTempAndAdjustRelay(double temp) { if (_thermostat_mode_cooler == false) { // Main operation mode. Thermostat is HEATER. // if thermostat switched ON and t > max - switch it OFF and start cooling if (relayStatus(THERMOSTAT_RELAY) && temp > _temp_range.max) { _thermostat_cycle = cooling; switchThermostat(false, temp); // if thermostat switched ON for max time - switch it OFF for rest } else if (relayStatus(THERMOSTAT_RELAY) && lastSwitchEarlierThan(_thermostat_max_on_time)) { switchThermostat(false, temp); // if t < min and thermostat switched OFF for at least minimum time - switch it ON and start } else if (!relayStatus(THERMOSTAT_RELAY) && temp < _temp_range.min && (_thermostat.last_switch == 0 || lastSwitchEarlierThan(_thermostat_min_off_time))) { _thermostat_cycle = heating; switchThermostat(true, temp); // if heating cycle and thermostat switchaed OFF for more than min time - switch it ON // continue heating cycle } else if (!relayStatus(THERMOSTAT_RELAY) && _thermostat_cycle == heating && lastSwitchEarlierThan(_thermostat_min_off_time)) { switchThermostat(true, temp); } } else { // Thermostat is COOLER. Inverse logic. // if thermostat switched ON and t < min - switch it OFF and start heating if (relayStatus(THERMOSTAT_RELAY) && temp < _temp_range.min) { _thermostat_cycle = heating; switchThermostat(false, temp); // if thermostat switched ON for max time - switch it OFF for rest } else if (relayStatus(THERMOSTAT_RELAY) && lastSwitchEarlierThan(_thermostat_max_on_time)) { switchThermostat(false, temp); // if t > max and thermostat switched OFF for at least minimum time - switch it ON and start } else if (!relayStatus(THERMOSTAT_RELAY) && temp > _temp_range.max && (_thermostat.last_switch == 0 || lastSwitchEarlierThan(_thermostat_min_off_time))) { _thermostat_cycle = cooling; switchThermostat(true, temp); // if cooling cycle and thermostat switchaed OFF for more than min time - switch it ON // continue cooling cycle } else if (!relayStatus(THERMOSTAT_RELAY) && _thermostat_cycle == cooling && lastSwitchEarlierThan(_thermostat_min_off_time)) { switchThermostat(true, temp); } } } //------------------------------------------------------------------------------ void updateCounters() { if (relayStatus(THERMOSTAT_RELAY)) { setSetting(NAME_BURN_TOTAL, ++_thermostat_burn_total); setSetting(NAME_BURN_TODAY, ++_thermostat_burn_today); setSetting(NAME_BURN_THIS_MONTH, ++_thermostat_burn_this_month); } if (ntpSynced()) { const auto ts = now(); unsigned int now_day = day(ts); unsigned int now_month = month(ts); if (now_day != _thermostat_burn_day) { _thermostat_burn_yesterday = _thermostat_burn_today; _thermostat_burn_today = 0; _thermostat_burn_day = now_day; setSetting(NAME_BURN_YESTERDAY, _thermostat_burn_yesterday); setSetting(NAME_BURN_TODAY, _thermostat_burn_today); setSetting(NAME_BURN_DAY, _thermostat_burn_day); } if (now_month != _thermostat_burn_month) { _thermostat_burn_prev_month = _thermostat_burn_this_month; _thermostat_burn_this_month = 0; _thermostat_burn_month = now_month; setSetting(NAME_BURN_PREV_MONTH, _thermostat_burn_prev_month); setSetting(NAME_BURN_THIS_MONTH, _thermostat_burn_this_month); setSetting(NAME_BURN_MONTH, _thermostat_burn_month); } } } //------------------------------------------------------------------------------ double getLocalTemperature() { #if SENSOR_SUPPORT for (byte i=0; i -0.1 && temp < 0.1 ? DBL_MIN : temp; } } #endif return DBL_MIN; } //------------------------------------------------------------------------------ double getLocalHumidity() { #if SENSOR_SUPPORT for (byte i=0; i -0.1 && hum < 0.1 ? DBL_MIN : hum; } } #endif return DBL_MIN; } //------------------------------------------------------------------------------ // Loop //------------------------------------------------------------------------------ void thermostatLoop(void) { if (!thermostatEnabled()) return; // Update temperature range if (mqttConnected()) { if (millis() - _temp_range.ask_time > _temp_range.ask_interval) { _temp_range.ask_time = millis(); sendTempRangeRequest(); } } // Update thermostat state if (millis() - _thermostat.last_update > THERMOSTAT_STATE_UPDATE_INTERVAL) { _thermostat.last_update = millis(); updateCounters(); unsigned int last_temp_src = _thermostat.temperature_source; if (_remote_temp.last_update != 0 && millis() - _remote_temp.last_update < _thermostat_remote_temp_max_wait) { // we have remote temp _thermostat.temperature_source = temp_remote; DEBUG_MSG_P(PSTR("[THERMOSTAT] setup thermostat by remote temperature\n")); checkTempAndAdjustRelay(_remote_temp.temp); } else if (getLocalTemperature() != DBL_MIN) { // we have local temp _thermostat.temperature_source = temp_local; DEBUG_MSG_P(PSTR("[THERMOSTAT] setup thermostat by local temperature\n")); checkTempAndAdjustRelay(getLocalTemperature()); // updateRemoteTemp(false); } else { // we don't have any temp - switch thermostat on for N minutes every hour _thermostat.temperature_source = temp_none; DEBUG_MSG_P(PSTR("[THERMOSTAT] setup thermostat by timeout\n")); if (relayStatus(THERMOSTAT_RELAY) && millis() - _thermostat.last_switch > _thermostat_alone_on_time) { setThermostatState(false); } else if (!relayStatus(THERMOSTAT_RELAY) && millis() - _thermostat.last_switch > _thermostat_alone_off_time) { setThermostatState(false); } } if (last_temp_src != _thermostat.temperature_source) { updateOperationMode(); } } } //------------------------------------------------------------------------------ String getBurnTimeStr(unsigned int burn_time) { char burnTimeStr[18] = { 0 }; if (burn_time < 60) { sprintf(burnTimeStr, "%d мин.", burn_time); } else { sprintf(burnTimeStr, "%d ч. %d мин.", (int)floor(burn_time / 60), burn_time % 60); } return String(burnTimeStr); } //------------------------------------------------------------------------------ void resetBurnCounters() { DEBUG_MSG_P(PSTR("[THERMOSTAT] resetBurnCounters\n")); setSetting(NAME_BURN_TOTAL, 0); setSetting(NAME_BURN_TODAY, 0); setSetting(NAME_BURN_YESTERDAY, 0); setSetting(NAME_BURN_THIS_MONTH, 0); setSetting(NAME_BURN_PREV_MONTH, 0); _thermostat_burn_total = 0; _thermostat_burn_today = 0; _thermostat_burn_yesterday = 0; _thermostat_burn_this_month = 0; _thermostat_burn_prev_month = 0; } #endif // THERMOSTAT_SUPPORT //####################################################################### // ___ _ _ // | \ (_) ___ _ __ | | __ _ _ _ // | |) || |(_-<| '_ \| |/ _` || || | // |___/ |_|/__/| .__/|_|\__,_| \_, | // |_| |__/ //####################################################################### #if THERMOSTAT_DISPLAY_SUPPORT #include "SSD1306.h" // alias for `#include "SSD1306Wire.h"` #define wifi_on_width 16 #define wifi_on_height 16 const char wifi_on_bits[] PROGMEM = { 0x00, 0x00, 0x0E, 0x00, 0x7E, 0x00, 0xFE, 0x01, 0xE0, 0x03, 0x80, 0x07, 0x02, 0x0F, 0x1E, 0x1E, 0x3E, 0x1C, 0x78, 0x38, 0xE0, 0x38, 0xC0, 0x31, 0xC6, 0x71, 0x8E, 0x71, 0x8E, 0x73, 0x00, 0x00, }; #define mqtt_width 16 #define mqtt_height 16 const char mqtt_bits[] PROGMEM = { 0x00, 0x00, 0x00, 0x08, 0x00, 0x18, 0x00, 0x38, 0xEA, 0x7F, 0xEA, 0x7F, 0x00, 0x38, 0x10, 0x18, 0x18, 0x08, 0x1C, 0x00, 0xFE, 0x57, 0xFE, 0x57, 0x1C, 0x00, 0x18, 0x00, 0x10, 0x00, 0x00, 0x00, }; #define remote_temp_width 16 #define remote_temp_height 16 const char remote_temp_bits[] PROGMEM = { 0x00, 0x00, 0xE0, 0x18, 0x10, 0x25, 0x10, 0x25, 0x90, 0x19, 0x50, 0x01, 0x50, 0x01, 0xD0, 0x01, 0x50, 0x01, 0x50, 0x01, 0xD0, 0x01, 0x50, 0x01, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0x00, 0x00, }; #define server_width 16 #define server_height 16 const char server_bits[] PROGMEM = { 0x00, 0x00, 0xF8, 0x1F, 0xFC, 0x3F, 0x0C, 0x30, 0x0C, 0x30, 0x0C, 0x30, 0x0C, 0x30, 0x0C, 0x30, 0x0C, 0x30, 0xF8, 0x1F, 0xFC, 0x3F, 0xFE, 0x7F, 0x1E, 0x78, 0xFE, 0x7F, 0xFC, 0x3F, 0x00, 0x00, }; #define LOCAL_TEMP_UPDATE_INTERVAL 60000 #define LOCAL_HUM_UPDATE_INTERVAL 61000 SSD1306 display(0x3c, 1, 3); unsigned long _local_temp_last_update = 0xFFFF; unsigned long _local_hum_last_update = 0xFFFF; bool _display_wifi_status = true; bool _display_mqtt_status = true; bool _display_server_status = true; bool _display_remote_temp_status = true; bool _display_need_refresh = false; bool _temp_range_need_update = true; //------------------------------------------------------------------------------ void drawIco(int16_t x, int16_t y, const char *ico, bool on = true) { display.drawIco16x16(x, y, ico, !on); _display_need_refresh = true; } //------------------------------------------------------------------------------ void display_wifi_status(bool on) { _display_wifi_status = on; drawIco(0, 0, wifi_on_bits, on); } //------------------------------------------------------------------------------ void display_mqtt_status(bool on) { _display_mqtt_status = on; drawIco(17, 0, mqtt_bits, on); } //------------------------------------------------------------------------------ void display_server_status(bool on) { _display_server_status = on; drawIco(34, 0, server_bits, on); } //------------------------------------------------------------------------------ void display_remote_temp_status(bool on) { _display_remote_temp_status = on; drawIco(51, 0, remote_temp_bits, on); } //------------------------------------------------------------------------------ void display_temp_range() { _temp_range.need_display_update = false; display.setColor(BLACK); display.fillRect(68, 0, 60, 16); display.setColor(WHITE); display.setTextAlignment(TEXT_ALIGN_RIGHT); display.setFont(ArialMT_Plain_16); String temp_range = String(_temp_range.min) + "°- " + String(_temp_range.max) + "°"; display.drawString(128, 0, temp_range); _display_need_refresh = true; } //------------------------------------------------------------------------------ void display_remote_temp() { _remote_temp.need_display_update = false; display.setColor(BLACK); display.fillRect(0, 16, 128, 16); display.setColor(WHITE); display.setFont(ArialMT_Plain_16); display.setTextAlignment(TEXT_ALIGN_LEFT); String temp_range_title = String("Remote t"); display.drawString(0, 16, temp_range_title); String temp_range_vol = String("= ") + (_display_remote_temp_status ? String(_remote_temp.temp, 1) : String("?")) + "°"; display.drawString(75, 16, temp_range_vol); _display_need_refresh = true; } //------------------------------------------------------------------------------ void display_local_temp() { display.setColor(BLACK); display.fillRect(0, 32, 128, 16); display.setColor(WHITE); display.setFont(ArialMT_Plain_16); display.setTextAlignment(TEXT_ALIGN_LEFT); String local_temp_title = String("Local t"); display.drawString(0, 32, local_temp_title); String local_temp_vol = String("= ") + (getLocalTemperature() != DBL_MIN ? String(getLocalTemperature(), 1) : String("?")) + "°"; display.drawString(75, 32, local_temp_vol); _display_need_refresh = true; } //------------------------------------------------------------------------------ void display_local_humidity() { display.setColor(BLACK); display.fillRect(0, 48, 128, 16); display.setColor(WHITE); display.setFont(ArialMT_Plain_16); display.setTextAlignment(TEXT_ALIGN_LEFT); String local_hum_title = String("Local h "); display.drawString(0, 48, local_hum_title); String local_hum_vol = String("= ") + (getLocalHumidity() != DBL_MIN ? String(getLocalHumidity(), 0) : String("?")) + "%"; display.drawString(75, 48, local_hum_vol); _display_need_refresh = true; } //------------------------------------------------------------------------------ // Setup //------------------------------------------------------------------------------ void displaySetup() { display.init(); display.flipScreenVertically(); // display.setFont(ArialMT_Plain_24); // display.setTextAlignment(TEXT_ALIGN_CENTER); // display.drawString(64, 17, "Thermostat"); espurnaRegisterLoop(displayLoop); } //------------------------------------------------------------------------------ void displayLoop() { _display_need_refresh = false; //------------------------------------------------------------------------------ // Indicators //------------------------------------------------------------------------------ if (!_display_wifi_status) { if (wifiConnected() && WiFi.getMode() != WIFI_AP) display_wifi_status(true); } else if (!wifiConnected() || WiFi.getMode() == WIFI_AP) { display_wifi_status(false); } if (!_display_mqtt_status) { if (mqttConnected()) display_mqtt_status(true); } else if (!mqttConnected()) { display_mqtt_status(false); } if (millis() - _temp_range.last_update < THERMOSTAT_SERVER_LOST_INTERVAL) { if (!_display_server_status) display_server_status(true); } else if (_display_server_status) { display_server_status(false); } if (millis() - _remote_temp.last_update < _thermostat_remote_temp_max_wait) { if (!_display_remote_temp_status) display_remote_temp_status(true); } else if (_display_remote_temp_status) { display_remote_temp_status(false); display_remote_temp(); } //------------------------------------------------------------------------------ // Temp range //------------------------------------------------------------------------------ if (_temp_range.need_display_update) { display_temp_range(); } //------------------------------------------------------------------------------ // Remote temp //------------------------------------------------------------------------------ if (_remote_temp.need_display_update) { display_remote_temp(); } //------------------------------------------------------------------------------ // Local temp //------------------------------------------------------------------------------ if (millis() - _local_temp_last_update > LOCAL_TEMP_UPDATE_INTERVAL) { _local_temp_last_update = millis(); display_local_temp(); } //------------------------------------------------------------------------------ // Local temp //------------------------------------------------------------------------------ if (millis() - _local_hum_last_update > LOCAL_HUM_UPDATE_INTERVAL) { _local_hum_last_update = millis(); display_local_humidity(); } //------------------------------------------------------------------------------ // Display update //------------------------------------------------------------------------------ if (_display_need_refresh) { yield(); display.display(); } } #endif // THERMOSTAT_DISPLAY_SUPPORT