From 1b2bc8e2005cdcc1acc3384ef4b3f2d6d1cb47a2 Mon Sep 17 00:00:00 2001 From: Yurii Date: Tue, 20 Aug 2024 19:06:18 +0300 Subject: [PATCH] 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 --- src/MainTask.h | 101 +++++-- src/MqttTask.h | 11 - src/RegulatorTask.h | 4 + src/SensorsTask.h | 526 ++++++++++++++++++++++++++-------- src/Settings.h | 19 +- src/utils.h | 74 ++++- src_data/locales/en.json | 19 +- src_data/locales/ru.json | 19 +- src_data/pages/dashboard.html | 43 ++- src_data/pages/settings.html | 34 ++- 10 files changed, 682 insertions(+), 168 deletions(-) diff --git a/src/MainTask.h b/src/MainTask.h index bde3e7e..1462c5e 100644 --- a/src/MainTask.h +++ b/src/MainTask.h @@ -29,7 +29,6 @@ protected: enum class PumpStartReason {NONE, HEATING, ANTISTUCK}; Blinker* blinker = nullptr; - unsigned long firstFailConnect = 0; unsigned long lastHeapInfo = 0; unsigned int minFreeHeap = 0; unsigned int minMaxFreeBlockHeap = 0; @@ -39,6 +38,8 @@ protected: PumpStartReason extPumpStartReason = PumpStartReason::NONE; unsigned long externalPumpStartTime = 0; bool telnetStarted = false; + bool emergencyDetected = false; + unsigned long emergencyFlipTime = 0; #if defined(ARDUINO_ARCH_ESP32) const char* getTaskName() override { @@ -85,10 +86,6 @@ protected: vars.states.mqtt = tMqtt->isConnected(); vars.sensors.rssi = network->isConnected() ? WiFi.RSSI() : 0; - if (vars.states.emergency && !settings.emergency.enable) { - vars.states.emergency = false; - } - if (network->isConnected()) { if (!this->telnetStarted && telnetStream != nullptr) { telnetStream->begin(23, false); @@ -102,17 +99,6 @@ protected: 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 ) { Log.setLevel(TinyLogger::Level::INFO); @@ -129,21 +115,10 @@ protected: if (tMqtt->isEnabled()) { 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->emergency(); this->ledStatus(); this->externalPump(); 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() { uint8_t errors[4]; uint8_t errCount = 0; diff --git a/src/MqttTask.h b/src/MqttTask.h index a274abb..479d800 100644 --- a/src/MqttTask.h +++ b/src/MqttTask.h @@ -198,17 +198,6 @@ protected: 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) { return; } diff --git a/src/RegulatorTask.h b/src/RegulatorTask.h index a933b91..ea32b67 100644 --- a/src/RegulatorTask.h +++ b/src/RegulatorTask.h @@ -198,6 +198,10 @@ protected: etRegulator.Kt = 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 { etRegulator.Kt = settings.equitherm.t_factor; etRegulator.indoorTemp = indoorTemp; diff --git a/src/SensorsTask.h b/src/SensorsTask.h index b5cb9e2..3b77cdb 100644 --- a/src/SensorsTask.h +++ b/src/SensorsTask.h @@ -35,19 +35,18 @@ protected: unsigned long initOutdoorSensorTime = 0; unsigned long startOutdoorConversionTime = 0; float filteredOutdoorTemp = 0; - bool emptyOutdoorTemp = true; + float prevFilteredOutdoorTemp = 0; bool initIndoorSensor = false; unsigned long initIndoorSensorTime = 0; unsigned long startIndoorConversionTime = 0; float filteredIndoorTemp = 0; - bool emptyIndoorTemp = true; + float prevFilteredIndoorTemp = 0; #if defined(ARDUINO_ARCH_ESP32) #if USE_BLE - BLEClient* pBleClient = nullptr; - bool initBleSensor = false; - bool initBleNotify = false; + unsigned long outdoorConnectedTime = 0; + unsigned long indoorConnectedTime = 0; #endif const char* getTaskName() override { @@ -69,26 +68,62 @@ protected: #endif 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 - else if (settings.sensors.indoor.type == SensorType::BLUETOOTH) { - indoorTemperatureBluetoothSensor(); - indoorTempUpdated = true; + if (!NimBLEDevice::getInitialized() && millis() > 5000) { + Log.sinfoln(FPSTR(L_SENSORS_BLE), F("Init BLE")); + BLEDevice::init(""); + NimBLEDevice::setPower(ESP_PWR_LVL_P9); } #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; if (settings.system.unitSystem == UnitSystem::METRIC) { newTemp += this->filteredOutdoorTemp; @@ -101,9 +136,11 @@ protected: vars.temperatures.outdoor = newTemp; 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; if (settings.system.unitSystem == UnitSystem::METRIC) { newTemp += this->filteredIndoorTemp; @@ -116,127 +153,378 @@ protected: vars.temperatures.indoor = newTemp; Log.sinfoln(FPSTR(L_SENSORS_INDOOR), F("New temp: %f"), vars.temperatures.indoor); } + + this->prevFilteredIndoorTemp = this->filteredIndoorTemp; } } #if USE_BLE - void indoorTemperatureBluetoothSensor() { - static bool initBleNotify = false; - if (!initBleSensor && millis() > 5000) { - Log.sinfoln(FPSTR(L_SENSORS_BLE), F("Init BLE")); - BLEDevice::init(""); - - pBleClient = BLEDevice::createClient(); - pBleClient->setConnectTimeout(5); - - initBleSensor = true; + bool bluetoothSensor(const BLEAddress& address, int8_t* const pRssi, float* const pTemperature, float* const pHumidity = nullptr, float* const pBattery = nullptr) { + if (!NimBLEDevice::getInitialized()) { + return false; } - if (!initBleSensor || pBleClient->isConnected()) { - return; - } - - // Reset init notify flag - this->initBleNotify = false; + NimBLEClient* pClient = nullptr; + pClient = NimBLEDevice::getClientByPeerAddress(address); - // Connect to the remote BLE Server. - BLEAddress bleServerAddress(settings.sensors.indoor.bleAddress); - if (!pBleClient->connect(bleServerAddress)) { - Log.swarningln(FPSTR(L_SENSORS_BLE), "Failed connecting to device at %s", bleServerAddress.toString().c_str()); - return; + if (pClient == nullptr) { + pClient = NimBLEDevice::getDisconnectedClient(); } - 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); - BLERemoteService* pRemoteService = pBleClient->getService(serviceUUID); - if (!pRemoteService) { - Log.straceln(FPSTR(L_SENSORS_BLE), F("Failed to find service UUID: %s"), serviceUUID.toString().c_str()); - return; + pClient = NimBLEDevice::createClient(); + pClient->setConnectTimeout(5); } - 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 (!this->initBleNotify) { - 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()); + if (!pClient->connect(address)) { + Log.swarningln(FPSTR(L_SENSORS_BLE), "Device %s: failed connecting", address.toString().c_str()); - this->initBleNotify = pRemoteCharacteristic->subscribe(true, [this](NimBLERemoteCharacteristic*, uint8_t* pData, size_t length, bool isNotify) { - if (length != 2) { - Log.swarningln(FPSTR(L_SENSORS_BLE), F("Invalid notification data")); - return; - } + NimBLEDevice::deleteClient(pClient); + return false; + } - float rawTemp = ((pData[0] | (pData[1] << 8)) * 0.01f); - Log.straceln(FPSTR(L_SENSORS_INDOOR), F("Raw temp: %f"), rawTemp); + Log.sinfoln(FPSTR(L_SENSORS_BLE), "Device %s: connected", address.toString().c_str()); + NimBLERemoteService* pService = nullptr; + NimBLERemoteCharacteristic* pChar = nullptr; - if (this->emptyIndoorTemp) { - this->filteredIndoorTemp = rawTemp; - this->emptyIndoorTemp = false; + // ENV Service (0x181A) + pService = pClient->getService(NimBLEUUID((uint16_t) 0x181AU)); + 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 { - 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) { - Log.straceln(FPSTR(L_SENSORS_BLE), F("Subscribed to characteristic UUID: %s"), charUUID.toString().c_str()); + // 0x2A1F - Notify temperature x0.1C (atc1441/pvvx) + if (!tempNotifyCreated) { + pChar = pService->getCharacteristic(NimBLEUUID((uint16_t) 0x2A1F)); - } else { - Log.swarningln(FPSTR(L_SENSORS_BLE), F("Failed to subscribe to characteristic UUID: %s"), charUUID.toString().c_str()); + 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.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) { - if (length != 2) { - Log.swarningln(FPSTR(L_SENSORS_BLE), F("Invalid notification data")); - return; + // Battery Service (0x180F) + if (pBattery != nullptr) { + pService = pClient->getService(NimBLEUUID((uint16_t) 0x180F)); + 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); - Log.straceln(FPSTR(L_SENSORS_INDOOR), F("Raw temp: %f"), rawTemp); - - if (this->emptyIndoorTemp) { - this->filteredIndoorTemp = rawTemp; - 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 (!batteryNotifyCreated) { + Log.swarningln( + FPSTR(L_SENSORS_BLE), + F("Device %s: not found supported battery chars in battery service"), + address.toString().c_str() + ); } } } - if (!this->initBleNotify) { - Log.swarningln(FPSTR(L_SENSORS_BLE), F("Not found supported characteristics")); - pBleClient->disconnect(); - } + return true; } #endif - void outdoorTemperatureSensor() { + void outdoorDallasSensor() { if (!this->initOutdoorSensor) { if (this->initOutdoorSensorTime && millis() - this->initOutdoorSensorTime < EXT_SENSORS_INTERVAL * 10) { return; @@ -265,6 +553,10 @@ protected: Log.sinfoln(FPSTR(L_SENSORS_OUTDOOR), F("Started")); } else { + if (vars.sensors.outdoor.connected) { + vars.sensors.outdoor.connected = false; + } + return; } } @@ -294,9 +586,12 @@ protected: } else { 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->emptyOutdoorTemp = false; } else { this->filteredOutdoorTemp += (rawTemp - this->filteredOutdoorTemp) * EXT_SENSORS_FILTER_K; @@ -308,7 +603,7 @@ protected: } } - void indoorTemperatureSensor() { + void indoorDallasSensor() { if (!this->initIndoorSensor) { if (this->initIndoorSensorTime && millis() - this->initIndoorSensorTime < EXT_SENSORS_INTERVAL * 10) { return; @@ -337,6 +632,10 @@ protected: Log.sinfoln(FPSTR(L_SENSORS_INDOOR), F("Started")); } else { + if (vars.sensors.indoor.connected) { + vars.sensors.indoor.connected = false; + } + return; } } @@ -366,9 +665,12 @@ protected: } else { 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->emptyIndoorTemp = false; } else { this->filteredIndoorTemp += (rawTemp - this->filteredIndoorTemp) * EXT_SENSORS_FILTER_K; diff --git a/src/Settings.h b/src/Settings.h index 43dfa27..75d8d37 100644 --- a/src/Settings.h +++ b/src/Settings.h @@ -77,13 +77,15 @@ struct Settings { } mqtt; struct { - bool enable = true; + bool enable = false; float target = DEFAULT_HEATING_TARGET_TEMP; unsigned short tresholdTime = 120; bool useEquitherm = false; bool usePid = false; bool onNetworkFault = true; bool onMqttFault = true; + bool onIndoorSensorDisconnect = false; + bool onOutdoorSensorDisconnect = false; } emergency; struct { @@ -124,6 +126,7 @@ struct Settings { struct { SensorType type = SensorType::BOILER; byte gpio = DEFAULT_SENSOR_OUTDOOR_GPIO; + uint8_t bleAddress[6] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; float offset = 0.0f; } outdoor; @@ -165,6 +168,20 @@ struct Variables { float dhwFlowRate = 0.0f; byte faultCode = 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; struct { diff --git a/src/utils.h b/src/utils.h index 779c09b..8b63434 100644 --- a/src/utils.h +++ b/src/utils.h @@ -371,6 +371,8 @@ void settingsToJson(const Settings& src, JsonVariant dst, bool safe = false) { dst["emergency"]["usePid"] = src.emergency.usePid; dst["emergency"]["onNetworkFault"] = src.emergency.onNetworkFault; dst["emergency"]["onMqttFault"] = src.emergency.onMqttFault; + dst["emergency"]["onIndoorSensorDisconnect"] = src.emergency.onIndoorSensorDisconnect; + dst["emergency"]["onOutdoorSensorDisconnect"] = src.emergency.onOutdoorSensorDisconnect; } 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(src.sensors.outdoor.type); 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"]["indoor"]["type"] = static_cast(src.sensors.indoor.type); dst["sensors"]["indoor"]["gpio"] = src.sensors.indoor.gpio; - char bleAddress[18]; sprintf( bleAddress, "%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 value = src["emergency"]["useEquitherm"].as(); - 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) { dst.emergency.useEquitherm = value; changed = true; @@ -903,7 +917,7 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false if (src["emergency"]["usePid"].is()) { bool value = src["emergency"]["usePid"].as(); - 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) { dst.emergency.usePid = value; changed = true; @@ -937,6 +951,26 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false changed = true; } } + + if (src["emergency"]["onIndoorSensorDisconnect"].is()) { + bool value = src["emergency"]["onIndoorSensorDisconnect"].as(); + + if (value != dst.emergency.onIndoorSensorDisconnect) { + dst.emergency.onIndoorSensorDisconnect = value; + dst.emergency.usePid = false; + changed = true; + } + } + + if (src["emergency"]["onOutdoorSensorDisconnect"].is()) { + bool value = src["emergency"]["onOutdoorSensorDisconnect"].as(); + + 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; + #if USE_BLE + case static_cast(SensorType::BLUETOOTH): + if (dst.sensors.outdoor.type != SensorType::BLUETOOTH) { + dst.sensors.outdoor.type = SensorType::BLUETOOTH; + dst.emergency.useEquitherm = false; + changed = true; + } + break; + #endif + default: 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(); + 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()) { float value = src["sensors"]["outdoor"]["offset"].as(); @@ -1222,6 +1281,7 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false case static_cast(SensorType::BLUETOOTH): if (dst.sensors.indoor.type != SensorType::BLUETOOTH) { dst.sensors.indoor.type = SensorType::BLUETOOTH; + dst.emergency.usePid = false; changed = true; } break; @@ -1439,6 +1499,14 @@ void varsToJson(const Variables& src, JsonVariant dst) { dst["sensors"]["faultCode"] = src.sensors.faultCode; dst["sensors"]["rssi"] = src.sensors.rssi; 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"]["outdoor"] = roundd(src.temperatures.outdoor, 2); diff --git a/src_data/locales/en.json b/src_data/locales/en.json index f86c061..eb91d88 100644 --- a/src_data/locales/en.json +++ b/src_data/locales/en.json @@ -8,6 +8,7 @@ "issues": "Issues & questions", "releases": "Releases" }, + "dbm": "dBm", "button": { "upgrade": "Upgrade", @@ -87,6 +88,14 @@ "fault": "Fault", "diag": "Diagnostic", "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", "pressure": "Pressure", "dhwFlowRate": "DHW flow rate", @@ -208,7 +217,7 @@ }, "emergency": { - "desc": "! Emergency mode can be useful only 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": "! Emergency mode can be useful only 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": { "title": "Target temperature", @@ -218,12 +227,14 @@ "events": { "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": { - "equitherm": "Equitherm (requires at least an external/boiler outdoor sensor)", - "pid": "PID (requires at least an external/BLE indoor sensor)" + "equitherm": "Equitherm (requires at least an external (DS18B20) or boiler outdoor sensor)", + "pid": "PID (requires at least an external (DS18B20) indoor sensor)" } }, diff --git a/src_data/locales/ru.json b/src_data/locales/ru.json index 4349836..cbb581e 100644 --- a/src_data/locales/ru.json +++ b/src_data/locales/ru.json @@ -8,6 +8,7 @@ "issues": "Проблемы и вопросы", "releases": "Релизы" }, + "dbm": "дБм", "button": { "upgrade": "Обновить", @@ -87,6 +88,14 @@ "fault": "Ошибка", "diag": "Диагностика", "extpump": "Внешний насос", + "outdoorSensorConnected": "Датчик наруж. темп.", + "outdoorSensorRssi": "RSSI датчика наруж. темп.", + "outdoorSensorHumidity": "Влажность с наруж. датчика темп.", + "outdoorSensorBattery": "Заряд наруж. датчика темп.", + "indoorSensorConnected": "Датчик внутр. темп.", + "indoorSensorRssi": "RSSI датчика внутр. темп.", + "indoorSensorHumidity": "Влажность с внутр. датчика темп.", + "indoorSensorBattery": "Заряд внутр. датчика темп.", "modulation": "Уровень модуляции", "pressure": "Давление", "dhwFlowRate": "Расход ГВС", @@ -208,7 +217,7 @@ }, "emergency": { - "desc": "! Аварийный режим может быть полезен только при использовании ПЗА и/или ПИД и при передачи наружной/внутренней температуры через MQTT или API. В этом режиме значения датчиков, передаваемые через MQTT/API, не используются.", + "desc": "! Аварийный режим может быть полезен только при использовании ПЗА и/или ПИД и при передачи наружной/внутренней температуры через MQTT/API/BLE. В этом режиме значения датчиков, передаваемые через MQTT/API/BLE, не используются.", "target": { "title": "Целевая температура", @@ -218,12 +227,14 @@ "events": { "network": "При отключении сети", - "mqtt": "При отключении MQTT" + "mqtt": "При отключении MQTT", + "indoorSensorDisconnect": "При потере связи с датчиком внутренней темп.", + "outdoorSensorDisconnect": "При потере связи с датчиком наружной темп." }, "regulators": { - "equitherm": "ПЗА (требуется внешний или подключенный к котлу датчик наружной температуры)", - "pid": "ПИД (требуется внешний/BLE датчик внутренней температуры)" + "equitherm": "ПЗА (требуется внешний (DS18B20) или подключенный к котлу датчик наружной температуры)", + "pid": "ПИД (требуется внешний (DS18B20) датчик внутренней температуры)" } }, diff --git a/src_data/pages/dashboard.html b/src_data/pages/dashboard.html index adfe22d..4f05b15 100644 --- a/src_data/pages/dashboard.html +++ b/src_data/pages/dashboard.html @@ -114,6 +114,38 @@ dashboard.state.extpump + + dashboard.state.outdoorSensorConnected + + + + dashboard.state.outdoorSensorRssi + dbm + + + dashboard.state.outdoorSensorHumidity + % + + + dashboard.state.outdoorSensorBattery + % + + + dashboard.state.indoorSensorConnected + + + + dashboard.state.indoorSensorRssi + dbm + + + dashboard.state.indoorSensorHumidity + % + + + dashboard.state.indoorSensorBattery + % + dashboard.state.modulation % @@ -377,6 +409,7 @@ setValue('#thermostat-dhw-current', result.temperatures.dhw); setState('#ot-connected', result.states.otStatus); + setState('#mqtt-connected', result.states.mqtt); setState('#ot-emergency', result.states.emergency); setState('#ot-heating', result.states.heating); setState('#ot-dhw', result.states.dhw); @@ -384,7 +417,15 @@ setState('#ot-fault', result.states.fault); setState('#ot-diagnostic', result.states.diagnostic); 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-pressure', result.sensors.pressure); diff --git a/src_data/pages/settings.html b/src_data/pages/settings.html index 2a45e9e..e16371e 100644 --- a/src_data/pages/settings.html +++ b/src_data/pages/settings.html @@ -233,6 +233,16 @@ settings.emergency.events.mqtt + + + +
@@ -544,6 +554,11 @@ settings.tempSensor.source.ext + +
- +
+ + + +
@@ -724,6 +747,7 @@ setRadioValue('.outdoor-sensor-type', data.sensors.outdoor.type); setInputValue('#outdoor-sensor-gpio', data.sensors.outdoor.gpio < 255 ? data.sensors.outdoor.gpio : ''); 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); // Indoor sensor @@ -772,6 +796,8 @@ setCheckboxValue('#emergency-use-pid', data.emergency.usePid); setCheckboxValue('#emergency-on-network-fault', data.emergency.onNetworkFault); 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, { "min": (!data.emergency.useEquitherm && !data.emergency.usePid) ? data.heating.minTemp : 10, "max": (!data.emergency.useEquitherm && !data.emergency.usePid) ? data.heating.maxTemp : 30,