From af07494f0097073a550c254c33468e423805dd4a Mon Sep 17 00:00:00 2001 From: Dmitry Blinov Date: Sat, 2 Mar 2019 15:50:46 +0200 Subject: [PATCH] Add thermostat module --- code/espurna/config/general.h | 39 +- code/espurna/config/prototypes.h | 11 + code/espurna/espurna.ino | 6 + code/espurna/sensor.ino | 9 + code/espurna/thermostat.ino | 766 +++++++++++++++++++++++++++++++ code/espurna/utils.ino | 21 +- code/html/custom.css | 4 + code/html/custom.js | 27 ++ code/html/index.html | 138 +++++- code/platformio.ini | 1 + 10 files changed, 1013 insertions(+), 9 deletions(-) create mode 100644 code/espurna/thermostat.ino diff --git a/code/espurna/config/general.h b/code/espurna/config/general.h index 8c74394c..1103d318 100644 --- a/code/espurna/config/general.h +++ b/code/espurna/config/general.h @@ -162,6 +162,21 @@ #define EEPROM_ROTATE_DATA 11 // Reserved for the EEPROM_ROTATE library (3 bytes) #define EEPROM_DATA_END 14 // End of custom EEPROM data block +//------------------------------------------------------------------------------ +// THERMOSTAT +//------------------------------------------------------------------------------ + +#ifndef THERMOSTAT_SUPPORT +#define THERMOSTAT_SUPPORT 0 +#endif + +#ifndef THERMOSTAT_DISPLAY_SUPPORT +#define THERMOSTAT_DISPLAY_SUPPORT 0 +#endif + +#define THERMOSTAT_SERVER_LOST_INTERVAL 120000 //server means lost after 2 min from last response +#define THERMOSTAT_REMOTE_TEMP_MAX_WAIT 120 // 2 min + //------------------------------------------------------------------------------ // HEARTBEAT //------------------------------------------------------------------------------ @@ -259,6 +274,18 @@ #define HEARTBEAT_REPORT_INTERVAL 0 #endif +#if THERMOSTAT_SUPPORT && ! defined HEARTBEAT_REPORT_RANGE +#define HEARTBEAT_REPORT_RANGE 1 +#else +#define HEARTBEAT_REPORT_RANGE 0 +#endif + +#if THERMOSTAT_SUPPORT && ! defined HEARTBEAT_REPORT_REMOTE_TEMP +#define HEARTBEAT_REPORT_REMOTE_TEMP 1 +#else +#define HEARTBEAT_REPORT_REMOTE_TEMP 0 +#endif + //------------------------------------------------------------------------------ // Load average //------------------------------------------------------------------------------ @@ -753,7 +780,7 @@ #ifndef MQTT_USE_JSON -#define MQTT_USE_JSON 0 // Group messages in a JSON body +#define MQTT_USE_JSON 1 // Group messages in a JSON body #endif #ifndef MQTT_USE_JSON_DELAY @@ -833,6 +860,16 @@ #define MQTT_TOPIC_KELVIN "kelvin" #define MQTT_TOPIC_TRANSITION "transition" +// Thermostat module +#define MQTT_TOPIC_HOLD_TEMP "hold_temp" +#define MQTT_TOPIC_HOLD_TEMP_MIN "min" +#define MQTT_TOPIC_HOLD_TEMP_MAX "max" +#define MQTT_TOPIC_REMOTE_TEMP "remote_temp" +#define MQTT_TOPIC_ASK_TEMP_RANGE "ask_temp_range" +#define MQTT_TOPIC_NOTIFY_TEMP_RANGE_MIN "notify_temp_range_min" +#define MQTT_TOPIC_NOTIFY_TEMP_RANGE_MAX "notify_temp_range_max" + + #define MQTT_STATUS_ONLINE "1" // Value for the device ON message #define MQTT_STATUS_OFFLINE "0" // Value for the device OFF message (will) diff --git a/code/espurna/config/prototypes.h b/code/espurna/config/prototypes.h index d9b5d435..599985a0 100644 --- a/code/espurna/config/prototypes.h +++ b/code/espurna/config/prototypes.h @@ -210,3 +210,14 @@ void webRequestRegister(web_request_callback_f callback); typedef std::function wifi_callback_f; void wifiRegister(wifi_callback_f callback); bool wifiConnected(); + +// ----------------------------------------------------------------------------- +// THERMOSTAT +// ----------------------------------------------------------------------------- +#if THERMOSTAT_SUPPORT + typedef std::function thermostat_callback_f; + void thermostatRegister(thermostat_callback_f callback); +#else + #define thermostat_callback_f void * +#endif + diff --git a/code/espurna/espurna.ino b/code/espurna/espurna.ino index 0da20639..e30901ef 100644 --- a/code/espurna/espurna.ino +++ b/code/espurna/espurna.ino @@ -189,6 +189,12 @@ void setup() { #if UART_MQTT_SUPPORT uartmqttSetup(); #endif + #if THERMOSTAT_SUPPORT + thermostatSetup(); + #endif + #if THERMOSTAT_DISPLAY_SUPPORT + displaySetup(); + #endif // 3rd party code hook diff --git a/code/espurna/sensor.ino b/code/espurna/sensor.ino index 82f271b9..5400b9d4 100644 --- a/code/espurna/sensor.ino +++ b/code/espurna/sensor.ino @@ -15,6 +15,8 @@ Copyright (C) 2016-2019 by Xose Pérez #include "filters/MovingAverageFilter.h" #include "sensors/BaseSensor.h" +#include + typedef struct { BaseSensor * sensor; // Sensor object BaseFilter * filter; // Filter object @@ -1286,6 +1288,13 @@ unsigned char magnitudeType(unsigned char index) { return MAGNITUDE_NONE; } +double magnitudeValue(unsigned char index) { + if (index < _magnitudes.size()) { + return _sensor_realtime ? _magnitudes[index].current : _magnitudes[index].reported; + } + return DBL_MIN; +} + unsigned char magnitudeIndex(unsigned char index) { if (index < _magnitudes.size()) { return int(_magnitudes[index].global); diff --git a/code/espurna/thermostat.ino b/code/espurna/thermostat.ino new file mode 100644 index 00000000..5c337ec4 --- /dev/null +++ b/code/espurna/thermostat.ino @@ -0,0 +1,766 @@ +/* + +THERMOSTAT MODULE + +Copyright (C) 2017 by Dmitry Blinov + +*/ + +#if THERMOSTAT_SUPPORT + +#include +#include + +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; + +//------------------------------------------------------------------------------ +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[6]; + if (remote_temp_actual) { + dtostrf(_remote_temp.temp, 1-sizeof(tmp_str), 1, tmp_str); + } else { + strcpy(tmp_str, "\"?\""); + } + char buffer[100]; + 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() { + _temp_range.min = getSetting(NAME_TEMP_RANGE_MIN, THERMOSTAT_TEMP_RANGE_MIN).toInt(); + _temp_range.max = getSetting(NAME_TEMP_RANGE_MAX, THERMOSTAT_TEMP_RANGE_MAX).toInt(); + 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).toInt() * MILLIS_IN_SEC; + _thermostat_alone_on_time = getSetting(NAME_ALONE_ON_TIME, THERMOSTAT_ALONE_ON_TIME).toInt() * MILLIS_IN_MIN; + _thermostat_alone_off_time = getSetting(NAME_ALONE_OFF_TIME, THERMOSTAT_ALONE_OFF_TIME).toInt() * MILLIS_IN_MIN; + _thermostat_max_on_time = getSetting(NAME_MAX_ON_TIME, THERMOSTAT_MAX_ON_TIME).toInt() * MILLIS_IN_MIN; + _thermostat_min_off_time = getSetting(NAME_MIN_OFF_TIME, THERMOSTAT_MIN_OFF_TIME).toInt() * MILLIS_IN_MIN; +} + +//------------------------------------------------------------------------------ +void thermostatConfigure() { + commonSetup(); + + _thermostat.temperature_source = temp_none; + _thermostat_burn_total = getSetting(NAME_BURN_TOTAL).toInt(); + _thermostat_burn_today = getSetting(NAME_BURN_TODAY).toInt(); + _thermostat_burn_yesterday = getSetting(NAME_BURN_YESTERDAY).toInt(); + _thermostat_burn_this_month = getSetting(NAME_BURN_THIS_MONTH).toInt(); + _thermostat_burn_prev_month = getSetting(NAME_BURN_PREV_MONTH).toInt(); + _thermostat_burn_day = getSetting(NAME_BURN_DAY).toInt(); + _thermostat_burn_month = getSetting(NAME_BURN_MONTH).toInt(); +} + +//------------------------------------------------------------------------------ +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 _thermostatWebSocketOnSend(JsonObject& root) { + 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 _thermostatWebSocketOnReceive(const char * key, JsonVariant& value) { + 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 + wsOnSendRegister(_thermostatWebSocketOnSend); + wsOnReceiveRegister(_thermostatWebSocketOnReceive); + wsOnActionRegister(_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[6]; + dtostrf(temp, 1-sizeof(tmp_str), 1, tmp_str); + DEBUG_MSG_P(PSTR("[THERMOSTAT] switch %s, temp: %s, min: %d, max: %d, relay: %s, last switch %d\n"), + state ? "ON" : "OFF", tmp_str, _temp_range.min, _temp_range.max, 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 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); + } +} + +//------------------------------------------------------------------------------ +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()) { + String value = NTP.getDateStr(); + unsigned int day = value.substring(0, 2).toInt(); + unsigned int month = value.substring(3, 5).toInt(); + if (day != _thermostat_burn_day) { + _thermostat_burn_yesterday = _thermostat_burn_today; + _thermostat_burn_today = 0; + _thermostat_burn_day = day; + setSetting(NAME_BURN_YESTERDAY, _thermostat_burn_yesterday); + setSetting(NAME_BURN_TODAY, _thermostat_burn_today); + setSetting(NAME_BURN_DAY, _thermostat_burn_day); + } + if (month != _thermostat_burn_month) { + _thermostat_burn_prev_month = _thermostat_burn_this_month; + _thermostat_burn_this_month = 0; + _thermostat_burn_month = 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) { + + // 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 \ No newline at end of file diff --git a/code/espurna/utils.ino b/code/espurna/utils.ino index 09e09e1b..8db13590 100644 --- a/code/espurna/utils.ino +++ b/code/espurna/utils.ino @@ -165,7 +165,9 @@ namespace Heartbeat { Board = 1 << 15, Loadavg = 1 << 16, Interval = 1 << 17, - Description = 1 << 18 + Description = 1 << 18, + Range = 1 << 19, + Remote_temp = 1 << 20 }; constexpr uint32_t defaultValue() { @@ -186,7 +188,9 @@ namespace Heartbeat { (Version * (HEARTBEAT_REPORT_VERSION)) | \ (Board * (HEARTBEAT_REPORT_BOARD)) | \ (Loadavg * (HEARTBEAT_REPORT_LOADAVG)) | \ - (Interval * (HEARTBEAT_REPORT_INTERVAL)); + (Interval * (HEARTBEAT_REPORT_INTERVAL)) | \ + (Range * (HEARTBEAT_REPORT_RANGE)) | \ + (Remote_temp * (HEARTBEAT_REPORT_REMOTE_TEMP)); } uint32_t currentValue() { @@ -295,6 +299,19 @@ void heartbeat() { if (hb_cfg & Heartbeat::Loadavg) mqttSend(MQTT_TOPIC_LOADAVG, String(systemLoadAverage()).c_str()); + #if THERMOSTAT_SUPPORT + if (hb_cfg & Heartbeat::Range) { + mqttSend(MQTT_TOPIC_HOLD_TEMP "_" MQTT_TOPIC_HOLD_TEMP_MIN, String(_temp_range.min).c_str()); + mqttSend(MQTT_TOPIC_HOLD_TEMP "_" MQTT_TOPIC_HOLD_TEMP_MAX, String(_temp_range.max).c_str()); + } + + if (hb_cfg & Heartbeat::Remote_temp) { + char remote_temp[6]; + dtostrf(_remote_temp.temp, 1-sizeof(remote_temp), 1, remote_temp); + mqttSend(MQTT_TOPIC_REMOTE_TEMP, String(remote_temp).c_str()); + } + #endif + } else if (!serial && _heartbeat_mode == HEARTBEAT_REPEAT_STATUS) { mqttSend(MQTT_TOPIC_STATUS, MQTT_STATUS_ONLINE, true); } diff --git a/code/html/custom.css b/code/html/custom.css index 97d88f5e..befed5d2 100644 --- a/code/html/custom.css +++ b/code/html/custom.css @@ -213,6 +213,10 @@ div.state { margin-left: 5px; } +.button-thermostat-reset-counters { + background: rgb(204, 139, 41); +} + /* ----------------------------------------------------------------------------- Sliders -------------------------------------------------------------------------- */ diff --git a/code/html/custom.js b/code/html/custom.js index 890b29a3..4463aaf9 100644 --- a/code/html/custom.js +++ b/code/html/custom.js @@ -364,6 +364,29 @@ function getJson(str) { } } +function checkTempRangeMin() { + var min = parseInt($("#tempRangeMinInput").val(), 10); + var max = parseInt($("#tempRangeMaxInput").val(), 10); + if (min > max - 1) { + $("#tempRangeMinInput").val(max - 1); + } +} + +function checkTempRangeMax() { + var min = parseInt($("#tempRangeMinInput").val(), 10); + var max = parseInt($("#tempRangeMaxInput").val(), 10); + if (max < min + 1) { + $("#tempRangeMaxInput").val(min + 1); + } +} + +function doResetThermostatCounters(ask) { + var question = (typeof ask === "undefined" || false === ask) ? + null : + "Are you sure you want to reset burning counters?"; + return doAction(question, "thermostat_reset_counters"); +} + // ----------------------------------------------------------------------------- // Actions // ----------------------------------------------------------------------------- @@ -1536,6 +1559,9 @@ function processData(data) { var days = uptime; value = days + "d " + zeroPad(hours, 2) + "h " + zeroPad(minutes, 2) + "m " + zeroPad(seconds, 2) + "s"; } + if ("tmpUnits" == key) { + $("span.tmpUnit").html(data[key] == 1 ? "ºF" : "ºC"); + } // --------------------------------------------------------------------- // Matching @@ -1709,6 +1735,7 @@ $(function() { $(".button-settings-factory").on("click", doFactoryReset); $("#uploader").on("change", onFileUpload); $(".button-upgrade").on("click", doUpgrade); + $(".button-thermostat-reset-counters").on('click', doResetThermostatCounters); $(".button-apikey").on("click", generateAPIKey); $(".button-upgrade-browse").on("click", function() { diff --git a/code/html/index.html b/code/html/index.html index 4676d2f2..f2b5684c 100644 --- a/code/html/index.html +++ b/code/html/index.html @@ -96,6 +96,10 @@ GENERAL +
  • + THERMOSTAT +
  • +
  • DOMOTICZ
  • @@ -183,9 +187,9 @@ @@ -661,7 +665,7 @@
    -
    The device has bytes available for OTA updates. If your image is larger than this consider doing a two-step update.
    +
    The device has bytes available for OTA updates. If your image is larger than this consider doing a two-step update.
    @@ -907,7 +911,7 @@
    This is the fingerprint for the SSL certificate of the server.
    - You can get it using https://www.grc.com/fingerprints.htm
    + You can get it using https://www.grc.com/fingerprints.htm
    or using openssl from a linux box by typing:
    $ openssl s_client -connect <host>:<port> < /dev/null 2>/dev/null | openssl x509 -fingerprint -noout -in /dev/stdin
    @@ -1024,6 +1028,128 @@ +
    +
    + +
    +

    THERMOSTAT

    +

    Thermostat configuration

    +
    + +
    + +
    + + +
    + +
    + + Temperature range + +
    + + +
    + +
    + + +
    + +
    + +
    + + Remote sensor + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + +
    + + Operation mode + +
    + + +
    + +
    + + +
    + +
    + +
    + + Autonomous mode + +
    + + +
    + +
    + + +
    + +
    + +
    + + Time worked + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + +
    + +
    +
    +
    +
    +
    @@ -1457,7 +1583,7 @@ To learn a new code click LEARN (the Sonoff RFBridge will beep) then press a button on the remote, the new code should show up (and the RFBridge will double beep). If the device double beeps but the code does not update it has not been properly learnt. Keep trying.

    Modify or create new codes manually and then click SAVE to store them in the device memory. If your controlled device uses the same code to switch ON and OFF, learn the code with the ON button and copy paste it to the OFF input box, then click SAVE on the last one to store the value.

    Delete any code clicking the FORGET button. -

    You can also specify 116-chars long RAW codes. Raw codes require a specific firmware for for the EFM8BB1.
    +

    You can also specify 116-chars long RAW codes. Raw codes require a specific firmware for for the EFM8BB1.
    diff --git a/code/platformio.ini b/code/platformio.ini index 2ae0baa8..fb16c4b3 100644 --- a/code/platformio.ini +++ b/code/platformio.ini @@ -102,6 +102,7 @@ lib_deps = https://github.com/sparkfun/SparkFun_VEML6075_Arduino_Library#V_1.0.3 https://github.com/pololu/vl53l1x-arduino#1.0.1 https://github.com/mcleng/MAX6675-Library#2.0.1 + https://github.com/ElderJoy/esp8266-oled-ssd1306#4.0.1 lib_ignore = # ------------------------------------------------------------------------------