10 Commits

14 changed files with 382 additions and 167 deletions

View File

@@ -210,6 +210,7 @@ build_flags =
${esp32_defaults.build_flags} ${esp32_defaults.build_flags}
-D ARDUINO_USB_MODE=0 -D ARDUINO_USB_MODE=0
-D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_CDC_ON_BOOT=1
-D CONFIG_BT_NIMBLE_EXT_ADV=1
-D USE_BLE=1 -D USE_BLE=1
-D DEFAULT_OT_IN_GPIO=35 -D DEFAULT_OT_IN_GPIO=35
-D DEFAULT_OT_OUT_GPIO=36 -D DEFAULT_OT_OUT_GPIO=36
@@ -233,6 +234,7 @@ build_unflags =
build_type = ${esp32_defaults.build_type} build_type = ${esp32_defaults.build_type}
build_flags = build_flags =
${esp32_defaults.build_flags} ${esp32_defaults.build_flags}
-D CONFIG_BT_NIMBLE_EXT_ADV=1
-D USE_BLE=1 -D USE_BLE=1
-D DEFAULT_OT_IN_GPIO=8 -D DEFAULT_OT_IN_GPIO=8
-D DEFAULT_OT_OUT_GPIO=10 -D DEFAULT_OT_OUT_GPIO=10
@@ -316,6 +318,7 @@ build_unflags =
build_type = ${esp32_defaults.build_type} build_type = ${esp32_defaults.build_type}
build_flags = build_flags =
${esp32_defaults.build_flags} ${esp32_defaults.build_flags}
-D CONFIG_BT_NIMBLE_EXT_ADV=1
-D USE_BLE=1 -D USE_BLE=1
-D DEFAULT_OT_IN_GPIO=3 -D DEFAULT_OT_IN_GPIO=3
-D DEFAULT_OT_OUT_GPIO=1 -D DEFAULT_OT_OUT_GPIO=1

View File

@@ -120,7 +120,6 @@ protected:
#endif #endif
// client settings // client settings
this->client->setKeepAliveInterval(15000);
this->client->setTxPayloadSize(256); this->client->setTxPayloadSize(256);
#ifdef ARDUINO_ARCH_ESP8266 #ifdef ARDUINO_ARCH_ESP8266
this->client->setConnectionTimeout(1000); this->client->setConnectionTimeout(1000);
@@ -199,6 +198,7 @@ protected:
this->haHelper->updateCachedTopics(); this->haHelper->updateCachedTopics();
this->client->stop(); this->client->stop();
this->client->setKeepAliveInterval(settings.mqtt.interval * 10000);
this->client->setId(networkSettings.hostname); this->client->setId(networkSettings.hostname);
this->client->setUsernamePassword(settings.mqtt.user, settings.mqtt.password); this->client->setUsernamePassword(settings.mqtt.user, settings.mqtt.password);
this->client->beginWill(this->haHelper->getDeviceTopic(F("status")).c_str(), 7, true, 1); this->client->beginWill(this->haHelper->getDeviceTopic(F("status")).c_str(), 7, true, 1);

View File

@@ -1062,7 +1062,10 @@ protected:
); );
} else { } else {
Log.swarningln(FPSTR(L_OT_HEATING), F("Failed set max heating temp")); Log.swarningln(
FPSTR(L_OT_HEATING), F("Failed set max heating temp: %.2f (converted: %.2f)"),
vars.master.heating.setpointTemp, convertedTemp
);
} }
} }

View File

@@ -196,6 +196,7 @@ protected:
//if (vars.parameters.heatingEnabled) { //if (vars.parameters.heatingEnabled) {
if (settings.heating.enabled && this->indoorSensorsConnected) { if (settings.heating.enabled && this->indoorSensorsConnected) {
pidRegulator.Kp = settings.heating.turbo ? 0.0f : settings.pid.p_factor; pidRegulator.Kp = settings.heating.turbo ? 0.0f : settings.pid.p_factor;
pidRegulator.Ki = settings.pid.i_factor;
pidRegulator.Kd = settings.pid.d_factor; pidRegulator.Kd = settings.pid.d_factor;
pidRegulator.setLimits(settings.pid.minTemp, settings.pid.maxTemp); pidRegulator.setLimits(settings.pid.minTemp, settings.pid.maxTemp);
@@ -203,12 +204,22 @@ protected:
pidRegulator.input = vars.master.heating.indoorTemp; pidRegulator.input = vars.master.heating.indoorTemp;
pidRegulator.setpoint = settings.heating.target; pidRegulator.setpoint = settings.heating.target;
if (fabsf(pidRegulator.Ki - settings.pid.i_factor) >= 0.0001f) { /*if (fabsf(pidRegulator.Ki - settings.pid.i_factor) >= 0.0001f) {
pidRegulator.Ki = settings.pid.i_factor; pidRegulator.Ki = settings.pid.i_factor;
pidRegulator.integral = 0.0f; pidRegulator.integral = 0.0f;
pidRegulator.getResultNow(); pidRegulator.getResultNow();
Log.sinfoln(FPSTR(L_REGULATOR_PID), F("Integral sum has been reset")); Log.sinfoln(FPSTR(L_REGULATOR_PID), F("Integral sum has been reset"));
}*/
float error = pidRegulator.setpoint - pidRegulator.input;
bool hasDeadband = (error > -(settings.pid.deadband.thresholdHigh))
&& (error < settings.pid.deadband.thresholdLow);
if (hasDeadband) {
pidRegulator.Kp *= settings.pid.deadband.p_multiplier;
pidRegulator.Ki *= settings.pid.deadband.i_multiplier;
pidRegulator.Kd *= settings.pid.deadband.d_multiplier;
} }
float pidResult = pidRegulator.getResultTimer(); float pidResult = pidRegulator.getResultTimer();

View File

@@ -445,6 +445,14 @@ protected:
} }
pClient = NimBLEDevice::createClient(); pClient = NimBLEDevice::createClient();
/**
* Set initial connection parameters:
* These settings are safe for 3 clients to connect reliably, can go faster if you have less
* connections. Timeout should be a multiple of the interval, minimum is 100ms.
* Min interval: 12 * 1.25ms = 15, Max interval: 12 * 1.25ms = 15, 0 latency, 1000 * 10ms = 10000ms timeout
*/
pClient->setConnectionParams(12, 12, 0, 1000);
pClient->setConnectTimeout(5000); pClient->setConnectTimeout(5000);
pClient->setSelfDelete(false, true); pClient->setSelfDelete(false, true);
} }
@@ -495,55 +503,59 @@ protected:
NimBLEUUID charUuid((uint16_t) 0x2A6E); NimBLEUUID charUuid((uint16_t) 0x2A6E);
pChar = pService->getCharacteristic(charUuid); pChar = pService->getCharacteristic(charUuid);
if (pChar && pChar->canNotify()) { if (pChar && (pChar->canNotify() || pChar->canIndicate())) {
Log.straceln( Log.straceln(
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': found temp char (%s) in env service on device %s"), FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': found temp char (%s) in env service on device %s"),
sensorId, sSensor.name, charUuid.toString().c_str(), address.toString().c_str() sensorId, sSensor.name, charUuid.toString().c_str(), address.toString().c_str()
); );
tempNotifyCreated = pChar->subscribe(true, [sensorId](NimBLERemoteCharacteristic* pChar, uint8_t* pData, size_t length, bool isNotify) { pChar->unsubscribe();
if (pChar == nullptr) { tempNotifyCreated = pChar->subscribe(
return; pChar->canNotify(),
} [sensorId](NimBLERemoteCharacteristic* pChar, uint8_t* pData, size_t length, bool isNotify) {
if (pChar == nullptr) {
return;
}
const NimBLERemoteService* pService = pChar->getRemoteService(); const NimBLERemoteService* pService = pChar->getRemoteService();
if (pService == nullptr) { if (pService == nullptr) {
return; return;
} }
NimBLEClient* pClient = pService->getClient(); NimBLEClient* pClient = pService->getClient();
if (pClient == nullptr) { if (pClient == nullptr) {
return; return;
} }
auto& sSensor = Sensors::settings[sensorId]; auto& sSensor = Sensors::settings[sensorId];
if (length != 2) { if (length != 2) {
Log.swarningln( Log.swarningln(
FPSTR(L_SENSORS_BLE),
F("Sensor #%hhu '%s': invalid notification data at temp char (%s) on device %s"),
sensorId,
sSensor.name,
pChar->getUUID().toString().c_str(),
pClient->getPeerAddress().toString().c_str()
);
return;
}
float rawTemp = (pChar->getValue<int16_t>() * 0.01f);
Log.straceln(
FPSTR(L_SENSORS_BLE), FPSTR(L_SENSORS_BLE),
F("Sensor #%hhu '%s': invalid notification data at temp char (%s) on device %s"), F("Sensor #%hhu '%s': received temp: %.2f"),
sensorId, sensorId, sSensor.name, rawTemp
sSensor.name,
pChar->getUUID().toString().c_str(),
pClient->getPeerAddress().toString().c_str()
); );
return; // set temp
Sensors::setValueById(sensorId, rawTemp, Sensors::ValueType::TEMPERATURE, true, true);
// update rssi
Sensors::setValueById(sensorId, pClient->getRssi(), Sensors::ValueType::RSSI, false, false);
} }
);
float rawTemp = (pChar->getValue<int16_t>() * 0.01f);
Log.straceln(
FPSTR(L_SENSORS_BLE),
F("Sensor #%hhu '%s': received temp: %.2f"),
sensorId, sSensor.name, rawTemp
);
// set temp
Sensors::setValueById(sensorId, rawTemp, Sensors::ValueType::TEMPERATURE, true, true);
// update rssi
Sensors::setValueById(sensorId, pClient->getRssi(), Sensors::ValueType::RSSI, false, false);
});
if (tempNotifyCreated) { if (tempNotifyCreated) {
Log.straceln( Log.straceln(
@@ -568,55 +580,59 @@ protected:
NimBLEUUID charUuid((uint16_t) 0x2A1F); NimBLEUUID charUuid((uint16_t) 0x2A1F);
pChar = pService->getCharacteristic(charUuid); pChar = pService->getCharacteristic(charUuid);
if (pChar && pChar->canNotify()) { if (pChar && (pChar->canNotify() || pChar->canIndicate())) {
Log.straceln( Log.straceln(
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': found temp char (%s) in env service on device %s"), FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': found temp char (%s) in env service on device %s"),
sensorId, sSensor.name, charUuid.toString().c_str(), address.toString().c_str() sensorId, sSensor.name, charUuid.toString().c_str(), address.toString().c_str()
); );
tempNotifyCreated = pChar->subscribe(true, [sensorId](NimBLERemoteCharacteristic* pChar, uint8_t* pData, size_t length, bool isNotify) { pChar->unsubscribe();
if (pChar == nullptr) { tempNotifyCreated = pChar->subscribe(
return; pChar->canNotify(),
} [sensorId](NimBLERemoteCharacteristic* pChar, uint8_t* pData, size_t length, bool isNotify) {
if (pChar == nullptr) {
return;
}
const NimBLERemoteService* pService = pChar->getRemoteService(); const NimBLERemoteService* pService = pChar->getRemoteService();
if (pService == nullptr) { if (pService == nullptr) {
return; return;
} }
NimBLEClient* pClient = pService->getClient(); NimBLEClient* pClient = pService->getClient();
if (pClient == nullptr) { if (pClient == nullptr) {
return; return;
} }
auto& sSensor = Sensors::settings[sensorId]; auto& sSensor = Sensors::settings[sensorId];
if (length != 2) { if (length != 2) {
Log.swarningln( Log.swarningln(
FPSTR(L_SENSORS_BLE),
F("Sensor #%hhu '%s': invalid notification data at temp char (%s) on device %s"),
sensorId,
sSensor.name,
pChar->getUUID().toString().c_str(),
pClient->getPeerAddress().toString().c_str()
);
return;
}
float rawTemp = (pChar->getValue<int16_t>() * 0.1f);
Log.straceln(
FPSTR(L_SENSORS_BLE), FPSTR(L_SENSORS_BLE),
F("Sensor #%hhu '%s': invalid notification data at temp char (%s) on device %s"), F("Sensor #%hhu '%s': received temp: %.2f"),
sensorId, sensorId, sSensor.name, rawTemp
sSensor.name,
pChar->getUUID().toString().c_str(),
pClient->getPeerAddress().toString().c_str()
); );
return; // set temp
Sensors::setValueById(sensorId, rawTemp, Sensors::ValueType::TEMPERATURE, true, true);
// update rssi
Sensors::setValueById(sensorId, pClient->getRssi(), Sensors::ValueType::RSSI, false, false);
} }
);
float rawTemp = (pChar->getValue<int16_t>() * 0.1f);
Log.straceln(
FPSTR(L_SENSORS_BLE),
F("Sensor #%hhu '%s': received temp: %.2f"),
sensorId, sSensor.name, rawTemp
);
// set temp
Sensors::setValueById(sensorId, rawTemp, Sensors::ValueType::TEMPERATURE, true, true);
// update rssi
Sensors::setValueById(sensorId, pClient->getRssi(), Sensors::ValueType::RSSI, false, false);
});
if (tempNotifyCreated) { if (tempNotifyCreated) {
Log.straceln( Log.straceln(
@@ -653,55 +669,59 @@ protected:
NimBLEUUID charUuid((uint16_t) 0x2A6F); NimBLEUUID charUuid((uint16_t) 0x2A6F);
pChar = pService->getCharacteristic(charUuid); pChar = pService->getCharacteristic(charUuid);
if (pChar && pChar->canNotify()) { if (pChar && (pChar->canNotify() || pChar->canIndicate())) {
Log.straceln( Log.straceln(
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': found humidity char (%s) in env service on device %s"), FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': found humidity char (%s) in env service on device %s"),
sensorId, sSensor.name, charUuid.toString().c_str(), address.toString().c_str() sensorId, sSensor.name, charUuid.toString().c_str(), address.toString().c_str()
); );
humidityNotifyCreated = pChar->subscribe(true, [sensorId](NimBLERemoteCharacteristic* pChar, uint8_t* pData, size_t length, bool isNotify) { pChar->unsubscribe();
if (pChar == nullptr) { humidityNotifyCreated = pChar->subscribe(
return; pChar->canNotify(),
} [sensorId](NimBLERemoteCharacteristic* pChar, uint8_t* pData, size_t length, bool isNotify) {
if (pChar == nullptr) {
return;
}
const NimBLERemoteService* pService = pChar->getRemoteService(); const NimBLERemoteService* pService = pChar->getRemoteService();
if (pService == nullptr) { if (pService == nullptr) {
return; return;
} }
NimBLEClient* pClient = pService->getClient(); NimBLEClient* pClient = pService->getClient();
if (pClient == nullptr) { if (pClient == nullptr) {
return; return;
} }
auto& sSensor = Sensors::settings[sensorId]; auto& sSensor = Sensors::settings[sensorId];
if (length != 2) { if (length != 2) {
Log.swarningln( Log.swarningln(
FPSTR(L_SENSORS_BLE),
F("Sensor #%hhu '%s': invalid notification data at humidity char (%s) on device %s"),
sensorId,
sSensor.name,
pChar->getUUID().toString().c_str(),
pClient->getPeerAddress().toString().c_str()
);
return;
}
float rawHumidity = (pChar->getValue<uint16_t>() * 0.01f);
Log.straceln(
FPSTR(L_SENSORS_BLE), FPSTR(L_SENSORS_BLE),
F("Sensor #%hhu '%s': invalid notification data at humidity char (%s) on device %s"), F("Sensor #%hhu '%s': received humidity: %.2f"),
sensorId, sensorId, sSensor.name, rawHumidity
sSensor.name,
pChar->getUUID().toString().c_str(),
pClient->getPeerAddress().toString().c_str()
); );
return; // set humidity
Sensors::setValueById(sensorId, rawHumidity, Sensors::ValueType::HUMIDITY, true, true);
// update rssi
Sensors::setValueById(sensorId, pClient->getRssi(), Sensors::ValueType::RSSI, false, false);
} }
);
float rawHumidity = (pChar->getValue<uint16_t>() * 0.01f);
Log.straceln(
FPSTR(L_SENSORS_BLE),
F("Sensor #%hhu '%s': received humidity: %.2f"),
sensorId, sSensor.name, rawHumidity
);
// set humidity
Sensors::setValueById(sensorId, rawHumidity, Sensors::ValueType::HUMIDITY, true, true);
// update rssi
Sensors::setValueById(sensorId, pClient->getRssi(), Sensors::ValueType::RSSI, false, false);
});
if (humidityNotifyCreated) { if (humidityNotifyCreated) {
Log.straceln( Log.straceln(
@@ -752,55 +772,59 @@ protected:
NimBLEUUID charUuid((uint16_t) 0x2A19); NimBLEUUID charUuid((uint16_t) 0x2A19);
pChar = pService->getCharacteristic(charUuid); pChar = pService->getCharacteristic(charUuid);
if (pChar && pChar->canNotify()) { if (pChar && (pChar->canNotify() || pChar->canIndicate())) {
Log.straceln( Log.straceln(
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': found battery char (%s) in battery service on device %s"), FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': found battery char (%s) in battery service on device %s"),
sensorId, sSensor.name, charUuid.toString().c_str(), address.toString().c_str() sensorId, sSensor.name, charUuid.toString().c_str(), address.toString().c_str()
); );
batteryNotifyCreated = pChar->subscribe(true, [sensorId](NimBLERemoteCharacteristic* pChar, uint8_t* pData, size_t length, bool isNotify) { pChar->unsubscribe();
if (pChar == nullptr) { batteryNotifyCreated = pChar->subscribe(
return; pChar->canNotify(),
} [sensorId](NimBLERemoteCharacteristic* pChar, uint8_t* pData, size_t length, bool isNotify) {
if (pChar == nullptr) {
return;
}
const NimBLERemoteService* pService = pChar->getRemoteService(); const NimBLERemoteService* pService = pChar->getRemoteService();
if (pService == nullptr) { if (pService == nullptr) {
return; return;
} }
NimBLEClient* pClient = pService->getClient(); NimBLEClient* pClient = pService->getClient();
if (pClient == nullptr) { if (pClient == nullptr) {
return; return;
} }
auto& sSensor = Sensors::settings[sensorId]; auto& sSensor = Sensors::settings[sensorId];
if (length != 1) { if (length != 1) {
Log.swarningln( Log.swarningln(
FPSTR(L_SENSORS_BLE),
F("Sensor #%hhu '%s': invalid notification data at battery char (%s) on device %s"),
sensorId,
sSensor.name,
pChar->getUUID().toString().c_str(),
pClient->getPeerAddress().toString().c_str()
);
return;
}
auto rawBattery = pChar->getValue<uint8_t>();
Log.straceln(
FPSTR(L_SENSORS_BLE), FPSTR(L_SENSORS_BLE),
F("Sensor #%hhu '%s': invalid notification data at battery char (%s) on device %s"), F("Sensor #%hhu '%s': received battery: %hhu"),
sensorId, sensorId, sSensor.name, rawBattery
sSensor.name,
pChar->getUUID().toString().c_str(),
pClient->getPeerAddress().toString().c_str()
); );
return; // set battery
Sensors::setValueById(sensorId, rawBattery, Sensors::ValueType::BATTERY, true, true);
// update rssi
Sensors::setValueById(sensorId, pClient->getRssi(), Sensors::ValueType::RSSI, false, false);
} }
);
auto rawBattery = pChar->getValue<uint8_t>();
Log.straceln(
FPSTR(L_SENSORS_BLE),
F("Sensor #%hhu '%s': received battery: %.2f"),
sensorId, sSensor.name, rawBattery
);
// set battery
Sensors::setValueById(sensorId, rawBattery, Sensors::ValueType::BATTERY, true, true);
// update rssi
Sensors::setValueById(sensorId, pClient->getRssi(), Sensors::ValueType::RSSI, false, false);
});
if (batteryNotifyCreated) { if (batteryNotifyCreated) {
Log.straceln( Log.straceln(

View File

@@ -110,11 +110,20 @@ struct Settings {
struct { struct {
bool enabled = false; bool enabled = false;
float p_factor = 2.0f; float p_factor = 2.0f;
float i_factor = 0.0055f; float i_factor = 0.002f;
float d_factor = 0.0f; float d_factor = 0.0f;
unsigned short dt = 180; unsigned short dt = 300;
short minTemp = 0; short minTemp = 0;
short maxTemp = DEFAULT_HEATING_MAX_TEMP; short maxTemp = DEFAULT_HEATING_MAX_TEMP;
struct {
bool enabled = true;
float p_multiplier = 1.0f;
float i_multiplier = 0.05f;
float d_multiplier = 1.0f;
float thresholdHigh = 0.5f;
float thresholdLow = 1.0f;
} deadband;
} pid; } pid;
struct { struct {

View File

@@ -63,6 +63,7 @@ const char S_CRASH[] PROGMEM = "crash";
const char S_CURRENT_TEMP[] PROGMEM = "currentTemp"; const char S_CURRENT_TEMP[] PROGMEM = "currentTemp";
const char S_DATA[] PROGMEM = "data"; const char S_DATA[] PROGMEM = "data";
const char S_DATE[] PROGMEM = "date"; const char S_DATE[] PROGMEM = "date";
const char S_DEADBAND[] PROGMEM = "deadband";
const char S_DHW[] PROGMEM = "dhw"; const char S_DHW[] PROGMEM = "dhw";
const char S_DHW_BLOCKING[] PROGMEM = "dhwBlocking"; const char S_DHW_BLOCKING[] PROGMEM = "dhwBlocking";
const char S_DHW_SUPPORT[] PROGMEM = "dhwSupport"; const char S_DHW_SUPPORT[] PROGMEM = "dhwSupport";
@@ -71,6 +72,7 @@ const char S_DIAG[] PROGMEM = "diag";
const char S_DNS[] PROGMEM = "dns"; const char S_DNS[] PROGMEM = "dns";
const char S_DT[] PROGMEM = "dt"; const char S_DT[] PROGMEM = "dt";
const char S_D_FACTOR[] PROGMEM = "d_factor"; const char S_D_FACTOR[] PROGMEM = "d_factor";
const char S_D_MULTIPLIER[] PROGMEM = "d_multiplier";
const char S_EMERGENCY[] PROGMEM = "emergency"; const char S_EMERGENCY[] PROGMEM = "emergency";
const char S_ENABLED[] PROGMEM = "enabled"; const char S_ENABLED[] PROGMEM = "enabled";
const char S_ENV[] PROGMEM = "env"; const char S_ENV[] PROGMEM = "env";
@@ -108,6 +110,7 @@ const char S_INTERVAL[] PROGMEM = "interval";
const char S_INVERT_STATE[] PROGMEM = "invertState"; const char S_INVERT_STATE[] PROGMEM = "invertState";
const char S_IP[] PROGMEM = "ip"; const char S_IP[] PROGMEM = "ip";
const char S_I_FACTOR[] PROGMEM = "i_factor"; 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_K_FACTOR[] PROGMEM = "k_factor";
const char S_LOGIN[] PROGMEM = "login"; const char S_LOGIN[] PROGMEM = "login";
const char S_LOG_LEVEL[] PROGMEM = "logLevel"; const char S_LOG_LEVEL[] PROGMEM = "logLevel";
@@ -152,6 +155,7 @@ const char S_PREFIX[] PROGMEM = "prefix";
const char S_PROTOCOL_VERSION[] PROGMEM = "protocolVersion"; const char S_PROTOCOL_VERSION[] PROGMEM = "protocolVersion";
const char S_PURPOSE[] PROGMEM = "purpose"; const char S_PURPOSE[] PROGMEM = "purpose";
const char S_P_FACTOR[] PROGMEM = "p_factor"; const char S_P_FACTOR[] PROGMEM = "p_factor";
const char S_P_MULTIPLIER[] PROGMEM = "p_multiplier";
const char S_REAL_SIZE[] PROGMEM = "realSize"; const char S_REAL_SIZE[] PROGMEM = "realSize";
const char S_REASON[] PROGMEM = "reason"; const char S_REASON[] PROGMEM = "reason";
const char S_RESET_DIAGNOSTIC[] PROGMEM = "resetDiagnostic"; const char S_RESET_DIAGNOSTIC[] PROGMEM = "resetDiagnostic";
@@ -183,6 +187,8 @@ const char S_TARGET[] PROGMEM = "target";
const char S_TARGET_TEMP[] PROGMEM = "targetTemp"; const char S_TARGET_TEMP[] PROGMEM = "targetTemp";
const char S_TELNET[] PROGMEM = "telnet"; const char S_TELNET[] PROGMEM = "telnet";
const char S_TEMPERATURE[] PROGMEM = "temperature"; const char S_TEMPERATURE[] PROGMEM = "temperature";
const char S_THRESHOLD_HIGH[] PROGMEM = "thresholdHigh";
const char S_THRESHOLD_LOW[] PROGMEM = "thresholdLow";
const char S_THRESHOLD_TIME[] PROGMEM = "thresholdTime"; const char S_THRESHOLD_TIME[] PROGMEM = "thresholdTime";
const char S_TOTAL[] PROGMEM = "total"; const char S_TOTAL[] PROGMEM = "total";
const char S_TRESHOLD_TIME[] PROGMEM = "tresholdTime"; const char S_TRESHOLD_TIME[] PROGMEM = "tresholdTime";

View File

@@ -438,6 +438,14 @@ void settingsToJson(const Settings& src, JsonVariant dst, bool safe = false) {
pid[FPSTR(S_MIN_TEMP)] = src.pid.minTemp; pid[FPSTR(S_MIN_TEMP)] = src.pid.minTemp;
pid[FPSTR(S_MAX_TEMP)] = src.pid.maxTemp; pid[FPSTR(S_MAX_TEMP)] = src.pid.maxTemp;
auto pidDeadband = pid[FPSTR(S_DEADBAND)].to<JsonObject>();
pidDeadband[FPSTR(S_ENABLED)] = src.pid.deadband.enabled;
pidDeadband[FPSTR(S_P_MULTIPLIER)] = src.pid.deadband.p_multiplier;
pidDeadband[FPSTR(S_I_MULTIPLIER)] = src.pid.deadband.i_multiplier;
pidDeadband[FPSTR(S_D_MULTIPLIER)] = src.pid.deadband.d_multiplier;
pidDeadband[FPSTR(S_THRESHOLD_HIGH)] = src.pid.deadband.thresholdHigh;
pidDeadband[FPSTR(S_THRESHOLD_LOW)] = src.pid.deadband.thresholdLow;
if (!safe) { if (!safe) {
auto externalPump = dst[FPSTR(S_EXTERNAL_PUMP)].to<JsonObject>(); auto externalPump = dst[FPSTR(S_EXTERNAL_PUMP)].to<JsonObject>();
externalPump[FPSTR(S_USE)] = src.externalPump.use; externalPump[FPSTR(S_USE)] = src.externalPump.use;
@@ -1075,6 +1083,60 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false
changed = true; changed = true;
} }
if (src[FPSTR(S_PID)][FPSTR(S_DEADBAND)][FPSTR(S_ENABLED)].is<bool>()) {
bool value = src[FPSTR(S_PID)][FPSTR(S_DEADBAND)][FPSTR(S_ENABLED)].as<bool>();
if (value != dst.pid.deadband.enabled) {
dst.pid.deadband.enabled = value;
changed = true;
}
}
if (!src[FPSTR(S_PID)][FPSTR(S_DEADBAND)][FPSTR(S_P_MULTIPLIER)].isNull()) {
float value = src[FPSTR(S_PID)][FPSTR(S_DEADBAND)][FPSTR(S_P_MULTIPLIER)].as<float>();
if (value >= 0 && value <= 1 && fabsf(value - dst.pid.deadband.p_multiplier) > 0.0001f) {
dst.pid.deadband.p_multiplier = roundf(value, 3);
changed = true;
}
}
if (!src[FPSTR(S_PID)][FPSTR(S_DEADBAND)][FPSTR(S_I_MULTIPLIER)].isNull()) {
float value = src[FPSTR(S_PID)][FPSTR(S_DEADBAND)][FPSTR(S_I_MULTIPLIER)].as<float>();
if (value >= 0 && value <= 1 && fabsf(value - dst.pid.deadband.i_multiplier) > 0.0001f) {
dst.pid.deadband.i_multiplier = roundf(value, 3);
changed = true;
}
}
if (!src[FPSTR(S_PID)][FPSTR(S_DEADBAND)][FPSTR(S_D_MULTIPLIER)].isNull()) {
float value = src[FPSTR(S_PID)][FPSTR(S_DEADBAND)][FPSTR(S_D_MULTIPLIER)].as<float>();
if (value >= 0 && value <= 1 && fabsf(value - dst.pid.deadband.d_multiplier) > 0.0001f) {
dst.pid.deadband.d_multiplier = roundf(value, 3);
changed = true;
}
}
if (!src[FPSTR(S_PID)][FPSTR(S_DEADBAND)][FPSTR(S_THRESHOLD_HIGH)].isNull()) {
float value = src[FPSTR(S_PID)][FPSTR(S_DEADBAND)][FPSTR(S_THRESHOLD_HIGH)].as<float>();
if (value >= 0.0f && value <= 5.0f && fabsf(value - dst.pid.deadband.thresholdHigh) > 0.0001f) {
dst.pid.deadband.thresholdHigh = roundf(value, 2);
changed = true;
}
}
if (!src[FPSTR(S_PID)][FPSTR(S_DEADBAND)][FPSTR(S_THRESHOLD_LOW)].isNull()) {
float value = src[FPSTR(S_PID)][FPSTR(S_DEADBAND)][FPSTR(S_THRESHOLD_LOW)].as<float>();
if (value >= 0.0f && value <= 5.0f && fabsf(value - dst.pid.deadband.thresholdLow) > 0.0001f) {
dst.pid.deadband.thresholdLow = roundf(value, 2);
changed = true;
}
}
// heating // heating
if (src[FPSTR(S_HEATING)][FPSTR(S_ENABLED)].is<bool>()) { if (src[FPSTR(S_HEATING)][FPSTR(S_ENABLED)].is<bool>()) {

View File

@@ -347,7 +347,19 @@
"i": "I factor", "i": "I factor",
"d": "D factor", "d": "D factor",
"dt": "DT <small>in seconds</small>", "dt": "DT <small>in seconds</small>",
"noteMinMaxTemp": "<b>Important:</b> When using «Equitherm» and «PID» at the same time, the min and max temperatures limit the influence on the «Equitherm» result temperature.<br />Thus, if the min temperature is set to -15 and the max temperature is set to 15, then the final heat carrier setpoint will be from <code>equitherm_result - 15</code> to <code>equitherm_result + 15</code>." "limits": {
"title": "Limits",
"note": "<b>Important:</b> When using «Equitherm» and «PID» at the same time, the min and max temperatures limit the influence on the «Equitherm» result temperature.<br />Thus, if the min temperature is set to -15 and the max temperature is set to 15, then the final heat carrier setpoint will be from <code>equitherm_result - 15</code> to <code>equitherm_result + 15</code>."
},
"deadband": {
"title": "Deadband",
"note": "Deadband is a range around the target temperature where PID regulation becomes less active. Within this range, the algorithm can reduce intensity or pause adjustments to avoid overreacting to small fluctuations.<br /><br />For instance, with a target temperature of 22°, a lower threshold of 1.0, and an upper threshold of 0.5, the deadband operates between 21° and 22.5°. If the I coefficient is 0.0005 and the I multiplier is 0.05, then within the deadband, the I coefficient becomes: <code>0.0005 * 0.05 = 0.000025</code>",
"p_multiplier": "Multiplier for P factor",
"i_multiplier": "Multiplier for I factor",
"d_multiplier": "Multiplier for D factor",
"thresholdHigh": "Threshold high",
"thresholdLow": "Threshold low"
}
}, },
"ot": { "ot": {

View File

@@ -347,7 +347,19 @@
"i": "Fattore I", "i": "Fattore I",
"d": "Fattore D", "d": "Fattore D",
"dt": "DT <small>in secondi</small>", "dt": "DT <small>in secondi</small>",
"noteMinMaxTemp": "<b>Importante:</b> Quando usi «Equitherm» e «PID» allo stesso tempo, i limiti della temperatura min e max influenzano il risultato della temperatura «Equitherm».<br />Thus, se la temperatura minima è impostata a -15 e la massima a 15, il riscaldamento finale sarà impostato fra <code>equitherm_result - 15</code> a <code>equitherm_result + 15</code>." "limits": {
"title": "Limiti",
"note": "<b>Importante:</b> Quando usi «Equitherm» e «PID» allo stesso tempo, i limiti della temperatura min e max influenzano il risultato della temperatura «Equitherm».<br />Thus, se la temperatura minima è impostata a -15 e la massima a 15, il riscaldamento finale sarà impostato fra <code>equitherm_result - 15</code> a <code>equitherm_result + 15</code>."
},
"deadband": {
"title": "Zona morta (Deadband)",
"note": "La zona morta è un intervallo intorno alla temperatura target in cui la regolazione PID diventa meno attiva. In questo intervallo, l'algoritmo può ridurre l'intensità o interrompere gli aggiustamenti per evitare di reagire eccessivamente a piccole fluttuazioni.<br /><br />Ad esempio, con una temperatura target di 22°, una soglia inferiore di 1.0 e una soglia superiore di 0.5, la zona morta opera tra 21° e 22.5°. Se il coefficiente I è 0.0005 e il moltiplicatore I è 0.05, allora nella zona morta, il coefficiente I diventa: <code>0.0005 * 0.05 = 0.000025</code>",
"p_multiplier": "Moltiplicatore P",
"i_multiplier": "Moltiplicatore I",
"d_multiplier": "Moltiplicatore D",
"thresholdHigh": "Soglia superiore",
"thresholdLow": "Soglia inferiore"
}
}, },
"ot": { "ot": {

View File

@@ -347,7 +347,19 @@
"i": "Коэффициент I", "i": "Коэффициент I",
"d": "Коэффициент D", "d": "Коэффициент D",
"dt": "DT <small>(сек)</small>", "dt": "DT <small>(сек)</small>",
"noteMinMaxTemp": "<b>Важно:</b> При использовании «ПЗА» и «ПИД» одновременно, мин. и макс. температура ограничивает влияние на расчётную температуру «ПЗА».<br />Таким образом, если мин. температура задана как -15, а макс. как 15, то конечная температура теплоносителя будет от <code>equitherm_result - 15</code> до <code>equitherm_result + 15</code>." "limits": {
"title": "Лимиты",
"note": "<b>Важно:</b> При использовании «ПЗА» и «ПИД» одновременно, мин. и макс. температура ограничивает влияние на расчётную температуру «ПЗА».<br />Таким образом, если мин. температура задана как -15, а макс. как 15, то конечная температура теплоносителя будет от <code>equitherm_result - 15</code> до <code>equitherm_result + 15</code>."
},
"deadband": {
"title": "Зона нечувствительности (Deadband)",
"note": "Deadband - это зона нечувствительности вокруг целевой температуры, в которой PID-регулирование становится менее активным. В этом диапазоне алгоритм может снижать интенсивность или полностью прекращать корректировку температуры, чтобы избежать излишней чувствительности к небольшим колебаниям.<br /><br />Например, при целевой температуре 22°, нижнем пороге 1.0 и верхнем 0.5, deadband активен в диапазоне от 21° до 22.5°. Если коэфф. I=0.0005, а множитель I=0.05, то при включении зоны нечувствительности коэфф. I будет равен: <code>0.0005 * 0.05 = 0.000025</code>",
"p_multiplier": "Множитель для коэф. P",
"i_multiplier": "Множитель для коэф. I",
"d_multiplier": "Множитель для коэф. D",
"thresholdHigh": "Верхний порог",
"thresholdLow": "Нижний порог"
}
}, },
"ot": { "ot": {

View File

@@ -259,7 +259,7 @@
<b>Type:</b> <span class="sType"></span> <b>Type:</b> <span class="sType"></span>
<b>AppVersion:</b> <span class="sAppVersion"></span> <b>AppVersion:</b> <span class="sAppVersion"></span>
<b>OT version:</b> <span class="sProtocolVersion"></span> <b>OT version:</b> <span class="sProtocolVersion"></span>
<b>Modulation limits:</b> <span class="sModMin"></span>...<span class="sModMax"></span> % <b>Modulation limits:</b> <span class="sModMin"></span>...<span class="sAbsModMax"></span> %, curr. max: <span class="sModMax"></span> %
<b>Power limits:</b> <span class="sPowerMin"></span>...<span class="sPowerMax"></span> kW <b>Power limits:</b> <span class="sPowerMin"></span>...<span class="sPowerMax"></span> kW
<b>Heating limits:</b> <span class="sHeatMinTemp"></span>...<span class="sHeatMaxTemp"></span> <span class="tempUnit"></span> <b>Heating limits:</b> <span class="sHeatMinTemp"></span>...<span class="sHeatMaxTemp"></span> <span class="tempUnit"></span>
<b>DHW limits:</b> <span class="sDhwMinTemp"></span>...<span class="sDhwMaxTemp"></span> <span class="tempUnit"></span></pre> <b>DHW limits:</b> <span class="sDhwMinTemp"></span>...<span class="sDhwMaxTemp"></span> <span class="tempUnit"></span></pre>
@@ -511,6 +511,7 @@
setValue('.tempUnit', temperatureUnit(unitSystem)); setValue('.tempUnit', temperatureUnit(unitSystem));
setValue('.pressureUnit', pressureUnit(unitSystem)); setValue('.pressureUnit', pressureUnit(unitSystem));
setValue('.volumeUnit', volumeUnit(unitSystem)); setValue('.volumeUnit', volumeUnit(unitSystem));
setValue('.sAbsModMax', result.opentherm.maxModulation);
} catch (error) { } catch (error) {
console.log(error); console.log(error);

View File

@@ -211,7 +211,7 @@
sensorNode.classList.remove("hidden"); sensorNode.classList.remove("hidden");
sensorNode.dataset.id = sensorId; sensorNode.dataset.id = sensorId;
setValue(".id", sensorId, sensorNode); setValue(".id", sensorId, sensorNode);
setValue(".pos", sensorId + 1, sensorNode); setValue(".pos", parseInt(sensorId) + 1, sensorNode);
setValue(".name", result[sensorId], sensorNode); setValue(".name", result[sensorId], sensorNode);
container.appendChild(sensorNode); container.appendChild(sensorNode);

View File

@@ -313,19 +313,73 @@
<hr /> <hr />
<div class="grid"> <details>
<label> <summary><b data-i18n>settings.pid.limits.title</b></summary>
<span data-i18n>settings.temp.min</span>
<input type="number" inputmode="numeric" name="pid[minTemp]" min="0" max="0" step="1" required>
</label>
<label> <div>
<span data-i18n>settings.temp.max</span> <div class="grid">
<input type="number" inputmode="numeric" name="pid[maxTemp]" min="0" max="0" step="1" required> <label>
</label> <span data-i18n>settings.temp.min</span>
</div> <input type="number" inputmode="decimal" name="pid[minTemp]" min="0" max="0" step="1" required>
</label>
<label>
<span data-i18n>settings.temp.max</span>
<input type="number" inputmode="numeric" name="pid[maxTemp]" min="0" max="0" step="1" required>
</label>
</div>
<small data-i18n>settings.pid.noteMinMaxTemp</small> <small data-i18n>settings.pid.limits.note</small>
</div>
</details>
<hr />
<details>
<summary><b data-i18n>settings.pid.deadband.title</b></summary>
<div>
<fieldset>
<label>
<input type="checkbox" name="pid[deadband][enabled]" value="true">
<span data-i18n>settings.enable</span>
</label>
</fieldset>
<div class="grid">
<label>
<span data-i18n>settings.pid.deadband.p_multiplier</span>
<input type="number" inputmode="decimal" name="pid[deadband][p_multiplier]" min="0" max="5" step="0.001" required>
</label>
<label>
<span data-i18n>settings.pid.deadband.i_multiplier</span>
<input type="number" inputmode="decimal" name="pid[deadband][i_multiplier]" min="0" max="1" step="0.001" required>
</label>
<label>
<span data-i18n>settings.pid.deadband.d_multiplier</span>
<input type="number" inputmode="decimal" name="pid[deadband][d_multiplier]" min="0" max="1" step="0.001" required>
</label>
</div>
<div class="grid">
<label>
<span data-i18n>settings.pid.deadband.thresholdHigh</span>
<input type="number" inputmode="decimal" name="pid[deadband][thresholdHigh]" min="0" max="5" step="0.01" required>
</label>
<label>
<span data-i18n>settings.pid.deadband.thresholdLow</span>
<input type="number" inputmode="decimal" name="pid[deadband][thresholdLow]" min="0" max="5" step="0.01" required>
</label>
</div>
<small data-i18n>settings.pid.deadband.note</small>
</div>
</details>
<br />
<button type="submit" data-i18n>button.save</button> <button type="submit" data-i18n>button.save</button>
</form> </form>
@@ -633,12 +687,12 @@
<div class="grid"> <div class="grid">
<label> <label>
<span data-i18n>settings.cascadeControl.output.gpio</span> <span data-i18n>settings.cascadeControl.output.gpio</span>
<input type="number" outputmode="numeric" name="cascadeControl[output][gpio]" min="0" max="254" step="1"> <input type="number" inputmode="numeric" name="cascadeControl[output][gpio]" min="0" max="254" step="1">
</label> </label>
<label> <label>
<span data-i18n>settings.cascadeControl.output.thresholdTime</span> <span data-i18n>settings.cascadeControl.output.thresholdTime</span>
<input type="number" outputmode="numeric" name="cascadeControl[output][thresholdTime]" min="5" max="600" step="1" required> <input type="number" inputmode="numeric" name="cascadeControl[output][thresholdTime]" min="5" max="600" step="1" required>
</label> </label>
</div> </div>
@@ -823,6 +877,12 @@
"min": (data.system.unitSystem == 0 ? 0 : 33), "min": (data.system.unitSystem == 0 ? 0 : 33),
"max": (data.system.unitSystem == 0 ? 100 : 212) "max": (data.system.unitSystem == 0 ? 100 : 212)
}); });
setCheckboxValue("[name='pid[deadband][enabled]']", data.pid.deadband.enabled);
setInputValue("[name='pid[deadband][p_multiplier]']", data.pid.deadband.p_multiplier);
setInputValue("[name='pid[deadband][i_multiplier]']", data.pid.deadband.i_multiplier);
setInputValue("[name='pid[deadband][d_multiplier]']", data.pid.deadband.d_multiplier);
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); setBusy('#pid-settings-busy', '#pid-settings', false);
}; };