feat: added feat use of BLE external sensor; added events onIndoorSensorDisconnect and onOutdoorSensorDisconnect for emergency mode; added polling of rssi, humidity, battery for BLE sensors

This commit is contained in:
Yurii
2024-08-20 19:06:18 +03:00
parent d5acb44648
commit 1b2bc8e200
10 changed files with 682 additions and 168 deletions

View File

@@ -29,7 +29,6 @@ protected:
enum class PumpStartReason {NONE, HEATING, ANTISTUCK}; enum class PumpStartReason {NONE, HEATING, ANTISTUCK};
Blinker* blinker = nullptr; Blinker* blinker = nullptr;
unsigned long firstFailConnect = 0;
unsigned long lastHeapInfo = 0; unsigned long lastHeapInfo = 0;
unsigned int minFreeHeap = 0; unsigned int minFreeHeap = 0;
unsigned int minMaxFreeBlockHeap = 0; unsigned int minMaxFreeBlockHeap = 0;
@@ -39,6 +38,8 @@ protected:
PumpStartReason extPumpStartReason = PumpStartReason::NONE; PumpStartReason extPumpStartReason = PumpStartReason::NONE;
unsigned long externalPumpStartTime = 0; unsigned long externalPumpStartTime = 0;
bool telnetStarted = false; bool telnetStarted = false;
bool emergencyDetected = false;
unsigned long emergencyFlipTime = 0;
#if defined(ARDUINO_ARCH_ESP32) #if defined(ARDUINO_ARCH_ESP32)
const char* getTaskName() override { const char* getTaskName() override {
@@ -85,10 +86,6 @@ protected:
vars.states.mqtt = tMqtt->isConnected(); vars.states.mqtt = tMqtt->isConnected();
vars.sensors.rssi = network->isConnected() ? WiFi.RSSI() : 0; vars.sensors.rssi = network->isConnected() ? WiFi.RSSI() : 0;
if (vars.states.emergency && !settings.emergency.enable) {
vars.states.emergency = false;
}
if (network->isConnected()) { if (network->isConnected()) {
if (!this->telnetStarted && telnetStream != nullptr) { if (!this->telnetStarted && telnetStream != nullptr) {
telnetStream->begin(23, false); telnetStream->begin(23, false);
@@ -102,17 +99,6 @@ protected:
tMqtt->disable(); tMqtt->disable();
} }
if (!vars.states.emergency && settings.emergency.enable && settings.emergency.onMqttFault && !tMqtt->isEnabled()) {
vars.states.emergency = true;
} else if (vars.states.emergency && !settings.emergency.onMqttFault) {
vars.states.emergency = false;
}
if (this->firstFailConnect != 0) {
this->firstFailConnect = 0;
}
if ( Log.getLevel() != TinyLogger::Level::INFO && !settings.system.debug ) { if ( Log.getLevel() != TinyLogger::Level::INFO && !settings.system.debug ) {
Log.setLevel(TinyLogger::Level::INFO); Log.setLevel(TinyLogger::Level::INFO);
@@ -129,21 +115,10 @@ protected:
if (tMqtt->isEnabled()) { if (tMqtt->isEnabled()) {
tMqtt->disable(); tMqtt->disable();
} }
if (!vars.states.emergency && settings.emergency.enable && settings.emergency.onNetworkFault) {
if (this->firstFailConnect == 0) {
this->firstFailConnect = millis();
}
if (millis() - this->firstFailConnect > (settings.emergency.tresholdTime * 1000)) {
vars.states.emergency = true;
Log.sinfoln(FPSTR(L_MAIN), F("Emergency mode enabled"));
}
}
} }
this->yield(); this->yield();
this->emergency();
this->ledStatus(); this->ledStatus();
this->externalPump(); this->externalPump();
this->yield(); this->yield();
@@ -213,6 +188,76 @@ protected:
} }
} }
void emergency() {
if (!settings.emergency.enable && vars.states.emergency) {
this->emergencyDetected = false;
vars.states.emergency = false;
Log.sinfoln(FPSTR(L_MAIN), F("Emergency mode disabled"));
}
if (!settings.emergency.enable) {
return;
}
// flags
uint8_t emergencyFlags = 0b00000000;
// set network flag
if (settings.emergency.onNetworkFault && !network->isConnected()) {
emergencyFlags |= 0b00000001;
}
// set mqtt flag
if (settings.emergency.onMqttFault && (!tMqtt->isEnabled() || !tMqtt->isConnected())) {
emergencyFlags |= 0b00000010;
}
// set outdoor sensor flag
if (settings.sensors.outdoor.type == SensorType::DS18B20 || settings.sensors.outdoor.type == SensorType::BLUETOOTH) {
if (settings.emergency.onOutdoorSensorDisconnect && !vars.sensors.outdoor.connected) {
emergencyFlags |= 0b00000100;
}
}
// set indoor sensor flag
if (settings.sensors.indoor.type == SensorType::DS18B20 || settings.sensors.indoor.type == SensorType::BLUETOOTH) {
if (settings.emergency.onIndoorSensorDisconnect && !vars.sensors.indoor.connected) {
emergencyFlags |= 0b00001000;
}
}
// if any flags is true
if ((emergencyFlags & 0b00001111) != 0) {
if (!this->emergencyDetected) {
// flip flag
this->emergencyDetected = true;
this->emergencyFlipTime = millis();
} else if (this->emergencyDetected && !vars.states.emergency) {
// enable emergency
if (millis() - this->emergencyFlipTime > (settings.emergency.tresholdTime * 1000)) {
vars.states.emergency = true;
Log.sinfoln(FPSTR(L_MAIN), F("Emergency mode enabled (%hhu)"), emergencyFlags);
}
}
} else {
if (this->emergencyDetected) {
// flip flag
this->emergencyDetected = false;
this->emergencyFlipTime = millis();
} else if (!this->emergencyDetected && vars.states.emergency) {
// disable emergency
if (millis() - this->emergencyFlipTime > 30000) {
vars.states.emergency = false;
Log.sinfoln(FPSTR(L_MAIN), F("Emergency mode disabled"));
}
}
}
}
void ledStatus() { void ledStatus() {
uint8_t errors[4]; uint8_t errors[4];
uint8_t errCount = 0; uint8_t errCount = 0;

View File

@@ -198,17 +198,6 @@ protected:
this->onConnect(); this->onConnect();
} }
if (settings.emergency.enable && settings.emergency.onMqttFault) {
if (!this->connected && !vars.states.emergency && millis() - this->disconnectedTime > (settings.emergency.tresholdTime * 1000)) {
vars.states.emergency = true;
Log.sinfoln(FPSTR(L_MQTT), F("Emergency mode enabled"));
} else if (this->connected && vars.states.emergency && millis() - this->connectedTime > 10000) {
vars.states.emergency = false;
Log.sinfoln(FPSTR(L_MQTT), F("Emergency mode disabled"));
}
}
if (!this->connected) { if (!this->connected) {
return; return;
} }

View File

@@ -198,6 +198,10 @@ protected:
etRegulator.Kt = 0; etRegulator.Kt = 0;
etRegulator.indoorTemp = 0; etRegulator.indoorTemp = 0;
} else if ((settings.sensors.indoor.type == SensorType::DS18B20 || settings.sensors.indoor.type == SensorType::BLUETOOTH) && !vars.sensors.indoor.connected) {
etRegulator.Kt = 0;
etRegulator.indoorTemp = 0;
} else { } else {
etRegulator.Kt = settings.equitherm.t_factor; etRegulator.Kt = settings.equitherm.t_factor;
etRegulator.indoorTemp = indoorTemp; etRegulator.indoorTemp = indoorTemp;

View File

@@ -35,19 +35,18 @@ protected:
unsigned long initOutdoorSensorTime = 0; unsigned long initOutdoorSensorTime = 0;
unsigned long startOutdoorConversionTime = 0; unsigned long startOutdoorConversionTime = 0;
float filteredOutdoorTemp = 0; float filteredOutdoorTemp = 0;
bool emptyOutdoorTemp = true; float prevFilteredOutdoorTemp = 0;
bool initIndoorSensor = false; bool initIndoorSensor = false;
unsigned long initIndoorSensorTime = 0; unsigned long initIndoorSensorTime = 0;
unsigned long startIndoorConversionTime = 0; unsigned long startIndoorConversionTime = 0;
float filteredIndoorTemp = 0; float filteredIndoorTemp = 0;
bool emptyIndoorTemp = true; float prevFilteredIndoorTemp = 0;
#if defined(ARDUINO_ARCH_ESP32) #if defined(ARDUINO_ARCH_ESP32)
#if USE_BLE #if USE_BLE
BLEClient* pBleClient = nullptr; unsigned long outdoorConnectedTime = 0;
bool initBleSensor = false; unsigned long indoorConnectedTime = 0;
bool initBleNotify = false;
#endif #endif
const char* getTaskName() override { const char* getTaskName() override {
@@ -69,26 +68,62 @@ protected:
#endif #endif
void loop() { void loop() {
bool indoorTempUpdated = false;
bool outdoorTempUpdated = false;
if (settings.sensors.outdoor.type == SensorType::DS18B20 && GPIO_IS_VALID(settings.sensors.outdoor.gpio)) {
outdoorTemperatureSensor();
outdoorTempUpdated = true;
}
if (settings.sensors.indoor.type == SensorType::DS18B20 && GPIO_IS_VALID(settings.sensors.indoor.gpio)) {
indoorTemperatureSensor();
indoorTempUpdated = true;
}
#if USE_BLE #if USE_BLE
else if (settings.sensors.indoor.type == SensorType::BLUETOOTH) { if (!NimBLEDevice::getInitialized() && millis() > 5000) {
indoorTemperatureBluetoothSensor(); Log.sinfoln(FPSTR(L_SENSORS_BLE), F("Init BLE"));
indoorTempUpdated = true; BLEDevice::init("");
NimBLEDevice::setPower(ESP_PWR_LVL_P9);
} }
#endif #endif
if (outdoorTempUpdated) { if (settings.sensors.outdoor.type == SensorType::DS18B20 && GPIO_IS_VALID(settings.sensors.outdoor.gpio)) {
outdoorDallasSensor();
}
#if USE_BLE
else if (settings.sensors.outdoor.type == SensorType::BLUETOOTH) {
bool connected = this->bluetoothSensor(
BLEAddress(settings.sensors.outdoor.bleAddress),
&vars.sensors.outdoor.rssi,
&this->filteredOutdoorTemp,
&vars.sensors.outdoor.humidity,
&vars.sensors.outdoor.battery
);
if (connected) {
this->outdoorConnectedTime = millis();
vars.sensors.outdoor.connected = true;
} else if (millis() - this->outdoorConnectedTime > 60000) {
vars.sensors.outdoor.connected = false;
}
}
#endif
if (settings.sensors.indoor.type == SensorType::DS18B20 && GPIO_IS_VALID(settings.sensors.indoor.gpio)) {
indoorDallasSensor();
}
#if USE_BLE
else if (settings.sensors.indoor.type == SensorType::BLUETOOTH) {
bool connected = this->bluetoothSensor(
BLEAddress(settings.sensors.indoor.bleAddress),
&vars.sensors.indoor.rssi,
&this->filteredIndoorTemp,
&vars.sensors.indoor.humidity,
&vars.sensors.indoor.battery
);
if (connected) {
this->indoorConnectedTime = millis();
vars.sensors.indoor.connected = true;
} else if (millis() - this->indoorConnectedTime > 60000) {
vars.sensors.indoor.connected = false;
}
}
#endif
// convert
if (fabs(this->prevFilteredOutdoorTemp - this->filteredOutdoorTemp) >= 0.1f) {
float newTemp = settings.sensors.outdoor.offset; float newTemp = settings.sensors.outdoor.offset;
if (settings.system.unitSystem == UnitSystem::METRIC) { if (settings.system.unitSystem == UnitSystem::METRIC) {
newTemp += this->filteredOutdoorTemp; newTemp += this->filteredOutdoorTemp;
@@ -101,9 +136,11 @@ protected:
vars.temperatures.outdoor = newTemp; vars.temperatures.outdoor = newTemp;
Log.sinfoln(FPSTR(L_SENSORS_OUTDOOR), F("New temp: %f"), vars.temperatures.outdoor); Log.sinfoln(FPSTR(L_SENSORS_OUTDOOR), F("New temp: %f"), vars.temperatures.outdoor);
} }
this->prevFilteredOutdoorTemp = this->filteredOutdoorTemp;
} }
if (indoorTempUpdated) { if (fabs(this->prevFilteredIndoorTemp - this->filteredIndoorTemp) > 0.1f) {
float newTemp = settings.sensors.indoor.offset; float newTemp = settings.sensors.indoor.offset;
if (settings.system.unitSystem == UnitSystem::METRIC) { if (settings.system.unitSystem == UnitSystem::METRIC) {
newTemp += this->filteredIndoorTemp; newTemp += this->filteredIndoorTemp;
@@ -116,127 +153,378 @@ protected:
vars.temperatures.indoor = newTemp; vars.temperatures.indoor = newTemp;
Log.sinfoln(FPSTR(L_SENSORS_INDOOR), F("New temp: %f"), vars.temperatures.indoor); Log.sinfoln(FPSTR(L_SENSORS_INDOOR), F("New temp: %f"), vars.temperatures.indoor);
} }
this->prevFilteredIndoorTemp = this->filteredIndoorTemp;
} }
} }
#if USE_BLE #if USE_BLE
void indoorTemperatureBluetoothSensor() { bool bluetoothSensor(const BLEAddress& address, int8_t* const pRssi, float* const pTemperature, float* const pHumidity = nullptr, float* const pBattery = nullptr) {
static bool initBleNotify = false; if (!NimBLEDevice::getInitialized()) {
if (!initBleSensor && millis() > 5000) { return false;
Log.sinfoln(FPSTR(L_SENSORS_BLE), F("Init BLE"));
BLEDevice::init("");
pBleClient = BLEDevice::createClient();
pBleClient->setConnectTimeout(5);
initBleSensor = true;
} }
if (!initBleSensor || pBleClient->isConnected()) { NimBLEClient* pClient = nullptr;
return; pClient = NimBLEDevice::getClientByPeerAddress(address);
}
// Reset init notify flag
this->initBleNotify = false;
// Connect to the remote BLE Server. if (pClient == nullptr) {
BLEAddress bleServerAddress(settings.sensors.indoor.bleAddress); pClient = NimBLEDevice::getDisconnectedClient();
if (!pBleClient->connect(bleServerAddress)) {
Log.swarningln(FPSTR(L_SENSORS_BLE), "Failed connecting to device at %s", bleServerAddress.toString().c_str());
return;
} }
Log.sinfoln(FPSTR(L_SENSORS_BLE), "Connected to device at %s", bleServerAddress.toString().c_str()); if (pClient == nullptr) {
if (NimBLEDevice::getClientListSize() >= NIMBLE_MAX_CONNECTIONS) {
return false;
}
NimBLEUUID serviceUUID((uint16_t) 0x181AU); pClient = NimBLEDevice::createClient();
BLERemoteService* pRemoteService = pBleClient->getService(serviceUUID); pClient->setConnectTimeout(5);
if (!pRemoteService) {
Log.straceln(FPSTR(L_SENSORS_BLE), F("Failed to find service UUID: %s"), serviceUUID.toString().c_str());
return;
} }
Log.straceln(FPSTR(L_SENSORS_BLE), F("Found service UUID: %s"), serviceUUID.toString().c_str()); if(pClient->isConnected()) {
*pRssi = pClient->getRssi();
return true;
}
// 0x2A6E - Notify temperature x0.01C (pvvx) if (!pClient->connect(address)) {
if (!this->initBleNotify) { Log.swarningln(FPSTR(L_SENSORS_BLE), "Device %s: failed connecting", address.toString().c_str());
NimBLEUUID charUUID((uint16_t) 0x2A6E);
BLERemoteCharacteristic* pRemoteCharacteristic = pRemoteService->getCharacteristic(charUUID);
if (pRemoteCharacteristic && pRemoteCharacteristic->canNotify()) {
Log.straceln(FPSTR(L_SENSORS_BLE), F("Found characteristic UUID: %s"), charUUID.toString().c_str());
this->initBleNotify = pRemoteCharacteristic->subscribe(true, [this](NimBLERemoteCharacteristic*, uint8_t* pData, size_t length, bool isNotify) { NimBLEDevice::deleteClient(pClient);
if (length != 2) { return false;
Log.swarningln(FPSTR(L_SENSORS_BLE), F("Invalid notification data")); }
return;
}
float rawTemp = ((pData[0] | (pData[1] << 8)) * 0.01f); Log.sinfoln(FPSTR(L_SENSORS_BLE), "Device %s: connected", address.toString().c_str());
Log.straceln(FPSTR(L_SENSORS_INDOOR), F("Raw temp: %f"), rawTemp); NimBLERemoteService* pService = nullptr;
NimBLERemoteCharacteristic* pChar = nullptr;
if (this->emptyIndoorTemp) { // ENV Service (0x181A)
this->filteredIndoorTemp = rawTemp; pService = pClient->getService(NimBLEUUID((uint16_t) 0x181AU));
this->emptyIndoorTemp = false; if (!pService) {
Log.straceln(
FPSTR(L_SENSORS_BLE),
F("Device %s: failed to find env service (%s)"),
address.toString().c_str(),
pService->getUUID().toString().c_str()
);
} else {
Log.straceln(
FPSTR(L_SENSORS_BLE),
F("Device %s: found env service (%s)"),
address.toString().c_str(),
pService->getUUID().toString().c_str()
);
// 0x2A6E - Notify temperature x0.01C (pvvx)
bool tempNotifyCreated = false;
if (!tempNotifyCreated) {
pChar = pService->getCharacteristic(NimBLEUUID((uint16_t) 0x2A6E));
if (pChar && pChar->canNotify()) {
Log.straceln(
FPSTR(L_SENSORS_BLE),
F("Device %s: found temperature char (%s) in env service"),
address.toString().c_str(),
pChar->getUUID().toString().c_str()
);
tempNotifyCreated = pChar->subscribe(true, [pTemperature](NimBLERemoteCharacteristic* pChar, uint8_t* pData, size_t length, bool isNotify) {
NimBLEClient* pClient = pChar->getRemoteService()->getClient();
if (length != 2) {
Log.swarningln(
FPSTR(L_SENSORS_BLE),
F("Device %s: invalid notification data at temperature char (%s)"),
pClient->getPeerAddress().toString().c_str(),
pChar->getUUID().toString().c_str()
);
return;
}
float rawTemp = ((pData[0] | (pData[1] << 8)) * 0.01f);
Log.straceln(
FPSTR(L_SENSORS_INDOOR),
F("Device %s: raw temp %f"),
pClient->getPeerAddress().toString().c_str(),
rawTemp
);
if (fabs(*pTemperature) < 0.1f) {
*pTemperature = rawTemp;
} else {
*pTemperature += (rawTemp - (*pTemperature)) * EXT_SENSORS_FILTER_K;
}
*pTemperature = floor((*pTemperature) * 100) / 100;
});
if (tempNotifyCreated) {
Log.straceln(
FPSTR(L_SENSORS_BLE),
F("Device %s: subscribed to temperature char (%s) in env service"),
address.toString().c_str(),
pChar->getUUID().toString().c_str()
);
} else { } else {
this->filteredIndoorTemp += (rawTemp - this->filteredIndoorTemp) * EXT_SENSORS_FILTER_K; Log.swarningln(
FPSTR(L_SENSORS_BLE),
F("Device %s: failed to subscribe to temperature char (%s) in env service"),
address.toString().c_str(),
pChar->getUUID().toString().c_str()
);
} }
}
}
this->filteredIndoorTemp = floor(this->filteredIndoorTemp * 100) / 100;
});
if (this->initBleNotify) { // 0x2A1F - Notify temperature x0.1C (atc1441/pvvx)
Log.straceln(FPSTR(L_SENSORS_BLE), F("Subscribed to characteristic UUID: %s"), charUUID.toString().c_str()); if (!tempNotifyCreated) {
pChar = pService->getCharacteristic(NimBLEUUID((uint16_t) 0x2A1F));
} else { if (pChar && pChar->canNotify()) {
Log.swarningln(FPSTR(L_SENSORS_BLE), F("Failed to subscribe to characteristic UUID: %s"), charUUID.toString().c_str()); Log.straceln(
FPSTR(L_SENSORS_BLE),
F("Device %s: found temperature char (%s) in env service"),
address.toString().c_str(),
pChar->getUUID().toString().c_str()
);
tempNotifyCreated = pChar->subscribe(true, [pTemperature](NimBLERemoteCharacteristic* pChar, uint8_t* pData, size_t length, bool isNotify) {
NimBLEClient* pClient = pChar->getRemoteService()->getClient();
if (length != 2) {
Log.swarningln(
FPSTR(L_SENSORS_BLE),
F("Device %s: invalid notification data at temperature char (%s)"),
pClient->getPeerAddress().toString().c_str(),
pChar->getUUID().toString().c_str()
);
return;
}
float rawTemp = ((pData[0] | (pData[1] << 8)) * 0.1f);
Log.straceln(
FPSTR(L_SENSORS_INDOOR),
F("Device %s: raw temp %f"),
pClient->getPeerAddress().toString().c_str(),
rawTemp
);
if (fabs(*pTemperature) < 0.1f) {
*pTemperature = rawTemp;
} else {
*pTemperature += (rawTemp - (*pTemperature)) * EXT_SENSORS_FILTER_K;
}
*pTemperature = floor((*pTemperature) * 100) / 100;
});
if (tempNotifyCreated) {
Log.straceln(
FPSTR(L_SENSORS_BLE),
F("Device %s: subscribed to temperature char (%s) in env service"),
address.toString().c_str(),
pChar->getUUID().toString().c_str()
);
} else {
Log.swarningln(
FPSTR(L_SENSORS_BLE),
F("Device %s: failed to subscribe to temperature char (%s) in env service"),
address.toString().c_str(),
pChar->getUUID().toString().c_str()
);
}
}
}
if (!tempNotifyCreated) {
Log.swarningln(
FPSTR(L_SENSORS_BLE),
F("Device %s: not found supported temperature chars in env service"),
address.toString().c_str()
);
pClient->disconnect();
return false;
}
// 0x2A6F - Notify about humidity x0.01% (pvvx)
if (pHumidity != nullptr) {
bool humidityNotifyCreated = false;
if (!humidityNotifyCreated) {
pChar = pService->getCharacteristic(NimBLEUUID((uint16_t) 0x2A6F));
if (pChar && pChar->canNotify()) {
Log.straceln(
FPSTR(L_SENSORS_BLE),
F("Device %s: found humidity char (%s) in env service"),
address.toString().c_str(),
pChar->getUUID().toString().c_str()
);
humidityNotifyCreated = pChar->subscribe(true, [pHumidity](NimBLERemoteCharacteristic* pChar, uint8_t* pData, size_t length, bool isNotify) {
NimBLEClient* pClient = pChar->getRemoteService()->getClient();
if (length != 2) {
Log.swarningln(
FPSTR(L_SENSORS_BLE),
F("Device %s: invalid notification data at humidity char (%s)"),
pClient->getPeerAddress().toString().c_str(),
pChar->getUUID().toString().c_str()
);
return;
}
float rawHumidity = ((pData[0] | (pData[1] << 8)) * 0.01f);
Log.straceln(
FPSTR(L_SENSORS_INDOOR),
F("Device %s: raw humidity %f"),
pClient->getPeerAddress().toString().c_str(),
rawHumidity
);
if (fabs(*pHumidity) < 0.1f) {
*pHumidity = rawHumidity;
} else {
*pHumidity += (rawHumidity - (*pHumidity)) * EXT_SENSORS_FILTER_K;
}
*pHumidity = floor((*pHumidity) * 100) / 100;
});
if (humidityNotifyCreated) {
Log.straceln(
FPSTR(L_SENSORS_BLE),
F("Device %s: subscribed to humidity char (%s) in env service"),
address.toString().c_str(),
pChar->getUUID().toString().c_str()
);
} else {
Log.swarningln(
FPSTR(L_SENSORS_BLE),
F("Device %s: failed to subscribe to humidity char (%s) in env service"),
address.toString().c_str(),
pChar->getUUID().toString().c_str()
);
}
}
}
if (!humidityNotifyCreated) {
Log.swarningln(
FPSTR(L_SENSORS_BLE),
F("Device %s: not found supported humidity chars in env service"),
address.toString().c_str()
);
} }
} }
} }
// 0x2A1F - Notify temperature x0.1C (atc1441/pvvx)
if (!this->initBleNotify) {
NimBLEUUID charUUID((uint16_t) 0x2A1F);
BLERemoteCharacteristic* pRemoteCharacteristic = pRemoteService->getCharacteristic(charUUID);
if (pRemoteCharacteristic && pRemoteCharacteristic->canNotify()) {
Log.straceln(FPSTR(L_SENSORS_BLE), F("Found characteristic UUID: %s"), charUUID.toString().c_str());
this->initBleNotify = pRemoteCharacteristic->subscribe(true, [this](NimBLERemoteCharacteristic*, uint8_t* pData, size_t length, bool isNotify) { // Battery Service (0x180F)
if (length != 2) { if (pBattery != nullptr) {
Log.swarningln(FPSTR(L_SENSORS_BLE), F("Invalid notification data")); pService = pClient->getService(NimBLEUUID((uint16_t) 0x180F));
return; if (!pService) {
Log.straceln(
FPSTR(L_SENSORS_BLE),
F("Device %s: failed to find battery service (%s)"),
address.toString().c_str(),
pService->getUUID().toString().c_str()
);
} else {
Log.straceln(
FPSTR(L_SENSORS_BLE),
F("Device %s: found battery service (%s)"),
address.toString().c_str(),
pService->getUUID().toString().c_str()
);
// 0x2A19 - Notify the battery charge level 0..99% (pvvx)
bool batteryNotifyCreated = false;
if (!batteryNotifyCreated) {
pChar = pService->getCharacteristic(NimBLEUUID((uint16_t) 0x2A19));
if (pChar && pChar->canNotify()) {
Log.straceln(
FPSTR(L_SENSORS_BLE),
F("Device %s: found battery char (%s) in battery service"),
address.toString().c_str(),
pChar->getUUID().toString().c_str()
);
batteryNotifyCreated = pChar->subscribe(true, [pBattery](NimBLERemoteCharacteristic* pChar, uint8_t* pData, size_t length, bool isNotify) {
NimBLEClient* pClient = pChar->getRemoteService()->getClient();
if (length != 1) {
Log.swarningln(
FPSTR(L_SENSORS_BLE),
F("Device %s: invalid notification data at battery char (%s)"),
pClient->getPeerAddress().toString().c_str(),
pChar->getUUID().toString().c_str()
);
return;
}
uint8_t rawBattery = pData[0];
Log.straceln(
FPSTR(L_SENSORS_INDOOR),
F("Device %s: raw battery %hhu"),
pClient->getPeerAddress().toString().c_str(),
rawBattery
);
if (fabs(*pBattery) < 0.1f) {
*pBattery = rawBattery;
} else {
*pBattery += (rawBattery - (*pBattery)) * EXT_SENSORS_FILTER_K;
}
*pBattery = floor((*pBattery) * 100) / 100;
});
if (batteryNotifyCreated) {
Log.straceln(
FPSTR(L_SENSORS_BLE),
F("Device %s: subscribed to battery char (%s) in battery service"),
address.toString().c_str(),
pChar->getUUID().toString().c_str()
);
} else {
Log.swarningln(
FPSTR(L_SENSORS_BLE),
F("Device %s: failed to subscribe to battery char (%s) in battery service"),
address.toString().c_str(),
pChar->getUUID().toString().c_str()
);
}
} }
}
float rawTemp = ((pData[0] | (pData[1] << 8)) * 0.1); if (!batteryNotifyCreated) {
Log.straceln(FPSTR(L_SENSORS_INDOOR), F("Raw temp: %f"), rawTemp); Log.swarningln(
FPSTR(L_SENSORS_BLE),
if (this->emptyIndoorTemp) { F("Device %s: not found supported battery chars in battery service"),
this->filteredIndoorTemp = rawTemp; address.toString().c_str()
this->emptyIndoorTemp = false; );
} else {
this->filteredIndoorTemp += (rawTemp - this->filteredIndoorTemp) * EXT_SENSORS_FILTER_K;
}
this->filteredIndoorTemp = floor(this->filteredIndoorTemp * 100) / 100;
});
if (this->initBleNotify) {
Log.straceln(FPSTR(L_SENSORS_BLE), F("Subscribed to characteristic UUID: %s"), charUUID.toString().c_str());
} else {
Log.swarningln(FPSTR(L_SENSORS_BLE), F("Failed to subscribe to characteristic UUID: %s"), charUUID.toString().c_str());
} }
} }
} }
if (!this->initBleNotify) { return true;
Log.swarningln(FPSTR(L_SENSORS_BLE), F("Not found supported characteristics"));
pBleClient->disconnect();
}
} }
#endif #endif
void outdoorTemperatureSensor() { void outdoorDallasSensor() {
if (!this->initOutdoorSensor) { if (!this->initOutdoorSensor) {
if (this->initOutdoorSensorTime && millis() - this->initOutdoorSensorTime < EXT_SENSORS_INTERVAL * 10) { if (this->initOutdoorSensorTime && millis() - this->initOutdoorSensorTime < EXT_SENSORS_INTERVAL * 10) {
return; return;
@@ -265,6 +553,10 @@ protected:
Log.sinfoln(FPSTR(L_SENSORS_OUTDOOR), F("Started")); Log.sinfoln(FPSTR(L_SENSORS_OUTDOOR), F("Started"));
} else { } else {
if (vars.sensors.outdoor.connected) {
vars.sensors.outdoor.connected = false;
}
return; return;
} }
} }
@@ -294,9 +586,12 @@ protected:
} else { } else {
Log.straceln(FPSTR(L_SENSORS_OUTDOOR), F("Raw temp: %f"), rawTemp); Log.straceln(FPSTR(L_SENSORS_OUTDOOR), F("Raw temp: %f"), rawTemp);
if (this->emptyOutdoorTemp) { if (!vars.sensors.outdoor.connected) {
vars.sensors.outdoor.connected = true;
}
if (fabs(this->filteredOutdoorTemp) < 0.1f) {
this->filteredOutdoorTemp = rawTemp; this->filteredOutdoorTemp = rawTemp;
this->emptyOutdoorTemp = false;
} else { } else {
this->filteredOutdoorTemp += (rawTemp - this->filteredOutdoorTemp) * EXT_SENSORS_FILTER_K; this->filteredOutdoorTemp += (rawTemp - this->filteredOutdoorTemp) * EXT_SENSORS_FILTER_K;
@@ -308,7 +603,7 @@ protected:
} }
} }
void indoorTemperatureSensor() { void indoorDallasSensor() {
if (!this->initIndoorSensor) { if (!this->initIndoorSensor) {
if (this->initIndoorSensorTime && millis() - this->initIndoorSensorTime < EXT_SENSORS_INTERVAL * 10) { if (this->initIndoorSensorTime && millis() - this->initIndoorSensorTime < EXT_SENSORS_INTERVAL * 10) {
return; return;
@@ -337,6 +632,10 @@ protected:
Log.sinfoln(FPSTR(L_SENSORS_INDOOR), F("Started")); Log.sinfoln(FPSTR(L_SENSORS_INDOOR), F("Started"));
} else { } else {
if (vars.sensors.indoor.connected) {
vars.sensors.indoor.connected = false;
}
return; return;
} }
} }
@@ -366,9 +665,12 @@ protected:
} else { } else {
Log.straceln(FPSTR(L_SENSORS_INDOOR), F("Raw temp: %f"), rawTemp); Log.straceln(FPSTR(L_SENSORS_INDOOR), F("Raw temp: %f"), rawTemp);
if (this->emptyIndoorTemp) { if (!vars.sensors.indoor.connected) {
vars.sensors.indoor.connected = true;
}
if (fabs(this->filteredIndoorTemp) < 0.1f) {
this->filteredIndoorTemp = rawTemp; this->filteredIndoorTemp = rawTemp;
this->emptyIndoorTemp = false;
} else { } else {
this->filteredIndoorTemp += (rawTemp - this->filteredIndoorTemp) * EXT_SENSORS_FILTER_K; this->filteredIndoorTemp += (rawTemp - this->filteredIndoorTemp) * EXT_SENSORS_FILTER_K;

View File

@@ -77,13 +77,15 @@ struct Settings {
} mqtt; } mqtt;
struct { struct {
bool enable = true; bool enable = false;
float target = DEFAULT_HEATING_TARGET_TEMP; float target = DEFAULT_HEATING_TARGET_TEMP;
unsigned short tresholdTime = 120; unsigned short tresholdTime = 120;
bool useEquitherm = false; bool useEquitherm = false;
bool usePid = false; bool usePid = false;
bool onNetworkFault = true; bool onNetworkFault = true;
bool onMqttFault = true; bool onMqttFault = true;
bool onIndoorSensorDisconnect = false;
bool onOutdoorSensorDisconnect = false;
} emergency; } emergency;
struct { struct {
@@ -124,6 +126,7 @@ struct Settings {
struct { struct {
SensorType type = SensorType::BOILER; SensorType type = SensorType::BOILER;
byte gpio = DEFAULT_SENSOR_OUTDOOR_GPIO; byte gpio = DEFAULT_SENSOR_OUTDOOR_GPIO;
uint8_t bleAddress[6] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
float offset = 0.0f; float offset = 0.0f;
} outdoor; } outdoor;
@@ -165,6 +168,20 @@ struct Variables {
float dhwFlowRate = 0.0f; float dhwFlowRate = 0.0f;
byte faultCode = 0; byte faultCode = 0;
int8_t rssi = 0; int8_t rssi = 0;
struct {
bool connected = false;
int8_t rssi = 0;
float battery = 0.0f;
float humidity = 0.0f;
} outdoor;
struct {
bool connected = false;
int8_t rssi = 0;
float battery = 0.0f;
float humidity = 0.0f;
} indoor;
} sensors; } sensors;
struct { struct {

View File

@@ -371,6 +371,8 @@ void settingsToJson(const Settings& src, JsonVariant dst, bool safe = false) {
dst["emergency"]["usePid"] = src.emergency.usePid; dst["emergency"]["usePid"] = src.emergency.usePid;
dst["emergency"]["onNetworkFault"] = src.emergency.onNetworkFault; dst["emergency"]["onNetworkFault"] = src.emergency.onNetworkFault;
dst["emergency"]["onMqttFault"] = src.emergency.onMqttFault; dst["emergency"]["onMqttFault"] = src.emergency.onMqttFault;
dst["emergency"]["onIndoorSensorDisconnect"] = src.emergency.onIndoorSensorDisconnect;
dst["emergency"]["onOutdoorSensorDisconnect"] = src.emergency.onOutdoorSensorDisconnect;
} }
dst["heating"]["enable"] = src.heating.enable; dst["heating"]["enable"] = src.heating.enable;
@@ -401,12 +403,24 @@ void settingsToJson(const Settings& src, JsonVariant dst, bool safe = false) {
dst["sensors"]["outdoor"]["type"] = static_cast<byte>(src.sensors.outdoor.type); dst["sensors"]["outdoor"]["type"] = static_cast<byte>(src.sensors.outdoor.type);
dst["sensors"]["outdoor"]["gpio"] = src.sensors.outdoor.gpio; dst["sensors"]["outdoor"]["gpio"] = src.sensors.outdoor.gpio;
char bleAddress[18];
sprintf(
bleAddress,
"%02x:%02x:%02x:%02x:%02x:%02x",
src.sensors.outdoor.bleAddress[0],
src.sensors.outdoor.bleAddress[1],
src.sensors.outdoor.bleAddress[2],
src.sensors.outdoor.bleAddress[3],
src.sensors.outdoor.bleAddress[4],
src.sensors.outdoor.bleAddress[5]
);
dst["sensors"]["outdoor"]["bleAddress"] = String(bleAddress);
dst["sensors"]["outdoor"]["offset"] = roundd(src.sensors.outdoor.offset, 2); dst["sensors"]["outdoor"]["offset"] = roundd(src.sensors.outdoor.offset, 2);
dst["sensors"]["indoor"]["type"] = static_cast<byte>(src.sensors.indoor.type); dst["sensors"]["indoor"]["type"] = static_cast<byte>(src.sensors.indoor.type);
dst["sensors"]["indoor"]["gpio"] = src.sensors.indoor.gpio; dst["sensors"]["indoor"]["gpio"] = src.sensors.indoor.gpio;
char bleAddress[18];
sprintf( sprintf(
bleAddress, bleAddress,
"%02x:%02x:%02x:%02x:%02x:%02x", "%02x:%02x:%02x:%02x:%02x:%02x",
@@ -883,7 +897,7 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false
if (src["emergency"]["useEquitherm"].is<bool>()) { if (src["emergency"]["useEquitherm"].is<bool>()) {
bool value = src["emergency"]["useEquitherm"].as<bool>(); bool value = src["emergency"]["useEquitherm"].as<bool>();
if (!dst.opentherm.nativeHeatingControl && dst.sensors.outdoor.type != SensorType::MANUAL) { if (!dst.opentherm.nativeHeatingControl && dst.sensors.outdoor.type != SensorType::MANUAL && dst.sensors.outdoor.type != SensorType::BLUETOOTH) {
if (value != dst.emergency.useEquitherm) { if (value != dst.emergency.useEquitherm) {
dst.emergency.useEquitherm = value; dst.emergency.useEquitherm = value;
changed = true; changed = true;
@@ -903,7 +917,7 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false
if (src["emergency"]["usePid"].is<bool>()) { if (src["emergency"]["usePid"].is<bool>()) {
bool value = src["emergency"]["usePid"].as<bool>(); bool value = src["emergency"]["usePid"].as<bool>();
if (!dst.opentherm.nativeHeatingControl && dst.sensors.indoor.type != SensorType::MANUAL) { if (!dst.opentherm.nativeHeatingControl && dst.sensors.indoor.type != SensorType::MANUAL && dst.sensors.indoor.type != SensorType::BLUETOOTH) {
if (value != dst.emergency.usePid) { if (value != dst.emergency.usePid) {
dst.emergency.usePid = value; dst.emergency.usePid = value;
changed = true; changed = true;
@@ -937,6 +951,26 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false
changed = true; changed = true;
} }
} }
if (src["emergency"]["onIndoorSensorDisconnect"].is<bool>()) {
bool value = src["emergency"]["onIndoorSensorDisconnect"].as<bool>();
if (value != dst.emergency.onIndoorSensorDisconnect) {
dst.emergency.onIndoorSensorDisconnect = value;
dst.emergency.usePid = false;
changed = true;
}
}
if (src["emergency"]["onOutdoorSensorDisconnect"].is<bool>()) {
bool value = src["emergency"]["onOutdoorSensorDisconnect"].as<bool>();
if (value != dst.emergency.onOutdoorSensorDisconnect) {
dst.emergency.onOutdoorSensorDisconnect = value;
dst.emergency.useEquitherm = false;
changed = true;
}
}
} }
@@ -1167,6 +1201,16 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false
} }
break; break;
#if USE_BLE
case static_cast<byte>(SensorType::BLUETOOTH):
if (dst.sensors.outdoor.type != SensorType::BLUETOOTH) {
dst.sensors.outdoor.type = SensorType::BLUETOOTH;
dst.emergency.useEquitherm = false;
changed = true;
}
break;
#endif
default: default:
break; break;
} }
@@ -1189,6 +1233,21 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false
} }
} }
#if USE_BLE
if (!src["sensors"]["outdoor"]["bleAddress"].isNull()) {
String value = src["sensors"]["outdoor"]["bleAddress"].as<String>();
int tmp[6];
if(sscanf(value.c_str(), "%02x:%02x:%02x:%02x:%02x:%02x", &tmp[0], &tmp[1], &tmp[2], &tmp[3], &tmp[4], &tmp[5]) == 6) {
for(uint8_t i = 0; i < 6; i++) {
if (dst.sensors.outdoor.bleAddress[i] != (uint8_t) tmp[i]) {
dst.sensors.outdoor.bleAddress[i] = (uint8_t) tmp[i];
changed = true;
}
}
}
}
#endif
if (!src["sensors"]["outdoor"]["offset"].isNull()) { if (!src["sensors"]["outdoor"]["offset"].isNull()) {
float value = src["sensors"]["outdoor"]["offset"].as<float>(); float value = src["sensors"]["outdoor"]["offset"].as<float>();
@@ -1222,6 +1281,7 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false
case static_cast<byte>(SensorType::BLUETOOTH): case static_cast<byte>(SensorType::BLUETOOTH):
if (dst.sensors.indoor.type != SensorType::BLUETOOTH) { if (dst.sensors.indoor.type != SensorType::BLUETOOTH) {
dst.sensors.indoor.type = SensorType::BLUETOOTH; dst.sensors.indoor.type = SensorType::BLUETOOTH;
dst.emergency.usePid = false;
changed = true; changed = true;
} }
break; break;
@@ -1439,6 +1499,14 @@ void varsToJson(const Variables& src, JsonVariant dst) {
dst["sensors"]["faultCode"] = src.sensors.faultCode; dst["sensors"]["faultCode"] = src.sensors.faultCode;
dst["sensors"]["rssi"] = src.sensors.rssi; dst["sensors"]["rssi"] = src.sensors.rssi;
dst["sensors"]["uptime"] = millis() / 1000ul; dst["sensors"]["uptime"] = millis() / 1000ul;
dst["sensors"]["outdoor"]["connected"] = src.sensors.outdoor.connected;
dst["sensors"]["outdoor"]["rssi"] = src.sensors.outdoor.rssi;
dst["sensors"]["outdoor"]["battery"] = roundd(src.sensors.outdoor.battery, 2);
dst["sensors"]["outdoor"]["humidity"] = roundd(src.sensors.outdoor.humidity, 2);
dst["sensors"]["indoor"]["connected"] = src.sensors.indoor.connected;
dst["sensors"]["indoor"]["rssi"] = src.sensors.indoor.rssi;
dst["sensors"]["indoor"]["battery"] = roundd(src.sensors.indoor.battery, 2);
dst["sensors"]["indoor"]["humidity"] = roundd(src.sensors.indoor.humidity, 2);
dst["temperatures"]["indoor"] = roundd(src.temperatures.indoor, 2); dst["temperatures"]["indoor"] = roundd(src.temperatures.indoor, 2);
dst["temperatures"]["outdoor"] = roundd(src.temperatures.outdoor, 2); dst["temperatures"]["outdoor"] = roundd(src.temperatures.outdoor, 2);

View File

@@ -8,6 +8,7 @@
"issues": "Issues & questions", "issues": "Issues & questions",
"releases": "Releases" "releases": "Releases"
}, },
"dbm": "dBm",
"button": { "button": {
"upgrade": "Upgrade", "upgrade": "Upgrade",
@@ -87,6 +88,14 @@
"fault": "Fault", "fault": "Fault",
"diag": "Diagnostic", "diag": "Diagnostic",
"extpump": "External pump", "extpump": "External pump",
"outdoorSensorConnected": "Outdoor sensor connected",
"outdoorSensorRssi": "Outdoor sensor RSSI",
"outdoorSensorHumidity": "Outdoor sensor humidity",
"outdoorSensorBattery": "Outdoor sensor battery",
"indoorSensorConnected": "Indoor sensor connected",
"indoorSensorRssi": "Indoor sensor RSSI",
"indoorSensorHumidity": "Indoor sensor humidity",
"indoorSensorBattery": "Indoor sensor battery",
"modulation": "Modulation", "modulation": "Modulation",
"pressure": "Pressure", "pressure": "Pressure",
"dhwFlowRate": "DHW flow rate", "dhwFlowRate": "DHW flow rate",
@@ -208,7 +217,7 @@
}, },
"emergency": { "emergency": {
"desc": "<b>!</b> Emergency mode can be useful <u>only</u> when using Equitherm and/or PID (when normal work) and when reporting indoor/outdoor temperature via MQTT or API. In this mode, sensor values that are reported via MQTT/API are not used.", "desc": "<b>!</b> Emergency mode can be useful <u>only</u> when using Equitherm and/or PID (when normal work) and when reporting indoor/outdoor temperature via MQTT/API/BLE. In this mode, sensor values that are reported via MQTT/API/BLE are not used.",
"target": { "target": {
"title": "Target temperature", "title": "Target temperature",
@@ -218,12 +227,14 @@
"events": { "events": {
"network": "On network fault", "network": "On network fault",
"mqtt": "On MQTT fault" "mqtt": "On MQTT fault",
"indoorSensorDisconnect": "On loss connection with indoor sensor",
"outdoorSensorDisconnect": "On loss connection with outdoor sensor"
}, },
"regulators": { "regulators": {
"equitherm": "Equitherm <small>(requires at least an external/boiler <u>outdoor</u> sensor)</small>", "equitherm": "Equitherm <small>(requires at least an external (DS18B20) or boiler <u>outdoor</u> sensor)</small>",
"pid": "PID <small>(requires at least an external/BLE <u>indoor</u> sensor)</small>" "pid": "PID <small>(requires at least an external (DS18B20) <u>indoor</u> sensor)</small>"
} }
}, },

View File

@@ -8,6 +8,7 @@
"issues": "Проблемы и вопросы", "issues": "Проблемы и вопросы",
"releases": "Релизы" "releases": "Релизы"
}, },
"dbm": "дБм",
"button": { "button": {
"upgrade": "Обновить", "upgrade": "Обновить",
@@ -87,6 +88,14 @@
"fault": "Ошибка", "fault": "Ошибка",
"diag": "Диагностика", "diag": "Диагностика",
"extpump": "Внешний насос", "extpump": "Внешний насос",
"outdoorSensorConnected": "Датчик наруж. темп.",
"outdoorSensorRssi": "RSSI датчика наруж. темп.",
"outdoorSensorHumidity": "Влажность с наруж. датчика темп.",
"outdoorSensorBattery": "Заряд наруж. датчика темп.",
"indoorSensorConnected": "Датчик внутр. темп.",
"indoorSensorRssi": "RSSI датчика внутр. темп.",
"indoorSensorHumidity": "Влажность с внутр. датчика темп.",
"indoorSensorBattery": "Заряд внутр. датчика темп.",
"modulation": "Уровень модуляции", "modulation": "Уровень модуляции",
"pressure": "Давление", "pressure": "Давление",
"dhwFlowRate": "Расход ГВС", "dhwFlowRate": "Расход ГВС",
@@ -208,7 +217,7 @@
}, },
"emergency": { "emergency": {
"desc": "<b>!</b> Аварийный режим может быть полезен <u>только</u> при использовании ПЗА и/или ПИД и при передачи наружной/внутренней температуры через MQTT или API. В этом режиме значения датчиков, передаваемые через MQTT/API, не используются.", "desc": "<b>!</b> Аварийный режим может быть полезен <u>только</u> при использовании ПЗА и/или ПИД и при передачи наружной/внутренней температуры через MQTT/API/BLE. В этом режиме значения датчиков, передаваемые через MQTT/API/BLE, не используются.",
"target": { "target": {
"title": "Целевая температура", "title": "Целевая температура",
@@ -218,12 +227,14 @@
"events": { "events": {
"network": "При отключении сети", "network": "При отключении сети",
"mqtt": "При отключении MQTT" "mqtt": "При отключении MQTT",
"indoorSensorDisconnect": "При потере связи с датчиком внутренней темп.",
"outdoorSensorDisconnect": "При потере связи с датчиком наружной темп."
}, },
"regulators": { "regulators": {
"equitherm": "ПЗА <small>(требуется внешний или подключенный к котлу датчик <u>наружной</u> температуры)</small>", "equitherm": "ПЗА <small>(требуется внешний (DS18B20) или подключенный к котлу датчик <u>наружной</u> температуры)</small>",
"pid": "ПИД <small>(требуется внешний/BLE датчик <u>внутренней</u> температуры)</small>" "pid": "ПИД <small>(требуется внешний (DS18B20) датчик <u>внутренней</u> температуры)</small>"
} }
}, },

View File

@@ -114,6 +114,38 @@
<th scope="row" data-i18n>dashboard.state.extpump</th> <th scope="row" data-i18n>dashboard.state.extpump</th>
<td><input type="radio" id="ot-external-pump" aria-invalid="false" checked disabled /></td> <td><input type="radio" id="ot-external-pump" aria-invalid="false" checked disabled /></td>
</tr> </tr>
<tr>
<th scope="row" data-i18n>dashboard.state.outdoorSensorConnected</th>
<td><input type="radio" id="outdoor-sensor-connected" aria-invalid="false" checked disabled /></td>
</tr>
<tr>
<th scope="row" data-i18n>dashboard.state.outdoorSensorRssi</th>
<td><b id="outdoor-sensor-rssi"></b> <span data-i18n>dbm</span></td>
</tr>
<tr>
<th scope="row" data-i18n>dashboard.state.outdoorSensorHumidity</th>
<td><b id="outdoor-sensor-humidity"></b> %</td>
</tr>
<tr>
<th scope="row" data-i18n>dashboard.state.outdoorSensorBattery</th>
<td><b id="outdoor-sensor-battery"></b> %</td>
</tr>
<tr>
<th scope="row" data-i18n>dashboard.state.indoorSensorConnected</th>
<td><input type="radio" id="indoor-sensor-connected" aria-invalid="false" checked disabled /></td>
</tr>
<tr>
<th scope="row" data-i18n>dashboard.state.indoorSensorRssi</th>
<td><b id="indoor-sensor-rssi"></b> <span data-i18n>dbm</span></td>
</tr>
<tr>
<th scope="row" data-i18n>dashboard.state.indoorSensorHumidity</th>
<td><b id="indoor-sensor-humidity"></b> %</td>
</tr>
<tr>
<th scope="row" data-i18n>dashboard.state.indoorSensorBattery</th>
<td><b id="indoor-sensor-battery"></b> %</td>
</tr>
<tr> <tr>
<th scope="row" data-i18n>dashboard.state.modulation</th> <th scope="row" data-i18n>dashboard.state.modulation</th>
<td><b id="ot-modulation"></b> %</td> <td><b id="ot-modulation"></b> %</td>
@@ -377,6 +409,7 @@
setValue('#thermostat-dhw-current', result.temperatures.dhw); setValue('#thermostat-dhw-current', result.temperatures.dhw);
setState('#ot-connected', result.states.otStatus); setState('#ot-connected', result.states.otStatus);
setState('#mqtt-connected', result.states.mqtt);
setState('#ot-emergency', result.states.emergency); setState('#ot-emergency', result.states.emergency);
setState('#ot-heating', result.states.heating); setState('#ot-heating', result.states.heating);
setState('#ot-dhw', result.states.dhw); setState('#ot-dhw', result.states.dhw);
@@ -384,7 +417,15 @@
setState('#ot-fault', result.states.fault); setState('#ot-fault', result.states.fault);
setState('#ot-diagnostic', result.states.diagnostic); setState('#ot-diagnostic', result.states.diagnostic);
setState('#ot-external-pump', result.states.externalPump); setState('#ot-external-pump', result.states.externalPump);
setState('#mqtt-connected', result.states.mqtt); setState('#outdoor-sensor-connected', result.sensors.outdoor.connected);
setState('#indoor-sensor-connected', result.sensors.indoor.connected);
setValue('#outdoor-sensor-rssi', result.sensors.outdoor.rssi);
setValue('#outdoor-sensor-humidity', result.sensors.outdoor.humidity);
setValue('#outdoor-sensor-battery', result.sensors.outdoor.battery);
setValue('#indoor-sensor-rssi', result.sensors.indoor.rssi);
setValue('#indoor-sensor-humidity', result.sensors.indoor.humidity);
setValue('#indoor-sensor-battery', result.sensors.indoor.battery);
setValue('#ot-modulation', result.sensors.modulation); setValue('#ot-modulation', result.sensors.modulation);
setValue('#ot-pressure', result.sensors.pressure); setValue('#ot-pressure', result.sensors.pressure);

View File

@@ -233,6 +233,16 @@
<input type="checkbox" id="emergency-on-mqtt-fault" name="emergency[onMqttFault]" value="true"> <input type="checkbox" id="emergency-on-mqtt-fault" name="emergency[onMqttFault]" value="true">
<span data-i18n>settings.emergency.events.mqtt</span> <span data-i18n>settings.emergency.events.mqtt</span>
</label> </label>
<label for="emergency-on-indoor-sensor-disconnect">
<input type="checkbox" id="emergency-on-indoor-sensor-disconnect" name="emergency[onIndoorSensorDisconnect]" value="true">
<span data-i18n>settings.emergency.events.indoorSensorDisconnect</span>
</label>
<label for="emergency-on-outdoor-sensor-disconnect">
<input type="checkbox" id="emergency-on-outdoor-sensor-disconnect" name="emergency[onOutdoorSensorDisconnect]" value="true">
<span data-i18n>settings.emergency.events.outdoorSensorDisconnect</span>
</label>
</fieldset> </fieldset>
<fieldset> <fieldset>
@@ -544,6 +554,11 @@
<input type="radio" class="outdoor-sensor-type" name="sensors[outdoor][type]" value="2" /> <input type="radio" class="outdoor-sensor-type" name="sensors[outdoor][type]" value="2" />
<span data-i18n>settings.tempSensor.source.ext</span> <span data-i18n>settings.tempSensor.source.ext</span>
</label> </label>
<label>
<input type="radio" class="outdoor-sensor-type" name="sensors[outdoor][type]" value="3" />
<span data-i18n>settings.tempSensor.source.ble</span>
</label>
</fieldset> </fieldset>
<label for="outdoor-sensor-gpio"> <label for="outdoor-sensor-gpio">
@@ -551,10 +566,18 @@
<input type="number" inputmode="numeric" id="outdoor-sensor-gpio" name="sensors[outdoor][gpio]" min="0" max="254" step="1"> <input type="number" inputmode="numeric" id="outdoor-sensor-gpio" name="sensors[outdoor][gpio]" min="0" max="254" step="1">
</label> </label>
<label for="outdoor-sensor-offset"> <div class="grid">
<span data-i18n>settings.tempSensor.offset</span> <label for="outdoor-sensor-offset">
<input type="number" inputmode="numeric" id="outdoor-sensor-offset" name="sensors[outdoor][offset]" min="-10" max="10" step="0.01" required> <span data-i18n>settings.tempSensor.offset</span>
</label> <input type="number" inputmode="numeric" id="outdoor-sensor-offset" name="sensors[outdoor][offset]" min="-10" max="10" step="0.01" required>
</label>
<label for="outdoor-sensor-ble-addresss">
<span data-i18n>settings.tempSensor.bleAddress.title</span>
<input type="text" id="outdoor-sensor-ble-addresss" name="sensors[outdoor][bleAddress]" pattern="([A-Fa-f0-9]{2}:){5}[A-Fa-f0-9]{2}">
<small data-i18n>settings.tempSensor.bleAddress.note</small>
</label>
</div>
<button type="submit" data-i18n>button.save</button> <button type="submit" data-i18n>button.save</button>
</form> </form>
@@ -724,6 +747,7 @@
setRadioValue('.outdoor-sensor-type', data.sensors.outdoor.type); setRadioValue('.outdoor-sensor-type', data.sensors.outdoor.type);
setInputValue('#outdoor-sensor-gpio', data.sensors.outdoor.gpio < 255 ? data.sensors.outdoor.gpio : ''); setInputValue('#outdoor-sensor-gpio', data.sensors.outdoor.gpio < 255 ? data.sensors.outdoor.gpio : '');
setInputValue('#outdoor-sensor-offset', data.sensors.outdoor.offset); setInputValue('#outdoor-sensor-offset', data.sensors.outdoor.offset);
setInputValue('#outdoor-sensor-ble-addresss', data.sensors.outdoor.bleAddress);
setBusy('#outdoor-sensor-settings-busy', '#outdoor-sensor-settings', false); setBusy('#outdoor-sensor-settings-busy', '#outdoor-sensor-settings', false);
// Indoor sensor // Indoor sensor
@@ -772,6 +796,8 @@
setCheckboxValue('#emergency-use-pid', data.emergency.usePid); setCheckboxValue('#emergency-use-pid', data.emergency.usePid);
setCheckboxValue('#emergency-on-network-fault', data.emergency.onNetworkFault); setCheckboxValue('#emergency-on-network-fault', data.emergency.onNetworkFault);
setCheckboxValue('#emergency-on-mqtt-fault', data.emergency.onMqttFault); setCheckboxValue('#emergency-on-mqtt-fault', data.emergency.onMqttFault);
setCheckboxValue('#emergency-on-indoor-sensor-disconnect', data.emergency.onIndoorSensorDisconnect);
setCheckboxValue('#emergency-on-outdoor-sensor-disconnect', data.emergency.onOutdoorSensorDisconnect);
setInputValue('#emergency-target', data.emergency.target, { setInputValue('#emergency-target', data.emergency.target, {
"min": (!data.emergency.useEquitherm && !data.emergency.usePid) ? data.heating.minTemp : 10, "min": (!data.emergency.useEquitherm && !data.emergency.usePid) ? data.heating.minTemp : 10,
"max": (!data.emergency.useEquitherm && !data.emergency.usePid) ? data.heating.maxTemp : 30, "max": (!data.emergency.useEquitherm && !data.emergency.usePid) ? data.heating.maxTemp : 30,