diff --git a/platformio.ini b/platformio.ini index 20c7c2d..92aebdf 100644 --- a/platformio.ini +++ b/platformio.ini @@ -50,6 +50,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 @@ -174,6 +175,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 3d03a86..0b3d826 100644 --- a/src/MqttTask.h +++ b/src/MqttTask.h @@ -290,7 +290,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/SensorsTask.h b/src/SensorsTask.h index 497f50e..45323a3 100644 --- a/src/SensorsTask.h +++ b/src/SensorsTask.h @@ -1,8 +1,20 @@ #include #include +#if defined(ESP32) + #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 defined(ESP32) + BLEClient* pBleClient = nullptr; + BLERemoteService* pBleServiceBattery = nullptr; + BLERemoteService* pBleServiceEnvironment = nullptr; + bool initBleSensor = false; +#endif const char* getTaskName() { return "Sensors"; @@ -39,15 +57,69 @@ 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 defined(ESP32) + else if (settings.sensors.indoor.type == 3 && strlen(settings.sensors.indoor.bleAddresss)) { + bluetoothSensor(); + } } + 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 82d06cf..e2a4721 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; @@ -153,6 +154,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); + wm.addParameter(wmSep); wmExtPumpUse = new CheckboxParameter("ext_pump_use", "Use external pump", settings.externalPump.use); @@ -427,6 +431,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();