mirror of
https://github.com/xoseperez/espurna.git
synced 2026-03-12 03:07:13 +01:00
Merge pull request #1603 from ElderJoy/thermostat
Add thermostat module
This commit is contained in:
@@ -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
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -756,8 +783,14 @@
|
||||
#endif
|
||||
|
||||
|
||||
#ifndef MQTT_USE_JSON
|
||||
#define MQTT_USE_JSON 0 // Group messages in a JSON body
|
||||
#if THERMOSTAT_SUPPORT == 1
|
||||
#ifndef MQTT_USE_JSON
|
||||
#define MQTT_USE_JSON 1 // Group messages in a JSON body
|
||||
#endif
|
||||
#else
|
||||
#ifndef MQTT_USE_JSON
|
||||
#define MQTT_USE_JSON 0 // Don't group messages in a JSON body (default)
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifndef MQTT_USE_JSON_DELAY
|
||||
@@ -837,6 +870,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)
|
||||
|
||||
|
||||
@@ -118,6 +118,12 @@ PROGMEM const char espurna_modules[] =
|
||||
#if TERMINAL_SUPPORT
|
||||
"TERMINAL "
|
||||
#endif
|
||||
#if THERMOSTAT_SUPPORT
|
||||
"THERMOSTAT "
|
||||
#endif
|
||||
#if THERMOSTAT_DISPLAY_SUPPORT
|
||||
"THERMOSTAT_DISPLAY "
|
||||
#endif
|
||||
#if THINGSPEAK_SUPPORT
|
||||
"THINGSPEAK "
|
||||
#endif
|
||||
|
||||
@@ -211,3 +211,14 @@ void webRequestRegister(web_request_callback_f callback);
|
||||
typedef std::function<void(justwifi_messages_t code, char * parameter)> wifi_callback_f;
|
||||
void wifiRegister(wifi_callback_f callback);
|
||||
bool wifiConnected();
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// THERMOSTAT
|
||||
// -----------------------------------------------------------------------------
|
||||
#if THERMOSTAT_SUPPORT
|
||||
typedef std::function<void(bool)> thermostat_callback_f;
|
||||
void thermostatRegister(thermostat_callback_f callback);
|
||||
#else
|
||||
#define thermostat_callback_f void *
|
||||
#endif
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#define WEBUI_IMAGE_RFBRIDGE 4
|
||||
#define WEBUI_IMAGE_RFM69 8
|
||||
#define WEBUI_IMAGE_LIGHTFOX 16
|
||||
#define WEBUI_IMAGE_THERMOSTAT 32
|
||||
#define WEBUI_IMAGE_FULL 15
|
||||
|
||||
#if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
|
||||
@@ -53,6 +54,15 @@
|
||||
#define WEBUI_IMAGE WEBUI_IMAGE_LIGHTFOX
|
||||
#endif
|
||||
|
||||
#if THERMOSTAT_SUPPORT == 1
|
||||
#ifndef WEBUI_IMAGE
|
||||
#define WEBUI_IMAGE WEBUI_IMAGE_THERMOSTAT
|
||||
#else
|
||||
#undef WEBUI_IMAGE
|
||||
#define WEBUI_IMAGE WEBUI_IMAGE_FULL
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifndef WEBUI_IMAGE
|
||||
#define WEBUI_IMAGE WEBUI_IMAGE_SMALL
|
||||
#endif
|
||||
@@ -78,6 +88,9 @@ PROGMEM const char espurna_webui[] =
|
||||
#if WEBUI_IMAGE == WEBUI_IMAGE_LIGHTFOX
|
||||
"LIGHTFOX"
|
||||
#endif
|
||||
#if WEBUI_IMAGE == WEBUI_IMAGE_THERMOSTAT
|
||||
"THERMOSTAT"
|
||||
#endif
|
||||
#if WEBUI_IMAGE == WEBUI_IMAGE_FULL
|
||||
"FULL"
|
||||
#endif
|
||||
|
||||
BIN
code/espurna/data/index.thermostat.html.gz
Normal file
BIN
code/espurna/data/index.thermostat.html.gz
Normal file
Binary file not shown.
@@ -192,6 +192,12 @@ void setup() {
|
||||
#ifdef FOXEL_LIGHTFOX_DUAL
|
||||
lightfoxSetup();
|
||||
#endif
|
||||
#if THERMOSTAT_SUPPORT
|
||||
thermostatSetup();
|
||||
#endif
|
||||
#if THERMOSTAT_DISPLAY_SUPPORT
|
||||
displaySetup();
|
||||
#endif
|
||||
|
||||
|
||||
// 3rd party code hook
|
||||
|
||||
@@ -15,6 +15,8 @@ Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
|
||||
#include "filters/MovingAverageFilter.h"
|
||||
#include "sensors/BaseSensor.h"
|
||||
|
||||
#include <float.h>
|
||||
|
||||
typedef struct {
|
||||
BaseSensor * sensor; // Sensor object
|
||||
BaseFilter * filter; // Filter object
|
||||
@@ -1290,6 +1292,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);
|
||||
|
||||
2604
code/espurna/static/index.thermostat.html.gz.h
generated
Normal file
2604
code/espurna/static/index.thermostat.html.gz.h
generated
Normal file
File diff suppressed because it is too large
Load Diff
766
code/espurna/thermostat.ino
Normal file
766
code/espurna/thermostat.ino
Normal file
@@ -0,0 +1,766 @@
|
||||
/*
|
||||
|
||||
THERMOSTAT MODULE
|
||||
|
||||
Copyright (C) 2017 by Dmitry Blinov <dblinov76 at gmail dot com>
|
||||
|
||||
*/
|
||||
|
||||
#if THERMOSTAT_SUPPORT
|
||||
|
||||
#include <ArduinoJson.h>
|
||||
#include <float.h>
|
||||
|
||||
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_callback_f> _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<magnitudeCount(); i++) {
|
||||
if (magnitudeType(i) == MAGNITUDE_TEMPERATURE) {
|
||||
double temp = magnitudeValue(i);
|
||||
char tmp_str[6];
|
||||
dtostrf(temp, 1-sizeof(tmp_str), 1, tmp_str);
|
||||
DEBUG_MSG_P(PSTR("[THERMOSTAT] getLocalTemperature temp: %s\n"), tmp_str);
|
||||
return temp > -0.1 && temp < 0.1 ? DBL_MIN : temp;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return DBL_MIN;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
double getLocalHumidity() {
|
||||
#if SENSOR_SUPPORT
|
||||
for (byte i=0; i<magnitudeCount(); i++) {
|
||||
if (magnitudeType(i) == MAGNITUDE_HUMIDITY) {
|
||||
double hum = magnitudeValue(i);
|
||||
char tmp_str[4];
|
||||
dtostrf(hum, 1-sizeof(tmp_str), 0, tmp_str);
|
||||
DEBUG_MSG_P(PSTR("[THERMOSTAT] getLocalHumidity hum: %s\%\n"), tmp_str);
|
||||
return hum > -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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
|
||||
#include "static/index.rfm69.html.gz.h"
|
||||
#elif WEBUI_IMAGE == WEBUI_IMAGE_LIGHTFOX
|
||||
#include "static/index.lightfox.html.gz.h"
|
||||
#elif WEBUI_IMAGE == WEBUI_IMAGE_THERMOSTAT
|
||||
#include "static/index.thermostat.html.gz.h"
|
||||
#elif WEBUI_IMAGE == WEBUI_IMAGE_FULL
|
||||
#include "static/index.all.html.gz.h"
|
||||
#endif
|
||||
|
||||
@@ -109,13 +109,14 @@ var htmllintReporter = function(filepath, issues) {
|
||||
|
||||
var buildWebUI = function(module) {
|
||||
|
||||
var modules = {'light': false, 'sensor': false, 'rfbridge': false, 'rfm69': false};
|
||||
var modules = {'light': false, 'sensor': false, 'rfbridge': false, 'rfm69': false, 'thermostat': false};
|
||||
if ('all' === module) {
|
||||
modules['light'] = true;
|
||||
modules['sensor'] = true;
|
||||
modules['rfbridge'] = true;
|
||||
modules['rfm69'] = false; // we will never be adding this except when building RFM69GW
|
||||
modules['lightfox'] = false; // we will never be adding this except when building lightfox
|
||||
modules['thermostat'] = true;
|
||||
} else if ('small' !== module) {
|
||||
modules[module] = true;
|
||||
}
|
||||
@@ -191,6 +192,10 @@ gulp.task('webui_lightfox', function() {
|
||||
return buildWebUI('lightfox');
|
||||
});
|
||||
|
||||
gulp.task('webui_thermostat', function() {
|
||||
return buildWebUI('thermostat');
|
||||
});
|
||||
|
||||
gulp.task('webui_all', function() {
|
||||
return buildWebUI('all');
|
||||
});
|
||||
@@ -203,6 +208,7 @@ gulp.task('webui',
|
||||
'webui_rfbridge',
|
||||
'webui_rfm69',
|
||||
'webui_lightfox',
|
||||
'webui_thermostat',
|
||||
'webui_all'
|
||||
)
|
||||
);
|
||||
|
||||
@@ -215,6 +215,10 @@ div.state {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.button-thermostat-reset-counters {
|
||||
background: rgb(204, 139, 41);
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
Sliders
|
||||
-------------------------------------------------------------------------- */
|
||||
|
||||
@@ -364,6 +364,31 @@ function getJson(str) {
|
||||
}
|
||||
}
|
||||
|
||||
<!-- removeIf(!thermostat)-->
|
||||
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");
|
||||
}
|
||||
<!-- endRemoveIf(!thermostat)-->
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Actions
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -1625,6 +1650,11 @@ function processData(data) {
|
||||
var days = uptime;
|
||||
value = days + "d " + zeroPad(hours, 2) + "h " + zeroPad(minutes, 2) + "m " + zeroPad(seconds, 2) + "s";
|
||||
}
|
||||
<!-- removeIf(!thermostat)-->
|
||||
if ("tmpUnits" == key) {
|
||||
$("span.tmpUnit").html(data[key] == 1 ? "ºF" : "ºC");
|
||||
}
|
||||
<!-- endRemoveIf(!thermostat)-->
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Matching
|
||||
@@ -1799,6 +1829,10 @@ $(function() {
|
||||
$("#uploader").on("change", onFileUpload);
|
||||
$(".button-upgrade").on("click", doUpgrade);
|
||||
|
||||
<!-- removeIf(!thermostat)-->
|
||||
$(".button-thermostat-reset-counters").on('click', doResetThermostatCounters);
|
||||
<!-- endRemoveIf(!thermostat)-->
|
||||
|
||||
$(".button-apikey").on("click", generateAPIKey);
|
||||
$(".button-upgrade-browse").on("click", function() {
|
||||
$("input[name='upgrade']")[0].click();
|
||||
|
||||
@@ -96,6 +96,12 @@
|
||||
<a href="#" class="pure-menu-link" data="panel-general">GENERAL</a>
|
||||
</li>
|
||||
|
||||
<!-- removeIf(!thermostat) -->
|
||||
<li class="pure-menu-item module module-thermostat">
|
||||
<a href="#" class="pure-menu-link" data="panel-thermostat">THERMOSTAT</a>
|
||||
</li>
|
||||
<!-- endRemoveIf(!thermostat) -->
|
||||
|
||||
<!-- removeIf(!lightfox) -->
|
||||
<li class="pure-menu-item module module-lightfox">
|
||||
<a href="#" class="pure-menu-link" data="panel-lightfox">LIGHTFOX RF</a>
|
||||
@@ -1043,6 +1049,130 @@
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- removeIf(!thermostat) -->
|
||||
<form id="form-thermostat" class="pure-form form-settings">
|
||||
<div class="panel" id="panel-thermostat">
|
||||
|
||||
<div class="header">
|
||||
<h1>THERMOSTAT</h1>
|
||||
<h2>Thermostat configuration</h2>
|
||||
</div>
|
||||
|
||||
<div class="page">
|
||||
|
||||
<div class="pure-g">
|
||||
<label class="pure-u-1 pure-u-lg-1-4" for="thermostatOperationMode">Operation mode</label>
|
||||
<input class="pure-u-1 pure-u-lg-1-4" name="thermostatOperationMode" type="text" readonly />
|
||||
</div>
|
||||
|
||||
<fieldset>
|
||||
|
||||
<legend>Temperature range</legend>
|
||||
|
||||
<div class="pure-g">
|
||||
<label class="pure-u-1 pure-u-lg-1-4">Max (<span class="tmpUnit"></span>)</label>
|
||||
<input class="pure-u-1 pure-u-lg-1-4" id="tempRangeMaxInput" name="tempRangeMax" type="number" min="1" max="100" tabindex="32" data="20" onchange="checkTempRangeMax()" />
|
||||
</div>
|
||||
|
||||
<div class="pure-g">
|
||||
<label class="pure-u-1 pure-u-lg-1-4">Min (<span class="tmpUnit"></span>)</label>
|
||||
<input class="pure-u-1 pure-u-lg-1-4" id="tempRangeMinInput" name="tempRangeMin" type="number" min="0" max="99" tabindex="31" data="10" onchange="checkTempRangeMin()" />
|
||||
</div>
|
||||
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
|
||||
<legend>Remote sensor</legend>
|
||||
|
||||
<div class="pure-g">
|
||||
<label class="pure-u-1 pure-u-lg-1-4" for="remoteSensorName">Remote sensor name</label>
|
||||
<input class="pure-u-1 pure-u-lg-1-4" name="remoteSensorName" type="text" />
|
||||
</div>
|
||||
|
||||
<div class="pure-g">
|
||||
<label class="pure-u-1 pure-u-lg-1-4" for="remoteTmp">Remote temperature (<span class="tmpUnit"></span>)</label>
|
||||
<input class="pure-u-1 pure-u-lg-1-4" name="remoteTmp" type="text" readonly />
|
||||
</div>
|
||||
|
||||
<div class="pure-g">
|
||||
<label class="pure-u-1 pure-u-lg-1-4" for="remoteTempMaxWait">Remote temperature waiting (s)</label>
|
||||
<input class="pure-u-1 pure-u-lg-1-4" name="remoteTempMaxWait" type="number" min="0" max="1800" tabindex="33" data="120" />
|
||||
</div>
|
||||
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
|
||||
<legend>Operation mode</legend>
|
||||
|
||||
<div class="pure-g">
|
||||
<label class="pure-u-1 pure-u-lg-1-4" for="maxOnTime">Max heating time (m)</label>
|
||||
<input class="pure-u-1 pure-u-lg-1-4" name="maxOnTime" type="number" min="0" max="180" tabindex="34" data="30" />
|
||||
</div>
|
||||
|
||||
<div class="pure-g">
|
||||
<label class="pure-u-1 pure-u-lg-1-4" for="minOffTime">Min rest time (m)</label>
|
||||
<input class="pure-u-1 pure-u-lg-1-4" name="minOffTime" type="number" min="0" max="60" tabindex="35" data="10" />
|
||||
</div>
|
||||
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
|
||||
<legend>Autonomous mode</legend>
|
||||
|
||||
<div class="pure-g">
|
||||
<label class="pure-u-1 pure-u-lg-1-4" for="aloneOnTime">Heating time (m)</label>
|
||||
<input class="pure-u-1 pure-u-lg-1-4" name="aloneOnTime" type="number" min="0" max="180" tabindex="36" data="5" />
|
||||
</div>
|
||||
|
||||
<div class="pure-g">
|
||||
<label class="pure-u-1 pure-u-lg-1-4" for="aloneOffTime">Rest time (m)</label>
|
||||
<input class="pure-u-1 pure-u-lg-1-4" name="aloneOffTime" type="number" min="0" max="180" tabindex="37" data="55" />
|
||||
</div>
|
||||
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
|
||||
<legend>Time worked</legend>
|
||||
|
||||
<div class="pure-g">
|
||||
<label class="pure-u-1 pure-u-lg-1-4" for="burnToday">Today</label>
|
||||
<input class="pure-u-1 pure-u-lg-1-4" type="text" name="burnToday" readonly />
|
||||
</div>
|
||||
|
||||
<div class="pure-g">
|
||||
<label class="pure-u-1 pure-u-lg-1-4" for="burnYesterday">Yesterday</label>
|
||||
<input class="pure-u-1 pure-u-lg-1-4" type="text" name="burnYesterday" readonly />
|
||||
</div>
|
||||
|
||||
<div class="pure-g">
|
||||
<label class="pure-u-1 pure-u-lg-1-4" for="burnThisMonth">Current month</label>
|
||||
<input class="pure-u-1 pure-u-lg-1-4" type="text" name="burnThisMonth" readonly />
|
||||
</div>
|
||||
|
||||
<div class="pure-g">
|
||||
<label class="pure-u-1 pure-u-lg-1-4" for="burnPrevMonth">Previous month</label>
|
||||
<input class="pure-u-1 pure-u-lg-1-4" type="text" name="burnPrevMonth" readonly />
|
||||
</div>
|
||||
|
||||
<div class="pure-g">
|
||||
<label class="pure-u-1 pure-u-lg-1-4" for="burnTotal">Total</label>
|
||||
<input class="pure-u-1 pure-u-lg-1-4" type="text" name="burnTotal" readonly />
|
||||
</div>
|
||||
|
||||
<div class="pure-g">
|
||||
<button type="button" class="pure-button button-thermostat-reset-counters">Reset counters</button>
|
||||
</div>
|
||||
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<!-- endRemoveIf(!thermostat) -->
|
||||
|
||||
<form id="form-domoticz" class="pure-form form-settings">
|
||||
<div class="panel" id="panel-domoticz">
|
||||
|
||||
|
||||
@@ -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 =
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user