diff --git a/platformio.ini b/platformio.ini index 532f828..7ea1684 100644 --- a/platformio.ini +++ b/platformio.ini @@ -51,6 +51,7 @@ platform = espressif32 lib_deps = ${env.lib_deps} laxilef/ESP32Scheduler@^1.0.1 + h2zero/NimBLE-Arduino@^1.4.1 lib_ignore = extra_scripts = post:tools/esp32.py @@ -58,6 +59,7 @@ extra_scripts = build_flags = ${env.build_flags} -D CORE_DEBUG_LEVEL=0 + -D USE_BLE=1 ; Boards @@ -175,6 +177,7 @@ board = wemos_d1_mini32 lib_deps = ${esp32_defaults.lib_deps} lib_ignore = ${esp32_defaults.lib_ignore} extra_scripts = ${esp32_defaults.extra_scripts} +;board_build.partitions = huge_app.csv build_flags = ${esp32_defaults.build_flags} -D OT_IN_PIN_DEFAULT=21 diff --git a/src/HaHelper.h b/src/HaHelper.h index 03865ac..90319bb 100644 --- a/src/HaHelper.h +++ b/src/HaHelper.h @@ -29,16 +29,17 @@ public: bool publishSelectIndoorSensorType(bool enabledByDefault = true) { StaticJsonDocument<1536> doc; doc[FPSTR(HA_COMMAND_TOPIC)] = devicePrefix + F("/settings/set"); - doc[FPSTR(HA_COMMAND_TEMPLATE)] = F("{\"sensors\": {\"indoor\": {\"type\": {% if value == 'Manual' %}1{% elif value == 'External' %}2{% endif %}}}}"); + doc[FPSTR(HA_COMMAND_TEMPLATE)] = F("{\"sensors\": {\"indoor\": {\"type\": {% if value == 'Manual' %}1{% elif value == 'External' %}2{% elif value == 'Bluetooth' %}3{% endif %}}}}"); doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; doc[FPSTR(HA_UNIQUE_ID)] = devicePrefix + F("_indoor_sensor_type"); doc[FPSTR(HA_OBJECT_ID)] = devicePrefix + F("_indoor_sensor_type"); doc[FPSTR(HA_ENTITY_CATEGORY)] = F("config"); doc[FPSTR(HA_NAME)] = F("Indoor temperature source"); doc[FPSTR(HA_STATE_TOPIC)] = devicePrefix + F("/settings"); - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{% if value_json.sensors.indoor.type == 1 %}Manual{% elif value_json.sensors.indoor.type == 2 %}External{% endif %}"); + doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{% if value_json.sensors.indoor.type == 1 %}Manual{% elif value_json.sensors.indoor.type == 2 %}External{% elif value_json.sensors.indoor.type == 3 %}Bluetooth{% endif %}"); doc[FPSTR(HA_OPTIONS)][0] = F("Manual"); doc[FPSTR(HA_OPTIONS)][1] = F("External"); + doc[FPSTR(HA_OPTIONS)][2] = F("Bluetooth"); return publish(getTopic("select", "indoor_sensor_type").c_str(), doc); } diff --git a/src/MqttTask.h b/src/MqttTask.h index a30b2d3..e8c8c8a 100644 --- a/src/MqttTask.h +++ b/src/MqttTask.h @@ -299,7 +299,7 @@ protected: } if (!doc["sensors"]["indoor"]["type"].isNull() && doc["sensors"]["indoor"]["type"].is()) { - if (doc["sensors"]["indoor"]["type"].as() >= 1 && doc["sensors"]["indoor"]["type"].as() <= 2) { + if (doc["sensors"]["indoor"]["type"].as() >= 1 && doc["sensors"]["indoor"]["type"].as() <= 3) { settings.sensors.indoor.type = doc["sensors"]["indoor"]["type"].as(); flag = true; } diff --git a/src/OpenThermTask.h b/src/OpenThermTask.h index da792d1..bfd5bf0 100644 --- a/src/OpenThermTask.h +++ b/src/OpenThermTask.h @@ -394,7 +394,7 @@ protected: } bool updateSlaveConfig() { - unsigned long response = ot->sendRequest(ot->buildRequest(OpenThermRequestType::READ, OpenThermMessageID::SConfigSMemberIDcode, 0)); + unsigned long response = ot->sendRequest(ot->buildRequest(OpenThermRequestType::READ_DATA, OpenThermMessageID::SConfigSMemberIDcode, 0)); if (!ot->isValidResponse(response)) { return false; } @@ -451,7 +451,7 @@ protected: } unsigned long response = ot->sendRequest(ot->buildRequest( - OpenThermRequestType::WRITE, + OpenThermRequestType::WRITE_DATA, OpenThermMessageID::MConfigMMemberIDcode, request )); @@ -461,7 +461,7 @@ protected: bool setMaxModulationLevel(byte value) { unsigned long response = ot->sendRequest(ot->buildRequest( - OpenThermRequestType::WRITE, + OpenThermRequestType::WRITE_DATA, OpenThermMessageID::MaxRelModLevelSetting, ot->toF88(value) )); @@ -475,7 +475,7 @@ protected: } bool updateSlaveOtVersion() { - unsigned long response = ot->sendRequest(ot->buildRequest(OpenThermRequestType::READ, OpenThermMessageID::OpenThermVersionSlave, 0)); + unsigned long response = ot->sendRequest(ot->buildRequest(OpenThermRequestType::READ_DATA, OpenThermMessageID::OpenThermVersionSlave, 0)); if (!ot->isValidResponse(response)) { return false; } @@ -501,7 +501,7 @@ protected: } bool updateSlaveVersion() { - unsigned long response = ot->sendRequest(ot->buildRequest(OpenThermRequestType::READ, OpenThermMessageID::SlaveVersion, 0)); + unsigned long response = ot->sendRequest(ot->buildRequest(OpenThermRequestType::READ_DATA, OpenThermMessageID::SlaveVersion, 0)); if (!ot->isValidResponse(response)) { return false; } @@ -514,7 +514,7 @@ protected: bool setMasterVersion(uint8_t version, uint8_t type) { unsigned long response = ot->sendRequest(ot->buildRequest( - OpenThermRequestType::WRITE, + OpenThermRequestType::WRITE_DATA, OpenThermMessageID::MasterVersion, (unsigned int) version | (unsigned int) type << 8 // 0x013F )); @@ -530,7 +530,7 @@ protected: } bool updateMinMaxDhwTemp() { - unsigned long response = ot->sendRequest(ot->buildRequest(OpenThermRequestType::READ, OpenThermMessageID::TdhwSetUBTdhwSetLB, 0)); + unsigned long response = ot->sendRequest(ot->buildRequest(OpenThermRequestType::READ_DATA, OpenThermMessageID::TdhwSetUBTdhwSetLB, 0)); if (!ot->isValidResponse(response)) { return false; } @@ -549,7 +549,7 @@ protected: } bool updateMinMaxHeatingTemp() { - unsigned long response = ot->sendRequest(ot->buildRequest(OpenThermRequestType::READ, OpenThermMessageID::MaxTSetUBMaxTSetLB, 0)); + unsigned long response = ot->sendRequest(ot->buildRequest(OpenThermRequestType::READ_DATA, OpenThermMessageID::MaxTSetUBMaxTSetLB, 0)); if (!ot->isValidResponse(response)) { return false; } @@ -576,7 +576,7 @@ protected: } bool updateOutsideTemp() { - unsigned long response = ot->sendRequest(ot->buildRequest(OpenThermRequestType::READ, OpenThermMessageID::Toutside, 0)); + unsigned long response = ot->sendRequest(ot->buildRequest(OpenThermRequestType::READ_DATA, OpenThermMessageID::Toutside, 0)); if (!ot->isValidResponse(response)) { return false; } @@ -597,7 +597,7 @@ protected: bool updateDhwTemp() { - unsigned long response = ot->sendRequest(ot->buildRequest(OpenThermMessageType::READ, OpenThermMessageID::Tdhw, 0)); + unsigned long response = ot->sendRequest(ot->buildRequest(OpenThermMessageType::READ_DATA, OpenThermMessageID::Tdhw, 0)); if (!ot->isValidResponse(response)) { return false; } @@ -607,7 +607,7 @@ protected: } bool updateDhwFlowRate() { - unsigned long response = ot->sendRequest(ot->buildRequest(OpenThermMessageType::READ, OpenThermMessageID::DHWFlowRate, 0)); + unsigned long response = ot->sendRequest(ot->buildRequest(OpenThermMessageType::READ_DATA, OpenThermMessageID::DHWFlowRate, 0)); if (!ot->isValidResponse(response)) { return false; } @@ -617,7 +617,7 @@ protected: } bool updateFaultCode() { - unsigned long response = ot->sendRequest(ot->buildRequest(OpenThermRequestType::READ, OpenThermMessageID::ASFflags, 0)); + unsigned long response = ot->sendRequest(ot->buildRequest(OpenThermRequestType::READ_DATA, OpenThermMessageID::ASFflags, 0)); if (!ot->isValidResponse(response)) { return false; @@ -628,7 +628,7 @@ protected: } bool updateModulationLevel() { - unsigned long response = ot->sendRequest(ot->buildRequest(OpenThermRequestType::READ, OpenThermMessageID::RelModLevel, 0)); + unsigned long response = ot->sendRequest(ot->buildRequest(OpenThermRequestType::READ_DATA, OpenThermMessageID::RelModLevel, 0)); if (!ot->isValidResponse(response)) { return false; @@ -645,7 +645,7 @@ protected: } bool updatePressure() { - unsigned long response = ot->sendRequest(ot->buildRequest(OpenThermRequestType::READ, OpenThermMessageID::CHPressure, 0)); + unsigned long response = ot->sendRequest(ot->buildRequest(OpenThermRequestType::READ_DATA, OpenThermMessageID::CHPressure, 0)); if (!ot->isValidResponse(response)) { return false; diff --git a/src/SensorsTask.h b/src/SensorsTask.h index 497f50e..8634f58 100644 --- a/src/SensorsTask.h +++ b/src/SensorsTask.h @@ -1,8 +1,20 @@ #include #include +#if USE_BLE + #include + + // BLE services and characterstics that we are interested in + const uint16_t bleUuidServiceBattery = 0x180F; + const uint16_t bleUuidServiceEnvironment = 0x181AU; + const uint16_t bleUuidCharacteristicBatteryLevel = 0x2A19; + const uint16_t bleUuidCharacteristicTemperature = 0x2A6E; + const uint16_t bleUuidCharacteristicHumidity = 0x2A6F; +#endif + const char S_SENSORS_OUTDOOR[] PROGMEM = "SENSORS.OUTDOOR"; const char S_SENSORS_INDOOR[] PROGMEM = "SENSORS.INDOOR"; +const char S_SENSORS_BLE[] PROGMEM = "SENSORS.BLE"; class SensorsTask : public LeanTask { public: @@ -25,6 +37,12 @@ protected: float filteredIndoorTemp = 0; bool emptyIndoorTemp = true; +#if USE_BLE + BLEClient* pBleClient = nullptr; + BLERemoteService* pBleServiceBattery = nullptr; + BLERemoteService* pBleServiceEnvironment = nullptr; + bool initBleSensor = false; +#endif const char* getTaskName() { return "Sensors"; @@ -39,15 +57,71 @@ protected: } void loop() { - if (settings.sensors.outdoor.type == 2) { + if (settings.sensors.outdoor.type == 2 && settings.sensors.outdoor.pin) { outdoorTemperatureSensor(); } - if (settings.sensors.indoor.type == 2) { + if (settings.sensors.indoor.type == 2 && settings.sensors.indoor.pin) { indoorTemperatureSensor(); } +#if USE_BLE + else if (settings.sensors.indoor.type == 3 && strlen(settings.sensors.indoor.bleAddresss)) { + bluetoothSensor(); + } +#endif } +#if USE_BLE + void bluetoothSensor() { + if (!initBleSensor && millis() > 5000) { + Log.sinfoln(FPSTR(S_SENSORS_BLE), "Init BLE. Free heap %u bytes", ESP.getFreeHeap()); + BLEDevice::init(""); + + pBleClient = BLEDevice::createClient(); + + // Connect to the remote BLE Server. + BLEAddress bleServerAddress(std::string(settings.sensors.indoor.bleAddresss)); + if (pBleClient->connect(bleServerAddress)) { + Log.sinfoln(FPSTR(S_SENSORS_BLE), "Connected to BLE device at %s", bleServerAddress.toString().c_str()); + // Obtain a reference to the services we are interested in + pBleServiceBattery = pBleClient->getService(BLEUUID(bleUuidServiceBattery)); + if (pBleServiceBattery == nullptr) { + Log.sinfoln(FPSTR(S_SENSORS_BLE), "Failed to find battery service"); + } + pBleServiceEnvironment = pBleClient->getService(BLEUUID(bleUuidServiceEnvironment)); + if (pBleServiceEnvironment == nullptr) { + Log.sinfoln(FPSTR(S_SENSORS_BLE), "Failed to find environmental service"); + } + + } + else { + Log.swarningln(FPSTR(S_SENSORS_BLE), "Error connecting to BLE device at %s", bleServerAddress.toString().c_str()); + } + initBleSensor = true; + } + + if (pBleClient && pBleClient->isConnected()) { + Log.straceln(FPSTR(S_SENSORS_BLE), "Connected. Free heap %u bytes", ESP.getFreeHeap()); + if (pBleServiceBattery) { + uint8_t batteryLevel = *reinterpret_cast(pBleServiceBattery->getValue(bleUuidCharacteristicBatteryLevel).data()); + Log.straceln(FPSTR(S_SENSORS_BLE), "Battery: %d", batteryLevel); + } + if (pBleServiceEnvironment) { + float temperature = *reinterpret_cast(pBleServiceEnvironment->getValue(bleUuidCharacteristicTemperature).data()) / 100.0f; + Log.straceln(FPSTR(S_SENSORS_BLE), "Temperature: %.2f", temperature); + float humidity = *reinterpret_cast(pBleServiceEnvironment->getValue(bleUuidCharacteristicHumidity).data()) / 100.0f; + Log.straceln(FPSTR(S_SENSORS_BLE), "Humidity: %.2f", humidity); + + vars.temperatures.indoor = temperature + settings.sensors.indoor.offset; + } + } + else + { + Log.straceln(FPSTR(S_SENSORS_BLE), "Not connected"); + } + } +#endif + void outdoorTemperatureSensor() { if (!initOutdoorSensor) { oneWireOutdoorSensor = new OneWire(settings.sensors.outdoor.pin); diff --git a/src/Settings.h b/src/Settings.h index 6e34f50..383c2a5 100644 --- a/src/Settings.h +++ b/src/Settings.h @@ -72,9 +72,10 @@ struct Settings { } outdoor; struct { - // 1 - manual, 2 - ds18b20 + // 1 - manual, 2 - ds18b20, 3 - ble byte type = 1; byte pin = SENSOR_INDOOR_PIN_DEFAULT; + char bleAddresss[18]; float offset = 0.0f; } indoor; } sensors; diff --git a/src/WifiManagerTask.h b/src/WifiManagerTask.h index 2cbe003..e325240 100644 --- a/src/WifiManagerTask.h +++ b/src/WifiManagerTask.h @@ -28,6 +28,7 @@ CheckboxParameter* wmOtModSyncWithHeating; UnsignedIntParameter* wmOutdoorSensorPin; UnsignedIntParameter* wmIndoorSensorPin; +WiFiManagerParameter* wmBleAddress; CheckboxParameter* wmExtPumpUse; UnsignedIntParameter* wmExtPumpPin; @@ -170,6 +171,9 @@ protected: wmIndoorSensorPin = new UnsignedIntParameter("indoor_sensor_pin", "Indoor sensor GPIO", settings.sensors.indoor.pin, 2); wm.addParameter(wmIndoorSensorPin); + wmBleAddress = new WiFiManagerParameter("ble_address", "BLE sensor address", settings.sensors.indoor.bleAddresss, 17); + wm.addParameter(wmBleAddress); + wmExtPumpHeader = new HeaderParameter("External pump"); wm.addParameter(wmExtPumpHeader); @@ -445,6 +449,13 @@ protected: Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New sensors.indoor.pin: %hhu"), settings.sensors.indoor.pin); } + if (strcmp(wmBleAddress->getValue(), settings.sensors.indoor.bleAddresss) != 0) { + changed = true; + strcpy(settings.sensors.indoor.bleAddresss, wmBleAddress->getValue()); + + Log.sinfoln(FPSTR(S_WIFI_SETTINGS), F("New BLE address: %s"), settings.sensors.indoor.bleAddresss); + } + if (wmExtPumpUse->getCheckboxValue() != settings.externalPump.use) { changed = true; settings.externalPump.use = wmExtPumpUse->getCheckboxValue(); diff --git a/src/defines.h b/src/defines.h index dd9767c..fad1f58 100644 --- a/src/defines.h +++ b/src/defines.h @@ -34,6 +34,10 @@ #define USE_TELNET true #endif +#ifndef USE_BLE + #define USE_BLE false +#endif + #ifndef DEBUG_BY_DEFAULT #define DEBUG_BY_DEFAULT false #endif