14 Commits

Author SHA1 Message Date
Yurii
eb943dc447 Merge 587678f184 into d50b70c211 2025-03-16 13:31:48 +00:00
Yurii
d50b70c211 refactor: improved work with opentherm on esp32 2025-03-14 05:12:20 +03:00
Yurii
dd53d1ef3e chore: bump version to 1.5.4 2025-03-06 19:15:56 +03:00
Yurii
587678f184 Merge branch 'master' into new-equitherm 2025-03-06 04:52:12 +03:00
Yurii
72235286c0 chore: resolve conflicts 2025-03-06 04:50:02 +03:00
Yurii
3bd8010b74 refactor: BLE device support for ESP32 C6 (#147)
Building on ``Arduino``+``ESP-IDF`` with ``h2zero/esp-nimble-cpp`` for ESP32 C6
2025-03-06 04:45:13 +03:00
Yurii
6a26e27d39 refactor: heating temperature step changed
* step changed to 0.1
* added processing of long presses on thermostats in the dashboard
2025-03-06 04:29:01 +03:00
Yurii
8fa440810c refactor: status BLE sensors 2025-03-05 02:28:17 +03:00
Yurii
ee868b22ce chore: remove unused files 2025-03-04 21:31:44 +03:00
Yurii
8046a7e13f refactor: cosmetic changes 2025-03-03 04:03:08 +03:00
Yurii
0ec1fc5f24 chore: fix typo 2025-03-03 02:26:09 +03:00
Yurii
061136b13e refactor: cosmetic changes (equitherm chart) 2025-03-03 02:05:33 +03:00
Yurii
3e61dabeab refactor: refactoring after #144 2025-03-02 22:44:53 +03:00
P43YM
e5f4281d4c feat: new equitherm algorithm and chart for it (#144) 2025-02-28 23:21:55 +03:00
20 changed files with 626 additions and 318 deletions

View File

@@ -27,6 +27,9 @@ let paths = {
'src_data/scripts/i18n.min.js',
'src_data/scripts/lang.js',
'src_data/scripts/utils.js'
],
'chart.js': [
'src_data/scripts/chart.js'
]
}
},

View File

@@ -7,7 +7,7 @@ public:
typedef std::function<void(unsigned long, byte)> BeforeSendRequestCallback;
typedef std::function<void(unsigned long, unsigned long, OpenThermResponseStatus, byte)> AfterSendRequestCallback;
CustomOpenTherm(int inPin = 4, int outPin = 5, bool isSlave = false) : OpenTherm(inPin, outPin, isSlave) {}
CustomOpenTherm(int inPin = 4, int outPin = 5, bool isSlave = false, bool alwaysReceive = false) : OpenTherm(inPin, outPin, isSlave, alwaysReceive) {}
~CustomOpenTherm() {}
CustomOpenTherm* setDelayCallback(DelayCallback callback = nullptr) {
@@ -28,8 +28,8 @@ public:
return this;
}
unsigned long sendRequest(unsigned long request, byte attempts = 5, byte _attempt = 0) {
_attempt++;
unsigned long sendRequest(unsigned long request) override {
this->sendRequestAttempt++;
while (!this->isReady()) {
if (this->delayCallback) {
@@ -40,15 +40,10 @@ public:
}
if (this->beforeSendRequestCallback) {
this->beforeSendRequestCallback(request, _attempt);
this->beforeSendRequestCallback(request, this->sendRequestAttempt);
}
unsigned long _response;
OpenThermResponseStatus _responseStatus = OpenThermResponseStatus::NONE;
if (!this->sendRequestAsync(request)) {
_response = 0;
} else {
if (this->sendRequestAsync(request)) {
do {
if (this->delayCallback) {
this->delayCallback(150);
@@ -56,42 +51,25 @@ public:
this->process();
} while (this->status != OpenThermStatus::READY && this->status != OpenThermStatus::DELAY);
_response = this->getLastResponse();
_responseStatus = this->getLastResponseStatus();
}
if (this->afterSendRequestCallback) {
this->afterSendRequestCallback(request, _response, _responseStatus, _attempt);
this->afterSendRequestCallback(request, this->response, this->responseStatus, this->sendRequestAttempt);
}
if (_responseStatus == OpenThermResponseStatus::SUCCESS || _responseStatus == OpenThermResponseStatus::INVALID || _attempt >= attempts) {
return _response;
if (this->responseStatus == OpenThermResponseStatus::SUCCESS || this->responseStatus == OpenThermResponseStatus::INVALID) {
this->sendRequestAttempt = 0;
return this->response;
} else if (this->sendRequestAttempt >= this->sendRequestMaxAttempts) {
this->sendRequestAttempt = 0;
return this->response;
} else {
return this->sendRequest(request, attempts, _attempt);
return this->sendRequest(request);
}
}
unsigned long setBoilerStatus(bool enableCentralHeating, bool enableHotWater, bool enableCooling, bool enableOutsideTemperatureCompensation, bool enableCentralHeating2, bool summerWinterMode, bool dhwBlocking, uint8_t lb = 0) {
unsigned int data = enableCentralHeating
| (enableHotWater << 1)
| (enableCooling << 2)
| (enableOutsideTemperatureCompensation << 3)
| (enableCentralHeating2 << 4)
| (summerWinterMode << 5)
| (dhwBlocking << 6);
data <<= 8;
data |= lb;
return this->sendRequest(buildRequest(
OpenThermMessageType::READ_DATA,
OpenThermMessageID::Status,
data
));
}
bool sendBoilerReset() {
unsigned int data = 1;
data <<= 8;
@@ -130,7 +108,7 @@ public:
static bool isValidResponseId(unsigned long response, OpenThermMessageID id) {
byte responseId = (response >> 16) & 0xFF;
return (byte)id == responseId;
}
@@ -145,6 +123,8 @@ public:
}
protected:
const uint8_t sendRequestMaxAttempts = 5;
uint8_t sendRequestAttempt = 0;
DelayCallback delayCallback;
BeforeSendRequestCallback beforeSendRequestCallback;
AfterSendRequestCallback afterSendRequestCallback;

View File

@@ -1,63 +0,0 @@
#include <Arduino.h>
#if defined(EQUITHERM_INTEGER)
// расчёты с целыми числами
typedef int datatype;
#else
// расчёты с float числами
typedef float datatype;
#endif
class Equitherm {
public:
datatype targetTemp = 0;
datatype indoorTemp = 0;
datatype outdoorTemp = 0;
float Kn = 0.0;
float Kk = 0.0;
float Kt = 0.0;
Equitherm() = default;
// kn, kk, kt
Equitherm(float new_kn, float new_kk, float new_kt) {
Kn = new_kn;
Kk = new_kk;
Kt = new_kt;
}
// лимит выходной величины
void setLimits(unsigned short min_output, unsigned short max_output) {
_minOut = min_output;
_maxOut = max_output;
}
// возвращает новое значение при вызове
datatype getResult() {
datatype output = getResultN() + getResultK() + getResultT();
output = constrain(output, _minOut, _maxOut); // ограничиваем выход
return output;
}
private:
unsigned short _minOut = 20, _maxOut = 90;
// температура контура отопления в зависимости от наружной температуры
datatype getResultN() {
float a = (-0.21 * Kn) - 0.06; // a = -0,21k — 0,06
float b = (6.04 * Kn) + 1.98; // b = 6,04k + 1,98
float c = (-5.06 * Kn) + 18.06; // с = -5,06k + 18,06
float x = (-0.2 * outdoorTemp) + 5; // x = -0.2*t1 + 5
return (a * x * x) + (b * x) + c; // Tn = ax2 + bx + c
}
// поправка на желаемую комнатную температуру
datatype getResultK() {
return (targetTemp - 20) * Kk;
}
// Расчет поправки (ошибки) термостата
datatype getResultT() {
return constrain((targetTemp - indoorTemp), -3, 3) * Kt;
}
};

View File

@@ -14,12 +14,12 @@ extra_configs = secrets.default.ini
core_dir = .pio
[env]
version = 1.5.3
version = 1.5.4
framework = arduino
lib_deps =
bblanchon/ArduinoJson@^7.3.0
;ihormelnyk/OpenTherm Library@^1.1.5
https://github.com/ihormelnyk/opentherm_library#master
https://github.com/Laxilef/opentherm_library#esp32_timer
arduino-libraries/ArduinoMqttClient@^0.1.8
lennarthennigs/ESP Telnet@^2.2
gyverlibs/FileData@^1.0.2

View File

@@ -840,19 +840,19 @@ public:
return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_SWITCH), F("equitherm")).c_str(), doc);
}
bool publishInputEquithermFactorN(bool enabledByDefault = true) {
bool publishInputEquithermSlope(bool enabledByDefault = true) {
JsonDocument doc;
doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->statusTopic.c_str();
doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault;
doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("equitherm_n"));
doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("equitherm_slope"));
doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)];
doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_CONFIG);
doc[FPSTR(HA_NAME)] = F("Equitherm factor N");
doc[FPSTR(HA_ICON)] = F("mdi:alpha-n-circle-outline");
doc[FPSTR(HA_NAME)] = F("Equitherm slope");
doc[FPSTR(HA_ICON)] = F("mdi:slope-uphill");
doc[FPSTR(HA_STATE_TOPIC)] = this->settingsTopic.c_str();
doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.equitherm.n_factor|float(0)|round(3) }}");
doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.equitherm.slope|float(0)|round(3) }}");
doc[FPSTR(HA_COMMAND_TOPIC)] = this->setSettingsTopic.c_str();
doc[FPSTR(HA_COMMAND_TEMPLATE)] = F("{\"equitherm\": {\"n_factor\" : {{ value }}}}");
doc[FPSTR(HA_COMMAND_TEMPLATE)] = F("{\"equitherm\": {\"slope\" : {{ value }}}}");
doc[FPSTR(HA_MIN)] = 0.001f;
doc[FPSTR(HA_MAX)] = 10;
doc[FPSTR(HA_STEP)] = 0.001f;
@@ -860,56 +860,80 @@ public:
doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter;
doc.shrinkToFit();
return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_NUMBER), F("equitherm_n_factor")).c_str(), doc);
return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_NUMBER), F("equitherm_slope")).c_str(), doc);
}
bool publishInputEquithermFactorK(bool enabledByDefault = true) {
bool publishInputEquithermExponent(bool enabledByDefault = true) {
JsonDocument doc;
doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->statusTopic.c_str();
doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault;
doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("equitherm_k"));
doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("equitherm_exponent"));
doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)];
doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_CONFIG);
doc[FPSTR(HA_NAME)] = F("Equitherm factor K");
doc[FPSTR(HA_ICON)] = F("mdi:alpha-k-circle-outline");
doc[FPSTR(HA_NAME)] = F("Equitherm exponent");
doc[FPSTR(HA_ICON)] = F("mdi:exponent");
doc[FPSTR(HA_STATE_TOPIC)] = this->settingsTopic.c_str();
doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.equitherm.k_factor|float(0)|round(2) }}");
doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.equitherm.exponent|float(0)|round(3) }}");
doc[FPSTR(HA_COMMAND_TOPIC)] = this->setSettingsTopic.c_str();
doc[FPSTR(HA_COMMAND_TEMPLATE)] = F("{\"equitherm\": {\"k_factor\" : {{ value }}}}");
doc[FPSTR(HA_MIN)] = 0;
doc[FPSTR(HA_MAX)] = 10;
doc[FPSTR(HA_COMMAND_TEMPLATE)] = F("{\"equitherm\": {\"exponent\" : {{ value }}}}");
doc[FPSTR(HA_MIN)] = 0.1;
doc[FPSTR(HA_MAX)] = 2;
doc[FPSTR(HA_STEP)] = 0.001f;
doc[FPSTR(HA_MODE)] = FPSTR(HA_MODE_BOX);
doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter;
doc.shrinkToFit();
return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_NUMBER), F("equitherm_exponent")).c_str(), doc);
}
bool publishInputEquithermShift(bool enabledByDefault = true) {
JsonDocument doc;
doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->statusTopic.c_str();
doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault;
doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("equitherm_shift"));
doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)];
doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_CONFIG);
doc[FPSTR(HA_DEVICE_CLASS)] = FPSTR(S_TEMPERATURE);
doc[FPSTR(HA_NAME)] = F("Equitherm shift");
doc[FPSTR(HA_ICON)] = F("mdi:chart-areaspline");
doc[FPSTR(HA_STATE_TOPIC)] = this->settingsTopic.c_str();
doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.equitherm.shift|float(0)|round(2) }}");
doc[FPSTR(HA_COMMAND_TOPIC)] = this->setSettingsTopic.c_str();
doc[FPSTR(HA_COMMAND_TEMPLATE)] = F("{\"equitherm\": {\"shift\" : {{ value }}}}");
doc[FPSTR(HA_MIN)] = -15;
doc[FPSTR(HA_MAX)] = 15;
doc[FPSTR(HA_STEP)] = 0.01f;
doc[FPSTR(HA_MODE)] = FPSTR(HA_MODE_BOX);
doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter;
doc.shrinkToFit();
return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_NUMBER), F("equitherm_k_factor")).c_str(), doc);
return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_NUMBER), F("equitherm_shift")).c_str(), doc);
}
bool publishInputEquithermFactorT(bool enabledByDefault = true) {
bool publishInputEquithermTargetDiffFactor(bool enabledByDefault = true) {
JsonDocument doc;
doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->statusTopic.c_str();
doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_TOPIC)] = this->settingsTopic.c_str();
doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.pid.enabled, 'offline', 'online') }}");
doc[FPSTR(HA_AVAILABILITY_MODE)] = F("all");
doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault;
doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("equitherm_t"));
doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("equitherm_target_diff_factor"));
doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)];
doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_CONFIG);
doc[FPSTR(HA_NAME)] = F("Equitherm factor T");
doc[FPSTR(HA_ICON)] = F("mdi:alpha-t-circle-outline");
doc[FPSTR(HA_NAME)] = F("Equitherm target diff factor");
doc[FPSTR(HA_ICON)] = F("mdi:chart-timeline-variant-shimmer");
doc[FPSTR(HA_STATE_TOPIC)] = this->settingsTopic.c_str();
doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.equitherm.t_factor|float(0)|round(2) }}");
doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.equitherm.targetDiffFactor|float(0)|round(3) }}");
doc[FPSTR(HA_COMMAND_TOPIC)] = this->setSettingsTopic.c_str();
doc[FPSTR(HA_COMMAND_TEMPLATE)] = F("{\"equitherm\": {\"t_factor\" : {{ value }}}}");
doc[FPSTR(HA_COMMAND_TEMPLATE)] = F("{\"equitherm\": {\"targetDiffFactor\" : {{ value }}}}");
doc[FPSTR(HA_MIN)] = 0;
doc[FPSTR(HA_MAX)] = 10;
doc[FPSTR(HA_STEP)] = 0.01f;
doc[FPSTR(HA_STEP)] = 0.001f;
doc[FPSTR(HA_MODE)] = FPSTR(HA_MODE_BOX);
doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter;
doc.shrinkToFit();
return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_NUMBER), F("equitherm_t_factor")).c_str(), doc);
return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_NUMBER), F("equitherm_target_diff_factor")).c_str(), doc);
}
@@ -1215,7 +1239,7 @@ public:
doc[FPSTR(HA_MIN_TEMP)] = minTemp;
doc[FPSTR(HA_MAX_TEMP)] = maxTemp;
doc[FPSTR(HA_TEMP_STEP)] = 0.5f;
doc[FPSTR(HA_TEMP_STEP)] = 0.1f;
doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter;
doc.shrinkToFit();

View File

@@ -502,9 +502,10 @@ protected:
// equitherm
this->haHelper->publishSwitchEquitherm();
this->haHelper->publishInputEquithermFactorN(false);
this->haHelper->publishInputEquithermFactorK(false);
this->haHelper->publishInputEquithermFactorT(false);
this->haHelper->publishInputEquithermSlope(false);
this->haHelper->publishInputEquithermExponent(false);
this->haHelper->publishInputEquithermShift(false);
this->haHelper->publishInputEquithermTargetDiffFactor(false);
// states
this->haHelper->publishStatusState();

View File

@@ -138,7 +138,12 @@ protected:
return;
} else if (this->instance->status == OpenThermStatus::NOT_INITIALIZED) {
this->instance->begin();
if (!this->instance->begin()) {
Log.swarningln(FPSTR(L_OT), F("Failed begin"));
this->delay(5000);
return;
}
}
// RX LED GPIO setup
@@ -212,6 +217,20 @@ protected:
F("Failed receive boiler status: %s"),
CustomOpenTherm::statusToString(this->instance->getLastResponseStatus())
);
} else {
vars.slave.heating.active = CustomOpenTherm::isCentralHeatingActive(response);
vars.slave.dhw.active = settings.opentherm.options.dhwSupport ? CustomOpenTherm::isHotWaterActive(response) : false;
vars.slave.flame = CustomOpenTherm::isFlameOn(response);
vars.slave.cooling = CustomOpenTherm::isCoolingActive(response);
vars.slave.fault.active = CustomOpenTherm::isFault(response);
vars.slave.diag.active = CustomOpenTherm::isDiagnostic(response);
Log.snoticeln(
FPSTR(L_OT), F("Received boiler status. Heating: %hhu; DHW: %hhu; flame: %hhu; cooling: %hhu; fault: %hhu; diag: %hhu"),
vars.slave.heating.active, vars.slave.dhw.active,
vars.slave.flame, vars.slave.cooling, vars.slave.fault.active, vars.slave.diag.active
);
}
// 5 request retries
@@ -310,19 +329,6 @@ protected:
Log.sinfoln(FPSTR(L_OT_DHW), vars.master.dhw.enabled ? F("Enabled") : F("Disabled"));
}
vars.slave.heating.active = CustomOpenTherm::isCentralHeatingActive(response);
vars.slave.dhw.active = settings.opentherm.options.dhwSupport ? CustomOpenTherm::isHotWaterActive(response) : false;
vars.slave.flame = CustomOpenTherm::isFlameOn(response);
vars.slave.cooling = CustomOpenTherm::isCoolingActive(response);
vars.slave.fault.active = CustomOpenTherm::isFault(response);
vars.slave.diag.active = CustomOpenTherm::isDiagnostic(response);
Log.snoticeln(
FPSTR(L_OT), F("Received boiler status. Heating: %hhu; DHW: %hhu; flame: %hhu; cooling: %hhu; fault: %hhu; diag: %hhu"),
vars.slave.heating.active, vars.slave.dhw.active,
vars.slave.flame, vars.slave.cooling, vars.slave.fault.active, vars.slave.diag.active
);
// These parameters will be updated every minute
if (millis() - this->prevUpdateNonEssentialVars > 60000) {
if (this->updateMinModulationLevel()) {

View File

@@ -1,7 +1,5 @@
#include <Equitherm.h>
#include <GyverPID.h>
Equitherm etRegulator;
GyverPID pidRegulator(0, 0, 0);
@@ -146,39 +144,32 @@ protected:
// if use equitherm
if (settings.equitherm.enabled) {
unsigned short minTemp = settings.heating.minTemp;
unsigned short maxTemp = settings.heating.maxTemp;
float targetTemp = settings.heating.target;
float indoorTemp = vars.master.heating.indoorTemp;
float outdoorTemp = vars.master.heating.outdoorTemp;
float tempDelta = settings.heating.target - vars.master.heating.outdoorTemp;
float maxPoint = settings.heating.target - (
settings.heating.maxTemp - settings.heating.target
) / settings.equitherm.slope;
if (settings.system.unitSystem == UnitSystem::IMPERIAL) {
minTemp = f2c(minTemp);
maxTemp = f2c(maxTemp);
targetTemp = f2c(targetTemp);
indoorTemp = f2c(indoorTemp);
outdoorTemp = f2c(outdoorTemp);
float sf = (settings.heating.maxTemp - settings.heating.target) / pow(
settings.heating.target - maxPoint,
1.0f / settings.equitherm.exponent
);
float etResult = settings.heating.target + settings.equitherm.shift + sf * (
tempDelta >= 0
? pow(tempDelta, 1.0f / settings.equitherm.exponent)
: -(pow(-(tempDelta), 1.0f / settings.equitherm.exponent))
);
// add diff
if (this->indoorSensorsConnected && !settings.pid.enabled && !settings.heating.turbo) {
etResult += constrain(
settings.heating.target - vars.master.heating.indoorTemp,
-3.0f,
3.0f
) * settings.equitherm.targetDiffFactor;
}
if (!this->indoorSensorsConnected || settings.pid.enabled) {
etRegulator.Kt = 0.0f;
etRegulator.indoorTemp = 0.0f;
} else {
etRegulator.Kt = settings.heating.turbo ? 0.0f : settings.equitherm.t_factor;
etRegulator.indoorTemp = indoorTemp;
}
etRegulator.setLimits(minTemp, maxTemp);
etRegulator.Kn = settings.equitherm.n_factor;
etRegulator.Kk = settings.equitherm.k_factor;
etRegulator.targetTemp = targetTemp;
etRegulator.outdoorTemp = outdoorTemp;
float etResult = etRegulator.getResult();
if (settings.system.unitSystem == UnitSystem::IMPERIAL) {
etResult = c2f(etResult);
}
// limit
etResult = constrain(etResult, settings.heating.minTemp, settings.heating.maxTemp);
if (fabsf(prevEtResult - etResult) > 0.09f) {
prevEtResult = etResult;

View File

@@ -496,9 +496,8 @@ protected:
}
}
if (!rSensor.connected) {
rSensor.connected = true;
}
// Mark connected
Sensors::setConnectionStatusById(sensorId, true, true);
if (!this->bleLastSetDtTime[sensorId] || millis() - this->bleLastSetDtTime[sensorId] > this->bleSetDtInterval) {
struct tm ti;
@@ -521,7 +520,6 @@ protected:
this->bleLastSetDtTime[sensorId] = millis();
}
}
}
}
@@ -981,16 +979,16 @@ protected:
auto& rSensor = Sensors::results[sensorId];
if (rSensor.connected && !sSensor.enabled) {
rSensor.connected = false;
Sensors::setConnectionStatusById(sensorId, false, false);
} else if (rSensor.connected && sSensor.type == Sensors::Type::NOT_CONFIGURED) {
rSensor.connected = false;
Sensors::setConnectionStatusById(sensorId, false, false);
} else if (rSensor.connected && sSensor.purpose == Sensors::Purpose::NOT_CONFIGURED) {
rSensor.connected = false;
Sensors::setConnectionStatusById(sensorId, false, false);
} else if (sSensor.type != Sensors::Type::MANUAL && rSensor.connected && (millis() - rSensor.activityTime) > this->disconnectedTimeout) {
rSensor.connected = false;
Sensors::setConnectionStatusById(sensorId, false, false);
}/* else if (!rSensor.connected) {
rSensor.connected = true;

View File

@@ -134,9 +134,10 @@ struct Settings {
struct {
bool enabled = false;
float n_factor = 0.7f;
float k_factor = 3.0f;
float t_factor = 2.0f;
float slope = 0.7f;
float exponent = 1.3f;
float shift = 0.0f;
float targetDiffFactor = 2.0f;
} equitherm;
struct {

View File

@@ -78,6 +78,7 @@ const char S_ENABLED[] PROGMEM = "enabled";
const char S_ENV[] PROGMEM = "env";
const char S_EPC[] PROGMEM = "epc";
const char S_EQUITHERM[] PROGMEM = "equitherm";
const char S_EXPONENT[] PROGMEM = "exponent";
const char S_EXTERNAL_PUMP[] PROGMEM = "externalPump";
const char S_FACTOR[] PROGMEM = "factor";
const char S_FAULT[] PROGMEM = "fault";
@@ -111,7 +112,6 @@ const char S_INVERT_STATE[] PROGMEM = "invertState";
const char S_IP[] PROGMEM = "ip";
const char S_I_FACTOR[] PROGMEM = "i_factor";
const char S_I_MULTIPLIER[] PROGMEM = "i_multiplier";
const char S_K_FACTOR[] PROGMEM = "k_factor";
const char S_LOGIN[] PROGMEM = "login";
const char S_LOG_LEVEL[] PROGMEM = "logLevel";
const char S_MAC[] PROGMEM = "mac";
@@ -137,7 +137,6 @@ const char S_NAME[] PROGMEM = "name";
const char S_NATIVE_HEATING_CONTROL[] PROGMEM = "nativeHeatingControl";
const char S_NETWORK[] PROGMEM = "network";
const char S_NTP[] PROGMEM = "ntp";
const char S_N_FACTOR[] PROGMEM = "n_factor";
const char S_OFFSET[] PROGMEM = "offset";
const char S_ON_ENABLED_HEATING[] PROGMEM = "onEnabledHeating";
const char S_ON_FAULT[] PROGMEM = "onFault";
@@ -173,9 +172,11 @@ const char S_SENSORS[] PROGMEM = "sensors";
const char S_SERIAL[] PROGMEM = "serial";
const char S_SERVER[] PROGMEM = "server";
const char S_SETTINGS[] PROGMEM = "settings";
const char S_SHIFT[] PROGMEM = "shift";
const char S_SIGNAL_QUALITY[] PROGMEM = "signalQuality";
const char S_SIZE[] PROGMEM = "size";
const char S_SLAVE[] PROGMEM = "slave";
const char S_SLOPE[] PROGMEM = "slope";
const char S_SSID[] PROGMEM = "ssid";
const char S_STA[] PROGMEM = "sta";
const char S_STATE[] PROGMEM = "state";
@@ -186,6 +187,7 @@ const char S_SUBNET[] PROGMEM = "subnet";
const char S_SUMMER_WINTER_MODE[] PROGMEM = "summerWinterMode";
const char S_SYSTEM[] PROGMEM = "system";
const char S_TARGET[] PROGMEM = "target";
const char S_TARGET_DIFF_FACTOR[] PROGMEM = "targetDiffFactor";
const char S_TARGET_TEMP[] PROGMEM = "targetTemp";
const char S_TELNET[] PROGMEM = "telnet";
const char S_TEMPERATURE[] PROGMEM = "temperature";
@@ -198,7 +200,6 @@ const char S_TRESHOLD_TIME[] PROGMEM = "tresholdTime";
const char S_TURBO[] PROGMEM = "turbo";
const char S_TURBO_FACTOR[] PROGMEM = "turboFactor";
const char S_TYPE[] PROGMEM = "type";
const char S_T_FACTOR[] PROGMEM = "t_factor";
const char S_UNIT_SYSTEM[] PROGMEM = "unitSystem";
const char S_UPTIME[] PROGMEM = "uptime";
const char S_USE[] PROGMEM = "use";

View File

@@ -500,9 +500,10 @@ void settingsToJson(const Settings& src, JsonVariant dst, bool safe = false) {
auto equitherm = dst[FPSTR(S_EQUITHERM)].to<JsonObject>();
equitherm[FPSTR(S_ENABLED)] = src.equitherm.enabled;
equitherm[FPSTR(S_N_FACTOR)] = roundf(src.equitherm.n_factor, 3);
equitherm[FPSTR(S_K_FACTOR)] = roundf(src.equitherm.k_factor, 3);
equitherm[FPSTR(S_T_FACTOR)] = roundf(src.equitherm.t_factor, 3);
equitherm[FPSTR(S_SLOPE)] = roundf(src.equitherm.slope, 3);
equitherm[FPSTR(S_EXPONENT)] = roundf(src.equitherm.exponent, 3);
equitherm[FPSTR(S_SHIFT)] = roundf(src.equitherm.shift, 2);
equitherm[FPSTR(S_TARGET_DIFF_FACTOR)] = roundf(src.equitherm.targetDiffFactor, 3);
auto pid = dst[FPSTR(S_PID)].to<JsonObject>();
pid[FPSTR(S_ENABLED)] = src.pid.enabled;
@@ -1082,29 +1083,38 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false
}
}
if (!src[FPSTR(S_EQUITHERM)][FPSTR(S_N_FACTOR)].isNull()) {
float value = src[FPSTR(S_EQUITHERM)][FPSTR(S_N_FACTOR)].as<float>();
if (!src[FPSTR(S_EQUITHERM)][FPSTR(S_SLOPE)].isNull()) {
float value = src[FPSTR(S_EQUITHERM)][FPSTR(S_SLOPE)].as<float>();
if (value > 0 && value <= 10 && fabsf(value - dst.equitherm.n_factor) > 0.0001f) {
dst.equitherm.n_factor = roundf(value, 3);
if (value > 0.0f && value <= 10.0f && fabsf(value - dst.equitherm.slope) > 0.0001f) {
dst.equitherm.slope = roundf(value, 3);
changed = true;
}
}
if (!src[FPSTR(S_EQUITHERM)][FPSTR(S_K_FACTOR)].isNull()) {
float value = src[FPSTR(S_EQUITHERM)][FPSTR(S_K_FACTOR)].as<float>();
if (!src[FPSTR(S_EQUITHERM)][FPSTR(S_EXPONENT)].isNull()) {
float value = src[FPSTR(S_EQUITHERM)][FPSTR(S_EXPONENT)].as<float>();
if (value >= 0 && value <= 10 && fabsf(value - dst.equitherm.k_factor) > 0.0001f) {
dst.equitherm.k_factor = roundf(value, 3);
if (value > 0.0f && value <= 2.0f && fabsf(value - dst.equitherm.exponent) > 0.0001f) {
dst.equitherm.exponent = roundf(value, 3);
changed = true;
}
}
if (!src[FPSTR(S_EQUITHERM)][FPSTR(S_T_FACTOR)].isNull()) {
float value = src[FPSTR(S_EQUITHERM)][FPSTR(S_T_FACTOR)].as<float>();
if (!src[FPSTR(S_EQUITHERM)][FPSTR(S_SHIFT)].isNull()) {
float value = src[FPSTR(S_EQUITHERM)][FPSTR(S_SHIFT)].as<float>();
if (value >= 0 && value <= 10 && fabsf(value - dst.equitherm.t_factor) > 0.0001f) {
dst.equitherm.t_factor = roundf(value, 3);
if (value >= -15.0f && value <= 15.0f && fabsf(value - dst.equitherm.shift) > 0.0001f) {
dst.equitherm.shift = roundf(value, 2);
changed = true;
}
}
if (!src[FPSTR(S_EQUITHERM)][FPSTR(S_TARGET_DIFF_FACTOR)].isNull()) {
float value = src[FPSTR(S_EQUITHERM)][FPSTR(S_TARGET_DIFF_FACTOR)].as<float>();
if (value >= 0.0f && value <= 10.0f && fabsf(value - dst.equitherm.targetDiffFactor) > 0.0001f) {
dst.equitherm.targetDiffFactor = roundf(value, 3);
changed = true;
}
}

View File

@@ -340,11 +340,26 @@
},
"equitherm": {
"n": "N factor",
"k": "K factor",
"t": {
"slope": {
"title": "Slope",
"note": ""
},
"exponent": {
"title": "Exponent",
"note": ""
},
"shift": {
"title": "Shift",
"note": ""
},
"targetDiffFactor": {
"title": "T factor",
"note": "Not used if PID is enabled"
},
"chart": {
"targetTemp": "Target indoor temperature",
"setpointTemp": "Heat carrier temperature",
"outdoorTemp": "Outdoor temperature"
}
},

View File

@@ -340,11 +340,26 @@
},
"equitherm": {
"n": "Fattore N",
"k": "Fattore K",
"t": {
"slope": {
"title": "Pendenza",
"note": ""
},
"exponent": {
"title": "Esponente",
"note": ""
},
"shift": {
"title": "Spostare",
"note": ""
},
"targetDiffFactor": {
"title": "Fattore T",
"note": "Non usato se PID è attivato"
},
"chart": {
"targetTemp": "Temperatura interna target",
"setpointTemp": "Temperatura del portatore di calore",
"outdoorTemp": "Temperatura esterna"
}
},

View File

@@ -340,11 +340,26 @@
},
"equitherm": {
"n": "Коэффициент N",
"k": "Коэффициент K",
"t": {
"slope": {
"title": "Наклон",
"note": ""
},
"exponent": {
"title": "Экспонента",
"note": ""
},
"shift": {
"title": "Смещение",
"note": ""
},
"targetDiffFactor": {
"title": "Коэффициент T",
"note": "Не используется, если ПИД включен"
},
"chart": {
"targetTemp": "Целевая внутренняя температура",
"setpointTemp": "Температура теплоносителя",
"outdoorTemp": "Наружная температура"
}
},

View File

@@ -41,14 +41,18 @@
<details open>
<summary><b data-i18n>dashboard.section.control</b></summary>
<div class="grid">
<div class="thermostat" id="thermostat-heating">
<div class="thermostat tHeat" data-purpose="heating" data-min="0" data-max="100" data-step="0.1" data-big-step="1">
<div class="thermostat-header" data-i18n>dashboard.thermostat.heating</div>
<div class="thermostat-temp">
<div class="thermostat-temp-target"><span id="tHeatTargetTemp"></span> <span class="tempUnit"></span></div>
<div class="thermostat-temp-target"><span class="targetTemp"></span> <span class="tempUnit"></span></div>
<div class="thermostat-temp-current"><span data-i18n>dashboard.thermostat.temp.current</span>: <span id="tHeatCurrentTemp"></span> <span class="tempUnit"></span></div>
</div>
<div class="thermostat-minus"><button id="tHeatActionMinus" class="outline"><i class="icons-down"></i></button></div>
<div class="thermostat-plus"><button id="tHeatActionPlus" class="outline"><i class="icons-up"></i></button></div>
<div class="thermostat-minus">
<button class="tAction outline" data-action="decrement"><i class="icons-down"></i></button>
</div>
<div class="thermostat-plus">
<button class="tAction outline" data-action="increment"><i class="icons-up"></i></button>
</div>
<div class="thermostat-control">
<input type="checkbox" role="switch" id="tHeatEnabled" value="true">
<label htmlFor="tHeatEnabled" data-i18n>dashboard.thermostat.enable</label>
@@ -58,21 +62,25 @@
</div>
</div>
<div class="thermostat" id="thermostat-dhw">
<div class="thermostat tDhw" data-purpose="dhw" data-min="0" data-max="100" data-step="1" data-big-step="5">
<div class="thermostat-header" data-i18n>dashboard.thermostat.dhw</div>
<div class="thermostat-temp">
<div class="thermostat-temp-target"><span id="tDhwTargetTemp"></span> <span class="tempUnit"></span></div>
<div class="thermostat-temp-target"><span class="targetTemp"></span> <span class="tempUnit"></span></div>
<div class="thermostat-temp-current"><span data-i18n>dashboard.thermostat.temp.current</span>: <span id="tDhwCurrentTemp"></span> <span class="tempUnit"></span></div>
</div>
<div class="thermostat-minus"><button class="outline" id="tDhwActionMinus"><i class="icons-down"></i></button></div>
<div class="thermostat-plus"><button class="outline" id="tDhwActionPlus"><i class="icons-up"></i></button></div>
<div class="thermostat-minus">
<button class="tAction outline" data-action="decrement"><i class="icons-down"></i></button>
</div>
<div class="thermostat-plus">
<button class="tAction outline" data-action="increment"><i class="icons-up"></i></button>
</div>
<div class="thermostat-control">
<input type="checkbox" role="switch" id="tDhwEnabled" value="true">
<label htmlFor="tDhwEnabled" data-i18n>dashboard.thermostat.enable</label>
</div>
</div>
</div>
<div class="notify notify-error notify-fault hidden">
<div class="notify-icon">
<i class="icons-error"></i>
@@ -282,7 +290,6 @@
<script src="/static/app.js?{BUILD_TIME}"></script>
<script>
let modifiedTime = null;
let noRegulators;
let prevSettings;
let newSettings = {
heating: {
@@ -300,78 +307,69 @@
const lang = new Lang(document.getElementById('lang'));
lang.build();
document.querySelector('#tHeatActionMinus').addEventListener('click', (event) => {
if (!prevSettings) {
return;
let actionTimer = null;
let actionLongPress = false;
document.querySelectorAll('.tAction').forEach((item) => {
const action = item.dataset.action;
const tContainer = item.parentNode.parentNode;
for (const eName of ['pointerup', 'pointercancel']) {
item.addEventListener(eName, (event) => {
clearInterval(actionTimer);
const purpose = tContainer.dataset.purpose;
const minTemp = parseFloat(tContainer.dataset.min);
const maxTemp = parseFloat(tContainer.dataset.max);
const step = parseFloat(tContainer.dataset.step);
const bigStep = parseFloat(tContainer.dataset.bigStep);
if (!actionLongPress && prevSettings) {
let value = 0;
if (action == 'increment') {
value = step;
} else if (action == 'decrement') {
value = -(step);
}
newSettings[purpose].target = parseFloat(constrain(newSettings[purpose].target + value, minTemp, maxTemp).toFixed(2));
modifiedTime = Date.now();
setValue('.targetTemp', newSettings[purpose].target, tContainer);
}
});
}
newSettings.heating.target -= 0.5;
modifiedTime = Date.now();
item.addEventListener('pointerdown', (event) => {
if (!prevSettings) {
return;
}
let minTemp;
if (noRegulators) {
minTemp = prevSettings.heating.minTemp;
} else {
minTemp = prevSettings.system.unitSystem == 0 ? 5 : 41;
}
const purpose = tContainer.dataset.purpose;
const minTemp = parseFloat(tContainer.dataset.min);
const maxTemp = parseFloat(tContainer.dataset.max);
const step = parseFloat(tContainer.dataset.step);
const bigStep = parseFloat(tContainer.dataset.bigStep);
if (prevSettings && newSettings.heating.target < minTemp) {
newSettings.heating.target = minTemp;
}
actionLongPress = false;
actionTimer = setInterval(() => {
if (!actionLongPress) {
actionLongPress = true;
}
setValue('#tHeatTargetTemp', newSettings.heating.target);
});
let value = 0;
if (action == 'increment') {
value = bigStep;
document.querySelector('#tHeatActionPlus').addEventListener('click', (event) => {
if (!prevSettings) {
return;
}
} else if (action == 'decrement') {
value = -(bigStep);
}
newSettings.heating.target += 0.5;
modifiedTime = Date.now();
newSettings[purpose].target = parseFloat(constrain(newSettings[purpose].target + value, minTemp, maxTemp).toFixed(2));
modifiedTime = Date.now();
let maxTemp;
if (noRegulators) {
maxTemp = prevSettings.heating.maxTemp;
} else {
maxTemp = prevSettings.system.unitSystem == 0 ? 30 : 86;
}
if (prevSettings && newSettings.heating.target > maxTemp) {
newSettings.heating.target = maxTemp;
}
setValue('#tHeatTargetTemp', newSettings.heating.target);
});
document.querySelector('#tDhwActionMinus').addEventListener('click', (event) => {
if (!prevSettings) {
return;
}
newSettings.dhw.target -= 1.0;
modifiedTime = Date.now();
if (newSettings.dhw.target < prevSettings.dhw.minTemp) {
newSettings.dhw.target = prevSettings.dhw.minTemp;
}
setValue('#tDhwTargetTemp', newSettings.dhw.target);
});
document.querySelector('#tDhwActionPlus').addEventListener('click', (event) => {
if (!prevSettings) {
return;
}
newSettings.dhw.target += 1.0;
modifiedTime = Date.now();
if (newSettings.dhw.target > prevSettings.dhw.maxTemp) {
newSettings.dhw.target = prevSettings.dhw.maxTemp;
}
setValue('#tDhwTargetTemp', newSettings.dhw.target);
setValue('.targetTemp', newSettings[purpose].target, tContainer);
}, 500);
});
});
document.querySelector('#tHeatEnabled').addEventListener('change', (event) => {
@@ -486,7 +484,6 @@
}
const result = await response.json();
noRegulators = !result.opentherm.options.nativeHeatingControl && !result.equitherm.enabled && !result.pid.enabled;
prevSettings = result;
unitSystem = result.system.unitSystem;
newSettings.heating.enabled = result.heating.enabled;
@@ -496,17 +493,17 @@
newSettings.dhw.target = result.dhw.target;
if (result.opentherm.options.dhwSupport) {
show('#thermostat-dhw');
show('.tDhw');
} else {
hide('#thermostat-dhw');
hide('.tDhw');
}
setCheckboxValue('#tHeatEnabled', result.heating.enabled);
setCheckboxValue('#tHeatTurbo', result.heating.turbo);
setValue('#tHeatTargetTemp', result.heating.target);
setValue('.tHeat .targetTemp', result.heating.target);
setCheckboxValue('#tDhwEnabled', result.dhw.enabled);
setValue('#tDhwTargetTemp', result.dhw.target);
setValue('.tDhw .targetTemp', result.dhw.target);
setValue('.tempUnit', temperatureUnit(unitSystem));
setValue('.pressureUnit', pressureUnit(unitSystem));
@@ -523,20 +520,20 @@
cache: "no-cache",
credentials: "include"
});
if (!response.ok) {
throw new Error('Response not valid');
}
const result = await response.json();
// Graph
setValue('#tHeatCurrentTemp', result.master.heating.indoorTempControl
? result.master.heating.indoorTemp
setValue('#tHeatCurrentTemp', result.master.heating.indoorTempControl
? result.master.heating.indoorTemp
: result.master.heating.currentTemp
);
setValue('#tDhwCurrentTemp', result.master.dhw.currentTemp);
// SLAVE
setValue('.sMemberId', result.slave.memberId);
@@ -646,6 +643,14 @@
setState('.mCascadeControlInput', result.master.cascadeControl.input);
setState('.mCascadeControlOutput', result.master.cascadeControl.output);
const tHeat = document.querySelector('.tHeat');
tHeat.dataset.min = result.master.heating.minTemp;
tHeat.dataset.max = result.master.heating.maxTemp;
const tDhw = document.querySelector('.tDhw');
tDhw.dataset.min = result.master.dhw.minTemp;
tDhw.dataset.max = result.master.dhw.maxTemp;
setBusy('#dashboard-busy', '#dashboard-container', false);
} catch (error) {
@@ -658,7 +663,7 @@
cache: "no-cache",
credentials: "include"
});
if (!response.ok) {
throw new Error("Response not valid");
}
@@ -684,7 +689,7 @@
if (!sensorNode) {
continue;
}
const sData = result[sensorId];
if (!sData.enabled || sData.purpose == 255) {
sensorNode.classList.toggle("hidden", true);

View File

@@ -230,7 +230,9 @@
setCheckboxValue("[name='filtering']", data.filtering, sensorForm);
setInputValue("[name='filteringFactor']", data.filteringFactor, {}, sensorForm);
sensorForm.querySelector("[name='type']").dispatchEvent(new Event("change"));
setTimeout(() => {
sensorForm.querySelector("[name='type']").dispatchEvent(new Event("change"));
}, 10);
setBusy(".form-busy", "form", false, sensorNode);
};

View File

@@ -261,7 +261,7 @@
</details>
<hr />
<details>
<summary><b data-i18n>settings.section.equitherm</b></summary>
<div>
@@ -274,21 +274,44 @@
</label>
</fieldset>
<div>
<div>
<canvas id="etChart"></canvas>
</div>
<label>
<div>
<span data-i18n>settings.equitherm.chart.targetTemp</span>: <b class="etChartTargetTempValue"></b>°
</div>
<input class="etChartTargetTemp" type="range" value="0" min="0" max="0" step="0.5">
</label>
</div>
<div class="grid">
<label>
<span data-i18n>settings.equitherm.n</span>
<input type="number" inputmode="decimal" name="equitherm[n_factor]" min="0.001" max="10" step="0.001" required>
<span data-i18n>settings.equitherm.slope.title</span>
<input type="number" inputmode="decimal" name="equitherm[slope]" min="0.001" max="10" step="0.001" required>
<small data-i18n>settings.equitherm.slope.note</small>
</label>
<label>
<span data-i18n>settings.equitherm.k</span>
<input type="number" inputmode="decimal" name="equitherm[k_factor]" min="0" max="10" step="0.01" required>
<span data-i18n>settings.equitherm.exponent.title</span>
<input type="number" inputmode="decimal" name="equitherm[exponent]" min="0.1" max="2" step="0.001" required>
<small data-i18n>settings.equitherm.exponent.note</small>
</label>
</div>
<div class="grid">
<label>
<span data-i18n>settings.equitherm.shift.title</span>
<input type="number" inputmode="decimal" name="equitherm[shift]" min="-15" max="15" step="0.01" required>
<small data-i18n>settings.equitherm.shift.note</small>
</label>
<label>
<span data-i18n>settings.equitherm.t.title</span>
<input type="number" inputmode="decimal" name="equitherm[t_factor]" min="0" max="10" step="0.01" required>
<small data-i18n>settings.equitherm.t.note</small>
<span data-i18n>settings.equitherm.targetDiffFactor.title</span>
<input type="number" inputmode="decimal" name="equitherm[targetDiffFactor]" min="0" max="10" step="0.01" required>
<small data-i18n>settings.equitherm.targetDiffFactor.note</small>
</label>
</div>
@@ -298,7 +321,7 @@
</details>
<hr />
<details>
<summary><b data-i18n>settings.section.pid</b></summary>
<div>
@@ -756,11 +779,169 @@
</footer>
<script src="/static/app.js?{BUILD_TIME}"></script>
<script src="/static/chart.js?{BUILD_TIME}"></script>
<script>
document.addEventListener('DOMContentLoaded', async () => {
const lang = new Lang(document.getElementById('lang'));
lang.build();
let etChart = null;
let etChartConfig = {
slope: null,
exponent: null,
shift: null,
unitSystem: null,
targetTemp: null,
minTemp: null,
maxTemp: null
};
const hasNeedDecimationChart = () => {
return window.innerWidth <= 800;
}
const makeEquithermChart = () => {
if (etChart == null) {
const ctx = document.getElementById('etChart').getContext('2d');
try {
etChart = new Chart(ctx, {
type: 'line',
data: {
datasets: [{
borderColor: (context) => {
const chart = context.chart;
const {ctx, chartArea} = chart;
if (!chartArea) {
return;
}
const gradient = ctx.createLinearGradient(0, chartArea.bottom, 0, chartArea.top);
gradient.addColorStop(0, 'rgba(1, 114, 173, 1)');
gradient.addColorStop(0.5, 'rgba(255, 99, 132, 1)');
return gradient;
},
borderWidth: 3,
fill: false,
tension: 0.1,
pointRadius: 2,
pointHoverRadius: 4,
indexAxis: "x",
data: []
}]
},
options: {
responsive: true,
resizeDelay: 500,
parsing: false,
interaction: {
mode: 'nearest',
intersect: false
},
plugins: {
tooltip: {
enabled: true,
position: 'nearest',
displayColors: false,
callbacks: {
title: (items) => {
return `${i18n("settings.equitherm.chart.outdoorTemp")}: ${items[0].label}`;
}
}
},
legend: {
display: false
},
decimation: {
enabled: hasNeedDecimationChart(),
algorithm: 'lttb',
samples: 30,
threshold: 30
}
},
scales: {
x: {
display: true,
type: "linear",
reverse: true,
title: {
display: true
},
ticks: {
stepSize: 1,
format: {
style: "unit",
unit: "degree",
unitDisplay: "narrow"
}
}
},
y: {
display: true,
title: {
display: true
},
ticks: {
format: {
style: "unit",
unit: "degree",
unitDisplay: "narrow"
}
}
}
}
}
});
} catch (error) {
console.log(error);
}
}
if (!etChart) {
return;
}
while (etChart.data.datasets[0].data.length) {
etChart.data.datasets[0].data.pop();
}
for (let value = 30; value >= -30; value--) {
const outdoorTemp = etChartConfig.unitSystem == 0 ? value : c2f(value);
etChart.data.datasets[0].data.push({
x: parseFloat(outdoorTemp.toFixed(1)),
y: parseFloat(calculateEquithermTemp(outdoorTemp).toFixed(1))
});
}
etChart.data.datasets[0].label = i18n("settings.equitherm.chart.setpointTemp");
etChart.options.scales.x.title.text = i18n("settings.equitherm.chart.outdoorTemp");
etChart.options.scales.y.title.text = i18n("settings.equitherm.chart.setpointTemp");
etChart.update();
}
const calculateEquithermTemp = (outdoorTemp) => {
const tempDelta = etChartConfig.targetTemp - outdoorTemp;
const maxPoint = etChartConfig.targetTemp - (
etChartConfig.maxTemp - etChartConfig.targetTemp
) / etChartConfig.slope;
const sf = (etChartConfig.maxTemp - etChartConfig.targetTemp) / Math.pow(
etChartConfig.targetTemp - maxPoint,
1 / etChartConfig.exponent
);
const result = etChartConfig.targetTemp + etChartConfig.shift + sf * (
tempDelta >= 0
? Math.pow(tempDelta, 1 / etChartConfig.exponent)
: -(Math.pow(-(tempDelta), 1 / etChartConfig.exponent))
);
return Math.max(Math.min(result, etChartConfig.maxTemp), etChartConfig.minTemp);
}
const fillData = (data) => {
// System
setSelectValue("[name='system[logLevel]']", data.system.logLevel);
@@ -883,9 +1064,10 @@
// Equitherm
setCheckboxValue("[name='equitherm[enabled]']", data.equitherm.enabled);
setInputValue("[name='equitherm[n_factor]']", data.equitherm.n_factor);
setInputValue("[name='equitherm[k_factor]']", data.equitherm.k_factor);
setInputValue("[name='equitherm[t_factor]']", data.equitherm.t_factor);
setInputValue("[name='equitherm[slope]']", data.equitherm.slope);
setInputValue("[name='equitherm[exponent]']", data.equitherm.exponent);
setInputValue("[name='equitherm[shift]']", data.equitherm.shift);
setInputValue("[name='equitherm[targetDiffFactor]']", data.equitherm.targetDiffFactor);
setBusy('#equitherm-settings-busy', '#equitherm-settings', false);
// PID
@@ -909,6 +1091,24 @@
setInputValue("[name='pid[deadband][thresholdHigh]']", data.pid.deadband.thresholdHigh);
setInputValue("[name='pid[deadband][thresholdLow]']", data.pid.deadband.thresholdLow);
setBusy('#pid-settings-busy', '#pid-settings', false);
const etMinTemp = parseInt(data.system.unitSystem == 0 ? 5 : 41);
const etMaxTemp = parseInt(data.system.unitSystem == 0 ? 30 : 86);
const etTargetTemp = constrain(parseFloat(data.heating.target), etMinTemp, etMaxTemp);
setInputValue(".etChartTargetTemp", etTargetTemp.toFixed(1), {
"min": etMinTemp,
"max": etMaxTemp
});
etChartConfig.slope = data.equitherm.slope;
etChartConfig.exponent = data.equitherm.exponent;
etChartConfig.shift = data.equitherm.shift;
etChartConfig.unitSystem = data.system.unitSystem;
etChartConfig.minTemp = data.heating.minTemp;
etChartConfig.maxTemp = data.heating.maxTemp;
makeEquithermChart();
};
try {
@@ -961,6 +1161,58 @@
} catch (error) {
console.log(error);
}
document.querySelector(".etChartTargetTemp").addEventListener("input", async (event) => {
setValue('.etChartTargetTempValue', parseFloat(event.target.value).toFixed(1));
});
document.querySelector(".etChartTargetTemp").addEventListener("change", async (event) => {
if (!event.target.checkValidity()) {
return;
}
etChartConfig.targetTemp = parseFloat(event.target.value);
setValue('.etChartTargetTempValue', etChartConfig.targetTemp.toFixed(1));
makeEquithermChart();
});
document.querySelector("[name='equitherm[slope]']").addEventListener("change", async (event) => {
if (!event.target.checkValidity()) {
return;
}
etChartConfig.slope = parseFloat(event.target.value);
makeEquithermChart();
});
document.querySelector("[name='equitherm[exponent]']").addEventListener("change", async (event) => {
if (!event.target.checkValidity()) {
return;
}
etChartConfig.exponent = parseFloat(event.target.value);
makeEquithermChart();
});
document.querySelector("[name='equitherm[shift]']").addEventListener("change", async (event) => {
if (!event.target.checkValidity()) {
return;
}
etChartConfig.shift = parseFloat(event.target.value);
makeEquithermChart();
});
window.addEventListener('resize', async (event) => {
if (etChart) {
etChart.resize();
if (etChart.options.plugins.decimation.enabled != hasNeedDecimationChart()) {
etChart.options.plugins.decimation.enabled = hasNeedDecimationChart();
etChart.update();
}
}
});
});
</script>
</body>

20
src_data/scripts/chart.js Normal file

File diff suppressed because one or more lines are too long

View File

@@ -5,8 +5,13 @@ const setupForm = (formSelector, onResultCallback = null, noCastItems = []) => {
}
form.querySelectorAll('input').forEach(item => {
item.addEventListener('change', (e) => {
e.target.setAttribute('aria-invalid', !e.target.checkValidity());
item.addEventListener('change', (event) => {
if (!event.target.checkValidity()) {
event.target.setAttribute('aria-invalid', true);
} else if (event.target.hasAttribute('aria-invalid')) {
event.target.removeAttribute('aria-invalid');
}
})
});
@@ -630,6 +635,10 @@ const setCheckboxValue = (selector, value, parent = undefined) => {
}
item.checked = value;
setTimeout(() => {
item.dispatchEvent(new Event("change"));
}, 10);
}
const setRadioValue = (selector, value, parent = undefined) => {
@@ -643,7 +652,14 @@ const setRadioValue = (selector, value, parent = undefined) => {
}
for (let item of items) {
item.checked = item.value == value;
const checked = item.value == value;
if (item.checked != checked) {
item.checked = checked;
setTimeout(() => {
item.dispatchEvent(new Event("change"));
}, 10);
}
}
}
@@ -658,13 +674,17 @@ const setInputValue = (selector, value, attrs = {}, parent = undefined) => {
}
for (let item of items) {
item.value = value;
if (attrs instanceof Object) {
for (let attrKey of Object.keys(attrs)) {
item.setAttribute(attrKey, attrs[attrKey]);
}
}
item.value = value;
setTimeout(() => {
item.dispatchEvent(new Event("change"));
}, 10);
}
}
@@ -849,4 +869,16 @@ function dec2hex(i) {
}
return hex.toUpperCase();
}
function constrain(amt, low, high) {
return ((amt) < (low) ? (low) : ((amt) > (high) ? (high) : (amt)));
}
function c2f(value) {
return (9 / 5) * value + 32;
}
function f2c(value) {
return (value - 32) * (5 / 9);
}