diff --git a/main/config_mqttDiscovery.h b/main/config_mqttDiscovery.h index 14f4990..640da48 100644 --- a/main/config_mqttDiscovery.h +++ b/main/config_mqttDiscovery.h @@ -177,7 +177,7 @@ extern char discovery_prefix[]; #define jsonAlarm "{{ value_json.alarm | is_defined }}" #define jsonInuse "{{ value_json.power | is_defined | float > 0 }}" #define jsonInuseRN8209 "{% if value_json.power > 0.02 -%} on {% else %} off {%- endif %}" -#define jsonVoltBM2 "{% if value_json.uuid is not defined and value_json.volt is defined -%} {{value_json.volt}} {%- endif %}" +#define jsonVoltBM "{% if value_json.uuid is not defined and value_json.volt is defined -%} {{value_json.volt}} {%- endif %}" #define jsonRSSI "{{ value_json.rssi | is_defined }}" #define stateClassNone "" diff --git a/main/gatewayBLEConnect.cpp b/main/gatewayBLEConnect.cpp index 15e1cc2..206fdd0 100644 --- a/main/gatewayBLEConnect.cpp +++ b/main/gatewayBLEConnect.cpp @@ -332,6 +332,143 @@ void BM2_connect::publishData() { } } +/*-----------------------BM6 HANDLING-----------------------*/ +// BM6 AES encryption key: "leagend\xff\xfe0100009" +static const unsigned char BM6_AES_KEY[16] = { + 108, // l + 101, // e + 97, // a + 103, // g + 101, // e + 110, // n + 100, // d + 255, // 0xff + 254, // 0xfe + 48, // 0 + 49, // 1 + 48, // 0 + 48, // 0 + 48, // 0 + 48, // 0 + 57, // 9 +}; + +void BM6_connect::notifyCB(NimBLERemoteCharacteristic* pChar, uint8_t* pData, size_t length, bool isNotify) { + if (m_taskHandle == nullptr) { + return; // unexpected notification + } + + if (!BTProcessLock) { + THEENGS_LOG_TRACE(F("Callback from %s characteristic" CR), pChar->getUUID().toString().c_str()); + if (length == 16) { + THEENGS_LOG_TRACE(F("Device identified creating BLE buffer" CR)); + DynamicJsonDocument BLEdataBuffer(JSON_MSG_BUFFER); + JsonObject BLEdata = BLEdataBuffer.to(); + BLEdata["model"] = "BM6 Battery Monitor"; + BLEdata["id"] = m_pClient->getPeerAddress().toString(); + mbedtls_aes_context aes; + mbedtls_aes_init(&aes); + unsigned char output[16]; + unsigned char iv[16] = {}; + mbedtls_aes_setkey_dec(&aes, BM6_AES_KEY, 128); + mbedtls_aes_crypt_cbc(&aes, MBEDTLS_AES_DECRYPT, 16, iv, (uint8_t*)&pData[0], output); + mbedtls_aes_free(&aes); + + // Check for valid message signature (should start with D15507) + if (output[0] != 0xd1 || output[1] != 0x55 || output[2] != 0x07) { + THEENGS_LOG_NOTICE(F("BM6 invalid message signature" CR)); + xTaskNotifyGive(m_taskHandle); + return; + } + + // Parse according to BM6 protocol (from https://github.com/JeffWDH/bm6-battery-monitor) + // The decrypted data is converted to a hex string for parsing: + // - Voltage: hex string positions 15-17 (3 chars) / 100 + // - Temperature: hex string positions 8-9 (2 chars), negated if positions 6-7 == "01" + // - SoC (State of Charge): hex string positions 12-13 (2 chars) + char hexstr[33]; + sprintf(hexstr, "%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X", + output[0], output[1], output[2], output[3], output[4], output[5], output[6], output[7], + output[8], output[9], output[10], output[11], output[12], output[13], output[14], output[15]); + + // Extract voltage (positions 15-17 in hex string) + char volt_str[4] = {hexstr[15], hexstr[16], hexstr[17], 0}; + int volt_raw = (int)strtol(volt_str, NULL, 16); + float volt = volt_raw / 100.0f; + + // Extract SoC (positions 12-13 in hex string) + char soc_str[3] = {hexstr[12], hexstr[13], 0}; + int soc = (int)strtol(soc_str, NULL, 16); + + // Extract temperature (positions 8-9 in hex string) + char temp_str[3] = {hexstr[8], hexstr[9], 0}; + int temp_raw = (int)strtol(temp_str, NULL, 16); + + // Check temperature sign (positions 6-7 in hex string) + char temp_sign_str[3] = {hexstr[6], hexstr[7], 0}; + float temp = (strcmp(temp_sign_str, "01") == 0) ? -temp_raw : temp_raw; + + BLEdata["tempc"] = temp; + BLEdata["tempf"] = (float)convertTemp_CtoF(temp); + BLEdata["volt"] = volt; + BLEdata["batt"] = soc; + // to avoid the BM6 device tracker going offline because of the voltage/temp MQTT message without an RSSI value + BLEdata["rssi"] = -60; + buildTopicFromId(BLEdata, subjectBTtoMQTT); + enqueueJsonObject(BLEdata, QueueSemaphoreTimeOutTask); + } else { + THEENGS_LOG_NOTICE(F("Invalid notification data" CR)); + return; + } + } else { + THEENGS_LOG_TRACE(F("Callback process canceled by BTProcessLock" CR)); + } + + xTaskNotifyGive(m_taskHandle); +} + +void BM6_connect::publishData() { + NimBLEUUID serviceUUID("fff0"); + NimBLEUUID charWriteUUID("fff3"); + NimBLEUUID charNotifyUUID("fff4"); + + // Get write characteristic to send command + NimBLERemoteCharacteristic* pCharWrite = getCharacteristic(serviceUUID, charWriteUUID); + // Get notify characteristic to receive data + NimBLERemoteCharacteristic* pCharNotify = getCharacteristic(serviceUUID, charNotifyUUID); + + if (pCharWrite && (pCharWrite->canWrite() || pCharWrite->canWriteNoResponse()) && + pCharNotify && pCharNotify->canNotify()) { + THEENGS_LOG_TRACE(F("Registering notification" CR)); + if (pCharNotify->subscribe(true, std::bind(&BM6_connect::notifyCB, this, + std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3, std::placeholders::_4))) { + // Encrypt the command before sending (BM6 requires encrypted commands) + uint8_t command_plain[16] = {0xd1, 0x55, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t command_encrypted[16]; + unsigned char iv[16] = {}; + + mbedtls_aes_context aes; + mbedtls_aes_init(&aes); + mbedtls_aes_setkey_enc(&aes, BM6_AES_KEY, 128); + mbedtls_aes_crypt_cbc(&aes, MBEDTLS_AES_ENCRYPT, 16, iv, command_plain, command_encrypted); + mbedtls_aes_free(&aes); + + if (pCharWrite->writeValue(command_encrypted, 16, true)) { // Use write with response + m_taskHandle = xTaskGetCurrentTaskHandle(); + if (ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(BLE_CNCT_TIMEOUT)) == pdFALSE) { + m_taskHandle = nullptr; + } + } else { + THEENGS_LOG_NOTICE(F("Failed to send command" CR)); + } + } else { + THEENGS_LOG_NOTICE(F("Failed registering notification" CR)); + } + } +} + /*-----------------------HHCCJCY01HHCC HANDLING-----------------------*/ void HHCCJCY01HHCC_connect::publishData() { NimBLEUUID serviceUUID("00001204-0000-1000-8000-00805f9b34fb"); diff --git a/main/gatewayBLEConnect.h b/main/gatewayBLEConnect.h index c8c4e82..8e02840 100644 --- a/main/gatewayBLEConnect.h +++ b/main/gatewayBLEConnect.h @@ -48,6 +48,14 @@ public: void publishData() override; }; +class BM6_connect : public zBLEConnect { + void notifyCB(NimBLERemoteCharacteristic* pChar, uint8_t* pData, size_t length, bool isNotify); + +public: + BM6_connect(NimBLEAddress& addr) : zBLEConnect(addr) {} + void publishData() override; +}; + class GENERIC_connect : public zBLEConnect { std::vector m_data; diff --git a/main/gatewayBT.cpp b/main/gatewayBT.cpp index f4c467f..260315f 100644 --- a/main/gatewayBT.cpp +++ b/main/gatewayBT.cpp @@ -514,7 +514,7 @@ void BM2Discovery(const char* mac, const char* sensorModel_id) { # define BM2parametersCount 2 THEENGS_LOG_TRACE(F("BM2Discovery" CR)); const char* BM2sensor[BM2parametersCount][9] = { - {HASS_TYPE_SENSOR, "volt", mac, HASS_CLASS_VOLTAGE, jsonVoltBM2, "", "", HASS_UNIT_VOLT, stateClassMeasurement}, // We use a json definition that retrieve only data from the BM2 decoder, as this sensor also advertize volt as an iBeacon + {HASS_TYPE_SENSOR, "volt", mac, HASS_CLASS_VOLTAGE, jsonVoltBM, "", "", HASS_UNIT_VOLT, stateClassMeasurement}, // We use a json definition that retrieve only data from the BM decoder, as this sensor also advertizes volt as an iBeacon {HASS_TYPE_SENSOR, "batt", mac, HASS_CLASS_BATTERY, jsonBatt, "", "", HASS_UNIT_PERCENT, stateClassMeasurement} //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement }; @@ -522,6 +522,19 @@ void BM2Discovery(const char* mac, const char* sensorModel_id) { createDiscoveryFromList(mac, BM2sensor, BM2parametersCount, "BM2", "Generic", sensorModel_id); } +void BM6Discovery(const char* mac, const char* sensorModel_id) { +# define BM6parametersCount 3 + THEENGS_LOG_TRACE(F("BM6Discovery" CR)); + const char* BM6sensor[BM6parametersCount][9] = { + {HASS_TYPE_SENSOR, "volt", mac, HASS_CLASS_VOLTAGE, jsonVoltBM, "", "", HASS_UNIT_VOLT, stateClassMeasurement}, // We use a json definition that retrieve only data from the BM decoder, as this sensor also advertizes volt as an iBeacon + {HASS_TYPE_SENSOR, "temp", mac, HASS_CLASS_TEMPERATURE, jsonTempc, "", "", HASS_UNIT_CELSIUS, stateClassMeasurement}, + {HASS_TYPE_SENSOR, "batt", mac, HASS_CLASS_BATTERY, jsonBatt, "", "", HASS_UNIT_PERCENT, stateClassMeasurement} + //component type,name,availability topic,device class,value template,payload on, payload off, unit of measurement + }; + + createDiscoveryFromList(mac, BM6sensor, BM6parametersCount, "BM6", "Generic", sensorModel_id); +} + void LYWSD03MMCDiscovery(const char* mac, const char* sensorModel) { # define LYWSD03MMCparametersCount 4 THEENGS_LOG_TRACE(F("LYWSD03MMCDiscovery" CR)); @@ -585,6 +598,7 @@ void MHO_C401Discovery(const char* mac, const char* sensorModel) {} void HHCCJCY01HHCCDiscovery(const char* mac, const char* sensorModel) {} void DT24Discovery(const char* mac, const char* sensorModel_id) {} void BM2Discovery(const char* mac, const char* sensorModel_id) {} +void BM6Discovery(const char* mac, const char* sensorModel_id) {} void XMWSDJ04MMCDiscovery(const char* mac, const char* sensorModel_id) {} # endif @@ -747,6 +761,10 @@ void BLEconnect() { BM2_connect BLEclient(addr); BLEclient.processActions(BLEactions); BLEclient.publishData(); + } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::BM6) { + BM6_connect BLEclient(addr); + BLEclient.processActions(BLEactions); + BLEclient.publishData(); } else if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::HHCCJCY01HHCC) { HHCCJCY01HHCC_connect BLEclient(addr); BLEclient.processActions(BLEactions); @@ -782,6 +800,7 @@ void BLEconnect() { p->sensorModel_id != TheengsDecoder::BLE_ID_NUM::HHCCJCY01HHCC && p->sensorModel_id != BLEconectable::id::LYWSD03MMC && p->sensorModel_id != TheengsDecoder::BLE_ID_NUM::BM2 && + p->sensorModel_id != TheengsDecoder::BLE_ID_NUM::BM6 && p->sensorModel_id != BLEconectable::id::MHO_C401 && p->sensorModel_id != BLEconectable::id::XMWSDJ04MMC) { // if irregulary connected to and connection failed clear the connect flag. @@ -973,7 +992,9 @@ void launchBTDiscovery(bool overrideDiscovery) { if (!BTConfig.extDecoderEnable && // Do not decode if an external decoder is configured p->sensorModel_id > UNKWNON_MODEL && p->sensorModel_id < TheengsDecoder::BLE_ID_NUM::BLE_ID_MAX && - p->sensorModel_id != TheengsDecoder::BLE_ID_NUM::HHCCJCY01HHCC && p->sensorModel_id != TheengsDecoder::BLE_ID_NUM::BM2) { // Exception on HHCCJCY01HHCC and BM2 as these ones are discoverable and connectable + p->sensorModel_id != TheengsDecoder::BLE_ID_NUM::HHCCJCY01HHCC && + p->sensorModel_id != TheengsDecoder::BLE_ID_NUM::BM2 && + p->sensorModel_id != TheengsDecoder::BLE_ID_NUM::BM6) { // Exception on HHCCJCY01HHCC and BM2/BM6 as these ones are discoverable and connectable if (isTracker) { String tracker_name = String(model_id.c_str()) + "-tracker"; String tracker_id = macWOdots + "-tracker"; @@ -1124,7 +1145,7 @@ void launchBTDiscovery(bool overrideDiscovery) { } else { if ((p->sensorModel_id > BLEconectable::id::MIN && p->sensorModel_id < BLEconectable::id::MAX) || - p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::HHCCJCY01HHCC || p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::BM2) { + p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::HHCCJCY01HHCC || p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::BM2 || p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::BM6) { // Discovery of sensors from which we retrieve data only by connect if (p->sensorModel_id == BLEconectable::id::DT24_BLE) { DT24Discovery(macWOdots.c_str(), "DT24-BLE"); @@ -1142,6 +1163,19 @@ void launchBTDiscovery(bool overrideDiscovery) { model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, stateClassNone); } + if (p->sensorModel_id == TheengsDecoder::BLE_ID_NUM::BM6) { + // Sensor discovery + BM6Discovery(macWOdots.c_str(), "BM6"); + // Device tracker discovery + String tracker_id = macWOdots + "-tracker"; + createDiscovery(HASS_TYPE_DEVICE_TRACKER, + discovery_topic.c_str(), "BM6-tracker", tracker_id.c_str(), + will_Topic, "occupancy", "{% if value_json.get('rssi') -%}home{%- else -%}not_home{%- endif %}", + "", "", "", + 0, "", "", false, "", + model.c_str(), brand.c_str(), model_id.c_str(), macWOdots.c_str(), false, + stateClassNone); + } if (p->sensorModel_id == BLEconectable::id::LYWSD03MMC) { LYWSD03MMCDiscovery(macWOdots.c_str(), "LYWSD03MMC"); } @@ -1429,7 +1463,7 @@ void process_bledata(JsonObject& BLEdata) { if ((BLEdata["type"].as()).compare("RMAC") != 0 && model_id != TheengsDecoder::BLE_ID_NUM::IBEACON) { // Do not store in memory the random mac devices and iBeacons if (model_id >= 0) { // Broadcaster devices THEENGS_LOG_TRACE(F("Decoder found device: %s" CR), BLEdata["model_id"].as()); - if (model_id == TheengsDecoder::BLE_ID_NUM::HHCCJCY01HHCC || model_id == TheengsDecoder::BLE_ID_NUM::BM2) { // Device that broadcast and can be connected + if (model_id == TheengsDecoder::BLE_ID_NUM::HHCCJCY01HHCC || model_id == TheengsDecoder::BLE_ID_NUM::BM2 || model_id == TheengsDecoder::BLE_ID_NUM::BM6) { // Device that broadcast and can be connected createOrUpdateDevice(mac, device_flags_connect, model_id, mac_type, deviceName); } else { createOrUpdateDevice(mac, device_flags_init, model_id, mac_type, deviceName);