Compare commits

...

4 Commits

Author SHA1 Message Date
Yurii c6117f106e refactor: some changes 2026-05-16 17:57:10 +03:00
Yurii 66a433e0dd Merge branch 'async' into avg_temp 2026-05-16 17:24:02 +03:00
Yurii dcc9b37010 refactor: locales updated 2026-05-16 01:03:10 +03:00
Yurii 554342e7f5 feat: added a choice of averaging type for indoor and outdoor temperatures 2026-05-16 00:44:54 +03:00
12 changed files with 226 additions and 30 deletions
+1 -1
View File
@@ -225,7 +225,7 @@ protected:
uint8_t availableSensors = 0;
if (Sensors::existsConnectedSensorsByPurpose(Sensors::Purpose::INDOOR_TEMP)) {
auto value = Sensors::getMeanValueByPurpose(Sensors::Purpose::INDOOR_TEMP, Sensors::ValueType::PRIMARY);
auto value = Sensors::getMeanValueByPurpose(Sensors::Purpose::INDOOR_TEMP, Sensors::ValueType::PRIMARY, settings.heating.indoorTempAvgType);
if (value < lowTemp) {
lowTemp = value;
}
+47 -18
View File
@@ -77,6 +77,12 @@ public:
RSSI = 3
};
enum class AverageType : uint8_t {
MEAN = 0,
MINIMUM = 1,
MAXIMUM = 2
};
typedef struct {
bool enabled = false;
char name[33];
@@ -330,38 +336,61 @@ public:
return updated;
}
static float getMeanValueByPurpose(Purpose purpose, const ValueType valueType, bool onlyConnected = true) {
static float getMeanValueByPurpose(Purpose purpose, const ValueType valueType, const AverageType avgType = AverageType::MEAN, const bool onlyConnected = true, const float defaultValue = NAN) {
if (settings == nullptr || results == nullptr) {
return 0.0f;
return defaultValue;
}
uint8_t valueId = (uint8_t) valueType;
if (!isValidValueId(valueId)) {
return 0.0f;
return defaultValue;
}
float value = 0.0f;
uint8_t amount = 0;
for (uint8_t id = 0; id <= getMaxSensorId(); id++) {
auto& sSensor = settings[id];
auto& rSensor = results[id];
if (avgType == AverageType::MEAN) {
float sum = 0.0f;
for (uint8_t id = 0; id <= getMaxSensorId(); id++) {
auto& sSensor = settings[id];
auto& rSensor = results[id];
if (sSensor.purpose == purpose && (!onlyConnected || rSensor.connected)) {
value += rSensor.values[valueId];
amount++;
if (sSensor.purpose == purpose && (!onlyConnected || rSensor.connected)) {
sum += rSensor.values[valueId];
amount++;
}
}
value = amount == 1 ? sum : (sum / amount);
} else if (avgType == AverageType::MINIMUM) {
for (uint8_t id = 0; id <= getMaxSensorId(); id++) {
auto& sSensor = settings[id];
auto& rSensor = results[id];
if (sSensor.purpose == purpose && (!onlyConnected || rSensor.connected)) {
if (amount == 0 || rSensor.values[valueId] < value) {
value = rSensor.values[valueId];
amount++;
}
}
}
} else if (avgType == AverageType::MAXIMUM) {
for (uint8_t id = 0; id <= getMaxSensorId(); id++) {
auto& sSensor = settings[id];
auto& rSensor = results[id];
if (sSensor.purpose == purpose && (!onlyConnected || rSensor.connected)) {
if (amount == 0 || rSensor.values[valueId] > value) {
value = rSensor.values[valueId];
amount++;
}
}
}
}
if (!amount) {
return 0.0f;
} else if (amount == 1) {
return value;
} else {
return value / amount;
}
return amount > 0 ? value : defaultValue;
}
static bool existsConnectedSensorsByPurpose(Purpose purpose) {
+42 -6
View File
@@ -423,14 +423,50 @@ protected:
}
void updateMasterValues() {
vars.master.heating.outdoorTemp = Sensors::getMeanValueByPurpose(Sensors::Purpose::OUTDOOR_TEMP, Sensors::ValueType::PRIMARY);
vars.master.heating.indoorTemp = Sensors::getMeanValueByPurpose(Sensors::Purpose::INDOOR_TEMP, Sensors::ValueType::PRIMARY);
vars.master.heating.indoorTemp = Sensors::getMeanValueByPurpose(
Sensors::Purpose::INDOOR_TEMP,
Sensors::ValueType::PRIMARY,
settings.heating.indoorTempAvgType,
true,
0.0f
);
vars.master.heating.outdoorTemp = Sensors::getMeanValueByPurpose(
Sensors::Purpose::OUTDOOR_TEMP,
Sensors::ValueType::PRIMARY,
settings.heating.outdoorTempAvgType,
true,
0.0f
);
vars.master.heating.currentTemp = Sensors::getMeanValueByPurpose(Sensors::Purpose::HEATING_TEMP, Sensors::ValueType::PRIMARY);
vars.master.heating.returnTemp = Sensors::getMeanValueByPurpose(Sensors::Purpose::HEATING_RETURN_TEMP, Sensors::ValueType::PRIMARY);
vars.master.heating.currentTemp = Sensors::getMeanValueByPurpose(
Sensors::Purpose::HEATING_TEMP,
Sensors::ValueType::PRIMARY,
Sensors::AverageType::MEAN,
true,
0.0f
);
vars.master.heating.returnTemp = Sensors::getMeanValueByPurpose(
Sensors::Purpose::HEATING_RETURN_TEMP,
Sensors::ValueType::PRIMARY,
Sensors::AverageType::MEAN,
true,
0.0f
);
vars.master.dhw.currentTemp = Sensors::getMeanValueByPurpose(Sensors::Purpose::DHW_TEMP, Sensors::ValueType::PRIMARY);
vars.master.dhw.returnTemp = Sensors::getMeanValueByPurpose(Sensors::Purpose::DHW_RETURN_TEMP, Sensors::ValueType::PRIMARY);
vars.master.dhw.currentTemp = Sensors::getMeanValueByPurpose(
Sensors::Purpose::DHW_TEMP,
Sensors::ValueType::PRIMARY,
Sensors::AverageType::MEAN,
true,
0.0f
);
vars.master.dhw.returnTemp = Sensors::getMeanValueByPurpose(
Sensors::Purpose::DHW_RETURN_TEMP,
Sensors::ValueType::PRIMARY,
Sensors::AverageType::MEAN,
true,
0.0f
);
}
void makeDallasInstances() {
+2
View File
@@ -107,6 +107,8 @@ struct Settings {
uint8_t minTemp = DEFAULT_HEATING_MIN_TEMP;
uint8_t maxTemp = DEFAULT_HEATING_MAX_TEMP;
uint8_t maxModulation = 100;
Sensors::AverageType indoorTempAvgType = Sensors::AverageType::MEAN;
Sensors::AverageType outdoorTempAvgType = Sensors::AverageType::MEAN;
struct {
bool enabled = true;
+2
View File
@@ -115,6 +115,7 @@ const char S_IGNORE_DIAG_STATE[] PROGMEM = "ignoreDiagState";
const char S_IMMERGAS_FIX[] PROGMEM = "immergasFix";
const char S_ALWAYS_SEND_INDOOR_TEMP[] PROGMEM = "alwaysSendIndoorTemp";
const char S_INDOOR_TEMP[] PROGMEM = "indoorTemp";
const char S_INDOOR_TEMP_AVG_TYPE[] PROGMEM = "indoorTempAvgType";
const char S_INDOOR_TEMP_CONTROL[] PROGMEM = "indoorTempControl";
const char S_IN_GPIO[] PROGMEM = "inGpio";
const char S_INPUT[] PROGMEM = "input";
@@ -155,6 +156,7 @@ const char S_ON_LOSS_CONNECTION[] PROGMEM = "onLossConnection"
const char S_OPENTHERM[] PROGMEM = "opentherm";
const char S_OPTIONS[] PROGMEM = "options";
const char S_OUTDOOR_TEMP[] PROGMEM = "outdoorTemp";
const char S_OUTDOOR_TEMP_AVG_TYPE[] PROGMEM = "outdoorTempAvgType";
const char S_OUT_GPIO[] PROGMEM = "outGpio";
const char S_OUTPUT[] PROGMEM = "output";
const char S_OVERHEAT[] PROGMEM = "overheat";
+38
View File
@@ -498,6 +498,8 @@ void settingsToJson(const Settings& src, JsonVariant dst, bool safe = false) {
heating[FPSTR(S_MIN_TEMP)] = src.heating.minTemp;
heating[FPSTR(S_MAX_TEMP)] = src.heating.maxTemp;
heating[FPSTR(S_MAX_MODULATION)] = src.heating.maxModulation;
heating[FPSTR(S_INDOOR_TEMP_AVG_TYPE)] = static_cast<uint8_t>(src.heating.indoorTempAvgType);
heating[FPSTR(S_OUTDOOR_TEMP_AVG_TYPE)] = static_cast<uint8_t>(src.heating.outdoorTempAvgType);
auto heatingOverheatProtection = heating[FPSTR(S_OVERHEAT_PROTECTION)].to<JsonObject>();
heatingOverheatProtection[FPSTR(S_HIGH_TEMP)] = src.heating.overheatProtection.highTemp;
@@ -1393,6 +1395,42 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false
}
}
if (!src[FPSTR(S_HEATING)][FPSTR(S_INDOOR_TEMP_AVG_TYPE)].isNull()) {
uint8_t value = src[FPSTR(S_HEATING)][FPSTR(S_INDOOR_TEMP_AVG_TYPE)].as<uint8_t>();
switch (value) {
case static_cast<uint8_t>(Sensors::AverageType::MEAN):
case static_cast<uint8_t>(Sensors::AverageType::MINIMUM):
case static_cast<uint8_t>(Sensors::AverageType::MAXIMUM):
if (static_cast<uint8_t>(dst.heating.indoorTempAvgType) != value) {
dst.heating.indoorTempAvgType = static_cast<Sensors::AverageType>(value);
changed = true;
}
break;
default:
break;
}
}
if (!src[FPSTR(S_HEATING)][FPSTR(S_OUTDOOR_TEMP_AVG_TYPE)].isNull()) {
uint8_t value = src[FPSTR(S_HEATING)][FPSTR(S_OUTDOOR_TEMP_AVG_TYPE)].as<uint8_t>();
switch (value) {
case static_cast<uint8_t>(Sensors::AverageType::MEAN):
case static_cast<uint8_t>(Sensors::AverageType::MINIMUM):
case static_cast<uint8_t>(Sensors::AverageType::MAXIMUM):
if (static_cast<uint8_t>(dst.heating.outdoorTempAvgType) != value) {
dst.heating.outdoorTempAvgType = static_cast<Sensors::AverageType>(value);
changed = true;
}
break;
default:
break;
}
}
if (!src[FPSTR(S_HEATING)][FPSTR(S_OVERHEAT_PROTECTION)][FPSTR(S_HIGH_TEMP)].isNull()) {
unsigned char value = src[FPSTR(S_HEATING)][FPSTR(S_OVERHEAT_PROTECTION)][FPSTR(S_HIGH_TEMP)].as<unsigned char>();
+14 -1
View File
@@ -291,6 +291,11 @@
"min": "最低温度",
"max": "最高温度"
},
"avgType": {
"mean": "平均温度",
"min": "最低温度",
"max": "最高温度"
},
"maxModulation": "最大调制范围",
"ohProtection": {
"title": "超温保护",
@@ -352,7 +357,15 @@
"set0target": "设置空目标"
}
},
"turboFactor": "Turbo 模式系数"
"turboFactor": "Turbo 模式系数",
"indoorTempAvgType": {
"title": "室内温度平均类型",
"desc": "使用两个或更多室内温度传感器时可能有用(使用 «Equitherm» 和/或 «PID» 时)。"
},
"outdoorTempAvgType": {
"title": "室外温度平均类型",
"desc": "使用两个或更多室外温度传感器时可能有用(使用 «Equitherm» 时)。"
}
},
"emergency": {
"desc": "紧急模式会在以下情况自动激活(当PID或气候补偿无法计算热媒设定值时):<br />启用气候补偿但室外温度传感器断开连接;<br />启用PID或 OpenTherm 选项中启用<i>原生供暖控制</i>但室内温度传感器断开连接。<br /><b>注意:</b> 网络故障或MQTT 服务器连接故障时,类型为<i>通过MQTT/API手动控制</i>的传感器将显示为断开连接状态。",
+14 -1
View File
@@ -291,6 +291,11 @@
"min": "Minimum temperature",
"max": "Maximum temperature"
},
"avgType": {
"mean": "Mean temperature",
"min": "Minimum temperature",
"max": "Maximum temperature"
},
"maxModulation": "Max modulation level",
"ohProtection": {
"title": "Overheating protection",
@@ -352,7 +357,15 @@
"set0target": "Set null target"
}
},
"turboFactor": "Turbo mode coeff."
"turboFactor": "Turbo mode coeff.",
"indoorTempAvgType": {
"title": "Indoor temp. averaging type",
"desc": "May be useful when using two or more indoor temp. sensors (when using «Equitherm» and/or «PID»)."
},
"outdoorTempAvgType": {
"title": "Outdoor temp. averaging type",
"desc": "May be useful when using two or more outdoor temp. sensors (when using «Equitherm»)."
}
},
"emergency": {
"desc": "Emergency mode is activated automatically when «PID» or «Equitherm» cannot calculate the heat carrier setpoint:<br />- if «Equitherm» is enabled and the outdoor temperature sensor is disconnected;<br />- if «PID» or OT option <i>«Native heating control»</i> is enabled and the indoor temperature sensor is disconnected.<br /><b>Note:</b> On network fault or MQTT fault, sensors with <i>«Manual via MQTT/API»</i> type will be in DISCONNECTED state.",
+14 -1
View File
@@ -291,6 +291,11 @@
"min": "Temperatura minima",
"max": "Temperatura massima"
},
"avgType": {
"mean": "Temperatura media",
"min": "Temperatura minima",
"max": "Temperatura massima"
},
"maxModulation": "Max livello modulazione",
"ohProtection": {
"title": "Protezione contro il surriscaldamento",
@@ -352,7 +357,15 @@
"set0target": "Imposta target nullo"
}
},
"turboFactor": "Turbo mode coeff."
"turboFactor": "Turbo mode coeff.",
"indoorTempAvgType": {
"title": "Tipo di media temperatura interna",
"desc": "Utile con due o più sensori di temperatura interna (quando si usa «Equitherm» e/o «PID»)."
},
"outdoorTempAvgType": {
"title": "Tipo di media temperatura esterna",
"desc": "Utile con due o più sensori di temperatura esterna (quando si usa «Equitherm»)."
}
},
"emergency": {
"desc": "Il modo emergenza è attivato automaticamente quando «PID» o «Equitherm» non possono calcolare il setpoint:<br />- se «Equitherm» è attivato e il sensore della temperatura esternare è disconnesso;<br />- se «PID» o l'opzione OT <i>«Impostazioni riscaldamento native»</i> è attiva e il sensore di temperatura interno è disconnesso.<br /><b>Nota:</b> In mancanza di rete o MQTT, sensore di tipo <i>«Manuale via MQTT/API»</i> è in stato Disconnesso.",
+14 -1
View File
@@ -291,6 +291,11 @@
"min": "Minimumtemperatuur",
"max": "Maximumtemperatuur"
},
"avgType": {
"mean": "Gemiddelde temperatuur",
"min": "Minimum temperatuur",
"max": "Maximum temperatuur"
},
"maxModulation": "Max. modulatieniveau",
"ohProtection": {
"title": "Oververhittingsbeveiliging",
@@ -352,7 +357,15 @@
"set0target": "Stel null target in"
}
},
"turboFactor": "Turbomodus coëff."
"turboFactor": "Turbomodus coëff.",
"indoorTempAvgType": {
"title": "Binnentemperatuur gemiddelde type",
"desc": "Nuttig bij twee of meer binnentemperatuursensoren (bij gebruik van «Equitherm» en/of «PID»)."
},
"outdoorTempAvgType": {
"title": "Buitentemperatuur gemiddelde type",
"desc": "Nuttig bij twee of meer buitensensoren (bij gebruik van «Equitherm»)."
}
},
"emergency": {
"desc": "Noodmodus wordt automatisch geactiveerd wanneer «PID» of «Equitherm» het instelpunt van de warmtedrager niet kan berekenen:<br />- als «Equitherm» is ingeschakeld en de buitentemperatuursensor is losgekoppeld;<br />- als «PID» of OT-optie <i>«Natuurlijke verwarmingsregeling»</i> is ingeschakeld en de binnentemperatuursensor is losgekoppeld.<br /><b>Let op:</b> Bij een netwerk- of MQTT-storing krijgen sensoren van het type <i>«Handmatig via MQTT/API»</i> de status ONVERBONDEN.",
+14 -1
View File
@@ -291,6 +291,11 @@
"min": "Мин. температура",
"max": "Макс. температура"
},
"avgType": {
"mean": "Средняя температура",
"min": "Минимальная температура",
"max": "Максимальная температура"
},
"maxModulation": "Макс. уровень модуляции",
"ohProtection": {
"title": "Защита от перегрева",
@@ -352,7 +357,15 @@
"set0target": "Установить 0 в качестве целевой темп."
}
},
"turboFactor": "Коэфф. турбо режима"
"turboFactor": "Коэфф. турбо режима",
"indoorTempAvgType": {
"title": "Тип усреднения внутренней темп.",
"desc": "Полезно при использовании двух и более датчиков внутренней температуры (при использовании «Equitherm» и/или «PID»)."
},
"outdoorTempAvgType": {
"title": "Тип усреднения наружнной темп.",
"desc": "Полезно при использовании двух и более датчиков наружной температуры (при использовании «Equitherm»)."
}
},
"emergency": {
"desc": "Аварийный режим активируется автоматически, если «ПИД» или «ПЗА» не могут рассчитать уставку теплоносителя:<br />- если «ПЗА» включен и датчик наружной температуры отключен;<br />- если включен «ПИД» или OT опция <i>«Передать управление отоплением котлу»</i> и датчик внутренней температуры отключен.<br /><b>Примечание:</b> При сбое сети или MQTT датчики с типом <i>«Вручную через MQTT/API»</i> будут находиться в состоянии ОТКЛЮЧЕН.",
+24
View File
@@ -198,6 +198,28 @@
</label>
</div>
<div class="grid">
<label>
<span data-i18n>settings.heating.indoorTempAvgType.title</span>
<select name="heating[indoorTempAvgType]">
<option value="0" data-i18n>settings.avgType.mean</option>
<option value="1" data-i18n>settings.avgType.min</option>
<option value="2" data-i18n>settings.avgType.max</option>
</select>
<small data-i18n>settings.heating.indoorTempAvgType.desc</small>
</label>
<label>
<span data-i18n>settings.heating.outdoorTempAvgType.title</span>
<select name="heating[outdoorTempAvgType]">
<option value="0" data-i18n>settings.avgType.mean</option>
<option value="1" data-i18n>settings.avgType.min</option>
<option value="2" data-i18n>settings.avgType.max</option>
</select>
<small data-i18n>settings.heating.outdoorTempAvgType.desc</small>
</label>
</div>
<hr />
<details>
@@ -1171,6 +1193,8 @@
setSelectValue("[name='heating[hysteresis][action]']", data.heating.hysteresis.action);
setInputValue("[name='heating[turboFactor]']", data.heating.turboFactor);
setInputValue("[name='heating[maxModulation]']", data.heating.maxModulation);
setSelectValue("[name='heating[indoorTempAvgType]']", data.heating.indoorTempAvgType);
setSelectValue("[name='heating[outdoorTempAvgType]']", data.heating.outdoorTempAvgType);
setInputValue("[name='heating[overheatProtection][highTemp]']", data.heating.overheatProtection.highTemp, {
"min": 0,
"max": data.system.unitSystem == 0 ? 100 : 212