16 Commits

Author SHA1 Message Date
Yurii
e4cbc687ae chore: minimizing flash size with NimBLE 2026-02-20 20:48:06 +03:00
Yurii
43fe5765f7 Merge branch 'async' into passive_ble 2026-02-16 10:26:37 +03:00
Yurii
5482e0b70a chore: update version name 2026-02-16 10:19:26 +03:00
Yurii
3fdb48e016 feat: added support DHT11/DHT22 sensors (#223)
* feat: Added support DHT11/DHT22 sensors

* chore: formatting

* fix: typo

* fix: `HaHelper::publishDynamicSensor()` for DHT sensors fixed

* refactor: optimization of wired sensors
2026-02-16 10:17:38 +03:00
Yurii
11747dd8bd feat: added commit hash 2026-02-16 10:06:26 +03:00
Yurii
af1a9b59c4 fix: custom_sdkconfig only for ESP32 C6 2026-02-15 06:30:28 +03:00
Yurii
9c86ad4427 chore: misc 2026-02-15 05:37:09 +03:00
Yurii
5f8ca61d79 chore: added custom_sdkconfig for building 2026-02-15 01:51:30 +03:00
Yurii
67adb3b9cf Merge branch 'async' into passive_ble 2026-01-26 02:23:19 +03:00
Yurii
ced0385d5b Merge branch 'master' into async 2026-01-26 02:19:54 +03:00
Yurii
7fcca3c4aa chore: fix ha blueprints to support all source sensors 2026-01-26 02:09:57 +03:00
Yurii
2f6bd237c7 chore: updated ha blueprints for new releases 2026-01-26 01:59:03 +03:00
Yurii
e4d1ba7d7b refactor: added different timeouts for wired and wireless sensors 2026-01-26 01:03:26 +03:00
Yurii
aae53d605a fix: move OpenThermTask to 0 core 2026-01-25 22:44:58 +03:00
Yurii
de9276d04e refactor: cosmetic changes 2026-01-25 22:42:48 +03:00
Yurii
cb8cd7c26e feat: added support BTHome v2 format for BLE sensors #215 2026-01-25 22:25:58 +03:00
25 changed files with 479 additions and 158 deletions

2
.gitignore vendored
View File

@@ -1,4 +1,5 @@
.pio
.dummy
.vscode
.PVS-Studio
build/*
@@ -10,5 +11,4 @@ package-lock.json
*.lock
sdkconfig.*
CMakeLists.txt
!sdkconfig.defaults
!.gitkeep

View File

@@ -1,5 +1,5 @@
# Blueprint for reporting indoor/outdoor temperature to OpenTherm Gateway from any home assistant sensor
# Updated: 03.09.2024
# Updated: 26.01.2026
blueprint:
name: Report temp to OpenTherm Gateway
@@ -15,7 +15,6 @@ blueprint:
multiple: false
filter:
- domain: sensor
device_class: temperature
target_entity:
name: Target entity
description: "Usually ``number.opentherm_indoor_temp`` or ``number.opentherm_outdoor_temp``"
@@ -38,8 +37,12 @@ condition:
value_template: "{{ states(source_entity) != 'unavailable' and states(target_entity) != 'unavailable' }}"
action:
- if:
- condition: template
value_template: "{{ (states(source_entity)|float(0) - states(target_entity)|float(0)) | abs | round(2) >= 0.01 }}"
- condition: or
conditions:
- condition: template
value_template: "{{ (states(source_entity)|float(0) - states(target_entity)|float(0)) | abs | round(2) >= 0.01 }}"
- condition: template
value_template: "{{ (as_timestamp(now()) - as_timestamp(states[target_entity].last_updated)) | int(0) > 300 }}"
then:
- service: number.set_value
data:

View File

@@ -1,5 +1,5 @@
# Blueprint for reporting temperature to OpenTherm Gateway from home assistant weather integration
# Updated: 03.09.2024
# Updated: 26.01.2026
blueprint:
name: Report temp to OpenTherm Gateway from Weather
@@ -37,8 +37,12 @@ condition:
value_template: "{{ states(source_entity) != 'unavailable' and states(target_entity) != 'unavailable' }}"
action:
- if:
- condition: template
value_template: "{{ (state_attr(source_entity, 'temperature')|float(0) - states(target_entity)|float(0)) | abs | round(2) >= 0.1 }}"
- condition: or
conditions:
- condition: template
value_template: "{{ (state_attr(source_entity, 'temperature')|float(0) - states(target_entity)|float(0)) | abs | round(2) >= 0.1 }}"
- condition: template
value_template: "{{ (as_timestamp(now()) - as_timestamp(states[target_entity].last_updated)) | int(0) > 300 }}"
then:
- service: number.set_value
data:

View File

@@ -17,13 +17,14 @@ lib_deps = ESP32Async/AsyncTCP@^3.4.10
gyverlibs/GyverPID@^3.3.2
gyverlibs/GyverBlinker@^1.1.1
pstolarz/OneWireNg@^0.14.1
;milesburton/DallasTemperature@^4.0.5
https://github.com/Laxilef/Arduino-Temperature-Control-Library#fix_85c
milesburton/DallasTemperature@^4.0.6
https://github.com/Laxilef/esp32DHT#idf5
;laxilef/TinyLogger@^1.1.1
https://github.com/Laxilef/TinyLogger#custom_handlers
lib_ignore = paulstoffregen/OneWire
lib_ignore = OneWire
build_type = ${secrets.build_type}
build_flags = -mtext-section-literals
build_flags = ;-mtext-section-literals
-O2
-Wno-deprecated-declarations
-D MQTT_CLIENT_STD_FUNCTION_CALLBACK=1
;-D DEBUG_ESP_CORE -D DEBUG_ESP_WIFI -D DEBUG_ESP_HTTP_SERVER -D DEBUG_ESP_PORT=Serial
@@ -33,6 +34,7 @@ build_flags = -mtext-section-literals
-D ARDUINOJSON_USE_DOUBLE=0
-D ARDUINOJSON_USE_LONG_LONG=0
-D TINYLOGGER_GLOBAL
-D DHT_TASK_STACK_SIZE=4096
-D DEFAULT_SERIAL_ENABLED=${secrets.serial_enabled}
-D DEFAULT_SERIAL_BAUD=${secrets.serial_baud}
-D DEFAULT_WEBSERIAL_ENABLED=${secrets.webserial_enabled}
@@ -61,7 +63,7 @@ check_flags = ;pvs-studio: --analysis-mode=4 --exclude-path=./.pio/l
; Defaults
[esp32_defaults]
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.36/platform-espressif32.zip
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip
platform_packages = ${env.platform_packages}
board_build.partitions = esp32_partitions.csv
lib_deps = ${env.lib_deps}
@@ -96,10 +98,12 @@ custom_component_remove = espressif/esp_hosted
espressif/esp-zboss-lib
espressif/esp-zigbee-lib
chmorgan/esp-libhelix-mp3
extra_scripts = post:tools/esp32.py
extra_scripts = pre:tools/add_build_commit.py
post:tools/esp32.py
post:tools/build.py
build_type = ${env.build_type}
build_flags = ${env.build_flags}
-D MYNEWT_VAL_BLE_ROLE_CENTRAL=0
-D CORE_DEBUG_LEVEL=0
-Wl,--wrap=esp_panic_handler
check_tool = ${env.check_tool}
@@ -144,7 +148,7 @@ extends = esp32_defaults
board = lolin_c3_mini
lib_deps = ${esp32_defaults.lib_deps}
${esp32_defaults.nimble_lib}
build_unflags = -mtext-section-literals
;build_unflags = -mtext-section-literals
build_flags = ${esp32_defaults.build_flags}
-D MYNEWT_VAL_BLE_EXT_ADV=1
-D USE_BLE=1
@@ -192,7 +196,7 @@ extends = esp32_defaults
board = esp32-c6-devkitc-1
lib_deps = ${esp32_defaults.lib_deps}
${esp32_defaults.nimble_lib}
build_unflags = -mtext-section-literals
;build_unflags = -mtext-section-literals
build_flags = ${esp32_defaults.build_flags}
-D USE_BLE=1
-D DEFAULT_OT_IN_GPIO=15
@@ -201,13 +205,18 @@ build_flags = ${esp32_defaults.build_flags}
-D DEFAULT_SENSOR_INDOOR_GPIO=0
-D DEFAULT_STATUS_LED_GPIO=11
-D DEFAULT_OT_RX_LED_GPIO=10
custom_sdkconfig = '# CONFIG_OPENTHREAD_ENABLED is not set'
'# CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT is not set'
'# CONFIG_LWIP_IPV6 is not set'
'# CONFIG_LWIP_IPV6_AUTOCONFIG is not set'
'# CONFIG_LWIP_PPP_SUPPORT is not set'
[env:otthing]
extends = esp32_defaults
board = lolin_c3_mini
lib_deps = ${esp32_defaults.lib_deps}
${esp32_defaults.nimble_lib}
build_unflags = -mtext-section-literals
;build_unflags = -mtext-section-literals
build_flags = ${esp32_defaults.build_flags}
-D MYNEWT_VAL_BLE_EXT_ADV=1
-D USE_BLE=1

View File

@@ -1,33 +0,0 @@
# Source:
# https://github.com/pioarduino/platform-espressif32/tree/main/examples/espidf-arduino-h2zero-BLE_scan
CONFIG_FREERTOS_HZ=1000
CONFIG_MBEDTLS_PSK_MODES=y
CONFIG_MBEDTLS_KEY_EXCHANGE_PSK=y
CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_SIZE=y
CONFIG_COMPILER_OPTIMIZATION_SIZE=y
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
CONFIG_ESPTOOLPY_HEADER_FLASHSIZE_UPDATE=y
#
# BT config
CONFIG_BT_ENABLED=y
CONFIG_BTDM_CTRL_MODE_BLE_ONLY=y
CONFIG_BTDM_CTRL_MODE_BR_EDR_ONLY=n
CONFIG_BTDM_CTRL_MODE_BTDM=n
CONFIG_BT_BLUEDROID_ENABLED=n
CONFIG_BT_NIMBLE_ENABLED=y
#
# Arduino Configuration
CONFIG_AUTOSTART_ARDUINO=y
CONFIG_ARDUINO_SELECTIVE_COMPILATION=y
CONFIG_ARDUINO_SELECTIVE_Zigbee=n
CONFIG_ARDUINO_SELECTIVE_Matter=n
CONFIG_ARDUINO_SELECTIVE_WiFiProv=n
CONFIG_ARDUINO_SELECTIVE_BLE=n
CONFIG_ARDUINO_SELECTIVE_BluetoothSerial=n
CONFIG_ARDUINO_SELECTIVE_SimpleBLE=n
CONFIG_ARDUINO_SELECTIVE_RainMaker=n
CONFIG_ARDUINO_SELECTIVE_OpenThread=n
CONFIG_ARDUINO_SELECTIVE_Insights=n

View File

@@ -176,9 +176,10 @@ public:
objId.c_str()
);
// set device class, name, value template for bluetooth sensors
// set device class, name, value template for Bluetooth/DHT sensors
// or name & value template for another sensors
if (sSensor.type == Sensors::Type::BLUETOOTH) {
if (sSensor.type == Sensors::Type::BLUETOOTH ||
sSensor.type == Sensors::Type::DHT11 || sSensor.type == Sensors::Type::DHT22) {
// available state topic
doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_TOPIC)] = doc[FPSTR(HA_STATE_TOPIC)];
doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_VALUE_TEMPLATE)] = JsonString(AVAILABILITY_SENSOR_CONN, true);

View File

@@ -132,8 +132,6 @@ protected:
tMqtt->disable();
}
Sensors::setConnectionStatusByType(Sensors::Type::MANUAL, !settings.mqtt.enabled || vars.mqtt.connected, false);
} else {
if (this->ntpStarted) {
this->ntpStarted = false;

View File

@@ -292,6 +292,14 @@ protected:
this->haHelper->deleteDynamicSensor(prevSettings, Sensors::ValueType::RSSI);
break;
case Sensors::Type::DHT11:
case Sensors::Type::DHT22:
this->haHelper->deleteConnectionDynamicSensor(prevSettings);
this->haHelper->deleteSignalQualityDynamicSensor(prevSettings);
this->haHelper->deleteDynamicSensor(prevSettings, Sensors::ValueType::TEMPERATURE);
this->haHelper->deleteDynamicSensor(prevSettings, Sensors::ValueType::HUMIDITY);
break;
case Sensors::Type::DALLAS_TEMP:
this->haHelper->deleteConnectionDynamicSensor(prevSettings);
this->haHelper->deleteSignalQualityDynamicSensor(prevSettings);
@@ -319,6 +327,14 @@ protected:
this->haHelper->publishDynamicSensor(sSettings, Sensors::ValueType::RSSI, settings.system.unitSystem, false);
break;
case Sensors::Type::DHT11:
case Sensors::Type::DHT22:
this->haHelper->publishConnectionDynamicSensor(sSettings);
this->haHelper->publishSignalQualityDynamicSensor(sSettings, false);
this->haHelper->publishDynamicSensor(sSettings, Sensors::ValueType::TEMPERATURE, settings.system.unitSystem);
this->haHelper->publishDynamicSensor(sSettings, Sensors::ValueType::HUMIDITY, settings.system.unitSystem);
break;
case Sensors::Type::DALLAS_TEMP:
this->haHelper->publishConnectionDynamicSensor(sSettings);
this->haHelper->publishSignalQualityDynamicSensor(sSettings, false);

View File

@@ -43,7 +43,7 @@ protected:
}
BaseType_t getTaskCore() override {
return 1;
return 0;
}
int getTaskPriority() override {

View File

@@ -552,6 +552,7 @@ protected:
auto docBuild = doc[FPSTR(S_BUILD)].to<JsonObject>();
docBuild[FPSTR(S_VERSION)] = BUILD_VERSION;
docBuild[FPSTR(S_COMMIT)] = BUILD_COMMIT;
docBuild[FPSTR(S_DATE)] = __DATE__ " " __TIME__;
docBuild[FPSTR(S_ENV)] = BUILD_ENV;
docBuild[FPSTR(S_CORE)] = ESP.getCoreVersion();
@@ -592,6 +593,7 @@ protected:
auto docBuild = doc[FPSTR(S_BUILD)].to<JsonObject>();
docBuild[FPSTR(S_VERSION)] = BUILD_VERSION;
docBuild[FPSTR(S_COMMIT)] = BUILD_COMMIT;
docBuild[FPSTR(S_DATE)] = __DATE__ " " __TIME__;
docBuild[FPSTR(S_ENV)] = BUILD_ENV;
docBuild[FPSTR(S_CORE)] = ESP.getCoreVersion();

View File

@@ -39,6 +39,8 @@ public:
NTC_10K_TEMP = 50,
DALLAS_TEMP = 51,
BLUETOOTH = 52,
DHT11 = 53,
DHT22 = 54,
HEATING_SETPOINT_TEMP = 253,
MANUAL = 254,

View File

@@ -1,6 +1,7 @@
#include <unordered_map>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <esp32DHT.h>
#if USE_BLE
#include <NimBLEDevice.h>
@@ -40,15 +41,15 @@ public:
auto deviceName = device->getName();
auto deviceRssi = device->getRSSI();
Log.straceln(
Log.sinfoln(
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': discovered device %s, name: %s, RSSI: %hhd"),
sensorId, sSensor.name,
deviceAddress.toString().c_str(), deviceName.c_str(), deviceRssi
);
if (!device->haveServiceData()) {
Log.straceln(
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': not found service data"),
Log.swarningln(
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': not found service data!"),
sensorId, sSensor.name
);
return;
@@ -60,84 +61,243 @@ public:
sensorId, sSensor.name, serviceDataCount
);
if (parseAtcData(device, sensorId) || parsePvvxData(device, sensorId) || parseBTHomeData(device, sensorId)) {
// update rssi
Sensors::setValueById(sensorId, deviceRssi, Sensors::ValueType::RSSI, false, false);
} else {
Log.swarningln(
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': unsupported data format!"),
sensorId, sSensor.name
);
}
}
static bool parseAtcData(const NimBLEAdvertisedDevice* device, uint8_t sensorId) {
NimBLEUUID serviceUuid((uint16_t) 0x181A);
auto serviceData = device->getServiceData(serviceUuid);
if (!serviceData.size()) {
Log.straceln(
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': NOT found %s env service data"),
sensorId, sSensor.name, serviceUuid.toString().c_str()
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu, service %s: not found ATC1441 data"),
sensorId, serviceUuid.toString().c_str()
);
return;
}
return false;
Log.straceln(
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': found %s env service data"),
sensorId, sSensor.name, serviceUuid.toString().c_str()
);
float temperature, humidity;
uint16_t batteryMv;
uint8_t batteryLevel;
if (serviceData.size() == 13) {
// atc1441 format
// Temperature (2 bytes, big-endian)
temperature = (
(static_cast<uint8_t>(serviceData[6]) << 8) | static_cast<uint8_t>(serviceData[7])
) * 0.1f;
// Humidity (1 byte)
humidity = static_cast<uint8_t>(serviceData[8]);
// Battery mV (2 bytes, big-endian)
batteryMv = (static_cast<uint8_t>(serviceData[10]) << 8) | static_cast<uint8_t>(serviceData[11]);
// Battery level (1 byte)
batteryLevel = static_cast<uint8_t>(serviceData[9]);
} else if (serviceData.size() == 15) {
// custom pvvx format
// Temperature (2 bytes, little-endian)
temperature = (
(static_cast<uint8_t>(serviceData[7]) << 8) | static_cast<uint8_t>(serviceData[6])
) * 0.01f;
// Humidity (2 bytes, little-endian)
humidity = (
(static_cast<uint8_t>(serviceData[9]) << 8) | static_cast<uint8_t>(serviceData[8])
) * 0.01f;
// Battery mV (2 bytes, little-endian)
batteryMv = (static_cast<uint8_t>(serviceData[11]) << 8) | static_cast<uint8_t>(serviceData[10]);
// Battery level (1 byte)
batteryLevel = static_cast<uint8_t>(serviceData[12]);
} else {
// unknown format
} else if (serviceData.size() != 13) {
Log.straceln(
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': unknown data format (size: %i)"),
sensorId, sSensor.name, serviceData.size()
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu, service %s: not in ATC1441 format"),
sensorId, serviceUuid.toString().c_str()
);
return;
return false;
}
Log.straceln(
FPSTR(L_SENSORS_BLE),
F("Sensor #%hhu '%s', received temp: %.2f; humidity: %.2f, battery voltage: %hu, battery level: %hhu"),
sensorId, sSensor.name,
temperature, humidity, batteryMv, batteryLevel
Log.snoticeln(
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu, service %s: found ATC1441 format"),
sensorId, serviceUuid.toString().c_str()
);
// update data
// Temperature (2 bytes, big-endian)
float temperature = (
(static_cast<uint8_t>(serviceData[6]) << 8) | static_cast<uint8_t>(serviceData[7])
) * 0.1f;
Sensors::setValueById(sensorId, temperature, Sensors::ValueType::TEMPERATURE, true, true);
// Humidity (1 byte)
float humidity = static_cast<uint8_t>(serviceData[8]);
Sensors::setValueById(sensorId, humidity, Sensors::ValueType::HUMIDITY, true, true);
// Battery level (1 byte)
uint8_t batteryLevel = static_cast<uint8_t>(serviceData[9]);
Sensors::setValueById(sensorId, batteryLevel, Sensors::ValueType::BATTERY, true, true);
// update rssi
Sensors::setValueById(sensorId, deviceRssi, Sensors::ValueType::RSSI, false, false);
// Battery mV (2 bytes, big-endian)
uint16_t batteryMv = (static_cast<uint8_t>(serviceData[10]) << 8) | static_cast<uint8_t>(serviceData[11]);
// Log
Log.snoticeln(
FPSTR(L_SENSORS_BLE),
F("Sensor #%hhu, received temp: %.2f; humidity: %.2f, battery voltage: %hu, battery level: %hhu"),
sensorId, temperature, humidity, batteryMv, batteryLevel
);
return true;
}
static bool parsePvvxData(const NimBLEAdvertisedDevice* device, uint8_t sensorId) {
NimBLEUUID serviceUuid((uint16_t) 0x181A);
auto serviceData = device->getServiceData(serviceUuid);
if (!serviceData.size()) {
Log.straceln(
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu, service %s: not found PVVX data"),
sensorId, serviceUuid.toString().c_str()
);
return false;
} else if (serviceData.size() != 15) {
Log.straceln(
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu, service %s: not in PVVX format"),
sensorId, serviceUuid.toString().c_str()
);
return false;
}
Log.snoticeln(
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu, service %s: found PVVX format"),
sensorId, serviceUuid.toString().c_str()
);
// Temperature (2 bytes, little-endian)
float temperature = (
(static_cast<uint8_t>(serviceData[7]) << 8) | static_cast<uint8_t>(serviceData[6])
) * 0.01f;
Sensors::setValueById(sensorId, temperature, Sensors::ValueType::TEMPERATURE, true, true);
// Humidity (2 bytes, little-endian)
float humidity = (
(static_cast<uint8_t>(serviceData[9]) << 8) | static_cast<uint8_t>(serviceData[8])
) * 0.01f;
Sensors::setValueById(sensorId, humidity, Sensors::ValueType::HUMIDITY, true, true);
// Battery level (1 byte)
uint8_t batteryLevel = static_cast<uint8_t>(serviceData[12]);
Sensors::setValueById(sensorId, batteryLevel, Sensors::ValueType::BATTERY, true, true);
// Battery mV (2 bytes, little-endian)
uint16_t batteryMv = (static_cast<uint8_t>(serviceData[11]) << 8) | static_cast<uint8_t>(serviceData[10]);
// Log
Log.snoticeln(
FPSTR(L_SENSORS_BLE),
F("Sensor #%hhu, received temp: %.2f; humidity: %.2f, battery voltage: %hu, battery level: %hhu"),
sensorId, temperature, humidity, batteryMv, batteryLevel
);
return true;
}
static bool parseBTHomeData(const NimBLEAdvertisedDevice* device, uint8_t sensorId) {
NimBLEUUID serviceUuid((uint16_t) 0xFCD2);
auto serviceData = device->getServiceData(serviceUuid);
if (!serviceData.size()) {
Log.straceln(
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu, service %s: not found BTHome data"),
sensorId, serviceUuid.toString().c_str()
);
return false;
} else if ((serviceData[0] & 0xE0) != 0x40) {
Log.straceln(
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu, service %s: unsupported BTHome version"),
sensorId, serviceUuid.toString().c_str()
);
return false;
} else if ((serviceData[0] & 0x01) != 0) {
Log.straceln(
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu, service %s: unsupported BTHome encrypted data"),
sensorId, serviceUuid.toString().c_str()
);
return false;
}
Log.snoticeln(
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu, service %s: found BTHome format"),
sensorId, serviceUuid.toString().c_str()
);
bool foundData = false;
size_t serviceDataPos = 0;
while (serviceDataPos < serviceData.size()) {
uint8_t objectId = serviceData[serviceDataPos++];
switch (objectId) {
// Packet ID (1 byte)
case 0x00:
serviceDataPos += 1;
break;
// Battery (1 byte)
case 0x01: {
if (serviceDataPos + 1 > serviceData.size()) {
break;
}
uint8_t batteryLevel = static_cast<uint8_t>(serviceData[serviceDataPos]);
Sensors::setValueById(sensorId, batteryLevel, Sensors::ValueType::BATTERY, true, true);
serviceDataPos += 1;
foundData = true;
Log.snoticeln(
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu, received battery level: %hhu"),
sensorId, batteryLevel
);
break;
}
// Temperature (2 bytes, little-endian)
case 0x02: {
if (serviceDataPos + 2 > serviceData.size()) {
break;
}
int16_t rawTemp = (static_cast<int16_t>(serviceData[serviceDataPos + 1]) << 8)
| static_cast<uint8_t>(serviceData[serviceDataPos]);
float temperature = static_cast<float>(rawTemp) * 0.01f;
Sensors::setValueById(sensorId, temperature, Sensors::ValueType::TEMPERATURE, true, true);
serviceDataPos += 2;
foundData = true;
Log.snoticeln(
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu, received temp: %.2f"),
sensorId, temperature
);
break;
}
// Humidity (2 bytes, little-endian)
case 0x03: {
if (serviceDataPos + 2 > serviceData.size()) {
break;
}
uint16_t rawHumidity = (static_cast<uint16_t>(serviceData[serviceDataPos + 1]) << 8)
| static_cast<uint8_t>(serviceData[serviceDataPos]);
float humidity = static_cast<float>(rawHumidity) * 0.01f;
Sensors::setValueById(sensorId, humidity, Sensors::ValueType::HUMIDITY, true, true);
serviceDataPos += 2;
foundData = true;
Log.snoticeln(
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu, received humidity: %.2f"),
sensorId, humidity
);
break;
}
// Voltage (2 bytes, little-endian)
case 0x0C: {
if (serviceDataPos + 2 > serviceData.size()) {
break;
}
uint16_t batteryMv = (static_cast<uint16_t>(serviceData[serviceDataPos + 1]) << 8)
| static_cast<uint8_t>(serviceData[serviceDataPos]);
serviceDataPos += 2;
foundData = true;
Log.snoticeln(
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu, received battery voltage: %hu"),
sensorId, batteryMv
);
break;
}
}
}
return foundData;
}
};
#endif
@@ -145,11 +305,13 @@ public:
class SensorsTask : public LeanTask {
public:
SensorsTask(bool _enabled = false, unsigned long _interval = 0) : LeanTask(_enabled, _interval) {
this->gpioLastPollingTime.reserve(2);
// OneWire
this->owInstances.reserve(2);
this->dallasInstances.reserve(2);
this->dallasSearchTime.reserve(2);
this->dallasPolling.reserve(2);
this->dallasLastPollingTime.reserve(2);
#if USE_BLE
this->pBLEScanCallbacks = new BluetoothScanCallbacks();
@@ -157,11 +319,13 @@ public:
}
~SensorsTask() {
this->gpioLastPollingTime.clear();
// OneWire
this->dallasInstances.clear();
this->owInstances.clear();
this->dallasSearchTime.clear();
this->dallasPolling.clear();
this->dallasLastPollingTime.clear();
#if USE_BLE
delete this->pBLEScanCallbacks;
@@ -169,16 +333,25 @@ public:
}
protected:
const unsigned int disconnectedTimeout = 180000u;
const unsigned int wiredDisconnectTimeout = 180000u;
const unsigned int wirelessDisconnectTimeout = 600000u;
const unsigned short dallasSearchInterval = 60000u;
const unsigned short dallasPollingInterval = 10000u;
const unsigned short dhtPollingInterval = 15000;
const unsigned short globalPollingInterval = 15000u;
std::unordered_map<uint8_t, unsigned long> gpioLastPollingTime;
// OneWire
std::unordered_map<uint8_t, OneWire> owInstances;
std::unordered_map<uint8_t, DallasTemperature> dallasInstances;
std::unordered_map<uint8_t, unsigned long> dallasSearchTime;
std::unordered_map<uint8_t, bool> dallasPolling;
std::unordered_map<uint8_t, unsigned long> dallasLastPollingTime;
// DHT
DHT dhtInstance;
bool dhtIsPolling = false;
#if USE_BLE
NimBLEScan* pBLEScan = nullptr;
BluetoothScanCallbacks* pBLEScanCallbacks = nullptr;
@@ -219,6 +392,9 @@ protected:
this->yield();
}
pollingDhtSensors();
this->yield();
if (millis() - this->globalLastPollingTime > this->globalPollingInterval) {
cleanDallasInstances();
makeDallasInstances();
@@ -275,7 +451,7 @@ protected:
this->dallasSearchTime[sSensor.gpio] = 0;
this->dallasPolling[sSensor.gpio] = false;
this->dallasLastPollingTime[sSensor.gpio] = 0;
this->gpioLastPollingTime[sSensor.gpio] = 0;
auto& instance = this->dallasInstances[sSensor.gpio];
instance.setOneWire(&owInstance);
@@ -310,7 +486,7 @@ protected:
this->owInstances.erase(gpio);
this->dallasSearchTime.erase(gpio);
this->dallasPolling.erase(gpio);
this->dallasLastPollingTime.erase(gpio);
this->gpioLastPollingTime.erase(gpio);
Log.sinfoln(FPSTR(L_SENSORS_DALLAS), F("Stopped on GPIO %hhu"), gpio);
continue;
@@ -431,7 +607,7 @@ protected:
if (this->dallasPolling[gpio]) {
unsigned long minPollingTime = instance.millisToWaitForConversion(12) * 2;
unsigned long estimatePollingTime = ts - this->dallasLastPollingTime[gpio];
unsigned long estimatePollingTime = ts - this->gpioLastPollingTime[gpio];
// check conversion time
// isConversionComplete does not work with chinese clones!
@@ -483,7 +659,7 @@ protected:
this->dallasPolling[gpio] = false;
} else if (newPolling) {
auto estimateLastPollingTime = ts - this->dallasLastPollingTime[gpio];
auto estimateLastPollingTime = ts - this->gpioLastPollingTime[gpio];
// check last polling time
if (estimateLastPollingTime < this->dallasPollingInterval) {
@@ -494,13 +670,102 @@ protected:
instance.setResolution(12);
instance.requestTemperatures();
this->dallasPolling[gpio] = true;
this->dallasLastPollingTime[gpio] = ts;
this->gpioLastPollingTime[gpio] = ts;
Log.straceln(FPSTR(L_SENSORS_DALLAS), F("GPIO %hhu, polling..."), gpio);
}
}
}
void pollingDhtSensors() {
if (this->dhtIsPolling) {
// busy
return;
}
for (uint8_t sensorId = 0; sensorId <= Sensors::getMaxSensorId(); sensorId++) {
auto& sSensor = Sensors::settings[sensorId];
if (!sSensor.enabled || sSensor.purpose == Sensors::Purpose::NOT_CONFIGURED) {
continue;
}
if (sSensor.type != Sensors::Type::DHT11 && sSensor.type != Sensors::Type::DHT22) {
continue;
}
if (this->gpioLastPollingTime.count(sSensor.gpio) && millis() - this->gpioLastPollingTime[sSensor.gpio] < this->dhtPollingInterval) {
continue;
}
const auto sensorGpio = static_cast<gpio_num_t>(sSensor.gpio);
if (this->dhtInstance.getGpio() != sensorGpio) {
this->dhtInstance.reset();
this->dhtInstance.onData([this, sensorId](float humidity, float temperature) {
auto& sSensor = Sensors::settings[sensorId];
Log.straceln(
FPSTR(L_SENSORS_DHT), F("GPIO %hhu, sensor #%hhu '%s', temp: %.2f, humidity: %.2f%%"),
sSensor.gpio, sensorId, sSensor.name, temperature, humidity
);
// set temperature
Sensors::setValueById(sensorId, temperature, Sensors::ValueType::TEMPERATURE, true, true);
// set humidity
Sensors::setValueById(sensorId, humidity, Sensors::ValueType::HUMIDITY, true, true);
auto& rSensor = Sensors::results[sensorId];
if (rSensor.signalQuality < 100) {
rSensor.signalQuality++;
}
this->gpioLastPollingTime[sSensor.gpio] = millis();
this->dhtIsPolling = false;
});
this->dhtInstance.onError([this, sensorId](DHT::Status status) {
auto& sSensor = Sensors::settings[sensorId];
Log.swarningln(
FPSTR(L_SENSORS_DHT), F("GPIO %hhu, sensor #%hhu '%s': failed receiving data (err: %s)"),
sSensor.gpio, sensorId, sSensor.name, DHT::statusToString(this->dhtInstance.getStatus())
);
auto& rSensor = Sensors::results[sensorId];
if (rSensor.signalQuality > 0) {
rSensor.signalQuality--;
}
this->gpioLastPollingTime[sSensor.gpio] = millis();
this->dhtIsPolling = false;
});
DHT::Type sType = DHT::Type::DHT22;
if (sSensor.type == Sensors::Type::DHT11) {
sType = DHT::Type::DHT11;
} else if (sSensor.type == Sensors::Type::DHT22) {
sType = DHT::Type::DHT22;
}
if (this->dhtInstance.setup(sensorGpio, sType)) {
Log.sinfoln(FPSTR(L_SENSORS_DHT), F("Started on GPIO %hhu"), sSensor.gpio);
} else {
Log.swarningln(
FPSTR(L_SENSORS_DHT), F("Failed to start on GPIO %hhu (err: %s)"),
sSensor.gpio, DHT::statusToString(this->dhtInstance.getStatus())
);
}
}
this->dhtIsPolling = this->dhtInstance.poll();
break;
}
}
void pollingNtcSensors() {
for (uint8_t sensorId = 0; sensorId <= Sensors::getMaxSensorId(); sensorId++) {
auto& sSensor = Sensors::settings[sensorId];
@@ -629,12 +894,16 @@ protected:
} else if (rSensor.connected && sSensor.purpose == Sensors::Purpose::NOT_CONFIGURED) {
Sensors::setConnectionStatusById(sensorId, false, false);
} else if (sSensor.type != Sensors::Type::MANUAL && rSensor.connected && (millis() - rSensor.activityTime) > this->disconnectedTimeout) {
Sensors::setConnectionStatusById(sensorId, false, false);
} else if (rSensor.connected) {
if (sSensor.type == Sensors::Type::MANUAL || sSensor.type == Sensors::Type::BLUETOOTH) {
if ((millis() - rSensor.activityTime) > this->wirelessDisconnectTimeout) {
Sensors::setConnectionStatusById(sensorId, false, false);
}
}/* else if (!rSensor.connected) {
rSensor.connected = true;
}*/
} else if ((millis() - rSensor.activityTime) > this->wiredDisconnectTimeout) {
Sensors::setConnectionStatusById(sensorId, false, false);
}
}
}
}

View File

@@ -30,6 +30,10 @@
#define BUILD_VERSION "0.0.0"
#endif
#ifndef BUILD_COMMIT
#define BUILD_COMMIT "undefined"
#endif
#ifndef BUILD_ENV
#define BUILD_ENV "undefined"
#endif

View File

@@ -1,3 +0,0 @@
dependencies:
idf: ">=5.3.2"
h2zero/esp-nimble-cpp: ">=2.2.1"

View File

@@ -24,6 +24,7 @@ const char L_OT_CH2[] PROGMEM = "OT.CH2";
const char L_SENSORS[] PROGMEM = "SENSORS";
const char L_SENSORS_SETTINGS[] PROGMEM = "SENSORS.SETTINGS";
const char L_SENSORS_DALLAS[] PROGMEM = "SENSORS.DALLAS";
const char L_SENSORS_DHT[] PROGMEM = "SENSORS.DHT";
const char L_SENSORS_NTC[] PROGMEM = "SENSORS.NTC";
const char L_SENSORS_BLE[] PROGMEM = "SENSORS.BLE";
const char L_REGULATOR[] PROGMEM = "REGULATOR";
@@ -56,6 +57,7 @@ const char S_CHANNEL[] PROGMEM = "channel";
const char S_CH2_ALWAYS_ENABLED[] PROGMEM = "ch2AlwaysEnabled";
const char S_CHIP[] PROGMEM = "chip";
const char S_CODE[] PROGMEM = "code";
const char S_COMMIT[] PROGMEM = "commit";
const char S_CONNECTED[] PROGMEM = "connected";
const char S_CONTINUES[] PROGMEM = "continues";
const char S_COOLING[] PROGMEM = "cooling";

View File

@@ -1927,6 +1927,8 @@ bool jsonToSensorSettings(const uint8_t sensorId, const JsonVariantConst src, Se
case static_cast<uint8_t>(Sensors::Type::NTC_10K_TEMP):
case static_cast<uint8_t>(Sensors::Type::DALLAS_TEMP):
case static_cast<uint8_t>(Sensors::Type::BLUETOOTH):
case static_cast<uint8_t>(Sensors::Type::DHT11):
case static_cast<uint8_t>(Sensors::Type::DHT22):
case static_cast<uint8_t>(Sensors::Type::HEATING_SETPOINT_TEMP):
case static_cast<uint8_t>(Sensors::Type::MANUAL):
case static_cast<uint8_t>(Sensors::Type::NOT_CONFIGURED):
@@ -1943,7 +1945,8 @@ bool jsonToSensorSettings(const uint8_t sensorId, const JsonVariantConst src, Se
// gpio
if (!src[FPSTR(S_GPIO)].isNull()) {
if (dst.type != Sensors::Type::DALLAS_TEMP && dst.type != Sensors::Type::NTC_10K_TEMP) {
if (dst.type != Sensors::Type::DALLAS_TEMP && dst.type != Sensors::Type::NTC_10K_TEMP &&
dst.type != Sensors::Type::DHT11 && dst.type != Sensors::Type::DHT22) {
if (dst.gpio != GPIO_IS_NOT_CONFIGURED) {
dst.gpio = GPIO_IS_NOT_CONFIGURED;
changed = true;
@@ -2084,6 +2087,10 @@ void sensorResultToJson(const uint8_t sensorId, JsonVariant dst) {
dst[FPSTR(S_BATTERY)] = roundf(rSensor.values[static_cast<uint8_t>(Sensors::ValueType::BATTERY)], 1);
dst[FPSTR(S_RSSI)] = roundf(rSensor.values[static_cast<uint8_t>(Sensors::ValueType::RSSI)], 0);
} else if (sSensor.type == Sensors::Type::DHT11 || sSensor.type == Sensors::Type::DHT22) {
dst[FPSTR(S_TEMPERATURE)] = roundf(rSensor.values[static_cast<uint8_t>(Sensors::ValueType::TEMPERATURE)], 3);
dst[FPSTR(S_HUMIDITY)] = roundf(rSensor.values[static_cast<uint8_t>(Sensors::ValueType::HUMIDITY)], 3);
} else {
dst[FPSTR(S_VALUE)] = roundf(rSensor.values[static_cast<uint8_t>(Sensors::ValueType::PRIMARY)], 3);
}
@@ -2099,21 +2106,18 @@ bool jsonToSensorResult(const uint8_t sensorId, const JsonVariantConst src) {
return false;
}
auto& dst = Sensors::results[sensorId];
bool changed = false;
// value
if (!src[FPSTR(S_VALUE)].isNull()) {
float value = src[FPSTR(S_VALUE)].as<float>();
uint8_t vType = static_cast<uint8_t>(Sensors::ValueType::PRIMARY);
if (fabsf(value - dst.values[vType]) > 0.0001f) {
dst.values[vType] = roundf(value, 2);
changed = true;
}
return Sensors::setValueById(
sensorId,
src[FPSTR(S_VALUE)].as<float>(),
Sensors::ValueType::PRIMARY,
true,
true
);
}
return changed;
return false;
}
void varsToJson(const Variables& src, JsonVariant dst) {

View File

@@ -44,6 +44,7 @@
"build": {
"title": "Build",
"version": "固件版本",
"commit": "Commit",
"date": "日期",
"core": "内核版本",
"sdk": "SDK"
@@ -248,6 +249,8 @@
"ntcTemp": "NTC 传感器",
"dallasTemp": "DALLAS 传感器",
"bluetooth": "BLE 传感器",
"dht11": "DHT11 传感器",
"dht22": "DHT22 传感器",
"heatSetpointTemp": "Heating, setpoint temp",
"manual": "通过 MQTT/API 手动配置",
"notConfigured": "未配置"

View File

@@ -44,6 +44,7 @@
"build": {
"title": "Build",
"version": "Version",
"commit": "Commit",
"date": "Date",
"core": "Core",
"sdk": "SDK"
@@ -248,6 +249,8 @@
"ntcTemp": "NTC sensor",
"dallasTemp": "DALLAS sensor",
"bluetooth": "BLE sensor",
"dht11": "DHT11 sensor",
"dht22": "DHT22 sensor",
"heatSetpointTemp": "Heating, setpoint temp",
"manual": "Manual via MQTT/API",
"notConfigured": "Not configured"

View File

@@ -44,6 +44,7 @@
"build": {
"title": "Build",
"version": "Versione",
"commit": "Commit",
"date": "Data",
"core": "Core",
"sdk": "SDK"
@@ -248,6 +249,8 @@
"ntcTemp": "Sensore NTC",
"dallasTemp": "Sensore DALLAS",
"bluetooth": "Sensore BLE",
"dht11": "Sensore DHT11",
"dht22": "Sensore DHT22",
"heatSetpointTemp": "Riscaldamento, temp impostata",
"manual": "Manuale via MQTT/API",
"notConfigured": "Non configurato"

View File

@@ -40,6 +40,7 @@
"build": {
"title": "Build",
"version": "Versie",
"commit": "Commit",
"date": "Datum",
"core": "Core",
"sdk": "SDK"
@@ -227,6 +228,8 @@
"ntcTemp": "NTC-sensor",
"dallasTemp": "DALLAS-sensor",
"bluetooth": "BLE-sensor",
"dht11": "DHT11-sensor",
"dht22": "DHT22-sensor",
"heatSetpointTemp": "Verwarming, insteltemperatuur",
"manual": "Handmatig via MQTT/API",
"notConfigured": "Niet geconfigureerd"

View File

@@ -44,6 +44,7 @@
"build": {
"title": "Билд",
"version": "Версия",
"commit": "Коммит",
"date": "Дата",
"core": "Ядро",
"sdk": "SDK"
@@ -248,6 +249,8 @@
"ntcTemp": "NTC датчик",
"dallasTemp": "DALLAS датчик",
"bluetooth": "BLE датчик",
"dht11": "DHT11 датчик",
"dht22": "DHT22 датчик",
"heatSetpointTemp": "Отопление, температура уставки",
"manual": "Вручную через MQTT/API",
"notConfigured": "Не сконфигурировано"

View File

@@ -104,6 +104,7 @@
<th scope="row" data-i18n>index.system.build.title</th>
<td>
Env: <b id="build-env"></b><br />
<span data-i18n>index.system.build.commit</span>: <b id="build-commit"></b><br />
<span data-i18n>index.system.build.date</span>: <b id="build-date"></b><br />
<span data-i18n>index.system.build.core</span>: <b id="build-core"></b><br />
<span data-i18n>index.system.build.sdk</span>: <b id="build-sdk"></b>
@@ -206,6 +207,7 @@
setBusy('#main-busy', '#main-table', false);
setValue('#build-version', result.build.version);
setValue('#build-commit', result.build.commit);
setValue('#build-date', result.build.date);
setValue('#build-env', result.build.env);
setValue('#build-core', result.build.core);

View File

@@ -118,6 +118,8 @@
<option value="50" data-i18n>sensors.types.ntcTemp</option>
<option value="51" data-i18n>sensors.types.dallasTemp</option>
<option value="52" data-i18n>sensors.types.bluetooth</option>
<option value="53" data-i18n>sensors.types.dht11</option>
<option value="54" data-i18n>sensors.types.dht22</option>
<option value="253" data-i18n>sensors.types.heatSetpointTemp</option>
<option value="254" data-i18n>sensors.types.manual</option>
<option value="255" data-i18n>sensors.types.notConfigured</option>
@@ -271,27 +273,35 @@
}
switch(parseInt(event.target.value)) {
// ntc
// NTC10K
case 50:
parentGpio.classList.remove("hidden");
parentAddress.classList.add("hidden");
address.removeAttribute("pattern");
break;
// dallas
// OneWire
case 51:
parentGpio.classList.remove("hidden");
parentAddress.classList.remove("hidden");
address.setAttribute("pattern", "([A-Fa-f0-9]{2}:){7}[A-Fa-f0-9]{2}");
break;
// ble
// Bluetooth
case 52:
parentGpio.classList.add("hidden");
parentAddress.classList.remove("hidden");
address.setAttribute("pattern", "([A-Fa-f0-9]{2}:){5}[A-Fa-f0-9]{2}");
break;
// DHT
case 53:
case 54:
parentGpio.classList.remove("hidden");
parentAddress.classList.add("hidden");
address.removeAttribute("pattern");
break;
// other
default:
parentGpio.classList.add("hidden");

16
tools/add_build_commit.py Normal file
View File

@@ -0,0 +1,16 @@
import subprocess
Import("env")
try:
commit_hash = "undefined"
result = subprocess.check_output(
["git", "rev-parse", "--short", "HEAD"]
)
commit_hash = result.decode("utf-8").strip()
env.Append(
CPPDEFINES=[
("BUILD_COMMIT", '\\"{}\\"'.format(commit_hash))
]
)
except Exception as error:
print("Failed to get commit hash: {}".format(error))